[codex] namespace sleep under clock (#29907)

## Summary

- expose the interruptible sleep tool as `clock.sleep` instead of
top-level `sleep`
- keep `clock.curr_time` and `clock.sleep` in the same model-visible
namespace when both features are enabled
- update existing core and app-server integration coverage to issue
namespaced sleep calls

## Why

Sleep is a clock operation. Grouping it with `clock.curr_time` gives the
model a more coherent tool surface without changing the sleep feature
gate or runtime behavior.

## Validation

- `just test -p codex-core sleep_tool_follows_feature_gate`
- `just test -p codex-core any_new_input_interrupts_sleep`
- `just test -p codex-app-server
sleep_emits_started_and_completed_items`
This commit is contained in:
rka-oai
2026-06-24 17:17:28 -07:00
committed by GitHub
Unverified
parent 134646eff0
commit 800529218a
5 changed files with 32 additions and 21 deletions
+2 -1
View File
@@ -32,8 +32,9 @@ async fn sleep_emits_started_and_completed_items() -> Result<()> {
vec![
responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_function_call(
responses::ev_function_call_with_namespace(
CALL_ID,
"clock",
"sleep",
&serde_json::json!({ "duration_ms": DURATION_MS }).to_string(),
),
@@ -55,7 +55,7 @@ impl ToolExecutor<ToolInvocation> for CurrentTimeHandler {
fn spec(&self) -> ToolSpec {
ToolSpec::Namespace(ResponsesApiNamespace {
name: NAMESPACE.to_string(),
description: "Tools for reading the current time.".to_string(),
description: "Tools for reading and waiting on time.".to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: TOOL_NAME.to_string(),
description: "Return the current time in UTC.".to_string(),
+22 -15
View File
@@ -9,6 +9,8 @@ use crate::tools::registry::ToolExecutor;
use codex_protocol::items::SleepItem;
use codex_protocol::items::TurnItem;
use codex_tools::JsonSchema;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ResponsesApiTool;
use codex_tools::ToolName;
use codex_tools::ToolSpec;
@@ -17,7 +19,8 @@ use std::collections::BTreeMap;
use std::time::Duration;
use std::time::Instant;
const SLEEP_TOOL_NAME: &str = "sleep";
const NAMESPACE: &str = "clock";
const TOOL_NAME: &str = "sleep";
const MAX_SLEEP_DURATION_MS: u64 = 3_600_000;
pub struct SleepHandler;
@@ -36,24 +39,28 @@ fn create_sleep_tool() -> ToolSpec {
))),
)]);
ToolSpec::Function(ResponsesApiTool {
name: SLEEP_TOOL_NAME.to_string(),
description: "Pause execution for a specified duration. The sleep ends early when new input arrives for the active turn. Returns the elapsed wall-clock time."
.to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(
properties,
Some(vec!["duration_ms".to_string()]),
/*additional_properties*/ Some(false.into()),
),
output_schema: None,
ToolSpec::Namespace(ResponsesApiNamespace {
name: NAMESPACE.to_string(),
description: "Tools for reading and waiting on time.".to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: TOOL_NAME.to_string(),
description: "Pause execution for a specified duration. The sleep ends early when new input arrives for the active turn. Returns the elapsed wall-clock time."
.to_string(),
strict: false,
defer_loading: None,
parameters: JsonSchema::object(
properties,
Some(vec!["duration_ms".to_string()]),
/*additional_properties*/ Some(false.into()),
),
output_schema: None,
})],
})
}
impl ToolExecutor<ToolInvocation> for SleepHandler {
fn tool_name(&self) -> ToolName {
ToolName::plain(SLEEP_TOOL_NAME)
ToolName::namespaced(NAMESPACE, TOOL_NAME)
}
fn spec(&self) -> ToolSpec {
@@ -71,7 +78,7 @@ impl ToolExecutor<ToolInvocation> for SleepHandler {
} = invocation;
let ToolPayload::Function { arguments } = payload else {
return Err(FunctionCallError::RespondToModel(format!(
"{SLEEP_TOOL_NAME} handler received unsupported payload"
"{TOOL_NAME} handler received unsupported payload"
)));
};
let args: SleepArgs = parse_arguments(&arguments)?;
+3 -2
View File
@@ -726,13 +726,14 @@ async fn sleep_tool_follows_feature_gate() {
set_feature(turn, Feature::SleepTool, /*enabled*/ false);
})
.await;
disabled.assert_visible_lacks(&["sleep"]);
disabled.assert_visible_lacks(&["clock"]);
let enabled = probe(|turn| {
set_feature(turn, Feature::SleepTool, /*enabled*/ true);
})
.await;
enabled.assert_visible_contains(&["sleep"]);
enabled.assert_visible_contains(&["clock"]);
assert_eq!(enabled.namespace_function_names("clock"), ["sleep"]);
}
#[tokio::test]
+4 -2
View File
@@ -361,8 +361,9 @@ async fn any_new_input_interrupts_sleep() {
let first_chunks = vec![
chunk(ev_response_created("resp-1")),
chunk(ev_function_call(
chunk(ev_function_call_with_namespace(
FIRST_SLEEP_CALL_ID,
"clock",
"sleep",
&sleep_arguments,
)),
@@ -370,8 +371,9 @@ async fn any_new_input_interrupts_sleep() {
];
let second_chunks = vec![
chunk(ev_response_created("resp-2")),
chunk(ev_function_call(
chunk(ev_function_call_with_namespace(
SECOND_SLEEP_CALL_ID,
"clock",
"sleep",
&sleep_arguments,
)),