core/src/tools/handlers/sleep.rs
131 lines
use crate::function_tool::FunctionCallError;use crate::tools::context::FunctionToolOutput;use crate::tools::context::ToolInvocation;use crate::tools::context::ToolPayload;use crate::tools::context::boxed_tool_output;use crate::tools::handlers::parse_arguments;use crate::tools::registry::CoreToolRuntime;use crate::tools::registry::ToolExecutor;use codex_protocol::items::SleepItem;use codex_protocol::items::TurnItem;use codex_tools::JsonSchema;use codex_tools::ResponsesApiTool;use codex_tools::ToolName;use codex_tools::ToolSpec;use serde::Deserialize;use std::collections::BTreeMap;use std::time::Duration;use std::time::Instant;const SLEEP_TOOL_NAME: &str = "sleep";const MAX_SLEEP_DURATION_MS: u64 = 3_600_000;pub struct SleepHandler;#[derive(Debug, Deserialize)]#[serde(deny_unknown_fields)]struct SleepArgs { duration_ms: u64,}fn create_sleep_tool() -> ToolSpec { let properties = BTreeMap::from([( "duration_ms".to_string(), JsonSchema::number(Some(format!( "How long to sleep in milliseconds. Must be between 1 and {MAX_SLEEP_DURATION_MS}." ))), )]); 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, })}impl ToolExecutor<ToolInvocation> for SleepHandler { fn tool_name(&self) -> ToolName { ToolName::plain(SLEEP_TOOL_NAME) } fn spec(&self) -> ToolSpec { create_sleep_tool() } fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> { Box::pin(async move { let ToolInvocation { session, turn, call_id, payload, .. } = invocation; let ToolPayload::Function { arguments } = payload else { return Err(FunctionCallError::RespondToModel(format!( "{SLEEP_TOOL_NAME} handler received unsupported payload" ))); }; let args: SleepArgs = parse_arguments(&arguments)?; if !(1..=MAX_SLEEP_DURATION_MS).contains(&args.duration_ms) { return Err(FunctionCallError::RespondToModel(format!( "duration_ms must be between 1 and {MAX_SLEEP_DURATION_MS}" ))); } let started = Instant::now(); let item = TurnItem::Sleep(SleepItem { id: call_id, duration_ms: args.duration_ms, }); session.emit_turn_item_started(turn.as_ref(), &item).await; let turn_state = session .input_queue .turn_state_for_sub_id(&session.active_turn, &turn.sub_id) .await; let (mut activity_rx, pending_activity) = session .input_queue .subscribe_activity(turn_state.as_deref()) .await; let interrupted = if pending_activity.is_some() { true } else { let sleep = tokio::time::sleep(Duration::from_millis(args.duration_ms)); tokio::pin!(sleep); tokio::select! { () = &mut sleep => false, result = activity_rx.changed() => { if result.is_ok() { true } else { sleep.await; false } } } }; session.emit_turn_item_completed(turn.as_ref(), item).await; let message = if interrupted { "Sleep interrupted by new input." } else { "Sleep completed." }; let wall_time_seconds = started.elapsed().as_secs_f64(); Ok(boxed_tool_output(FunctionToolOutput::from_text( format!("Wall time: {wall_time_seconds:.4} seconds\n{message}"), /*success*/ Some(true), ))) }) }}impl CoreToolRuntime for SleepHandler {}