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::tools::tool_search_entry::ToolSearchInfo; use crate::turn_timing::now_unix_timestamp_ms; use codex_protocol::dynamic_tools::DynamicToolCallRequest; use codex_protocol::dynamic_tools::DynamicToolResponse; use codex_protocol::dynamic_tools::DynamicToolSpec; 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::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: Option, exposure: ToolExposure, search_text: String, } impl DynamicToolHandler { pub fn new(tool: &DynamicToolSpec) -> Option { let tool_name = ToolName::new(tool.namespace.clone(), tool.name.clone()); let output_tool = dynamic_tool_to_responses_api_tool(tool).ok()?; let spec = match tool.namespace.as_ref() { Some(namespace) => ToolSpec::Namespace(ResponsesApiNamespace { name: namespace.clone(), description: default_namespace_description(namespace), tools: vec![ResponsesApiNamespaceTool::Function(output_tool)], }), None => ToolSpec::Function(output_tool), }; Some(Self { tool_name, spec: Some(spec), exposure: if tool.defer_loading { ToolExposure::Deferred } else { ToolExposure::Direct }, search_text: build_dynamic_search_text(tool), }) } } #[async_trait::async_trait] impl ToolExecutor for DynamicToolHandler { fn tool_name(&self) -> ToolName { self.tool_name.clone() } fn spec(&self) -> Option { self.spec.clone() } fn exposure(&self) -> ToolExposure { self.exposure } async fn handle( &self, invocation: ToolInvocation, ) -> Result, 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::>(); Ok(boxed_tool_output(FunctionToolOutput::from_content( body, Some(success), ))) } } impl CoreToolRuntime for DynamicToolHandler { fn search_info(&self) -> Option { ToolSearchInfo::from_spec( self.search_text.clone(), self.spec()?, Some(ToolSearchSourceInfo { name: "Dynamic tools".to_string(), description: Some("Tools provided by the current Codex thread.".to_string()), }), ) } } #[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 { 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 } fn build_dynamic_search_text(tool: &DynamicToolSpec) -> String { let mut schema_properties = tool .input_schema .get("properties") .and_then(serde_json::Value::as_object) .map(|map| map.keys().cloned().collect::>()) .unwrap_or_default(); schema_properties.sort(); let mut parts = vec![ tool.name.clone(), tool.name.replace('_', " "), tool.description.clone(), ]; if let Some(namespace) = &tool.namespace { parts.push(namespace.clone()); } parts.extend(schema_properties); parts.join(" ") } #[cfg(test)] #[path = "dynamic_tests.rs"] mod tests;