[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`
This commit is contained in:
rka-oai
2026-06-18 18:46:57 -07:00
committed by GitHub
Unverified
parent 38c96866f0
commit 73251b2f00
6 changed files with 210 additions and 4 deletions
@@ -11,6 +11,12 @@ impl CurrentTimeReminder {
pub(crate) fn new(current_time: DateTime<Utc>) -> 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())
}
}
@@ -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<ToolInvocation> 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 {}
+2
View File
@@ -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;
+5
View File
@@ -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);
}
+45
View File
@@ -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<()> {
@@ -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(())
}