Codex Handbook
core/src/tools/handlers/dynamic.rs 238 lines
use crate::function_tool::FunctionCallError;use crate::session::session::Session;use crate::session::turn_context::TurnContext;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 crate::tools::registry::ToolExposure;use crate::turn_timing::now_unix_timestamp_ms;use codex_protocol::dynamic_tools::DynamicToolCallRequest;use codex_protocol::dynamic_tools::DynamicToolFunctionSpec;use codex_protocol::dynamic_tools::DynamicToolNamespaceSpec;use codex_protocol::dynamic_tools::DynamicToolResponse;use codex_protocol::models::FunctionCallOutputContentItem;use codex_protocol::protocol::DynamicToolCallResponseEvent;use codex_protocol::protocol::EventMsg;use codex_tools::ResponsesApiNamespace;use codex_tools::ResponsesApiNamespaceTool;use codex_tools::ToolName;use codex_tools::ToolSearchInfo;use codex_tools::ToolSearchSourceInfo;use codex_tools::ToolSpec;use codex_tools::default_namespace_description;use codex_tools::dynamic_tool_to_responses_api_tool;use serde_json::Value;use std::time::Instant;use tokio::sync::oneshot;use tracing::warn;pub struct DynamicToolHandler {    tool_name: ToolName,    spec: ToolSpec,    exposure: ToolExposure,}impl DynamicToolHandler {    pub fn new(tool: &DynamicToolFunctionSpec) -> Option<Self> {        Self::from_parts(tool, /*namespace*/ None)    }    pub fn new_in_namespace(        namespace: &DynamicToolNamespaceSpec,        tool: &DynamicToolFunctionSpec,    ) -> Option<Self> {        Self::from_parts(tool, Some(namespace))    }    fn from_parts(        tool: &DynamicToolFunctionSpec,        namespace: Option<&DynamicToolNamespaceSpec>,    ) -> Option<Self> {        let tool_name = ToolName::new(            namespace.map(|namespace| namespace.name.clone()),            tool.name.clone(),        );        let output_tool = dynamic_tool_to_responses_api_tool(tool).ok()?;        let spec = match namespace {            Some(namespace) => ToolSpec::Namespace(ResponsesApiNamespace {                name: namespace.name.clone(),                description: if namespace.description.trim().is_empty() {                    default_namespace_description(&namespace.name)                } else {                    namespace.description.clone()                },                tools: vec![ResponsesApiNamespaceTool::Function(output_tool)],            }),            None => ToolSpec::Function(output_tool),        };        Some(Self {            tool_name,            spec,            exposure: if tool.defer_loading {                ToolExposure::Deferred            } else {                ToolExposure::Direct            },        })    }}impl ToolExecutor<ToolInvocation> for DynamicToolHandler {    fn tool_name(&self) -> ToolName {        self.tool_name.clone()    }    fn spec(&self) -> ToolSpec {        self.spec.clone()    }    fn exposure(&self) -> ToolExposure {        self.exposure    }    fn search_info(&self) -> Option<ToolSearchInfo> {        ToolSearchInfo::from_tool_spec(            self.spec(),            Some(ToolSearchSourceInfo {                name: "Dynamic tools".to_string(),                description: Some("Tools provided by the current Codex thread.".to_string()),            }),        )    }    fn handle(&self, invocation: ToolInvocation) -> codex_tools::ToolExecutorFuture<'_> {        Box::pin(self.handle_call(invocation))    }}impl DynamicToolHandler {    async fn handle_call(        &self,        invocation: ToolInvocation,    ) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> {        let ToolInvocation {            session,            turn,            call_id,            payload,            ..        } = invocation;        let arguments = match payload {            ToolPayload::Function { arguments } => arguments,            _ => {                return Err(FunctionCallError::RespondToModel(                    "dynamic tool handler received unsupported payload".to_string(),                ));            }        };        let args: Value = parse_arguments(&arguments)?;        let response = request_dynamic_tool(            &session,            turn.as_ref(),            call_id,            self.tool_name.clone(),            args,        )        .await        .ok_or_else(|| {            FunctionCallError::RespondToModel(                "dynamic tool call was cancelled before receiving a response".to_string(),            )        })?;        let DynamicToolResponse {            content_items,            success,        } = response;        let body = content_items            .into_iter()            .map(FunctionCallOutputContentItem::from)            .collect::<Vec<_>>();        Ok(boxed_tool_output(FunctionToolOutput::from_content(            body,            Some(success),        )))    }}impl CoreToolRuntime for DynamicToolHandler {}#[expect(    clippy::await_holding_invalid_type,    reason = "active turn checks and dynamic tool response registration must remain atomic")]async fn request_dynamic_tool(    session: &Session,    turn_context: &TurnContext,    call_id: String,    tool_name: ToolName,    arguments: Value,) -> Option<DynamicToolResponse> {    let namespace = tool_name.namespace;    let tool = tool_name.name;    let turn_id = turn_context.sub_id.clone();    let (tx_response, rx_response) = oneshot::channel();    let event_id = call_id.clone();    let prev_entry = {        let mut active = session.active_turn.lock().await;        match active.as_mut() {            Some(at) => {                let mut ts = at.turn_state.lock().await;                ts.insert_pending_dynamic_tool(call_id.clone(), tx_response)            }            None => None,        }    };    if prev_entry.is_some() {        warn!("Overwriting existing pending dynamic tool call for call_id: {event_id}");    }    let started_at = Instant::now();    let started_at_ms = now_unix_timestamp_ms();    let event = EventMsg::DynamicToolCallRequest(DynamicToolCallRequest {        call_id: call_id.clone(),        turn_id: turn_id.clone(),        started_at_ms,        namespace: namespace.clone(),        tool: tool.clone(),        arguments: arguments.clone(),    });    session.send_event(turn_context, event).await;    let response = rx_response.await.ok();    let response_event = match &response {        Some(response) => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {            call_id,            turn_id,            completed_at_ms: now_unix_timestamp_ms(),            namespace,            tool,            arguments,            content_items: response.content_items.clone(),            success: response.success,            error: None,            duration: started_at.elapsed(),        }),        None => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {            call_id,            turn_id,            completed_at_ms: now_unix_timestamp_ms(),            namespace,            tool,            arguments,            content_items: Vec::new(),            success: false,            error: Some("dynamic tool call was cancelled before receiving a response".to_string()),            duration: started_at.elapsed(),        }),    };    session.send_event(turn_context, response_event).await;    response}