mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
[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:
committed by
GitHub
Unverified
parent
38c96866f0
commit
73251b2f00
@@ -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,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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user