From 8d720feb698341c9a1599253d5e903cd99c603cb Mon Sep 17 00:00:00 2001 From: jif-oai Date: Tue, 2 Jun 2026 00:24:41 +0200 Subject: [PATCH] Move tool search metadata onto ToolExecutor (#25684) Deferred tools need to be searchable even when they are not implemented inside `codex-core`. Extension-provided tools can be registered for later discovery, but the search metadata path was still owned by core-specific runtime hooks, which meant the shared `ToolExecutor` abstraction could not describe how a deferred extension tool should appear in `tool_search`. ## Changes - Move `ToolSearchEntry` and `ToolSearchInfo` into `codex-tools` and re-export them from the shared tools crate. - Add a default `ToolExecutor::search_info` implementation that derives loadable tool-search metadata from function and namespace specs. - Forward search metadata through extension adapters and exposure overrides while keeping custom search text/source metadata for dynamic, MCP, and multi-agent tools. - Remove the old core-local `tool_search_entry` module now that search metadata lives with the shared executor APIs. ## Testing - Added `deferred_extension_tools_are_discoverable_with_tool_search` coverage in `core/src/tools/spec_plan_tests.rs`. --- codex-rs/core/src/tools/handlers/dynamic.rs | 26 ++-- .../src/tools/handlers/extension_tools.rs | 5 + codex-rs/core/src/tools/handlers/mcp.rs | 54 +++---- .../core/src/tools/handlers/multi_agents.rs | 2 +- .../handlers/multi_agents/close_agent.rs | 14 +- .../handlers/multi_agents/resume_agent.rs | 14 +- .../tools/handlers/multi_agents/send_input.rs | 14 +- .../src/tools/handlers/multi_agents/spawn.rs | 14 +- .../src/tools/handlers/multi_agents/wait.rs | 14 +- .../core/src/tools/handlers/tool_search.rs | 4 +- codex-rs/core/src/tools/mod.rs | 1 - codex-rs/core/src/tools/registry.rs | 14 +- codex-rs/core/src/tools/spec_plan.rs | 9 +- codex-rs/core/src/tools/spec_plan_tests.rs | 57 +++++++ codex-rs/core/src/tools/tool_search_entry.rs | 56 ------- codex-rs/tools/src/lib.rs | 4 + codex-rs/tools/src/tool_executor.rs | 10 +- codex-rs/tools/src/tool_search.rs | 139 ++++++++++++++++++ 18 files changed, 302 insertions(+), 149 deletions(-) delete mode 100644 codex-rs/core/src/tools/tool_search_entry.rs create mode 100644 codex-rs/tools/src/tool_search.rs diff --git a/codex-rs/core/src/tools/handlers/dynamic.rs b/codex-rs/core/src/tools/handlers/dynamic.rs index e5e0d1053..a74f44fd6 100644 --- a/codex-rs/core/src/tools/handlers/dynamic.rs +++ b/codex-rs/core/src/tools/handlers/dynamic.rs @@ -9,7 +9,6 @@ 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; @@ -20,6 +19,7 @@ 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; @@ -75,6 +75,17 @@ impl ToolExecutor for DynamicToolHandler { self.exposure } + 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, @@ -126,18 +137,7 @@ impl ToolExecutor for DynamicToolHandler { } } -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()), - }), - ) - } -} +impl CoreToolRuntime for DynamicToolHandler {} #[expect( clippy::await_holding_invalid_type, diff --git a/codex-rs/core/src/tools/handlers/extension_tools.rs b/codex-rs/core/src/tools/handlers/extension_tools.rs index 95b272bbf..4ad88ac5b 100644 --- a/codex-rs/core/src/tools/handlers/extension_tools.rs +++ b/codex-rs/core/src/tools/handlers/extension_tools.rs @@ -7,6 +7,7 @@ use codex_tools::ExtensionTurnItem; use codex_tools::ImageGenerationCompletionFuture; use codex_tools::ToolCall as ExtensionToolCall; use codex_tools::ToolName; +use codex_tools::ToolSearchInfo; use codex_tools::ToolSpec; use codex_tools::TurnItemEmissionFuture; use codex_tools::TurnItemEmitter; @@ -49,6 +50,10 @@ impl ToolExecutor for ExtensionToolAdapter { self.0.supports_parallel_tool_calls() } + fn search_info(&self) -> Option { + self.0.search_info() + } + async fn handle( &self, invocation: ToolInvocation, diff --git a/codex-rs/core/src/tools/handlers/mcp.rs b/codex-rs/core/src/tools/handlers/mcp.rs index 8f18d5b55..77ff3c14a 100644 --- a/codex-rs/core/src/tools/handlers/mcp.rs +++ b/codex-rs/core/src/tools/handlers/mcp.rs @@ -15,11 +15,11 @@ use crate::tools::registry::PostToolUsePayload; use crate::tools::registry::PreToolUsePayload; use crate::tools::registry::ToolExecutor; use crate::tools::registry::ToolTelemetryTags; -use crate::tools::tool_search_entry::ToolSearchInfo; use codex_mcp::ToolInfo; 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::mcp_tool_to_responses_api_tool; @@ -87,6 +87,32 @@ impl ToolExecutor for McpHandler { .unwrap_or(false) } + fn search_info(&self) -> Option { + let source_name = self + .tool_info + .connector_name + .as_deref() + .map(str::trim) + .filter(|connector_name| !connector_name.is_empty()) + .unwrap_or_else(|| self.tool_info.server_name.trim()); + let source_info = (!source_name.is_empty()).then(|| ToolSearchSourceInfo { + name: source_name.to_string(), + description: self + .tool_info + .namespace_description + .as_deref() + .map(str::trim) + .filter(|description| !description.is_empty()) + .map(str::to_string), + }); + + ToolSearchInfo::from_spec( + build_mcp_search_text(&self.tool_info), + self.spec(), + source_info, + ) + } + async fn handle( &self, invocation: ToolInvocation, @@ -131,32 +157,6 @@ impl ToolExecutor for McpHandler { } impl CoreToolRuntime for McpHandler { - fn search_info(&self) -> Option { - let source_name = self - .tool_info - .connector_name - .as_deref() - .map(str::trim) - .filter(|connector_name| !connector_name.is_empty()) - .unwrap_or_else(|| self.tool_info.server_name.trim()); - let source_info = (!source_name.is_empty()).then(|| ToolSearchSourceInfo { - name: source_name.to_string(), - description: self - .tool_info - .namespace_description - .as_deref() - .map(str::trim) - .filter(|description| !description.is_empty()) - .map(str::to_string), - }); - - ToolSearchInfo::from_spec( - build_mcp_search_text(&self.tool_info), - self.spec(), - source_info, - ) - } - fn telemetry_tags<'a>( &'a self, _invocation: &'a ToolInvocation, diff --git a/codex-rs/core/src/tools/handlers/multi_agents.rs b/codex-rs/core/src/tools/handlers/multi_agents.rs index 00176572e..8bc96d19c 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents.rs @@ -19,7 +19,6 @@ use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE; use crate::tools::handlers::parse_arguments; use crate::tools::registry::CoreToolRuntime; use crate::tools::registry::ToolExecutor; -use crate::tools::tool_search_entry::ToolSearchInfo; use codex_protocol::ThreadId; use codex_protocol::models::ResponseInputItem; use codex_protocol::openai_models::ReasoningEffort; @@ -36,6 +35,7 @@ use codex_protocol::protocol::CollabWaitingBeginEvent; use codex_protocol::protocol::CollabWaitingEndEvent; use codex_protocol::user_input::UserInput; use codex_tools::ToolName; +use codex_tools::ToolSearchInfo; use codex_tools::ToolSearchSourceInfo; use serde::Deserialize; use serde::Serialize; 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 bcfa5231b..2007e3df7 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 @@ -16,6 +16,13 @@ impl ToolExecutor for Handler { create_close_agent_tool_v1() } + fn search_info(&self) -> Option { + multi_agent_tool_search_info( + "close_agent close shutdown stop agent subagent thread status target", + self.spec(), + ) + } + async fn handle( &self, invocation: ToolInvocation, @@ -109,13 +116,6 @@ async fn handle_close_agent( } impl CoreToolRuntime for Handler { - fn search_info(&self) -> Option { - multi_agent_tool_search_info( - "close_agent close shutdown stop agent subagent thread status target", - self.spec(), - ) - } - fn matches_kind(&self, payload: &ToolPayload) -> bool { matches!(payload, ToolPayload::Function { .. }) } 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 a536c8d70..ebe3678eb 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 @@ -17,6 +17,13 @@ impl ToolExecutor for Handler { create_resume_agent_tool() } + fn search_info(&self) -> Option { + multi_agent_tool_search_info( + "resume_agent resume reopen closed agent subagent thread id target", + self.spec(), + ) + } + async fn handle( &self, invocation: ToolInvocation, @@ -135,13 +142,6 @@ async fn handle_resume_agent( } impl CoreToolRuntime for Handler { - fn search_info(&self) -> Option { - multi_agent_tool_search_info( - "resume_agent resume reopen closed agent subagent thread id target", - self.spec(), - ) - } - fn matches_kind(&self, payload: &ToolPayload) -> bool { matches!(payload, ToolPayload::Function { .. }) } 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 8499bac52..d7f316ba3 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 @@ -16,6 +16,13 @@ impl ToolExecutor for Handler { create_send_input_tool_v1() } + fn search_info(&self) -> Option { + multi_agent_tool_search_info( + "send_input send message existing agent subagent follow up interrupt redirect queue target", + self.spec(), + ) + } + async fn handle( &self, invocation: ToolInvocation, @@ -91,13 +98,6 @@ impl ToolExecutor for Handler { } impl CoreToolRuntime for Handler { - fn search_info(&self) -> Option { - multi_agent_tool_search_info( - "send_input send message existing agent subagent follow up interrupt redirect queue target", - self.spec(), - ) - } - fn matches_kind(&self, payload: &ToolPayload) -> bool { matches!(payload, ToolPayload::Function { .. }) } 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 c4120dd5a..56707fb0c 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/spawn.rs @@ -32,6 +32,13 @@ impl ToolExecutor for Handler { create_spawn_agent_tool_v1(self.options.clone()) } + fn search_info(&self) -> Option { + multi_agent_tool_search_info( + "spawn_agent spawn agent subagent sub-agent delegate delegation parallel work worker explorer no-apps fork model reasoning", + self.spec(), + ) + } + async fn handle( &self, invocation: ToolInvocation, @@ -204,13 +211,6 @@ async fn handle_spawn_agent( } impl CoreToolRuntime for Handler { - fn search_info(&self) -> Option { - multi_agent_tool_search_info( - "spawn_agent spawn agent subagent sub-agent delegate delegation parallel work worker explorer no-apps fork model reasoning", - self.spec(), - ) - } - fn matches_kind(&self, payload: &ToolPayload) -> bool { matches!(payload, ToolPayload::Function { .. }) } 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 2e4fb6100..063b06457 100644 --- a/codex-rs/core/src/tools/handlers/multi_agents/wait.rs +++ b/codex-rs/core/src/tools/handlers/multi_agents/wait.rs @@ -37,6 +37,13 @@ impl ToolExecutor for Handler { create_wait_agent_tool_v1(self.options) } + fn search_info(&self) -> Option { + multi_agent_tool_search_info( + "wait_agent wait agent subagent status final result complete timeout targets", + self.spec(), + ) + } + async fn handle( &self, invocation: ToolInvocation, @@ -203,13 +210,6 @@ impl ToolExecutor for Handler { } impl CoreToolRuntime for Handler { - fn search_info(&self) -> Option { - multi_agent_tool_search_info( - "wait_agent wait agent subagent status final result complete timeout targets", - self.spec(), - ) - } - fn matches_kind(&self, payload: &ToolPayload) -> bool { matches!(payload, ToolPayload::Function { .. }) } diff --git a/codex-rs/core/src/tools/handlers/tool_search.rs b/codex-rs/core/src/tools/handlers/tool_search.rs index 442d77ce8..2de1e20dd 100644 --- a/codex-rs/core/src/tools/handlers/tool_search.rs +++ b/codex-rs/core/src/tools/handlers/tool_search.rs @@ -6,8 +6,6 @@ use crate::tools::context::boxed_tool_output; use crate::tools::handlers::tool_search_spec::create_tool_search_tool; use crate::tools::registry::CoreToolRuntime; use crate::tools::registry::ToolExecutor; -use crate::tools::tool_search_entry::ToolSearchEntry; -use crate::tools::tool_search_entry::ToolSearchInfo; use bm25::Document; use bm25::Language; use bm25::SearchEngine; @@ -16,6 +14,8 @@ use codex_tools::LoadableToolSpec; use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT; use codex_tools::TOOL_SEARCH_TOOL_NAME; use codex_tools::ToolName; +use codex_tools::ToolSearchEntry; +use codex_tools::ToolSearchInfo; use codex_tools::ToolSearchSourceInfo; use codex_tools::ToolSpec; use codex_tools::coalesce_loadable_tool_specs; diff --git a/codex-rs/core/src/tools/mod.rs b/codex-rs/core/src/tools/mod.rs index 5b7a17f42..cd10d246d 100644 --- a/codex-rs/core/src/tools/mod.rs +++ b/codex-rs/core/src/tools/mod.rs @@ -14,7 +14,6 @@ pub(crate) mod runtimes; pub(crate) mod sandboxing; pub(crate) mod spec_plan; pub(crate) mod tool_dispatch_trace; -pub(crate) mod tool_search_entry; use std::borrow::Cow; diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index f0fdf424a..b2a0b7704 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -24,13 +24,13 @@ use crate::tools::hook_names::HookToolName; use crate::tools::lifecycle::notify_tool_finish; use crate::tools::lifecycle::notify_tool_start; use crate::tools::tool_dispatch_trace::ToolDispatchTrace; -use crate::tools::tool_search_entry::ToolSearchInfo; use crate::util::error_or_panic; use codex_extension_api::ToolCallOutcome; use codex_protocol::models::FunctionCallOutputPayload; use codex_protocol::models::ResponseInputItem; use codex_protocol::protocol::EventMsg; use codex_tools::ToolName; +use codex_tools::ToolSearchInfo; use codex_tools::ToolSpec; use futures::future::BoxFuture; use serde_json::Value; @@ -46,10 +46,6 @@ pub use codex_tools::ToolExposure; /// Implementers provide the shared `ToolExecutor` behavior plus optional /// core-owned metadata for hooks, telemetry, tool search, and argument diffs. pub(crate) trait CoreToolRuntime: ToolExecutor { - fn search_info(&self) -> Option { - None - } - fn matches_kind(&self, payload: &ToolPayload) -> bool { matches!( payload, @@ -273,6 +269,10 @@ impl ToolExecutor for ExposureOverride { self.exposure != ToolExposure::Hidden && self.handler.supports_parallel_tool_calls() } + fn search_info(&self) -> Option { + self.handler.search_info() + } + async fn handle( &self, invocation: ToolInvocation, @@ -282,10 +282,6 @@ impl ToolExecutor for ExposureOverride { } impl CoreToolRuntime for ExposureOverride { - fn search_info(&self) -> Option { - self.handler.search_info() - } - fn matches_kind(&self, payload: &ToolPayload) -> bool { self.handler.matches_kind(payload) } diff --git a/codex-rs/core/src/tools/spec_plan.rs b/codex-rs/core/src/tools/spec_plan.rs index c7d9ae5ae..56d371407 100644 --- a/codex-rs/core/src/tools/spec_plan.rs +++ b/codex-rs/core/src/tools/spec_plan.rs @@ -72,6 +72,7 @@ use codex_tools::ToolEnvironmentMode; use codex_tools::ToolExecutor; use codex_tools::ToolName; use codex_tools::ToolOutput; +use codex_tools::ToolSearchInfo; use codex_tools::ToolSpec; use codex_tools::UnifiedExecShellMode; use codex_tools::can_request_original_image_detail; @@ -937,6 +938,10 @@ impl ToolExecutor for MultiAgentV2NamespaceOverride { self.handler.supports_parallel_tool_calls() } + fn search_info(&self) -> Option { + self.handler.search_info() + } + async fn handle( &self, invocation: ToolInvocation, @@ -950,10 +955,6 @@ impl CoreToolRuntime for MultiAgentV2NamespaceOverride { self.handler.matches_kind(payload) } - fn search_info(&self) -> Option { - self.handler.search_info() - } - fn create_diff_consumer( &self, ) -> Option> { diff --git a/codex-rs/core/src/tools/spec_plan_tests.rs b/codex-rs/core/src/tools/spec_plan_tests.rs index e129e4dda..b75e6d2ed 100644 --- a/codex-rs/core/src/tools/spec_plan_tests.rs +++ b/codex-rs/core/src/tools/spec_plan_tests.rs @@ -313,6 +313,44 @@ impl ToolExecutor for WebRunExtensionTool { } } +struct DeferredExtensionTool; + +#[async_trait::async_trait] +impl ToolExecutor for DeferredExtensionTool { + fn tool_name(&self) -> ToolName { + ToolName::plain("extension_echo") + } + + fn spec(&self) -> ToolSpec { + ToolSpec::Function(ResponsesApiTool { + name: "extension_echo".to_string(), + description: "Echoes arguments through an extension tool.".to_string(), + strict: true, + defer_loading: None, + parameters: codex_tools::JsonSchema::object( + BTreeMap::from([( + "message".to_string(), + codex_tools::JsonSchema::string(/*description*/ None), + )]), + Some(vec!["message".to_string()]), + Some(false.into()), + ), + output_schema: None, + }) + } + + fn exposure(&self) -> ToolExposure { + ToolExposure::Deferred + } + + async fn handle( + &self, + _call: ExtensionToolCall, + ) -> Result, codex_tools::FunctionCallError> { + panic!("spec planning should not execute extension tools") + } +} + fn duplicate_primary_environment(turn: &mut TurnContext) { let mut second_environment = turn.environments.turn_environments[0].clone(); second_environment.environment_id = "secondary".to_string(); @@ -702,6 +740,25 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() { ]); } +#[tokio::test] +async fn deferred_extension_tools_are_discoverable_with_tool_search() { + let plan = probe_with( + |turn| { + turn.model_info.supports_search_tool = true; + }, + ToolPlanInputs { + extension_tool_executors: vec![Arc::new(DeferredExtensionTool)], + ..ToolPlanInputs::default() + }, + ) + .await; + + plan.assert_visible_contains(&["tool_search"]); + plan.assert_visible_lacks(&["extension_echo"]); + plan.assert_registered_contains(&["extension_echo"]); + assert_eq!(plan.exposure("extension_echo"), ToolExposure::Deferred); +} + #[tokio::test] async fn invalid_mcp_tools_are_not_registered() { let plan = probe_with( diff --git a/codex-rs/core/src/tools/tool_search_entry.rs b/codex-rs/core/src/tools/tool_search_entry.rs deleted file mode 100644 index 8e37cdcff..000000000 --- a/codex-rs/core/src/tools/tool_search_entry.rs +++ /dev/null @@ -1,56 +0,0 @@ -use codex_tools::LoadableToolSpec; -use codex_tools::ResponsesApiNamespaceTool; -use codex_tools::ToolSearchSourceInfo; -use codex_tools::ToolSpec; -use codex_tools::default_namespace_description; - -#[derive(Clone)] -pub(crate) struct ToolSearchEntry { - pub(crate) search_text: String, - pub(crate) output: LoadableToolSpec, -} - -#[derive(Clone)] -pub(crate) struct ToolSearchInfo { - pub(crate) entry: ToolSearchEntry, - pub(crate) source_info: Option, -} - -impl ToolSearchInfo { - pub(crate) fn from_spec( - search_text: String, - spec: ToolSpec, - source_info: Option, - ) -> Option { - let output = match spec { - ToolSpec::Function(mut tool) => { - tool.defer_loading = Some(true); - tool.output_schema = None; - LoadableToolSpec::Function(tool) - } - ToolSpec::Namespace(mut namespace) => { - if namespace.description.trim().is_empty() { - namespace.description = default_namespace_description(&namespace.name); - } - for tool in &mut namespace.tools { - let ResponsesApiNamespaceTool::Function(tool) = tool; - tool.defer_loading = Some(true); - tool.output_schema = None; - } - LoadableToolSpec::Namespace(namespace) - } - ToolSpec::ToolSearch { .. } - | ToolSpec::ImageGeneration { .. } - | ToolSpec::WebSearch { .. } - | ToolSpec::Freeform(_) => return None, - }; - - Some(Self { - entry: ToolSearchEntry { - search_text, - output, - }, - source_info, - }) - } -} diff --git a/codex-rs/tools/src/lib.rs b/codex-rs/tools/src/lib.rs index 3adae5923..28c4f93bb 100644 --- a/codex-rs/tools/src/lib.rs +++ b/codex-rs/tools/src/lib.rs @@ -17,6 +17,7 @@ mod tool_discovery; mod tool_executor; mod tool_output; mod tool_payload; +mod tool_search; mod tool_spec; pub use code_mode::augment_tool_spec_for_code_mode; @@ -97,6 +98,9 @@ pub use tool_executor::ToolExposure; pub use tool_output::JsonToolOutput; pub use tool_output::ToolOutput; pub use tool_payload::ToolPayload; +pub use tool_search::ToolSearchEntry; +pub use tool_search::ToolSearchInfo; +pub use tool_search::default_tool_search_text; 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 index 1e5a94ee4..541d2443e 100644 --- a/codex-rs/tools/src/tool_executor.rs +++ b/codex-rs/tools/src/tool_executor.rs @@ -1,6 +1,7 @@ use crate::FunctionCallError; use crate::ToolName; use crate::ToolOutput; +use crate::ToolSearchInfo; use crate::ToolSpec; /// Controls where a tool is exposed to the model. @@ -13,7 +14,9 @@ pub enum ToolExposure { Direct, /// Register this tool for later discovery, but omit it from the initial - /// model-visible tool list. + /// model-visible tool list. Deferred tools must provide search metadata via + /// [`ToolExecutor::search_info`]. The default implementation derives + /// metadata from function and namespace specs. Deferred, /// Include this tool in the initial model-visible tool list only. @@ -48,6 +51,11 @@ pub trait ToolExecutor: Send + Sync { ToolExposure::Direct } + fn search_info(&self) -> Option { + let spec = self.spec(); + ToolSearchInfo::from_tool_spec(&self.tool_name(), spec, /*source_info*/ None) + } + fn supports_parallel_tool_calls(&self) -> bool { false } diff --git a/codex-rs/tools/src/tool_search.rs b/codex-rs/tools/src/tool_search.rs new file mode 100644 index 000000000..ecb5504d4 --- /dev/null +++ b/codex-rs/tools/src/tool_search.rs @@ -0,0 +1,139 @@ +use crate::JsonSchema; +use crate::LoadableToolSpec; +use crate::ResponsesApiNamespaceTool; +use crate::ResponsesApiTool; +use crate::ToolName; +use crate::ToolSearchSourceInfo; +use crate::ToolSpec; +use crate::default_namespace_description; + +#[derive(Clone)] +pub struct ToolSearchEntry { + pub search_text: String, + pub output: LoadableToolSpec, +} + +#[derive(Clone)] +pub struct ToolSearchInfo { + pub entry: ToolSearchEntry, + pub source_info: Option, +} + +impl ToolSearchInfo { + pub fn from_tool_spec( + tool_name: &ToolName, + spec: ToolSpec, + source_info: Option, + ) -> Option { + let search_text = default_tool_search_text(tool_name, &spec); + Self::from_spec(search_text, spec, source_info) + } + + pub fn from_spec( + search_text: String, + spec: ToolSpec, + source_info: Option, + ) -> Option { + let output = match spec { + ToolSpec::Function(mut tool) => { + tool.defer_loading = Some(true); + tool.output_schema = None; + LoadableToolSpec::Function(tool) + } + ToolSpec::Namespace(mut namespace) => { + if namespace.description.trim().is_empty() { + namespace.description = default_namespace_description(&namespace.name); + } + for tool in &mut namespace.tools { + let ResponsesApiNamespaceTool::Function(tool) = tool; + tool.defer_loading = Some(true); + tool.output_schema = None; + } + LoadableToolSpec::Namespace(namespace) + } + ToolSpec::ToolSearch { .. } + | ToolSpec::ImageGeneration { .. } + | ToolSpec::WebSearch { .. } + | ToolSpec::Freeform(_) => return None, + }; + + Some(Self { + entry: ToolSearchEntry { + search_text, + output, + }, + source_info, + }) + } +} + +pub fn default_tool_search_text(tool_name: &ToolName, spec: &ToolSpec) -> String { + let mut parts = Vec::new(); + push_search_part(&mut parts, tool_name.to_string()); + push_search_part(&mut parts, tool_name.name.replace('_', " ")); + if let Some(namespace) = &tool_name.namespace { + push_search_part(&mut parts, namespace.clone()); + } + + match spec { + ToolSpec::Function(tool) => append_function_search_text(tool, &mut parts), + ToolSpec::Namespace(namespace) => { + push_search_part(&mut parts, namespace.name.clone()); + push_search_part(&mut parts, namespace.description.clone()); + for tool in &namespace.tools { + let ResponsesApiNamespaceTool::Function(tool) = tool; + append_function_search_text(tool, &mut parts); + } + } + ToolSpec::ToolSearch { description, .. } => { + push_search_part(&mut parts, description.clone()); + } + ToolSpec::ImageGeneration { .. } => { + push_search_part(&mut parts, "image generation".to_string()); + } + ToolSpec::WebSearch { .. } => { + push_search_part(&mut parts, "web search".to_string()); + } + ToolSpec::Freeform(tool) => { + push_search_part(&mut parts, tool.name.clone()); + push_search_part(&mut parts, tool.description.clone()); + push_search_part(&mut parts, tool.format.syntax.clone()); + } + } + + parts.join(" ") +} + +fn append_function_search_text(tool: &ResponsesApiTool, parts: &mut Vec) { + push_search_part(parts, tool.name.clone()); + push_search_part(parts, tool.name.replace('_', " ")); + push_search_part(parts, tool.description.clone()); + append_schema_search_text(&tool.parameters, parts); +} + +fn append_schema_search_text(schema: &JsonSchema, parts: &mut Vec) { + if let Some(description) = &schema.description { + push_search_part(parts, description.clone()); + } + if let Some(properties) = &schema.properties { + for (name, schema) in properties { + push_search_part(parts, name.clone()); + append_schema_search_text(schema, parts); + } + } + if let Some(items) = &schema.items { + append_schema_search_text(items, parts); + } + if let Some(variants) = &schema.any_of { + for variant in variants { + append_schema_search_text(variant, parts); + } + } +} + +fn push_search_part(parts: &mut Vec, part: String) { + let part = part.trim(); + if !part.is_empty() { + parts.push(part.to_string()); + } +}