//! Hot-path helpers for recording canonical tool dispatch boundaries.//!//! Core owns tool routing and result conversion. The trace crate owns the raw//! event schema, payload shape, and no-op behavior, so core only adapts its//! domain objects into the small request/result structs defined here.use std::fmt::Display;use std::sync::Arc;use codex_protocol::models::AdditionalPermissionProfile;use codex_protocol::models::ResponseInputItem;use codex_protocol::models::SandboxPermissions;use codex_protocol::models::SearchToolCallParams;use serde::Serialize;use serde_json::Value as JsonValue;use serde_json::json;use tracing::warn;use crate::model::AgentThreadId;use crate::model::CodeModeRuntimeToolId;use crate::model::CodexTurnId;use crate::model::ExecutionStatus;use crate::model::ModelVisibleCallId;use crate::model::ToolCallId;use crate::model::ToolCallKind;use crate::model::ToolCallSummary;use crate::payload::RawPayloadKind;use crate::payload::RawPayloadRef;use crate::raw_event::RawToolCallRequester;use crate::raw_event::RawTraceEventContext;use crate::raw_event::RawTraceEventPayload;use crate::writer::TraceWriter;/// No-op capable trace handle for one resolved tool dispatch.#[derive(Clone, Debug)]pub struct ToolDispatchTraceContext { state: ToolDispatchTraceContextState,}#[derive(Clone, Debug)]enum ToolDispatchTraceContextState { Disabled, Enabled(EnabledToolDispatchTraceContext),}#[derive(Clone, Debug)]struct EnabledToolDispatchTraceContext { writer: Arc<TraceWriter>, thread_id: AgentThreadId, codex_turn_id: CodexTurnId, tool_call_id: ToolCallId,}/// Core-facing request data for the canonical Codex tool boundary.pub struct ToolDispatchInvocation { pub thread_id: AgentThreadId, pub codex_turn_id: CodexTurnId, pub tool_call_id: ToolCallId, pub tool_name: String, pub tool_namespace: Option<String>, pub requester: ToolDispatchRequester, pub payload: ToolDispatchPayload,}/// Runtime source that caused a dispatch-level tool call.pub enum ToolDispatchRequester { Model { model_visible_call_id: ModelVisibleCallId, }, CodeCell { runtime_cell_id: String, runtime_tool_call_id: CodeModeRuntimeToolId, },}/// Tool input observed at the registry boundary.pub enum ToolDispatchPayload { Function { arguments: String, }, ToolSearch { arguments: SearchToolCallParams, }, Custom { input: String, }, LocalShell { command: Vec<String>, workdir: Option<String>, timeout_ms: Option<u64>, sandbox_permissions: Option<SandboxPermissions>, prefix_rule: Option<Vec<String>>, additional_permissions: Option<AdditionalPermissionProfile>, justification: Option<String>, },}/// Result data returned from a dispatch-level tool call.#[derive(Serialize)]#[serde(rename_all = "snake_case", tag = "type")]pub enum ToolDispatchResult { DirectResponse { response_item: ResponseInputItem }, CodeModeResponse { value: JsonValue },}/// Raw invocation payload for the canonical Codex tool boundary.#[derive(Serialize)]struct DispatchedToolTraceRequest<'a> { tool_name: &'a str, tool_namespace: Option<&'a str>, payload: &'a JsonValue,}/// Raw response payload for dispatch-level tool trace events.#[derive(Serialize)]#[serde(rename_all = "snake_case", tag = "type")]enum DispatchedToolTraceResponse<'a> { DirectResponse { response_item: &'a ResponseInputItem, }, CodeModeResponse { value: &'a JsonValue, }, Error { error: String, },}impl ToolDispatchTraceContext { /// Builds a context that accepts trace calls and records nothing. pub(crate) fn disabled() -> Self { Self { state: ToolDispatchTraceContextState::Disabled, } } /// Returns whether caller-side result conversion would be recorded. /// /// Core uses this to avoid formatting or cloning tool outputs when the /// dispatch lifecycle is suppressed or tracing is disabled. pub fn is_enabled(&self) -> bool { matches!(self.state, ToolDispatchTraceContextState::Enabled(_)) } /// Starts one dispatch-level lifecycle and returns the handle for its result. pub(crate) fn start(writer: Arc<TraceWriter>, invocation: ToolDispatchInvocation) -> Self { if suppresses_tool_dispatch_trace(&invocation) { return Self::disabled(); } let context = EnabledToolDispatchTraceContext { writer, thread_id: invocation.thread_id.clone(), codex_turn_id: invocation.codex_turn_id.clone(), tool_call_id: invocation.tool_call_id.clone(), }; record_started(&context, invocation); Self { state: ToolDispatchTraceContextState::Enabled(context), } } /// Records the caller-facing successful or failed tool result. pub fn record_completed(&self, status: ExecutionStatus, result: ToolDispatchResult) { let ToolDispatchTraceContextState::Enabled(context) = &self.state else { return; }; let response = match &result { ToolDispatchResult::DirectResponse { response_item } => { DispatchedToolTraceResponse::DirectResponse { response_item } } ToolDispatchResult::CodeModeResponse { value } => { DispatchedToolTraceResponse::CodeModeResponse { value } } }; append_tool_call_ended(context, status, &response); } /// Records a dispatch failure before the tool produced a normal result payload. pub fn record_failed(&self, error: impl Display) { let ToolDispatchTraceContextState::Enabled(context) = &self.state else { return; }; append_tool_call_ended( context, ExecutionStatus::Failed, &DispatchedToolTraceResponse::Error { error: error.to_string(), }, ); }}fn suppresses_tool_dispatch_trace(invocation: &ToolDispatchInvocation) -> bool { matches!(invocation.payload, ToolDispatchPayload::Custom { .. }) && invocation.tool_namespace.is_none() && invocation.tool_name == codex_code_mode::PUBLIC_TOOL_NAME}fn record_started(context: &EnabledToolDispatchTraceContext, invocation: ToolDispatchInvocation) { let tool_name = invocation.tool_name; let tool_namespace = invocation.tool_namespace; let kind = dispatched_tool_kind(&tool_name, &invocation.payload); let label = dispatched_tool_label(&tool_name, tool_namespace.as_deref(), &invocation.payload); let input_preview = Some(invocation.payload.log_payload_preview()); let payload = invocation.payload.into_json_payload(); let request = DispatchedToolTraceRequest { tool_name: tool_name.as_str(), tool_namespace: tool_namespace.as_deref(), payload: &payload, }; let request_payload = write_json_payload_best_effort(&context.writer, RawPayloadKind::ToolInvocation, &request); let (model_visible_call_id, code_mode_runtime_tool_id, requester) = requester_fields(invocation.requester); append_with_context_best_effort( context, RawTraceEventPayload::ToolCallStarted { tool_call_id: context.tool_call_id.clone(), model_visible_call_id, code_mode_runtime_tool_id, requester, kind, summary: ToolCallSummary::Generic { label, input_preview, output_preview: None, }, invocation_payload: request_payload, }, );}fn requester_fields( requester: ToolDispatchRequester,) -> ( Option<ModelVisibleCallId>, Option<CodeModeRuntimeToolId>, RawToolCallRequester,) { match requester { ToolDispatchRequester::Model { model_visible_call_id, } => ( Some(model_visible_call_id), None, RawToolCallRequester::Model, ), ToolDispatchRequester::CodeCell { runtime_cell_id, runtime_tool_call_id, } => ( None, Some(runtime_tool_call_id), RawToolCallRequester::CodeCell { runtime_cell_id }, ), }}fn dispatched_tool_kind(tool_name: &str, _payload: &ToolDispatchPayload) -> ToolCallKind { match tool_name { "exec_command" | "local_shell" | "shell" | "shell_command" => ToolCallKind::ExecCommand, "write_stdin" => ToolCallKind::WriteStdin, "apply_patch" => ToolCallKind::ApplyPatch, "web_search" | "web_search_preview" => ToolCallKind::Web, "image_generation" | "image_query" => ToolCallKind::ImageGeneration, "spawn_agent" => ToolCallKind::SpawnAgent, "send_message" => ToolCallKind::SendMessage, "followup_task" | "assign_task" => ToolCallKind::AssignAgentTask, "wait_agent" => ToolCallKind::WaitAgent, "close_agent" | "interrupt_agent" => ToolCallKind::CloseAgent, other => ToolCallKind::Other { name: other.to_string(), }, }}fn dispatched_tool_label( tool_name: &str, tool_namespace: Option<&str>, _payload: &ToolDispatchPayload,) -> String { match tool_namespace { Some(namespace) => format!("{namespace}.{tool_name}"), None => tool_name.to_string(), }}impl ToolDispatchPayload { fn log_payload_preview(&self) -> String { match self { ToolDispatchPayload::Function { arguments } => truncate_preview(arguments), ToolDispatchPayload::ToolSearch { arguments } => truncate_preview(&arguments.query), ToolDispatchPayload::Custom { input } => truncate_preview(input), ToolDispatchPayload::LocalShell { command, .. } => truncate_preview(&command.join(" ")), } } fn into_json_payload(self) -> JsonValue { match self { ToolDispatchPayload::Function { arguments } => json!({ "type": "function", "arguments": arguments, }), ToolDispatchPayload::ToolSearch { arguments } => json!({ "type": "tool_search", "arguments": arguments, }), ToolDispatchPayload::Custom { input } => json!({ "type": "custom", "input": input, }), ToolDispatchPayload::LocalShell { command, workdir, timeout_ms, sandbox_permissions, prefix_rule, additional_permissions, justification, } => json!({ "type": "local_shell", "command": command, "workdir": workdir, "timeout_ms": timeout_ms, "sandbox_permissions": sandbox_permissions, "prefix_rule": prefix_rule, "additional_permissions": additional_permissions, "justification": justification, }), } }}fn truncate_preview(value: &str) -> String { const MAX_PREVIEW_CHARS: usize = 160; let mut chars = value.chars(); let mut preview = chars.by_ref().take(MAX_PREVIEW_CHARS).collect::<String>(); if chars.next().is_some() { preview.push_str("..."); } preview}fn append_tool_call_ended( context: &EnabledToolDispatchTraceContext, status: ExecutionStatus, response: &DispatchedToolTraceResponse<'_>,) { let response_payload = write_json_payload_best_effort(&context.writer, RawPayloadKind::ToolResult, response); append_with_context_best_effort( context, RawTraceEventPayload::ToolCallEnded { tool_call_id: context.tool_call_id.clone(), status, result_payload: response_payload, }, );}fn write_json_payload_best_effort( writer: &TraceWriter, kind: RawPayloadKind, payload: &impl Serialize,) -> Option<RawPayloadRef> { match writer.write_json_payload(kind, payload) { Ok(payload_ref) => Some(payload_ref), Err(err) => { warn!("failed to write rollout trace payload: {err:#}"); None } }}fn append_with_context_best_effort( context: &EnabledToolDispatchTraceContext, payload: RawTraceEventPayload,) { let event_context = RawTraceEventContext { thread_id: Some(context.thread_id.clone()), codex_turn_id: Some(context.codex_turn_id.clone()), }; if let Err(err) = context.writer.append_with_context(event_context, payload) { warn!("failed to append rollout trace event: {err:#}"); }}#[cfg(test)]mod tests { use super::*; #[test] fn suppresses_only_noncanonical_dispatch_boundaries() { assert!(suppresses_tool_dispatch_trace(&invocation( codex_code_mode::PUBLIC_TOOL_NAME, /*tool_namespace*/ None, ToolDispatchRequester::Model { model_visible_call_id: "call-exec".to_string(), }, ToolDispatchPayload::Custom { input: "1 + 1".to_string(), }, ))); assert!(!suppresses_tool_dispatch_trace(&invocation( "custom_tool", /*tool_namespace*/ None, ToolDispatchRequester::Model { model_visible_call_id: "call-custom".to_string(), }, ToolDispatchPayload::Custom { input: "payload".to_string(), }, ))); assert!(!suppresses_tool_dispatch_trace(&invocation( codex_code_mode::PUBLIC_TOOL_NAME, Some("mcp__server".to_string()), ToolDispatchRequester::Model { model_visible_call_id: "call-namespaced".to_string(), }, ToolDispatchPayload::Custom { input: "payload".to_string(), }, ))); } #[test] fn classifies_interrupt_agent_as_close_agent() { assert_eq!( dispatched_tool_kind( "interrupt_agent", &ToolDispatchPayload::Function { arguments: r#"{"target":"/root/child"}"#.to_string(), }, ), ToolCallKind::CloseAgent ); } fn invocation( tool_name: &str, tool_namespace: Option<String>, requester: ToolDispatchRequester, payload: ToolDispatchPayload, ) -> ToolDispatchInvocation { ToolDispatchInvocation { thread_id: "thread-1".to_string(), codex_turn_id: "turn-1".to_string(), tool_call_id: "tool-call-1".to_string(), tool_name: tool_name.to_string(), tool_namespace, requester, payload, } }}