Codex Handbook
core/src/tools/code_mode/wait_handler.rs 153 lines
use serde::Deserialize;use crate::function_tool::FunctionCallError;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::PostToolUsePayload;use crate::tools::registry::PreToolUsePayload;use crate::tools::registry::ToolExecutor;use codex_tools::ToolName;use codex_tools::ToolSpec;use super::DEFAULT_WAIT_YIELD_TIME_MS;use super::ExecContext;use super::WAIT_TOOL_NAME;use super::handle_runtime_response;use super::wait_spec::create_wait_tool;pub struct CodeModeWaitHandler;#[derive(Debug, Deserialize)]struct ExecWaitArgs {    cell_id: String,    #[serde(default = "default_wait_yield_time_ms")]    yield_time_ms: u64,    #[serde(default)]    max_tokens: Option<usize>,    #[serde(default)]    terminate: bool,}fn default_wait_yield_time_ms() -> u64 {    DEFAULT_WAIT_YIELD_TIME_MS}fn parse_arguments<T>(arguments: &str) -> Result<T, FunctionCallError>where    T: for<'de> Deserialize<'de>,{    serde_json::from_str(arguments).map_err(|err| {        FunctionCallError::RespondToModel(format!("failed to parse function arguments: {err}"))    })}impl ToolExecutor<ToolInvocation> for CodeModeWaitHandler {    fn tool_name(&self) -> ToolName {        ToolName::plain(WAIT_TOOL_NAME)    }    fn spec(&self) -> ToolSpec {        create_wait_tool()    }    fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> {        Box::pin(self.handle_call(invocation))    }}impl CodeModeWaitHandler {    async fn handle_call(        &self,        invocation: ToolInvocation,    ) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> {        let ToolInvocation {            session,            turn,            tool_name,            payload,            ..        } = invocation;        match payload {            ToolPayload::Function { arguments }                if tool_name.namespace.is_none() && tool_name.name.as_str() == WAIT_TOOL_NAME =>            {                let args: ExecWaitArgs = parse_arguments(&arguments)?;                let exec = ExecContext { session, turn };                let started_at = std::time::Instant::now();                let cell_id = codex_code_mode::CellId::new(args.cell_id);                let wait_response = if args.terminate {                    exec.session                        .services                        .code_mode_service                        .terminate(cell_id)                        .await                } else {                    exec.session                        .services                        .code_mode_service                        .wait(codex_code_mode::WaitRequest {                            cell_id,                            yield_time_ms: args.yield_time_ms,                        })                        .await                }                .map_err(FunctionCallError::RespondToModel)?;                if let codex_code_mode::WaitOutcome::LiveCell(response) = &wait_response                    && !matches!(response, codex_code_mode::RuntimeResponse::Yielded { .. })                {                    // Only a live-cell wait can close a CodeCell. A missing                    // cell is still an ordinary `wait` tool result, but there                    // is no runtime object for the reducer to complete.                    let runtime_cell_id = match response {                        codex_code_mode::RuntimeResponse::Yielded { cell_id, .. }                        | codex_code_mode::RuntimeResponse::Terminated { cell_id, .. }                        | codex_code_mode::RuntimeResponse::Result { cell_id, .. } => cell_id,                    };                    exec.session                        .services                        .rollout_thread_trace                        .code_cell_trace_context(                            exec.turn.sub_id.as_str(),                            runtime_cell_id.as_str(),                        )                        .record_ended(response);                    exec.session                        .services                        .code_mode_service                        .finish_cell_dispatch(runtime_cell_id);                }                handle_runtime_response(&exec, wait_response.into(), args.max_tokens, started_at)                    .await                    .map(boxed_tool_output)                    .map_err(FunctionCallError::RespondToModel)            }            _ => Err(FunctionCallError::RespondToModel(format!(                "{WAIT_TOOL_NAME} expects JSON arguments"            ))),        }    }}impl CoreToolRuntime for CodeModeWaitHandler {    fn pre_tool_use_payload(&self, _invocation: &ToolInvocation) -> Option<PreToolUsePayload> {        // Code-mode `wait` is runtime control for an existing code cell, not a        // standalone user action. Tool calls made from code mode still flow        // through normal dispatch, but hooks should not block or rewrite the        // wait loop itself.        None    }    fn post_tool_use_payload(        &self,        _invocation: &ToolInvocation,        _result: &dyn ToolOutput,    ) -> Option<PostToolUsePayload> {        // The wait result feeds code-mode control flow, so do not let        // PostToolUse replace it with model-facing hook feedback.        None    }}