Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions codex-rs/core/src/context/current_time_reminder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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())
}
}
108 changes: 108 additions & 0 deletions codex-rs/core/src/tools/handlers/current_time.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions codex-rs/core/src/tools/handlers/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions codex-rs/core/src/tools/spec_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
45 changes: 45 additions & 0 deletions codex-rs/core/tests/suite/code_mode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<()> {
Expand Down
43 changes: 43 additions & 0 deletions codex-rs/core/tests/suite/current_time_reminder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(())
}
Loading