From 800529218a4356c06b6e1e97f8dd16c42e75aa78 Mon Sep 17 00:00:00 2001 From: rka-oai Date: Wed, 24 Jun 2026 17:17:28 -0700 Subject: [PATCH] [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` --- codex-rs/app-server/tests/suite/v2/sleep.rs | 3 +- .../core/src/tools/handlers/current_time.rs | 2 +- codex-rs/core/src/tools/handlers/sleep.rs | 37 +++++++++++-------- codex-rs/core/src/tools/spec_plan_tests.rs | 5 ++- codex-rs/core/tests/suite/pending_input.rs | 6 ++- 5 files changed, 32 insertions(+), 21 deletions(-) diff --git a/codex-rs/app-server/tests/suite/v2/sleep.rs b/codex-rs/app-server/tests/suite/v2/sleep.rs index 6c6c99488..063823324 100644 --- a/codex-rs/app-server/tests/suite/v2/sleep.rs +++ b/codex-rs/app-server/tests/suite/v2/sleep.rs @@ -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(), ), diff --git a/codex-rs/core/src/tools/handlers/current_time.rs b/codex-rs/core/src/tools/handlers/current_time.rs index 73873ca74..d03e7ffb0 100644 --- a/codex-rs/core/src/tools/handlers/current_time.rs +++ b/codex-rs/core/src/tools/handlers/current_time.rs @@ -55,7 +55,7 @@ impl ToolExecutor 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(), diff --git a/codex-rs/core/src/tools/handlers/sleep.rs b/codex-rs/core/src/tools/handlers/sleep.rs index 96b96323c..51888227a 100644 --- a/codex-rs/core/src/tools/handlers/sleep.rs +++ b/codex-rs/core/src/tools/handlers/sleep.rs @@ -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 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 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)?; diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index 8837d4fd7..8c4274302 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -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] diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index 17e00513f..b706798e2 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -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, )),