diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index b902ae9e9..47cba4ad5 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -3729,10 +3729,12 @@ dependencies = [ "codex-protocol", "codex-utils-absolute-path", "codex-utils-pty", + "codex-utils-string", "pretty_assertions", "rmcp", "serde", "serde_json", + "thiserror 2.0.18", "tracing", ] diff --git a/codex-rs/core/src/function_tool.rs b/codex-rs/core/src/function_tool.rs index 240e04361..868636485 100644 --- a/codex-rs/core/src/function_tool.rs +++ b/codex-rs/core/src/function_tool.rs @@ -1,11 +1 @@ -use thiserror::Error; - -#[derive(Debug, Error, PartialEq)] -pub enum FunctionCallError { - #[error("{0}")] - RespondToModel(String), - #[error("LocalShellCall without call_id or id")] - MissingLocalShellCallId, - #[error("Fatal error: {0}")] - Fatal(String), -} +pub use codex_tools::FunctionCallError; diff --git a/codex-rs/core/src/session/tests.rs b/codex-rs/core/src/session/tests.rs index 43309ab0e..616633ee9 100644 --- a/codex-rs/core/src/session/tests.rs +++ b/codex-rs/core/src/session/tests.rs @@ -71,7 +71,7 @@ use crate::tools::handlers::CreateGoalHandler; use crate::tools::handlers::ExecCommandHandler; use crate::tools::handlers::ShellHandler; use crate::tools::handlers::UpdateGoalHandler; -use crate::tools::registry::ToolHandler; +use crate::tools::registry::ToolExecutor; use crate::tools::router::ToolCallSource; use crate::turn_diff_tracker::TurnDiffTracker; use codex_app_server_protocol::AppInfo; diff --git a/codex-rs/core/src/tools/code_mode/execute_handler.rs b/codex-rs/core/src/tools/code_mode/execute_handler.rs index 8eb1697fc..eaa415020 100644 --- a/codex-rs/core/src/tools/code_mode/execute_handler.rs +++ b/codex-rs/core/src/tools/code_mode/execute_handler.rs @@ -2,6 +2,7 @@ use crate::function_tool::FunctionCallError; use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_tools::ToolName; use codex_tools::ToolSpec; @@ -86,7 +87,7 @@ impl CodeModeExecuteHandler { } } -impl ToolHandler for CodeModeExecuteHandler { +impl ToolExecutor for CodeModeExecuteHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -97,10 +98,6 @@ impl ToolHandler for CodeModeExecuteHandler { Some(self.spec.clone()) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Custom { .. }) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -121,3 +118,9 @@ impl ToolHandler for CodeModeExecuteHandler { } } } + +impl ToolHandler for CodeModeExecuteHandler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Custom { .. }) + } +} diff --git a/codex-rs/core/src/tools/code_mode/wait_handler.rs b/codex-rs/core/src/tools/code_mode/wait_handler.rs index 40ca82939..070b9270d 100644 --- a/codex-rs/core/src/tools/code_mode/wait_handler.rs +++ b/codex-rs/core/src/tools/code_mode/wait_handler.rs @@ -4,6 +4,7 @@ use crate::function_tool::FunctionCallError; use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_tools::ToolName; use codex_tools::ToolSpec; @@ -40,7 +41,7 @@ where }) } -impl ToolHandler for CodeModeWaitHandler { +impl ToolExecutor for CodeModeWaitHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -105,3 +106,5 @@ impl ToolHandler for CodeModeWaitHandler { } } } + +impl ToolHandler for CodeModeWaitHandler {} diff --git a/codex-rs/core/src/tools/context.rs b/codex-rs/core/src/tools/context.rs index 76a99fe8f..0f3ae631a 100644 --- a/codex-rs/core/src/tools/context.rs +++ b/codex-rs/core/src/tools/context.rs @@ -8,13 +8,10 @@ use crate::tools::TELEMETRY_PREVIEW_TRUNCATION_NOTICE; use crate::turn_diff_tracker::TurnDiffTracker; use crate::unified_exec::resolve_max_tokens; use codex_protocol::mcp::CallToolResult; -use codex_protocol::models::DEFAULT_IMAGE_DETAIL; use codex_protocol::models::FunctionCallOutputBody; use codex_protocol::models::FunctionCallOutputContentItem; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ResponseInputItem; -use codex_protocol::models::SearchToolCallParams; -use codex_protocol::models::ShellToolCallParams; use codex_protocol::models::function_call_output_content_items_to_text; use codex_tools::LoadableToolSpec; use codex_tools::ToolName; @@ -23,12 +20,14 @@ use codex_utils_output_truncation::formatted_truncate_text; use codex_utils_string::take_bytes_at_char_boundary; use serde::Serialize; use serde_json::Value as JsonValue; -use std::borrow::Cow; use std::sync::Arc; use std::time::Duration; use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; +pub use codex_tools::ToolOutput; +pub use codex_tools::ToolPayload; + pub type SharedTurnDiffTracker = Arc>; #[derive(Clone, Debug, Eq, PartialEq)] @@ -56,73 +55,6 @@ pub struct ToolInvocation { pub payload: ToolPayload, } -#[derive(Clone, Debug)] -pub enum ToolPayload { - Function { arguments: String }, - ToolSearch { arguments: SearchToolCallParams }, - Custom { input: String }, - LocalShell { params: ShellToolCallParams }, -} - -impl ToolPayload { - pub fn log_payload(&self) -> Cow<'_, str> { - match self { - ToolPayload::Function { arguments } => Cow::Borrowed(arguments), - ToolPayload::ToolSearch { arguments } => Cow::Owned(arguments.query.clone()), - ToolPayload::Custom { input } => Cow::Borrowed(input), - ToolPayload::LocalShell { params } => Cow::Owned(params.command.join(" ")), - } - } -} - -pub trait ToolOutput: Send { - fn log_preview(&self) -> String; - - fn success_for_logging(&self) -> bool; - - fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem; - - /// Returns the stable value exposed to `PostToolUse` hooks for this tool output. - /// - /// Tool handlers decide whether a tool participates in `PostToolUse`, but - /// this method lets the output type own any conversion from model-facing - /// response content to hook-facing data. Returning `None` means the output - /// should not produce a post-use hook payload, not merely that the tool had - /// empty output. - fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option { - None - } - - fn code_mode_result(&self, payload: &ToolPayload) -> JsonValue { - response_input_to_code_mode_result(self.to_response_item("", payload)) - } -} - -impl ToolOutput for CallToolResult { - fn log_preview(&self) -> String { - let output = self.as_function_call_output_payload(); - let preview = output.body.to_text().unwrap_or_else(|| output.to_string()); - telemetry_preview(&preview) - } - - fn success_for_logging(&self) -> bool { - self.success() - } - - fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem { - ResponseInputItem::McpToolCallOutput { - call_id: call_id.to_string(), - output: self.clone(), - } - } - - fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue { - serde_json::to_value(self).unwrap_or_else(|err| { - JsonValue::String(format!("failed to serialize mcp result: {err}")) - }) - } -} - #[derive(Clone, Debug)] pub struct McpToolOutput { pub result: CallToolResult, @@ -469,62 +401,6 @@ impl ExecCommandToolOutput { } } -pub(crate) fn response_input_to_code_mode_result(response: ResponseInputItem) -> JsonValue { - match response { - ResponseInputItem::Message { content, .. } => content_items_to_code_mode_result( - &content - .into_iter() - .map(|item| match item { - codex_protocol::models::ContentItem::InputText { text } - | codex_protocol::models::ContentItem::OutputText { text } => { - FunctionCallOutputContentItem::InputText { text } - } - codex_protocol::models::ContentItem::InputImage { image_url, detail } => { - FunctionCallOutputContentItem::InputImage { - image_url, - detail: detail.or(Some(DEFAULT_IMAGE_DETAIL)), - } - } - }) - .collect::>(), - ), - ResponseInputItem::FunctionCallOutput { output, .. } - | ResponseInputItem::CustomToolCallOutput { output, .. } => match output.body { - FunctionCallOutputBody::Text(text) => JsonValue::String(text), - FunctionCallOutputBody::ContentItems(items) => { - content_items_to_code_mode_result(&items) - } - }, - ResponseInputItem::ToolSearchOutput { tools, .. } => JsonValue::Array(tools), - ResponseInputItem::McpToolCallOutput { output, .. } => { - output.code_mode_result(&ToolPayload::Function { - arguments: String::new(), - }) - } - } -} - -fn content_items_to_code_mode_result(items: &[FunctionCallOutputContentItem]) -> JsonValue { - JsonValue::String( - items - .iter() - .filter_map(|item| match item { - FunctionCallOutputContentItem::InputText { text } if !text.trim().is_empty() => { - Some(text.clone()) - } - FunctionCallOutputContentItem::InputImage { image_url, .. } - if !image_url.trim().is_empty() => - { - Some(image_url.clone()) - } - FunctionCallOutputContentItem::InputText { .. } - | FunctionCallOutputContentItem::InputImage { .. } => None, - }) - .collect::>() - .join("\n"), - ) -} - fn function_tool_response( call_id: &str, payload: &ToolPayload, diff --git a/codex-rs/core/src/tools/context_tests.rs b/codex-rs/core/src/tools/context_tests.rs index 6dc424831..fe40341e9 100644 --- a/codex-rs/core/src/tools/context_tests.rs +++ b/codex-rs/core/src/tools/context_tests.rs @@ -1,5 +1,6 @@ use super::*; use codex_protocol::models::DEFAULT_IMAGE_DETAIL; +use codex_protocol::models::SearchToolCallParams; use core_test_support::assert_regex_match; use pretty_assertions::assert_eq; use serde_json::json; diff --git a/codex-rs/core/src/tools/handlers/agent_jobs/report_agent_job_result.rs b/codex-rs/core/src/tools/handlers/agent_jobs/report_agent_job_result.rs index ad6815582..7797c8c61 100644 --- a/codex-rs/core/src/tools/handlers/agent_jobs/report_agent_job_result.rs +++ b/codex-rs/core/src/tools/handlers/agent_jobs/report_agent_job_result.rs @@ -3,6 +3,7 @@ use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::handlers::agent_jobs_spec::create_report_agent_job_result_tool; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_tools::ToolName; use codex_tools::ToolSpec; @@ -11,7 +12,7 @@ use super::*; pub struct ReportAgentJobResultHandler; -impl ToolHandler for ReportAgentJobResultHandler { +impl ToolExecutor for ReportAgentJobResultHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -22,10 +23,6 @@ impl ToolHandler for ReportAgentJobResultHandler { Some(create_report_agent_job_result_tool()) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, payload, .. @@ -44,6 +41,12 @@ impl ToolHandler for ReportAgentJobResultHandler { } } +impl ToolHandler for ReportAgentJobResultHandler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} + pub async fn handle( session: Arc, arguments: String, diff --git a/codex-rs/core/src/tools/handlers/agent_jobs/spawn_agents_on_csv.rs b/codex-rs/core/src/tools/handlers/agent_jobs/spawn_agents_on_csv.rs index 183387abf..6dd7ea2b3 100644 --- a/codex-rs/core/src/tools/handlers/agent_jobs/spawn_agents_on_csv.rs +++ b/codex-rs/core/src/tools/handlers/agent_jobs/spawn_agents_on_csv.rs @@ -3,6 +3,7 @@ use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::handlers::agent_jobs_spec::create_spawn_agents_on_csv_tool; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_tools::ToolName; use codex_tools::ToolSpec; @@ -11,7 +12,7 @@ use super::*; pub struct SpawnAgentsOnCsvHandler; -impl ToolHandler for SpawnAgentsOnCsvHandler { +impl ToolExecutor for SpawnAgentsOnCsvHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -22,10 +23,6 @@ impl ToolHandler for SpawnAgentsOnCsvHandler { Some(create_spawn_agents_on_csv_tool()) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -47,6 +44,12 @@ impl ToolHandler for SpawnAgentsOnCsvHandler { } } +impl ToolHandler for SpawnAgentsOnCsvHandler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} + /// Create a new agent job from a CSV and run it to completion. /// /// Each CSV row becomes a job item. The instruction string is a template where `{column}` diff --git a/codex-rs/core/src/tools/handlers/apply_patch.rs b/codex-rs/core/src/tools/handlers/apply_patch.rs index da70b3985..32edd21da 100644 --- a/codex-rs/core/src/tools/handlers/apply_patch.rs +++ b/codex-rs/core/src/tools/handlers/apply_patch.rs @@ -30,6 +30,7 @@ use crate::tools::orchestrator::ToolOrchestrator; use crate::tools::registry::PostToolUsePayload; use crate::tools::registry::PreToolUsePayload; use crate::tools::registry::ToolArgumentDiffConsumer; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use crate::tools::runtimes::apply_patch::ApplyPatchRequest; use crate::tools::runtimes::apply_patch::ApplyPatchRuntime; @@ -296,7 +297,7 @@ async fn effective_patch_permissions( ) } -impl ToolHandler for ApplyPatchHandler { +impl ToolExecutor for ApplyPatchHandler { type Output = ApplyPatchToolOutput; fn tool_name(&self) -> ToolName { @@ -307,53 +308,6 @@ impl ToolHandler for ApplyPatchHandler { Some(create_apply_patch_freeform_tool(self.multi_environment)) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Custom { .. }) - } - - fn create_diff_consumer(&self) -> Option> { - Some(Box::::default()) - } - - fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { - apply_patch_payload_command(&invocation.payload).map(|command| PreToolUsePayload { - tool_name: HookToolName::apply_patch(), - tool_input: serde_json::json!({ "command": command }), - }) - } - - fn with_updated_hook_input( - &self, - mut invocation: ToolInvocation, - updated_input: serde_json::Value, - ) -> Result { - let patch = updated_hook_command(&updated_input)?; - invocation.payload = match invocation.payload { - ToolPayload::Custom { .. } => ToolPayload::Custom { - input: patch.to_string(), - }, - payload => payload, - }; - Ok(invocation) - } - - fn post_tool_use_payload( - &self, - invocation: &ToolInvocation, - result: &Self::Output, - ) -> Option { - let tool_response = - result.post_tool_use_response(&invocation.call_id, &invocation.payload)?; - Some(PostToolUsePayload { - tool_name: HookToolName::apply_patch(), - tool_use_id: invocation.call_id.clone(), - tool_input: serde_json::json!({ - "command": apply_patch_payload_command(&invocation.payload)?, - }), - tool_response, - }) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -484,6 +438,55 @@ impl ToolHandler for ApplyPatchHandler { } } +impl ToolHandler for ApplyPatchHandler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Custom { .. }) + } + + fn create_diff_consumer(&self) -> Option> { + Some(Box::::default()) + } + + fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { + apply_patch_payload_command(&invocation.payload).map(|command| PreToolUsePayload { + tool_name: HookToolName::apply_patch(), + tool_input: serde_json::json!({ "command": command }), + }) + } + + fn with_updated_hook_input( + &self, + mut invocation: ToolInvocation, + updated_input: serde_json::Value, + ) -> Result { + let patch = updated_hook_command(&updated_input)?; + invocation.payload = match invocation.payload { + ToolPayload::Custom { .. } => ToolPayload::Custom { + input: patch.to_string(), + }, + payload => payload, + }; + Ok(invocation) + } + + fn post_tool_use_payload( + &self, + invocation: &ToolInvocation, + result: &Self::Output, + ) -> Option { + let tool_response = + result.post_tool_use_response(&invocation.call_id, &invocation.payload)?; + Some(PostToolUsePayload { + tool_name: HookToolName::apply_patch(), + tool_use_id: invocation.call_id.clone(), + tool_input: serde_json::json!({ + "command": apply_patch_payload_command(&invocation.payload)?, + }), + tool_response, + }) + } +} + #[allow(clippy::too_many_arguments)] pub(crate) async fn intercept_apply_patch( command: &[String], diff --git a/codex-rs/core/src/tools/handlers/dynamic.rs b/codex-rs/core/src/tools/handlers/dynamic.rs index 4637aac91..3f3e72e2f 100644 --- a/codex-rs/core/src/tools/handlers/dynamic.rs +++ b/codex-rs/core/src/tools/handlers/dynamic.rs @@ -5,6 +5,7 @@ use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::handlers::parse_arguments; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use crate::tools::tool_search_entry::ToolSearchInfo; use crate::turn_timing::now_unix_timestamp_ms; @@ -52,7 +53,7 @@ impl DynamicToolHandler { } } -impl ToolHandler for DynamicToolHandler { +impl ToolExecutor for DynamicToolHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -63,17 +64,6 @@ impl ToolHandler for DynamicToolHandler { self.spec.clone() } - 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()), - }), - ) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -119,6 +109,19 @@ impl ToolHandler for DynamicToolHandler { } } +impl ToolHandler 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" diff --git a/codex-rs/core/src/tools/handlers/extension_tools.rs b/codex-rs/core/src/tools/handlers/extension_tools.rs index da0a93515..7f1ee24e1 100644 --- a/codex-rs/core/src/tools/handlers/extension_tools.rs +++ b/codex-rs/core/src/tools/handlers/extension_tools.rs @@ -16,6 +16,7 @@ use crate::tools::flat_tool_name; use crate::tools::hook_names::HookToolName; use crate::tools::registry::PostToolUsePayload; use crate::tools::registry::PreToolUsePayload; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; pub(crate) struct BundledToolOutput { @@ -68,7 +69,7 @@ impl BundledToolHandler { } } -impl ToolHandler for BundledToolHandler { +impl ToolExecutor for BundledToolHandler { type Output = BundledToolOutput; fn tool_name(&self) -> ToolName { @@ -79,6 +80,30 @@ impl ToolHandler for BundledToolHandler { Some(self.spec.clone()) } + async fn handle(&self, invocation: ToolInvocation) -> Result { + let arguments = self + .arguments_from_payload(&invocation.payload) + .ok_or_else(|| { + FunctionCallError::Fatal(format!( + "tool {} invoked with incompatible payload", + self.bundle.tool_name() + )) + })? + .to_string(); + let value = self + .bundle + .executor() + .execute(codex_tool_api::ToolCall { + call_id: invocation.call_id, + arguments, + }) + .await + .map_err(map_extension_tool_error)?; + Ok(BundledToolOutput { value }) + } +} + +impl ToolHandler for BundledToolHandler { fn matches_kind(&self, payload: &ToolPayload) -> bool { self.arguments_from_payload(payload).is_some() } @@ -105,28 +130,6 @@ impl ToolHandler for BundledToolHandler { .post_tool_use_response(&invocation.call_id, &invocation.payload)?, }) } - - async fn handle(&self, invocation: ToolInvocation) -> Result { - let arguments = self - .arguments_from_payload(&invocation.payload) - .ok_or_else(|| { - FunctionCallError::Fatal(format!( - "tool {} invoked with incompatible payload", - self.bundle.tool_name() - )) - })? - .to_string(); - let value = self - .bundle - .executor() - .execute(codex_tool_api::ToolCall { - call_id: invocation.call_id, - arguments, - }) - .await - .map_err(map_extension_tool_error)?; - Ok(BundledToolOutput { value }) - } } pub(crate) fn extension_tool_spec( diff --git a/codex-rs/core/src/tools/handlers/goal/create_goal.rs b/codex-rs/core/src/tools/handlers/goal/create_goal.rs index db33445a2..34a4c9307 100644 --- a/codex-rs/core/src/tools/handlers/goal/create_goal.rs +++ b/codex-rs/core/src/tools/handlers/goal/create_goal.rs @@ -6,6 +6,7 @@ use crate::tools::context::ToolPayload; use crate::tools::handlers::goal_spec::CREATE_GOAL_TOOL_NAME; use crate::tools::handlers::goal_spec::create_create_goal_tool; use crate::tools::handlers::parse_arguments; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_tools::ToolName; use codex_tools::ToolSpec; @@ -17,7 +18,7 @@ use super::goal_response; pub struct CreateGoalHandler; -impl ToolHandler for CreateGoalHandler { +impl ToolExecutor for CreateGoalHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -71,3 +72,5 @@ impl ToolHandler for CreateGoalHandler { goal_response(Some(goal), CompletionBudgetReport::Omit) } } + +impl ToolHandler for CreateGoalHandler {} diff --git a/codex-rs/core/src/tools/handlers/goal/get_goal.rs b/codex-rs/core/src/tools/handlers/goal/get_goal.rs index d3c587b3a..3c0813329 100644 --- a/codex-rs/core/src/tools/handlers/goal/get_goal.rs +++ b/codex-rs/core/src/tools/handlers/goal/get_goal.rs @@ -4,6 +4,7 @@ use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::handlers::goal_spec::GET_GOAL_TOOL_NAME; use crate::tools::handlers::goal_spec::create_get_goal_tool; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_tools::ToolName; use codex_tools::ToolSpec; @@ -14,7 +15,7 @@ use super::goal_response; pub struct GetGoalHandler; -impl ToolHandler for GetGoalHandler { +impl ToolExecutor for GetGoalHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -44,3 +45,5 @@ impl ToolHandler for GetGoalHandler { } } } + +impl ToolHandler for GetGoalHandler {} diff --git a/codex-rs/core/src/tools/handlers/goal/update_goal.rs b/codex-rs/core/src/tools/handlers/goal/update_goal.rs index 8e80a9013..18b59c1b3 100644 --- a/codex-rs/core/src/tools/handlers/goal/update_goal.rs +++ b/codex-rs/core/src/tools/handlers/goal/update_goal.rs @@ -7,6 +7,7 @@ use crate::tools::context::ToolPayload; use crate::tools::handlers::goal_spec::UPDATE_GOAL_TOOL_NAME; use crate::tools::handlers::goal_spec::create_update_goal_tool; use crate::tools::handlers::parse_arguments; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_protocol::protocol::ThreadGoalStatus; use codex_tools::ToolName; @@ -19,7 +20,7 @@ use super::goal_response; pub struct UpdateGoalHandler; -impl ToolHandler for UpdateGoalHandler { +impl ToolExecutor for UpdateGoalHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -74,3 +75,5 @@ impl ToolHandler for UpdateGoalHandler { goal_response(Some(goal), CompletionBudgetReport::Include) } } + +impl ToolHandler for UpdateGoalHandler {} diff --git a/codex-rs/core/src/tools/handlers/mcp.rs b/codex-rs/core/src/tools/handlers/mcp.rs index 6e7d01f9e..2cd02e8a0 100644 --- a/codex-rs/core/src/tools/handlers/mcp.rs +++ b/codex-rs/core/src/tools/handlers/mcp.rs @@ -12,6 +12,7 @@ use crate::tools::flat_tool_name; use crate::tools::hook_names::HookToolName; use crate::tools::registry::PostToolUsePayload; use crate::tools::registry::PreToolUsePayload; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolTelemetryTags; use crate::tools::tool_search_entry::ToolSearchInfo; @@ -35,7 +36,7 @@ impl McpHandler { } } -impl ToolHandler for McpHandler { +impl ToolExecutor for McpHandler { type Output = McpToolOutput; fn tool_name(&self) -> ToolName { @@ -70,6 +71,51 @@ impl ToolHandler for McpHandler { })) } + fn supports_parallel_tool_calls(&self) -> bool { + self.tool_info.supports_parallel_tool_calls + } + + async fn handle(&self, invocation: ToolInvocation) -> Result { + let ToolInvocation { + session, + turn, + call_id, + payload, + .. + } = invocation; + + let payload = match payload { + ToolPayload::Function { arguments } => arguments, + _ => { + return Err(FunctionCallError::RespondToModel( + "mcp handler received unsupported payload".to_string(), + )); + } + }; + + let started = Instant::now(); + let result = handle_mcp_tool_call( + Arc::clone(&session), + &turn, + call_id.clone(), + self.tool_info.server_name.clone(), + self.tool_info.tool.name.to_string(), + self.tool_name().to_string(), + payload, + ) + .await; + + Ok(McpToolOutput { + result: result.result, + tool_input: result.tool_input, + wall_time: started.elapsed(), + original_image_detail_supported: can_request_original_image_detail(&turn.model_info), + truncation_policy: turn.truncation_policy, + }) + } +} + +impl ToolHandler for McpHandler { fn search_info(&self) -> Option { let source_name = self .tool_info @@ -96,10 +142,6 @@ impl ToolHandler for McpHandler { ) } - fn supports_parallel_tool_calls(&self) -> bool { - self.tool_info.supports_parallel_tool_calls - } - async fn telemetry_tags(&self, _invocation: &ToolInvocation) -> ToolTelemetryTags { let mut tags = vec![("mcp_server", self.tool_info.server_name.clone())]; if let Some(origin) = &self.tool_info.server_origin { @@ -159,45 +201,6 @@ impl ToolHandler for McpHandler { tool_response, }) } - - async fn handle(&self, invocation: ToolInvocation) -> Result { - let ToolInvocation { - session, - turn, - call_id, - payload, - .. - } = invocation; - - let payload = match payload { - ToolPayload::Function { arguments } => arguments, - _ => { - return Err(FunctionCallError::RespondToModel( - "mcp handler received unsupported payload".to_string(), - )); - } - }; - - let started = Instant::now(); - let result = handle_mcp_tool_call( - Arc::clone(&session), - &turn, - call_id.clone(), - self.tool_info.server_name.clone(), - self.tool_info.tool.name.to_string(), - self.tool_name().to_string(), - payload, - ) - .await; - - Ok(McpToolOutput { - result: result.result, - tool_input: result.tool_input, - wall_time: started.elapsed(), - original_image_detail_supported: can_request_original_image_detail(&turn.model_info), - truncation_policy: turn.truncation_policy, - }) - } } fn mcp_hook_tool_input(raw_arguments: &str) -> Value { diff --git a/codex-rs/core/src/tools/handlers/mcp_resource/list_mcp_resource_templates.rs b/codex-rs/core/src/tools/handlers/mcp_resource/list_mcp_resource_templates.rs index 14a5a9d2b..0aea2b335 100644 --- a/codex-rs/core/src/tools/handlers/mcp_resource/list_mcp_resource_templates.rs +++ b/codex-rs/core/src/tools/handlers/mcp_resource/list_mcp_resource_templates.rs @@ -5,6 +5,7 @@ use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resource_templates_tool; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_protocol::models::function_call_output_content_items_to_text; use codex_protocol::protocol::McpInvocation; @@ -25,7 +26,7 @@ use super::serialize_function_output; pub struct ListMcpResourceTemplatesHandler; -impl ToolHandler for ListMcpResourceTemplatesHandler { +impl ToolExecutor for ListMcpResourceTemplatesHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -163,3 +164,5 @@ impl ToolHandler for ListMcpResourceTemplatesHandler { } } } + +impl ToolHandler for ListMcpResourceTemplatesHandler {} diff --git a/codex-rs/core/src/tools/handlers/mcp_resource/list_mcp_resources.rs b/codex-rs/core/src/tools/handlers/mcp_resource/list_mcp_resources.rs index eaa5a54e0..b412f811b 100644 --- a/codex-rs/core/src/tools/handlers/mcp_resource/list_mcp_resources.rs +++ b/codex-rs/core/src/tools/handlers/mcp_resource/list_mcp_resources.rs @@ -5,6 +5,7 @@ use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::handlers::mcp_resource_spec::create_list_mcp_resources_tool; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_protocol::models::function_call_output_content_items_to_text; use codex_protocol::protocol::McpInvocation; @@ -25,7 +26,7 @@ use super::serialize_function_output; pub struct ListMcpResourcesHandler; -impl ToolHandler for ListMcpResourcesHandler { +impl ToolExecutor for ListMcpResourcesHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -161,3 +162,5 @@ impl ToolHandler for ListMcpResourcesHandler { } } } + +impl ToolHandler for ListMcpResourcesHandler {} diff --git a/codex-rs/core/src/tools/handlers/mcp_resource/read_mcp_resource.rs b/codex-rs/core/src/tools/handlers/mcp_resource/read_mcp_resource.rs index 51bb6fa67..89d061d69 100644 --- a/codex-rs/core/src/tools/handlers/mcp_resource/read_mcp_resource.rs +++ b/codex-rs/core/src/tools/handlers/mcp_resource/read_mcp_resource.rs @@ -5,6 +5,7 @@ use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::handlers::mcp_resource_spec::create_read_mcp_resource_tool; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_protocol::models::function_call_output_content_items_to_text; use codex_protocol::protocol::McpInvocation; @@ -25,7 +26,7 @@ use super::serialize_function_output; pub struct ReadMcpResourceHandler; -impl ToolHandler for ReadMcpResourceHandler { +impl ToolExecutor for ReadMcpResourceHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -144,3 +145,5 @@ impl ToolHandler for ReadMcpResourceHandler { } } } + +impl ToolHandler for ReadMcpResourceHandler {} diff --git a/codex-rs/core/src/tools/handlers/multi_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents.rs index 126263396..d0c5edfb7 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents.rs @@ -15,6 +15,7 @@ use crate::tools::context::ToolOutput; use crate::tools::context::ToolPayload; pub(crate) use crate::tools::handlers::multi_agents_common::*; use crate::tools::handlers::parse_arguments; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_protocol::ThreadId; use codex_protocol::models::ResponseInputItem; diff --git a/codex-rs/core/src/tools/handlers/multi_agents/close_agent.rs b/codex-rs/core/src/tools/handlers/multi_agents/close_agent.rs index d87bb6c66..beac3a96f 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/close_agent.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/close_agent.rs @@ -5,7 +5,7 @@ use codex_tools::ToolSpec; pub(crate) struct Handler; -impl ToolHandler for Handler { +impl ToolExecutor for Handler { type Output = CloseAgentResult; fn tool_name(&self) -> ToolName { @@ -16,10 +16,6 @@ impl ToolHandler for Handler { Some(create_close_agent_tool_v1()) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - fn handle( &self, invocation: ToolInvocation, @@ -111,6 +107,12 @@ async fn handle_close_agent( }) } +impl ToolHandler for Handler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} + #[derive(Debug, Deserialize, Serialize)] pub(crate) struct CloseAgentResult { pub(crate) previous_status: AgentStatus, diff --git a/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs b/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs index c8cc0f70b..1e9f5d9b0 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/resume_agent.rs @@ -7,7 +7,7 @@ use std::sync::Arc; pub(crate) struct Handler; -impl ToolHandler for Handler { +impl ToolExecutor for Handler { type Output = ResumeAgentResult; fn tool_name(&self) -> ToolName { @@ -18,10 +18,6 @@ impl ToolHandler for Handler { Some(create_resume_agent_tool()) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - fn handle( &self, invocation: ToolInvocation, @@ -139,6 +135,12 @@ async fn handle_resume_agent( Ok(ResumeAgentResult { status }) } +impl ToolHandler for Handler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} + #[derive(Debug, Deserialize)] struct ResumeAgentArgs { id: String, diff --git a/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs b/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs index cdd46a90d..ac212f755 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/send_input.rs @@ -6,7 +6,7 @@ use codex_tools::ToolSpec; pub(crate) struct Handler; -impl ToolHandler for Handler { +impl ToolExecutor for Handler { type Output = SendInputResult; fn tool_name(&self) -> ToolName { @@ -17,10 +17,6 @@ impl ToolHandler for Handler { Some(create_send_input_tool_v1()) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -92,6 +88,12 @@ impl ToolHandler for Handler { } } +impl ToolHandler for Handler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} + #[derive(Debug, Deserialize)] struct SendInputArgs { target: String, diff --git a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs index 8ac5ca085..8f9c93edb 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -22,7 +22,7 @@ impl Handler { } } -impl ToolHandler for Handler { +impl ToolExecutor for Handler { type Output = SpawnAgentResult; fn tool_name(&self) -> ToolName { @@ -33,10 +33,6 @@ impl ToolHandler for Handler { Some(create_spawn_agent_tool_v1(self.options.clone())) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - fn handle( &self, invocation: ToolInvocation, @@ -197,6 +193,12 @@ async fn handle_spawn_agent( }) } +impl ToolHandler for Handler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} + #[derive(Debug, Deserialize)] struct SpawnAgentArgs { message: Option, diff --git a/codex-rs/core/src/tools/handlers/multi_agents/wait.rs b/codex-rs/core/src/tools/handlers/multi_agents/wait.rs index 17739606f..aa9d6f658 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/wait.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/wait.rs @@ -27,7 +27,7 @@ impl Handler { } } -impl ToolHandler for Handler { +impl ToolExecutor for Handler { type Output = WaitAgentResult; fn tool_name(&self) -> ToolName { @@ -38,10 +38,6 @@ impl ToolHandler for Handler { Some(create_wait_agent_tool_v1(self.options)) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -204,6 +200,12 @@ impl ToolHandler for Handler { } } +impl ToolHandler for Handler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} + #[derive(Debug, Deserialize)] struct WaitArgs { #[serde(default)] diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2.rs index b40d85523..0190fe04a 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2.rs @@ -8,6 +8,7 @@ use crate::tools::context::ToolOutput; use crate::tools::context::ToolPayload; use crate::tools::handlers::multi_agents_common::*; use crate::tools::handlers::parse_arguments; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_protocol::AgentPath; use codex_protocol::models::ResponseInputItem; diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs index 246b97173..c901df9ae 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/close_agent.rs @@ -5,7 +5,7 @@ use codex_tools::ToolSpec; pub(crate) struct Handler; -impl ToolHandler for Handler { +impl ToolExecutor for Handler { type Output = CloseAgentResult; fn tool_name(&self) -> ToolName { @@ -16,10 +16,6 @@ impl ToolHandler for Handler { Some(create_close_agent_tool_v2()) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - fn handle( &self, invocation: ToolInvocation, @@ -123,6 +119,12 @@ async fn handle_close_agent( }) } +impl ToolHandler for Handler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] struct CloseAgentArgs { diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs index 2077d6693..608d7f2f6 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/followup_task.rs @@ -8,7 +8,7 @@ use codex_tools::ToolSpec; pub(crate) struct Handler; -impl ToolHandler for Handler { +impl ToolExecutor for Handler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -19,10 +19,6 @@ impl ToolHandler for Handler { Some(create_followup_task_tool()) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let arguments = function_arguments(invocation.payload.clone())?; let args: FollowupTaskArgs = parse_arguments(&arguments)?; @@ -35,3 +31,9 @@ impl ToolHandler for Handler { .await } } + +impl ToolHandler for Handler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/list_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/list_agents.rs index b75f28675..527d5a29e 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/list_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/list_agents.rs @@ -5,7 +5,7 @@ use codex_tools::ToolSpec; pub(crate) struct Handler; -impl ToolHandler for Handler { +impl ToolExecutor for Handler { type Output = ListAgentsResult; fn tool_name(&self) -> ToolName { @@ -16,10 +16,6 @@ impl ToolHandler for Handler { Some(create_list_agents_tool()) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -44,6 +40,12 @@ impl ToolHandler for Handler { } } +impl ToolHandler for Handler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] struct ListAgentsArgs { diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs index c9e206070..71909afb7 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/send_message.rs @@ -8,7 +8,7 @@ use codex_tools::ToolSpec; pub(crate) struct Handler; -impl ToolHandler for Handler { +impl ToolExecutor for Handler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -19,10 +19,6 @@ impl ToolHandler for Handler { Some(create_send_message_tool()) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let arguments = function_arguments(invocation.payload.clone())?; let args: SendMessageArgs = parse_arguments(&arguments)?; @@ -35,3 +31,9 @@ impl ToolHandler for Handler { .await } } + +impl ToolHandler for Handler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs index 9544c0b9f..a849bcd97 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/spawn.rs @@ -24,7 +24,7 @@ impl Handler { } } -impl ToolHandler for Handler { +impl ToolExecutor for Handler { type Output = SpawnAgentResult; fn tool_name(&self) -> ToolName { @@ -35,10 +35,6 @@ impl ToolHandler for Handler { Some(create_spawn_agent_tool_v2(self.options.clone())) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - fn handle( &self, invocation: ToolInvocation, @@ -228,6 +224,12 @@ async fn handle_spawn_agent( } } +impl ToolHandler for Handler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] struct SpawnAgentArgs { diff --git a/codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs b/codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs index 15790e4ce..48cbebdea 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents_v2/wait.rs @@ -19,7 +19,7 @@ impl Handler { } } -impl ToolHandler for Handler { +impl ToolExecutor for Handler { type Output = WaitAgentResult; fn tool_name(&self) -> ToolName { @@ -30,10 +30,6 @@ impl ToolHandler for Handler { Some(create_wait_agent_tool_v2(self.options)) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -101,6 +97,12 @@ impl ToolHandler for Handler { } } +impl ToolHandler for Handler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } +} + #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] struct WaitArgs { diff --git a/codex-rs/core/src/tools/handlers/plan.rs b/codex-rs/core/src/tools/handlers/plan.rs index a3c8aa1be..42f03e4e3 100644 --- a/codex-rs/core/src/tools/handlers/plan.rs +++ b/codex-rs/core/src/tools/handlers/plan.rs @@ -3,6 +3,7 @@ use crate::tools::context::ToolInvocation; use crate::tools::context::ToolOutput; use crate::tools::context::ToolPayload; use crate::tools::handlers::plan_spec::create_update_plan_tool; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_protocol::config_types::ModeKind; use codex_protocol::models::FunctionCallOutputPayload; @@ -43,7 +44,7 @@ impl ToolOutput for PlanToolOutput { } } -impl ToolHandler for PlanHandler { +impl ToolExecutor for PlanHandler { type Output = PlanToolOutput; fn tool_name(&self) -> ToolName { @@ -87,6 +88,8 @@ impl ToolHandler for PlanHandler { } } +impl ToolHandler for PlanHandler {} + fn parse_update_plan_arguments(arguments: &str) -> Result { serde_json::from_str::(arguments).map_err(|e| { FunctionCallError::RespondToModel(format!("failed to parse function arguments: {e}")) diff --git a/codex-rs/core/src/tools/handlers/request_permissions.rs b/codex-rs/core/src/tools/handlers/request_permissions.rs index 59c117905..243ed52d1 100644 --- a/codex-rs/core/src/tools/handlers/request_permissions.rs +++ b/codex-rs/core/src/tools/handlers/request_permissions.rs @@ -8,13 +8,14 @@ use crate::tools::context::ToolPayload; use crate::tools::handlers::parse_arguments_with_base_path; use crate::tools::handlers::shell_spec::create_request_permissions_tool; use crate::tools::handlers::shell_spec::request_permissions_tool_description; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_tools::ToolName; use codex_tools::ToolSpec; pub struct RequestPermissionsHandler; -impl ToolHandler for RequestPermissionsHandler { +impl ToolExecutor for RequestPermissionsHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -75,3 +76,5 @@ impl ToolHandler for RequestPermissionsHandler { Ok(FunctionToolOutput::from_text(content, Some(true))) } } + +impl ToolHandler for RequestPermissionsHandler {} diff --git a/codex-rs/core/src/tools/handlers/request_plugin_install.rs b/codex-rs/core/src/tools/handlers/request_plugin_install.rs index c1dbdee31..9e76352aa 100644 --- a/codex-rs/core/src/tools/handlers/request_plugin_install.rs +++ b/codex-rs/core/src/tools/handlers/request_plugin_install.rs @@ -34,6 +34,7 @@ use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::handlers::parse_arguments; use crate::tools::handlers::request_plugin_install_spec::create_request_plugin_install_tool; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; #[derive(Default)] @@ -49,7 +50,7 @@ impl RequestPluginInstallHandler { } } -impl ToolHandler for RequestPluginInstallHandler { +impl ToolExecutor for RequestPluginInstallHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -192,6 +193,8 @@ impl ToolHandler for RequestPluginInstallHandler { } } +impl ToolHandler for RequestPluginInstallHandler {} + async fn maybe_persist_disabled_install_request( session: &crate::session::session::Session, turn: &crate::session::turn_context::TurnContext, diff --git a/codex-rs/core/src/tools/handlers/request_user_input.rs b/codex-rs/core/src/tools/handlers/request_user_input.rs index 583f62c0c..ae0a2c769 100644 --- a/codex-rs/core/src/tools/handlers/request_user_input.rs +++ b/codex-rs/core/src/tools/handlers/request_user_input.rs @@ -8,6 +8,7 @@ use crate::tools::handlers::request_user_input_spec::create_request_user_input_t use crate::tools::handlers::request_user_input_spec::normalize_request_user_input_args; use crate::tools::handlers::request_user_input_spec::request_user_input_tool_description; use crate::tools::handlers::request_user_input_spec::request_user_input_unavailable_message; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_protocol::config_types::ModeKind; use codex_protocol::request_user_input::RequestUserInputArgs; @@ -18,7 +19,7 @@ pub struct RequestUserInputHandler { pub available_modes: Vec, } -impl ToolHandler for RequestUserInputHandler { +impl ToolExecutor for RequestUserInputHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -82,6 +83,8 @@ impl ToolHandler for RequestUserInputHandler { } } +impl ToolHandler for RequestUserInputHandler {} + #[cfg(test)] #[path = "request_user_input_tests.rs"] mod tests; diff --git a/codex-rs/core/src/tools/handlers/shell/container_exec.rs b/codex-rs/core/src/tools/handlers/shell/container_exec.rs index 515b8ca7d..ee3dd8221 100644 --- a/codex-rs/core/src/tools/handlers/shell/container_exec.rs +++ b/codex-rs/core/src/tools/handlers/shell/container_exec.rs @@ -9,6 +9,7 @@ use crate::tools::handlers::parse_arguments_with_base_path; use crate::tools::handlers::resolve_workdir_base_path; use crate::tools::registry::PostToolUsePayload; use crate::tools::registry::PreToolUsePayload; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use crate::tools::runtimes::shell::ShellRuntimeBackend; @@ -21,37 +22,13 @@ use super::shell_handler::ShellHandler; pub struct ContainerExecHandler; -impl ToolHandler for ContainerExecHandler { +impl ToolExecutor for ContainerExecHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { ToolName::plain("container.exec") } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - - fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { - shell_function_pre_tool_use_payload(invocation) - } - - fn with_updated_hook_input( - &self, - invocation: ToolInvocation, - updated_input: serde_json::Value, - ) -> Result { - rewrite_shell_function_updated_hook_input(invocation, updated_input, "container.exec") - } - - fn post_tool_use_payload( - &self, - invocation: &ToolInvocation, - result: &Self::Output, - ) -> Option { - shell_function_post_tool_use_payload(invocation, result) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -92,3 +69,29 @@ impl ToolHandler for ContainerExecHandler { .await } } + +impl ToolHandler for ContainerExecHandler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } + + fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { + shell_function_pre_tool_use_payload(invocation) + } + + fn with_updated_hook_input( + &self, + invocation: ToolInvocation, + updated_input: serde_json::Value, + ) -> Result { + rewrite_shell_function_updated_hook_input(invocation, updated_input, "container.exec") + } + + fn post_tool_use_payload( + &self, + invocation: &ToolInvocation, + result: &Self::Output, + ) -> Option { + shell_function_post_tool_use_payload(invocation, result) + } +} diff --git a/codex-rs/core/src/tools/handlers/shell/local_shell.rs b/codex-rs/core/src/tools/handlers/shell/local_shell.rs index 9312b6913..608d748d8 100644 --- a/codex-rs/core/src/tools/handlers/shell/local_shell.rs +++ b/codex-rs/core/src/tools/handlers/shell/local_shell.rs @@ -9,6 +9,7 @@ use crate::tools::handlers::updated_hook_command; use crate::tools::hook_names::HookToolName; use crate::tools::registry::PostToolUsePayload; use crate::tools::registry::PreToolUsePayload; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use crate::tools::runtimes::shell::ShellRuntimeBackend; use codex_tools::ToolSpec; @@ -30,7 +31,7 @@ impl LocalShellHandler { } } -impl ToolHandler for LocalShellHandler { +impl ToolExecutor for LocalShellHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -41,14 +42,50 @@ impl ToolHandler for LocalShellHandler { self.include_spec.then(create_local_shell_tool) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::LocalShell { .. }) - } - fn supports_parallel_tool_calls(&self) -> bool { self.include_spec } + async fn handle(&self, invocation: ToolInvocation) -> Result { + let ToolInvocation { + session, + turn, + tracker, + call_id, + payload, + .. + } = invocation; + + let ToolPayload::LocalShell { params } = payload else { + return Err(FunctionCallError::RespondToModel( + "unsupported payload for local_shell handler".to_string(), + )); + }; + + let exec_params = + ShellHandler::to_exec_params(¶ms, turn.as_ref(), session.conversation_id); + run_exec_like(RunExecLikeArgs { + tool_name: ToolName::plain("local_shell"), + exec_params, + hook_command: codex_shell_command::parse_command::shlex_join(¶ms.command), + additional_permissions: None, + prefix_rule: None, + session, + turn, + tracker, + call_id, + freeform: false, + shell_runtime_backend: ShellRuntimeBackend::Generic, + }) + .await + } +} + +impl ToolHandler for LocalShellHandler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::LocalShell { .. }) + } + fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { local_shell_payload_command(&invocation.payload).map(|command| PreToolUsePayload { tool_name: HookToolName::bash(), @@ -91,38 +128,4 @@ impl ToolHandler for LocalShellHandler { tool_response, }) } - - async fn handle(&self, invocation: ToolInvocation) -> Result { - let ToolInvocation { - session, - turn, - tracker, - call_id, - payload, - .. - } = invocation; - - let ToolPayload::LocalShell { params } = payload else { - return Err(FunctionCallError::RespondToModel( - "unsupported payload for local_shell handler".to_string(), - )); - }; - - let exec_params = - ShellHandler::to_exec_params(¶ms, turn.as_ref(), session.conversation_id); - run_exec_like(RunExecLikeArgs { - tool_name: ToolName::plain("local_shell"), - exec_params, - hook_command: codex_shell_command::parse_command::shlex_join(¶ms.command), - additional_permissions: None, - prefix_rule: None, - session, - turn, - tracker, - call_id, - freeform: false, - shell_runtime_backend: ShellRuntimeBackend::Generic, - }) - .await - } } diff --git a/codex-rs/core/src/tools/handlers/shell/shell_command.rs b/codex-rs/core/src/tools/handlers/shell/shell_command.rs index aa88da95d..8d9170bbd 100644 --- a/codex-rs/core/src/tools/handlers/shell/shell_command.rs +++ b/codex-rs/core/src/tools/handlers/shell/shell_command.rs @@ -21,6 +21,7 @@ use crate::tools::handlers::updated_hook_command; use crate::tools::hook_names::HookToolName; use crate::tools::registry::PostToolUsePayload; use crate::tools::registry::PreToolUsePayload; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use crate::tools::runtimes::shell::ShellRuntimeBackend; use codex_tools::ToolSpec; @@ -124,7 +125,7 @@ impl From for ShellCommandHandler { } } -impl ToolHandler for ShellCommandHandler { +impl ToolExecutor for ShellCommandHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -140,58 +141,10 @@ impl ToolHandler for ShellCommandHandler { }) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - fn supports_parallel_tool_calls(&self) -> bool { self.options.is_some() } - fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { - shell_command_payload_command(&invocation.payload).map(|command| PreToolUsePayload { - tool_name: HookToolName::bash(), - tool_input: serde_json::json!({ "command": command }), - }) - } - - fn with_updated_hook_input( - &self, - mut invocation: ToolInvocation, - updated_input: serde_json::Value, - ) -> Result { - let ToolPayload::Function { arguments } = invocation.payload else { - return Err(FunctionCallError::RespondToModel( - "hook input rewrite received unsupported shell_command payload".to_string(), - )); - }; - invocation.payload = ToolPayload::Function { - arguments: rewrite_function_string_argument( - &arguments, - "shell_command", - "command", - updated_hook_command(&updated_input)?, - )?, - }; - Ok(invocation) - } - - fn post_tool_use_payload( - &self, - invocation: &ToolInvocation, - result: &Self::Output, - ) -> Option { - let tool_response = - result.post_tool_use_response(&invocation.call_id, &invocation.payload)?; - let command = shell_command_payload_command(&invocation.payload)?; - Some(PostToolUsePayload { - tool_name: HookToolName::bash(), - tool_use_id: invocation.call_id.clone(), - tool_input: serde_json::json!({ "command": command }), - tool_response, - }) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -243,3 +196,53 @@ impl ToolHandler for ShellCommandHandler { .await } } + +impl ToolHandler for ShellCommandHandler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } + + fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { + shell_command_payload_command(&invocation.payload).map(|command| PreToolUsePayload { + tool_name: HookToolName::bash(), + tool_input: serde_json::json!({ "command": command }), + }) + } + + fn with_updated_hook_input( + &self, + mut invocation: ToolInvocation, + updated_input: serde_json::Value, + ) -> Result { + let ToolPayload::Function { arguments } = invocation.payload else { + return Err(FunctionCallError::RespondToModel( + "hook input rewrite received unsupported shell_command payload".to_string(), + )); + }; + invocation.payload = ToolPayload::Function { + arguments: rewrite_function_string_argument( + &arguments, + "shell_command", + "command", + updated_hook_command(&updated_input)?, + )?, + }; + Ok(invocation) + } + + fn post_tool_use_payload( + &self, + invocation: &ToolInvocation, + result: &Self::Output, + ) -> Option { + let tool_response = + result.post_tool_use_response(&invocation.call_id, &invocation.payload)?; + let command = shell_command_payload_command(&invocation.payload)?; + Some(PostToolUsePayload { + tool_name: HookToolName::bash(), + tool_use_id: invocation.call_id.clone(), + tool_input: serde_json::json!({ "command": command }), + tool_response, + }) + } +} diff --git a/codex-rs/core/src/tools/handlers/shell/shell_handler.rs b/codex-rs/core/src/tools/handlers/shell/shell_handler.rs index fdcb86230..8875e9602 100644 --- a/codex-rs/core/src/tools/handlers/shell/shell_handler.rs +++ b/codex-rs/core/src/tools/handlers/shell/shell_handler.rs @@ -14,6 +14,7 @@ use crate::tools::handlers::parse_arguments_with_base_path; use crate::tools::handlers::resolve_workdir_base_path; use crate::tools::registry::PostToolUsePayload; use crate::tools::registry::PreToolUsePayload; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use crate::tools::runtimes::shell::ShellRuntimeBackend; use codex_tools::ToolSpec; @@ -62,7 +63,7 @@ impl ShellHandler { } } -impl ToolHandler for ShellHandler { +impl ToolExecutor for ShellHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -73,34 +74,10 @@ impl ToolHandler for ShellHandler { self.options.map(create_shell_tool) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - fn supports_parallel_tool_calls(&self) -> bool { self.options.is_some() } - fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { - shell_function_pre_tool_use_payload(invocation) - } - - fn with_updated_hook_input( - &self, - invocation: ToolInvocation, - updated_input: serde_json::Value, - ) -> Result { - rewrite_shell_function_updated_hook_input(invocation, updated_input, "shell") - } - - fn post_tool_use_payload( - &self, - invocation: &ToolInvocation, - result: &Self::Output, - ) -> Option { - shell_function_post_tool_use_payload(invocation, result) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -141,3 +118,29 @@ impl ToolHandler for ShellHandler { .await } } + +impl ToolHandler for ShellHandler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } + + fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { + shell_function_pre_tool_use_payload(invocation) + } + + fn with_updated_hook_input( + &self, + invocation: ToolInvocation, + updated_input: serde_json::Value, + ) -> Result { + rewrite_shell_function_updated_hook_input(invocation, updated_input, "shell") + } + + fn post_tool_use_payload( + &self, + invocation: &ToolInvocation, + result: &Self::Output, + ) -> Option { + shell_function_post_tool_use_payload(invocation, result) + } +} diff --git a/codex-rs/core/src/tools/handlers/test_sync.rs b/codex-rs/core/src/tools/handlers/test_sync.rs index 6ef39ffd1..091f0ffce 100644 --- a/codex-rs/core/src/tools/handlers/test_sync.rs +++ b/codex-rs/core/src/tools/handlers/test_sync.rs @@ -14,6 +14,7 @@ use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::handlers::parse_arguments; use crate::tools::handlers::test_sync_spec::create_test_sync_tool; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_tools::ToolName; use codex_tools::ToolSpec; @@ -55,7 +56,7 @@ fn barrier_map() -> &'static tokio::sync::Mutex> { BARRIERS.get_or_init(|| tokio::sync::Mutex::new(HashMap::new())) } -impl ToolHandler for TestSyncHandler { +impl ToolExecutor for TestSyncHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> ToolName { @@ -104,6 +105,8 @@ impl ToolHandler for TestSyncHandler { } } +impl ToolHandler for TestSyncHandler {} + async fn wait_on_barrier(args: BarrierArgs) -> Result<(), FunctionCallError> { if args.participants == 0 { return Err(FunctionCallError::RespondToModel( diff --git a/codex-rs/core/src/tools/handlers/tool_search.rs b/codex-rs/core/src/tools/handlers/tool_search.rs index a85725721..aaa313858 100644 --- a/codex-rs/core/src/tools/handlers/tool_search.rs +++ b/codex-rs/core/src/tools/handlers/tool_search.rs @@ -3,6 +3,7 @@ use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::context::ToolSearchOutput; use crate::tools::handlers::tool_search_spec::create_tool_search_tool; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use crate::tools::tool_search_entry::ToolSearchEntry; use crate::tools::tool_search_entry::ToolSearchInfo; @@ -51,7 +52,7 @@ impl ToolSearchHandler { } } -impl ToolHandler for ToolSearchHandler { +impl ToolExecutor for ToolSearchHandler { type Output = ToolSearchOutput; fn tool_name(&self) -> ToolName { @@ -108,6 +109,8 @@ impl ToolHandler for ToolSearchHandler { } } +impl ToolHandler for ToolSearchHandler {} + impl ToolSearchHandler { fn search( &self, diff --git a/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs b/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs index 3a69b3c63..cef066ad5 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec/exec_command.rs @@ -17,6 +17,7 @@ use crate::tools::handlers::updated_hook_command; use crate::tools::hook_names::HookToolName; use crate::tools::registry::PostToolUsePayload; use crate::tools::registry::PreToolUsePayload; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use crate::unified_exec::ExecCommandRequest; use crate::unified_exec::UnifiedExecContext; @@ -67,7 +68,7 @@ impl ExecCommandHandler { } } -impl ToolHandler for ExecCommandHandler { +impl ToolExecutor for ExecCommandHandler { type Output = ExecCommandToolOutput; fn tool_name(&self) -> ToolName { @@ -84,56 +85,10 @@ impl ToolHandler for ExecCommandHandler { )) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - fn supports_parallel_tool_calls(&self) -> bool { true } - fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { - let ToolPayload::Function { arguments } = &invocation.payload else { - return None; - }; - - parse_arguments::(arguments) - .ok() - .map(|args| PreToolUsePayload { - tool_name: HookToolName::bash(), - tool_input: serde_json::json!({ "command": args.cmd }), - }) - } - - fn with_updated_hook_input( - &self, - mut invocation: ToolInvocation, - updated_input: serde_json::Value, - ) -> Result { - let ToolPayload::Function { arguments } = invocation.payload else { - return Err(FunctionCallError::RespondToModel( - "hook input rewrite received unsupported exec_command payload".to_string(), - )); - }; - invocation.payload = ToolPayload::Function { - arguments: rewrite_function_string_argument( - &arguments, - "exec_command", - "cmd", - updated_hook_command(&updated_input)?, - )?, - }; - Ok(invocation) - } - - fn post_tool_use_payload( - &self, - invocation: &ToolInvocation, - result: &Self::Output, - ) -> Option { - post_unified_exec_tool_use_payload(invocation, result) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -339,6 +294,54 @@ impl ToolHandler for ExecCommandHandler { } } +impl ToolHandler for ExecCommandHandler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } + + fn pre_tool_use_payload(&self, invocation: &ToolInvocation) -> Option { + let ToolPayload::Function { arguments } = &invocation.payload else { + return None; + }; + + parse_arguments::(arguments) + .ok() + .map(|args| PreToolUsePayload { + tool_name: HookToolName::bash(), + tool_input: serde_json::json!({ "command": args.cmd }), + }) + } + + fn with_updated_hook_input( + &self, + mut invocation: ToolInvocation, + updated_input: serde_json::Value, + ) -> Result { + let ToolPayload::Function { arguments } = invocation.payload else { + return Err(FunctionCallError::RespondToModel( + "hook input rewrite received unsupported exec_command payload".to_string(), + )); + }; + invocation.payload = ToolPayload::Function { + arguments: rewrite_function_string_argument( + &arguments, + "exec_command", + "cmd", + updated_hook_command(&updated_input)?, + )?, + }; + Ok(invocation) + } + + fn post_tool_use_payload( + &self, + invocation: &ToolInvocation, + result: &Self::Output, + ) -> Option { + post_unified_exec_tool_use_payload(invocation, result) + } +} + fn emit_unified_exec_tty_metric(session_telemetry: &SessionTelemetry, tty: bool) { session_telemetry.counter( TOOL_CALL_UNIFIED_EXEC_METRIC, diff --git a/codex-rs/core/src/tools/handlers/unified_exec/write_stdin.rs b/codex-rs/core/src/tools/handlers/unified_exec/write_stdin.rs index 455d7b641..cc79615c1 100644 --- a/codex-rs/core/src/tools/handlers/unified_exec/write_stdin.rs +++ b/codex-rs/core/src/tools/handlers/unified_exec/write_stdin.rs @@ -4,6 +4,7 @@ use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::handlers::parse_arguments; use crate::tools::registry::PostToolUsePayload; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use crate::unified_exec::WriteStdinRequest; use codex_protocol::protocol::EventMsg; @@ -30,7 +31,7 @@ struct WriteStdinArgs { pub struct WriteStdinHandler; -impl ToolHandler for WriteStdinHandler { +impl ToolExecutor for WriteStdinHandler { type Output = ExecCommandToolOutput; fn tool_name(&self) -> ToolName { @@ -41,18 +42,6 @@ impl ToolHandler for WriteStdinHandler { Some(create_write_stdin_tool()) } - fn matches_kind(&self, payload: &ToolPayload) -> bool { - matches!(payload, ToolPayload::Function { .. }) - } - - fn post_tool_use_payload( - &self, - invocation: &ToolInvocation, - result: &Self::Output, - ) -> Option { - post_unified_exec_tool_use_payload(invocation, result) - } - async fn handle(&self, invocation: ToolInvocation) -> Result { let ToolInvocation { session, @@ -99,3 +88,17 @@ impl ToolHandler for WriteStdinHandler { Ok(response) } } + +impl ToolHandler for WriteStdinHandler { + fn matches_kind(&self, payload: &ToolPayload) -> bool { + matches!(payload, ToolPayload::Function { .. }) + } + + fn post_tool_use_payload( + &self, + invocation: &ToolInvocation, + result: &Self::Output, + ) -> Option { + post_unified_exec_tool_use_payload(invocation, result) + } +} diff --git a/codex-rs/core/src/tools/handlers/view_image.rs b/codex-rs/core/src/tools/handlers/view_image.rs index f7f5cfd4d..3efbedc0e 100644 --- a/codex-rs/core/src/tools/handlers/view_image.rs +++ b/codex-rs/core/src/tools/handlers/view_image.rs @@ -20,6 +20,7 @@ use crate::tools::handlers::parse_arguments; use crate::tools::handlers::resolve_tool_environment; use crate::tools::handlers::view_image_spec::ViewImageToolOptions; use crate::tools::handlers::view_image_spec::create_view_image_tool; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use codex_tools::ToolName; use codex_tools::ToolSpec; @@ -61,7 +62,7 @@ enum ViewImageDetail { Original, } -impl ToolHandler for ViewImageHandler { +impl ToolExecutor for ViewImageHandler { type Output = ViewImageOutput; fn tool_name(&self) -> ToolName { @@ -201,6 +202,8 @@ impl ToolHandler for ViewImageHandler { } } +impl ToolHandler for ViewImageHandler {} + pub struct ViewImageOutput { image_url: String, image_detail: Option, diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index 17774aafb..baae26407 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::future::Future; use std::sync::Arc; use std::time::Duration; @@ -34,24 +35,13 @@ use tracing::warn; pub(crate) type ToolTelemetryTags = Vec<(&'static str, String)>; -pub trait ToolHandler: Send + Sync { - type Output: ToolOutput + 'static; - - /// The concrete tool name handled by this handler instance. - fn tool_name(&self) -> ToolName; - - fn spec(&self) -> Option { - None - } +pub use codex_tools::ToolExecutor; +pub trait ToolHandler: ToolExecutor { fn search_info(&self) -> Option { None } - fn supports_parallel_tool_calls(&self) -> bool { - false - } - fn matches_kind(&self, payload: &ToolPayload) -> bool { matches!( payload, @@ -62,7 +52,7 @@ pub trait ToolHandler: Send + Sync { fn telemetry_tags( &self, _invocation: &ToolInvocation, - ) -> impl std::future::Future + Send { + ) -> impl Future + Send { async { Vec::new() } } @@ -96,13 +86,6 @@ pub trait ToolHandler: Send + Sync { fn create_diff_consumer(&self) -> Option> { None } - - /// Perform the actual [ToolInvocation] and returns a [ToolOutput] containing - /// the final output to return to the model. - fn handle( - &self, - invocation: ToolInvocation, - ) -> impl std::future::Future> + Send; } /// Consumes streamed argument diffs for a tool call and emits protocol events @@ -209,11 +192,11 @@ where T: ToolHandler, { fn tool_name(&self) -> ToolName { - ToolHandler::tool_name(self) + ToolExecutor::tool_name(self) } fn spec(&self) -> Option { - ToolHandler::spec(self) + ToolExecutor::spec(self) } fn search_info(&self) -> Option { @@ -221,7 +204,7 @@ where } fn supports_parallel_tool_calls(&self) -> bool { - ToolHandler::supports_parallel_tool_calls(self) + ToolExecutor::supports_parallel_tool_calls(self) } fn matches_kind(&self, payload: &ToolPayload) -> bool { @@ -257,7 +240,7 @@ where Box::pin(async move { let call_id = invocation.call_id.clone(); let payload = invocation.payload.clone(); - let output = self.handle(invocation.clone()).await?; + let output = ToolExecutor::handle(self, invocation.clone()).await?; let post_tool_use_payload = ToolHandler::post_tool_use_payload(self, &invocation, &output); Ok(AnyToolResult { diff --git a/codex-rs/core/src/tools/registry_tests.rs b/codex-rs/core/src/tools/registry_tests.rs index dc744321d..1cc68be44 100644 --- a/codex-rs/core/src/tools/registry_tests.rs +++ b/codex-rs/core/src/tools/registry_tests.rs @@ -8,7 +8,7 @@ struct TestHandler { tool_name: codex_tools::ToolName, } -impl ToolHandler for TestHandler { +impl ToolExecutor for TestHandler { type Output = crate::tools::context::FunctionToolOutput; fn tool_name(&self) -> codex_tools::ToolName { @@ -23,6 +23,8 @@ impl ToolHandler for TestHandler { } } +impl ToolHandler for TestHandler {} + #[test] fn handler_looks_up_namespaced_aliases_explicitly() { let namespace = "mcp__codex_apps__gmail"; diff --git a/codex-rs/core/src/tools/tool_dispatch_trace_tests.rs b/codex-rs/core/src/tools/tool_dispatch_trace_tests.rs index f0c808b30..3badf1155 100644 --- a/codex-rs/core/src/tools/tool_dispatch_trace_tests.rs +++ b/codex-rs/core/src/tools/tool_dispatch_trace_tests.rs @@ -21,6 +21,7 @@ use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolCallSource; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; +use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolHandler; use crate::tools::registry::ToolRegistry; use crate::turn_diff_tracker::TurnDiffTracker; @@ -29,7 +30,7 @@ struct TestHandler { tool_name: codex_tools::ToolName, } -impl ToolHandler for TestHandler { +impl ToolExecutor for TestHandler { type Output = FunctionToolOutput; fn tool_name(&self) -> codex_tools::ToolName { @@ -41,6 +42,8 @@ impl ToolHandler for TestHandler { } } +impl ToolHandler for TestHandler {} + #[tokio::test] async fn dispatch_lifecycle_trace_records_direct_and_code_mode_requesters() -> anyhow::Result<()> { let temp = TempDir::new()?; diff --git a/codex-rs/tools/Cargo.toml b/codex-rs/tools/Cargo.toml index 0029352d4..b11ee2fe7 100644 --- a/codex-rs/tools/Cargo.toml +++ b/codex-rs/tools/Cargo.toml @@ -14,6 +14,7 @@ codex-features = { workspace = true } codex-protocol = { workspace = true } codex-utils-absolute-path = { workspace = true } codex-utils-pty = { workspace = true } +codex-utils-string = { workspace = true } rmcp = { workspace = true, default-features = false, features = [ "base64", "macros", @@ -22,6 +23,7 @@ rmcp = { workspace = true, default-features = false, features = [ ] } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } +thiserror = { workspace = true } tracing = { workspace = true } [dev-dependencies] diff --git a/codex-rs/tools/src/function_call_error.rs b/codex-rs/tools/src/function_call_error.rs new file mode 100644 index 000000000..3881c7af1 --- /dev/null +++ b/codex-rs/tools/src/function_call_error.rs @@ -0,0 +1,12 @@ +use thiserror::Error; + +/// Error returned while executing a model-visible tool invocation. +#[derive(Debug, Error, PartialEq)] +pub enum FunctionCallError { + #[error("{0}")] + RespondToModel(String), + #[error("LocalShellCall without call_id or id")] + MissingLocalShellCallId, + #[error("Fatal error: {0}")] + Fatal(String), +} diff --git a/codex-rs/tools/src/lib.rs b/codex-rs/tools/src/lib.rs index 8722796aa..554ee61af 100644 --- a/codex-rs/tools/src/lib.rs +++ b/codex-rs/tools/src/lib.rs @@ -3,6 +3,7 @@ mod code_mode; mod dynamic_tool; +mod function_call_error; mod image_detail; mod json_schema; mod mcp_tool; @@ -11,6 +12,9 @@ mod responses_api; mod tool_config; mod tool_definition; mod tool_discovery; +mod tool_executor; +mod tool_output; +mod tool_payload; mod tool_spec; pub use code_mode::augment_tool_spec_for_code_mode; @@ -20,6 +24,7 @@ pub use code_mode::collect_code_mode_tool_definitions; pub use code_mode::tool_spec_to_code_mode_tool_definition; pub use codex_protocol::ToolName; pub use dynamic_tool::parse_dynamic_tool; +pub use function_call_error::FunctionCallError; pub use image_detail::can_request_original_image_detail; pub use image_detail::normalize_output_image_detail; pub use image_detail::sanitize_original_image_detail; @@ -71,6 +76,9 @@ pub use tool_discovery::TOOL_SEARCH_TOOL_NAME; pub use tool_discovery::ToolSearchSourceInfo; pub use tool_discovery::collect_request_plugin_install_entries; pub use tool_discovery::filter_request_plugin_install_discoverable_tools_for_client; +pub use tool_executor::ToolExecutor; +pub use tool_output::ToolOutput; +pub use tool_payload::ToolPayload; pub use tool_spec::ResponsesApiWebSearchFilters; pub use tool_spec::ResponsesApiWebSearchUserLocation; pub use tool_spec::ToolSpec; diff --git a/codex-rs/tools/src/tool_executor.rs b/codex-rs/tools/src/tool_executor.rs new file mode 100644 index 000000000..8c38e9fc8 --- /dev/null +++ b/codex-rs/tools/src/tool_executor.rs @@ -0,0 +1,31 @@ +use std::future::Future; + +use crate::FunctionCallError; +use crate::ToolName; +use crate::ToolOutput; +use crate::ToolSpec; + +/// Shared runtime contract for model-visible tools. +/// +/// Implementations keep the model-visible spec tied to the executable runtime. +/// Host crates can layer routing, hooks, telemetry, or other orchestration on +/// top without reopening the spec/runtime split. +pub trait ToolExecutor: Send + Sync { + type Output: ToolOutput + 'static; + + /// The concrete tool name handled by this runtime instance. + fn tool_name(&self) -> ToolName; + + fn spec(&self) -> Option { + None + } + + fn supports_parallel_tool_calls(&self) -> bool { + false + } + + fn handle( + &self, + invocation: Invocation, + ) -> impl Future> + Send; +} diff --git a/codex-rs/tools/src/tool_output.rs b/codex-rs/tools/src/tool_output.rs new file mode 100644 index 000000000..8fc99d786 --- /dev/null +++ b/codex-rs/tools/src/tool_output.rs @@ -0,0 +1,156 @@ +use codex_protocol::models::DEFAULT_IMAGE_DETAIL; +use codex_protocol::models::FunctionCallOutputBody; +use codex_protocol::models::FunctionCallOutputContentItem; +use codex_protocol::models::ResponseInputItem; +use codex_utils_string::take_bytes_at_char_boundary; +use serde_json::Value as JsonValue; + +use crate::ToolPayload; + +const TELEMETRY_PREVIEW_MAX_BYTES: usize = 2 * 1024; +const TELEMETRY_PREVIEW_MAX_LINES: usize = 64; +const TELEMETRY_PREVIEW_TRUNCATION_NOTICE: &str = "[... telemetry preview truncated ...]"; + +/// Model-facing output contract returned by executable tool runtimes. +pub trait ToolOutput: Send { + fn log_preview(&self) -> String; + + fn success_for_logging(&self) -> bool; + + fn to_response_item(&self, call_id: &str, payload: &ToolPayload) -> ResponseInputItem; + + /// Returns the stable value exposed to `PostToolUse` hooks for this tool output. + /// + /// Tool handlers decide whether a tool participates in `PostToolUse`, but + /// this method lets the output type own any conversion from model-facing + /// response content to hook-facing data. Returning `None` means the output + /// should not produce a post-use hook payload, not merely that the tool had + /// empty output. + fn post_tool_use_response(&self, _call_id: &str, _payload: &ToolPayload) -> Option { + None + } + + fn code_mode_result(&self, payload: &ToolPayload) -> JsonValue { + response_input_to_code_mode_result(self.to_response_item("", payload)) + } +} + +impl ToolOutput for codex_protocol::mcp::CallToolResult { + fn log_preview(&self) -> String { + let output = self.as_function_call_output_payload(); + let preview = output.body.to_text().unwrap_or_else(|| output.to_string()); + telemetry_preview(&preview) + } + + fn success_for_logging(&self) -> bool { + self.success() + } + + fn to_response_item(&self, call_id: &str, _payload: &ToolPayload) -> ResponseInputItem { + ResponseInputItem::McpToolCallOutput { + call_id: call_id.to_string(), + output: self.clone(), + } + } + + fn code_mode_result(&self, _payload: &ToolPayload) -> JsonValue { + serde_json::to_value(self).unwrap_or_else(|err| { + JsonValue::String(format!("failed to serialize mcp result: {err}")) + }) + } +} + +fn response_input_to_code_mode_result(response: ResponseInputItem) -> JsonValue { + match response { + ResponseInputItem::Message { content, .. } => content_items_to_code_mode_result( + &content + .into_iter() + .map(|item| match item { + codex_protocol::models::ContentItem::InputText { text } + | codex_protocol::models::ContentItem::OutputText { text } => { + FunctionCallOutputContentItem::InputText { text } + } + codex_protocol::models::ContentItem::InputImage { image_url, detail } => { + FunctionCallOutputContentItem::InputImage { + image_url, + detail: detail.or(Some(DEFAULT_IMAGE_DETAIL)), + } + } + }) + .collect::>(), + ), + ResponseInputItem::FunctionCallOutput { output, .. } + | ResponseInputItem::CustomToolCallOutput { output, .. } => match output.body { + FunctionCallOutputBody::Text(text) => JsonValue::String(text), + FunctionCallOutputBody::ContentItems(items) => { + content_items_to_code_mode_result(&items) + } + }, + ResponseInputItem::ToolSearchOutput { tools, .. } => JsonValue::Array(tools), + ResponseInputItem::McpToolCallOutput { output, .. } => serde_json::to_value(output) + .unwrap_or_else(|err| { + JsonValue::String(format!("failed to serialize mcp result: {err}")) + }), + } +} + +fn content_items_to_code_mode_result(items: &[FunctionCallOutputContentItem]) -> JsonValue { + JsonValue::String( + items + .iter() + .filter_map(|item| match item { + FunctionCallOutputContentItem::InputText { text } if !text.trim().is_empty() => { + Some(text.clone()) + } + FunctionCallOutputContentItem::InputImage { image_url, .. } + if !image_url.trim().is_empty() => + { + Some(image_url.clone()) + } + FunctionCallOutputContentItem::InputText { .. } + | FunctionCallOutputContentItem::InputImage { .. } => None, + }) + .collect::>() + .join("\n"), + ) +} + +fn telemetry_preview(content: &str) -> String { + let truncated_slice = take_bytes_at_char_boundary(content, TELEMETRY_PREVIEW_MAX_BYTES); + let truncated_by_bytes = truncated_slice.len() < content.len(); + + let mut preview = String::new(); + let mut lines_iter = truncated_slice.lines(); + for idx in 0..TELEMETRY_PREVIEW_MAX_LINES { + match lines_iter.next() { + Some(line) => { + if idx > 0 { + preview.push('\n'); + } + preview.push_str(line); + } + None => break, + } + } + let truncated_by_lines = lines_iter.next().is_some(); + + if !truncated_by_bytes && !truncated_by_lines { + return content.to_string(); + } + + if preview.len() < truncated_slice.len() + && truncated_slice + .as_bytes() + .get(preview.len()) + .is_some_and(|byte| *byte == b'\n') + { + preview.push('\n'); + } + + if !preview.is_empty() && !preview.ends_with('\n') { + preview.push('\n'); + } + preview.push_str(TELEMETRY_PREVIEW_TRUNCATION_NOTICE); + + preview +} diff --git a/codex-rs/tools/src/tool_payload.rs b/codex-rs/tools/src/tool_payload.rs new file mode 100644 index 000000000..b335e5837 --- /dev/null +++ b/codex-rs/tools/src/tool_payload.rs @@ -0,0 +1,24 @@ +use std::borrow::Cow; + +use codex_protocol::models::SearchToolCallParams; +use codex_protocol::models::ShellToolCallParams; + +/// Canonical payload shapes accepted by model-visible tool runtimes. +#[derive(Clone, Debug)] +pub enum ToolPayload { + Function { arguments: String }, + ToolSearch { arguments: SearchToolCallParams }, + Custom { input: String }, + LocalShell { params: ShellToolCallParams }, +} + +impl ToolPayload { + pub fn log_payload(&self) -> Cow<'_, str> { + match self { + ToolPayload::Function { arguments } => Cow::Borrowed(arguments), + ToolPayload::ToolSearch { arguments } => Cow::Owned(arguments.query.clone()), + ToolPayload::Custom { input } => Cow::Borrowed(input), + ToolPayload::LocalShell { params } => Cow::Owned(params.command.join(" ")), + } + } +}