From 73251b2f00b3d9bda142f622fc11bde5af793bdd Mon Sep 17 00:00:00 2001 From: rka-oai Date: Thu, 18 Jun 2026 18:46:57 -0700 Subject: [PATCH] [codex] add clock current-time tool (#29011) ## Summary - expose `clock.curr_time` when current-time reminders are enabled - query the session's configured time provider with the calling thread id - return the existing UTC reminder text for direct model calls - return `{ "current_time": "YYYY-MM-DD HH:MM:SS UTC" }` in Code Mode Clock lookup failures remain fatal, matching pre-inference reminder behavior. ## Testing - `just test -p codex-core current_time_tool_returns_the_latest_time` - `just test -p codex-core code_mode_current_time_returns_structured_result` - `just fix -p codex-core` --- .../core/src/context/current_time_reminder.rs | 11 +- .../core/src/tools/handlers/current_time.rs | 108 ++++++++++++++++++ codex-rs/core/src/tools/handlers/mod.rs | 2 + codex-rs/core/src/tools/spec_plan.rs | 5 + codex-rs/core/tests/suite/code_mode.rs | 45 ++++++++ .../core/tests/suite/current_time_reminder.rs | 43 +++++++ 6 files changed, 210 insertions(+), 4 deletions(-) create mode 100644 codex-rs/core/src/tools/handlers/current_time.rs diff --git a/codex-rs/core/src/context/current_time_reminder.rs b/codex-rs/core/src/context/current_time_reminder.rs index 0e3fe0b1e..0baf56b28 100644 --- a/codex-rs/core/src/context/current_time_reminder.rs +++ b/codex-rs/core/src/context/current_time_reminder.rs @@ -11,6 +11,12 @@ impl CurrentTimeReminder { pub(crate) fn new(current_time: DateTime) -> Self { Self { current_time } } + + pub(crate) fn formatted_time(&self) -> String { + self.current_time + .format("%Y-%m-%d %H:%M:%S UTC") + .to_string() + } } impl ContextualUserFragment for CurrentTimeReminder { @@ -27,9 +33,6 @@ impl ContextualUserFragment for CurrentTimeReminder { } fn body(&self) -> String { - format!( - "It is {}.", - self.current_time.format("%Y-%m-%d %H:%M:%S UTC") - ) + format!("It is {}.", self.formatted_time()) } } diff --git a/codex-rs/core/src/tools/handlers/current_time.rs b/codex-rs/core/src/tools/handlers/current_time.rs new file mode 100644 index 000000000..73873ca74 --- /dev/null +++ b/codex-rs/core/src/tools/handlers/current_time.rs @@ -0,0 +1,108 @@ +use crate::context::ContextualUserFragment; +use crate::context::CurrentTimeReminder; +use crate::function_tool::FunctionCallError; +use crate::tools::context::FunctionToolOutput; +use crate::tools::context::ToolInvocation; +use crate::tools::context::ToolOutput; +use crate::tools::context::ToolPayload; +use crate::tools::context::boxed_tool_output; +use crate::tools::registry::CoreToolRuntime; +use crate::tools::registry::ToolExecutor; +use codex_protocol::models::ResponseInputItem; +use codex_tools::JsonSchema; +use codex_tools::ResponsesApiNamespace; +use codex_tools::ResponsesApiNamespaceTool; +use codex_tools::ResponsesApiTool; +use codex_tools::ToolName; +use codex_tools::ToolSpec; +use serde_json::Value as JsonValue; +use serde_json::json; +use std::collections::BTreeMap; + +const NAMESPACE: &str = "clock"; +const TOOL_NAME: &str = "curr_time"; + +struct CurrentTimeOutput(CurrentTimeReminder); + +impl ToolOutput for CurrentTimeOutput { + fn log_preview(&self) -> String { + self.0.render() + } + + fn success_for_logging(&self) -> bool { + true + } + + fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem { + FunctionToolOutput::from_text(self.0.render(), Some(true)) + .to_response_item(call_id, payload) + } + + fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue { + json!({ + "current_time": self.0.formatted_time(), + }) + } +} + +pub struct CurrentTimeHandler; + +impl ToolExecutor for CurrentTimeHandler { + fn tool_name(&self) -> ToolName { + ToolName::namespaced(NAMESPACE, TOOL_NAME) + } + + fn spec(&self) -> ToolSpec { + ToolSpec::Namespace(ResponsesApiNamespace { + name: NAMESPACE.to_string(), + description: "Tools for reading the current time.".to_string(), + tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool { + name: TOOL_NAME.to_string(), + description: "Return the current time in UTC.".to_string(), + strict: false, + defer_loading: None, + parameters: JsonSchema::object( + BTreeMap::new(), + /*required*/ None, + /*additional_properties*/ Some(false.into()), + ), + output_schema: Some(json!({ + "type": "object", + "properties": { + "current_time": { + "type": "string", + "description": "Current UTC time formatted as YYYY-MM-DD HH:MM:SS UTC." + } + }, + "required": ["current_time"], + "additionalProperties": false + })), + })], + }) + } + + fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> { + Box::pin(async move { + if !matches!(invocation.payload, ToolPayload::Function { .. }) { + return Err(FunctionCallError::RespondToModel(format!( + "{TOOL_NAME} handler received unsupported payload" + ))); + } + + let current_time = invocation + .session + .services + .time_provider + .current_time(invocation.session.thread_id) + .await + .map_err(|err| { + FunctionCallError::Fatal(format!("failed to read current time: {err:#}")) + })?; + Ok(boxed_tool_output(CurrentTimeOutput( + CurrentTimeReminder::new(current_time), + ))) + }) + } +} + +impl CoreToolRuntime for CurrentTimeHandler {} diff --git a/codex-rs/core/src/tools/handlers/mod.rs b/codex-rs/core/src/tools/handlers/mod.rs index d9c338e82..d1e931aa9 100644 --- a/codex-rs/core/src/tools/handlers/mod.rs +++ b/codex-rs/core/src/tools/handlers/mod.rs @@ -2,6 +2,7 @@ pub(crate) mod agent_jobs; pub(crate) mod agent_jobs_spec; pub(crate) mod apply_patch; pub(crate) mod apply_patch_spec; +mod current_time; mod dynamic; pub(crate) mod extension_tools; mod get_context_remaining; @@ -55,6 +56,7 @@ pub(crate) use crate::tools::code_mode::CodeModeWaitHandler; pub use apply_patch::ApplyPatchHandler; use codex_protocol::models::AdditionalPermissionProfile; use codex_protocol::protocol::AskForApproval; +pub use current_time::CurrentTimeHandler; pub use dynamic::DynamicToolHandler; pub use get_context_remaining::GetContextRemainingHandler; pub use list_available_plugins_to_install::ListAvailablePluginsToInstallHandler; diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index 660f3f418..af7acf17a 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -7,6 +7,7 @@ use crate::tools::effective_tool_mode; use crate::tools::handlers::ApplyPatchHandler; use crate::tools::handlers::CodeModeExecuteHandler; use crate::tools::handlers::CodeModeWaitHandler; +use crate::tools::handlers::CurrentTimeHandler; use crate::tools::handlers::DynamicToolHandler; use crate::tools::handlers::ExecCommandHandler; use crate::tools::handlers::ExecCommandHandlerOptions; @@ -711,6 +712,10 @@ fn add_core_utility_tools(context: &CoreToolPlanContext<'_>, planned_tools: &mut planned_tools.add(GetContextRemainingHandler); } + if features.enabled(Feature::CurrentTimeReminder) { + planned_tools.add(CurrentTimeHandler); + } + if features.enabled(Feature::SleepTool) { planned_tools.add(SleepHandler); } diff --git a/codex-rs/core/tests/suite/code_mode.rs b/codex-rs/core/tests/suite/code_mode.rs index 60b476d61..0ce33cc69 100644 --- a/codex-rs/core/tests/suite/code_mode.rs +++ b/codex-rs/core/tests/suite/code_mode.rs @@ -6,7 +6,9 @@ use base64::engine::general_purpose::STANDARD as BASE64_STANDARD; use codex_config::types::McpServerConfig; use codex_config::types::McpServerTransportConfig; use codex_core::config::Config; +use codex_core::config::CurrentTimeReminderConfig; use codex_extension_api::ExtensionRegistryBuilder; +use codex_features::CurrentTimeSource; use codex_features::Feature; use codex_login::CodexAuth; use codex_models_manager::bundled_models_response; @@ -848,6 +850,49 @@ text(JSON.stringify(result)); Ok(()) } +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn code_mode_current_time_returns_structured_result() -> Result<()> { + skip_if_no_network!(Ok(())); + + let server = responses::start_mock_server().await; + let (_test, second_mock) = run_code_mode_turn_with_config( + &server, + "use exec to get the current time", + r#" +const result = await tools.clock__curr_time({}); +text(JSON.stringify(result)); +"#, + |config| { + config + .features + .enable(Feature::CurrentTimeReminder) + .expect("test config should allow current-time reminders"); + config.current_time_reminder = Some(CurrentTimeReminderConfig { + reminder_interval_model_requests: 50, + clock_source: CurrentTimeSource::System, + }); + }, + ) + .await?; + + let req = second_mock.single_request(); + let (output, success) = custom_tool_output_body_and_success(&req, "call-1"); + assert_ne!( + success, + Some(false), + "exec clock.curr_time call failed unexpectedly: {output}" + ); + + let parsed: Value = serde_json::from_str(&output)?; + let current_time = parsed + .get("current_time") + .and_then(Value::as_str) + .expect("clock.curr_time should return current_time"); + assert_regex_match(r"^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} UTC$", current_time); + + Ok(()) +} + #[cfg_attr(windows, ignore = "flaky on windows")] #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn code_mode_nested_tool_calls_can_run_in_parallel() -> Result<()> { diff --git a/codex-rs/core/tests/suite/current_time_reminder.rs b/codex-rs/core/tests/suite/current_time_reminder.rs index 5b5970a0e..fda9b85b0 100644 --- a/codex-rs/core/tests/suite/current_time_reminder.rs +++ b/codex-rs/core/tests/suite/current_time_reminder.rs @@ -23,6 +23,7 @@ use core_test_support::responses::ResponsesRequest; use core_test_support::responses::ev_assistant_message; use core_test_support::responses::ev_completed; use core_test_support::responses::ev_function_call; +use core_test_support::responses::ev_function_call_with_namespace; use core_test_support::responses::ev_response_created; use core_test_support::responses::mount_sse_once; use core_test_support::responses::mount_sse_sequence; @@ -272,3 +273,45 @@ async fn time_provider_failure_stops_before_inference() -> Result<()> { Ok(()) } + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn current_time_tool_returns_the_latest_time() -> Result<()> { + skip_if_no_network!(Ok(())); + + const CALL_ID: &str = "current-time"; + + let server = start_mock_server().await; + let responses = mount_sse_sequence( + &server, + vec![ + sse(vec![ + ev_response_created("resp-1"), + ev_function_call_with_namespace(CALL_ID, "clock", "curr_time", "{}"), + ev_completed("resp-1"), + ]), + sse(vec![ev_response_created("resp-2"), ev_completed("resp-2")]), + ], + ) + .await; + let test = test_codex() + .with_config(|config| { + enable_current_time_reminder(config, /*interval*/ 50, CurrentTimeSource::External) + }) + .with_external_time_provider(Arc::new(TestTimeProvider::default())) + .build(&server) + .await?; + + test.submit_turn("check the current time").await?; + + let requests = responses.requests(); + assert!( + requests[0].tool_by_name("clock", "curr_time").is_some(), + "clock.curr_time should be exposed when current-time reminders are enabled" + ); + assert_eq!( + requests[1].function_call_output_text(CALL_ID), + Some(SECOND_REMINDER.to_string()) + ); + + Ok(()) +}