From f1825e874182b9bdf0d8bf7272a4b9418e2bc478 Mon Sep 17 00:00:00 2001 From: Steven Lee Date: Fri, 26 Jun 2026 05:08:19 +0000 Subject: [PATCH] Add app-server toolSearch debug RPC --- .../codex_app_server_protocol.schemas.json | 47 +++++ .../codex_app_server_protocol.v2.schemas.json | 47 +++++ .../typescript/v2/ToolSearchSearchResult.ts | 6 + .../schema/typescript/v2/index.ts | 1 + .../src/protocol/common.rs | 7 + .../src/protocol/v2/mod.rs | 2 + .../src/protocol/v2/tool_search.rs | 43 ++++ codex-rs/app-server/README.md | 1 + codex-rs/app-server/src/message_processor.rs | 3 + codex-rs/app-server/src/request_processors.rs | 3 + .../src/request_processors/mcp_processor.rs | 88 ++++++++ codex-rs/core/src/codex_thread.rs | 8 + codex-rs/core/src/lib.rs | 3 + codex-rs/core/src/session/mod.rs | 24 +++ codex-rs/core/src/tool_search_debug.rs | 23 +++ .../core/src/tools/handlers/tool_search.rs | 193 +++++++++++++++++- codex-rs/core/src/tools/registry.rs | 21 ++ codex-rs/core/src/tools/router.rs | 11 + 18 files changed, 525 insertions(+), 6 deletions(-) create mode 100644 codex-rs/app-server-protocol/schema/typescript/v2/ToolSearchSearchResult.ts create mode 100644 codex-rs/app-server-protocol/src/protocol/v2/tool_search.rs create mode 100644 codex-rs/core/src/tool_search_debug.rs diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 0eedef903a..b88b7e7cf2 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -19954,6 +19954,53 @@ ], "type": "object" }, + "ToolSearchSearchResult": { + "properties": { + "index": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "rank": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "score": { + "format": "float", + "type": [ + "number", + "null" + ] + }, + "searchableText": { + "type": "string" + }, + "sourceDescription": { + "type": [ + "string", + "null" + ] + }, + "sourceName": { + "type": [ + "string", + "null" + ] + }, + "tools": { + "items": true, + "type": "array" + } + }, + "required": [ + "index", + "rank", + "searchableText", + "tools" + ], + "type": "object" + }, "ToolsV2": { "properties": { "web_search": { diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index b428f25258..b804474294 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -17733,6 +17733,53 @@ ], "type": "object" }, + "ToolSearchSearchResult": { + "properties": { + "index": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "rank": { + "format": "uint32", + "minimum": 0.0, + "type": "integer" + }, + "score": { + "format": "float", + "type": [ + "number", + "null" + ] + }, + "searchableText": { + "type": "string" + }, + "sourceDescription": { + "type": [ + "string", + "null" + ] + }, + "sourceName": { + "type": [ + "string", + "null" + ] + }, + "tools": { + "items": true, + "type": "array" + } + }, + "required": [ + "index", + "rank", + "searchableText", + "tools" + ], + "type": "object" + }, "ToolsV2": { "properties": { "web_search": { diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ToolSearchSearchResult.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ToolSearchSearchResult.ts new file mode 100644 index 0000000000..bbc2f40ef2 --- /dev/null +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ToolSearchSearchResult.ts @@ -0,0 +1,6 @@ +// GENERATED CODE! DO NOT MODIFY BY HAND! + +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { JsonValue } from "../serde_json/JsonValue"; + +export type ToolSearchSearchResult = { rank: number, index: number, score: number | null, sourceName: string | null, sourceDescription: string | null, searchableText: string, tools: Array, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index c335f144ec..272869e324 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -474,6 +474,7 @@ export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption"; export type { ToolRequestUserInputParams } from "./ToolRequestUserInputParams"; export type { ToolRequestUserInputQuestion } from "./ToolRequestUserInputQuestion"; export type { ToolRequestUserInputResponse } from "./ToolRequestUserInputResponse"; +export type { ToolSearchSearchResult } from "./ToolSearchSearchResult"; export type { ToolsV2 } from "./ToolsV2"; export type { Turn } from "./Turn"; export type { TurnCompletedNotification } from "./TurnCompletedNotification"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index d8db99d4ff..33f0869006 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -969,6 +969,13 @@ client_request_definitions! { response: v2::ListMcpServerStatusResponse, }, + #[experimental("toolSearch/search")] + ToolSearchSearch => "toolSearch/search" { + params: v2::ToolSearchSearchParams, + serialization: thread_id(params.thread_id), + response: v2::ToolSearchSearchResponse, + }, + McpResourceRead => "mcpServer/resource/read" { params: v2::McpResourceReadParams, serialization: optional_thread_id(params.thread_id), diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mod.rs b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs index 1097b03811..581a2a6750 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mod.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs @@ -24,6 +24,7 @@ mod remote_control; mod review; mod thread; mod thread_data; +mod tool_search; mod turn; mod windows_sandbox; @@ -52,6 +53,7 @@ pub use review::*; pub use shared::*; pub use thread::*; pub use thread_data::*; +pub use tool_search::*; pub use turn::*; pub use windows_sandbox::*; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tool_search.rs b/codex-rs/app-server-protocol/src/protocol/v2/tool_search.rs new file mode 100644 index 0000000000..fd14a1ebfa --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/tool_search.rs @@ -0,0 +1,43 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use serde_json::Value as JsonValue; +use ts_rs::TS; + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ToolSearchSearchParams { + pub thread_id: String, + pub query: String, + /// Optional result limit. Defaults to the runtime `tool_search` default. + #[ts(optional = nullable)] + pub limit: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ToolSearchSearchResponse { + pub indexed_tool_count: u32, + pub matching_tool_count: u32, + pub requested_limit: u32, + pub effective_limit: u32, + pub top_k_truncated: bool, + /// The exact coalesced tool payload current `tool_search` returns for this query. + pub tools: Vec, + pub results: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ToolSearchSearchResult { + pub rank: u32, + pub index: u32, + pub score: Option, + pub source_name: Option, + pub source_description: Option, + pub searchable_text: String, + pub tools: Vec, +} diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index a7986753ee..428b59e26a 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -234,6 +234,7 @@ Example with notification opt-out: - `tool/requestUserInput` — prompt the user with 1–3 short questions for a tool call and return their answers (experimental). - `config/mcpServer/reload` — reload MCP server config from disk and queue a refresh for loaded threads (applied on each thread's next active turn); returns `{}`. Use this after editing `config.toml` without restarting the server. - `mcpServerStatus/list` — enumerate configured MCP servers with their tools, auth status, server info, plus resources/resource templates for `full` detail; supports optional `threadId` and cursor+limit pagination. If `threadId` is omitted, the server reads from the latest global config directly. If `detail` is omitted, the server defaults to `full`. +- `toolSearch/search` — experimental; run the current runtime `tool_search` path for a thread by `threadId`, `query`, and optional `limit`, returning the exact coalesced tools payload plus debug metadata such as indexed/matching counts, truncation, rank, score, source, and searchable text. - `mcpServer/resource/read` — read a resource from a configured MCP server by optional `threadId`, `server`, and `uri`, returning text/blob resource `contents`. If `threadId` is omitted, the server reads from the latest MCP config directly. - `mcpServer/tool/call` — call a tool on a thread's configured MCP server by `threadId`, `server`, `tool`, optional `arguments`, and optional `_meta`, returning the MCP tool result. - `windowsSandbox/setupStart` — start Windows sandbox setup for the selected mode (`elevated` or `unelevated`); accepts an optional absolute `cwd` to target setup for a specific workspace, returns `{ started: true }` immediately, and later emits `windowsSandbox/setupCompleted`. diff --git a/codex-rs/app-server/src/message_processor.rs b/codex-rs/app-server/src/message_processor.rs index 6ae73784ff..f19a81facf 100644 --- a/codex-rs/app-server/src/message_processor.rs +++ b/codex-rs/app-server/src/message_processor.rs @@ -1386,6 +1386,9 @@ impl MessageProcessor { .mcp_server_status_list(&request_id, params) .await } + ClientRequest::ToolSearchSearch { params, .. } => { + self.mcp_processor.tool_search_search(params).await + } ClientRequest::McpResourceRead { params, .. } => { self.mcp_processor .mcp_resource_read(&request_id, params) diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index f03351468f..280bdc6b9d 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -274,6 +274,9 @@ use codex_app_server_protocol::ThreadUnarchivedNotification; use codex_app_server_protocol::ThreadUnsubscribeParams; use codex_app_server_protocol::ThreadUnsubscribeResponse; use codex_app_server_protocol::ThreadUnsubscribeStatus; +use codex_app_server_protocol::ToolSearchSearchParams; +use codex_app_server_protocol::ToolSearchSearchResponse; +use codex_app_server_protocol::ToolSearchSearchResult; use codex_app_server_protocol::Turn; use codex_app_server_protocol::TurnEnvironmentParams; use codex_app_server_protocol::TurnError; diff --git a/codex-rs/app-server/src/request_processors/mcp_processor.rs b/codex-rs/app-server/src/request_processors/mcp_processor.rs index ac2125c2cf..a7eef53c01 100644 --- a/codex-rs/app-server/src/request_processors/mcp_processor.rs +++ b/codex-rs/app-server/src/request_processors/mcp_processor.rs @@ -53,6 +53,15 @@ impl McpRequestProcessor { .map(|()| None) } + pub(crate) async fn tool_search_search( + &self, + params: ToolSearchSearchParams, + ) -> Result, JSONRPCErrorError> { + self.tool_search_search_response(params) + .await + .map(|response| Some(response.into())) + } + pub(crate) async fn mcp_resource_read( &self, request_id: &ConnectionRequestId, @@ -109,6 +118,32 @@ impl McpRequestProcessor { Ok((thread_id, thread)) } + async fn tool_search_search_response( + &self, + params: ToolSearchSearchParams, + ) -> Result { + let ToolSearchSearchParams { + thread_id, + query, + limit, + } = params; + let query = query.trim(); + if query.is_empty() { + return Err(invalid_request("query must not be empty")); + } + let limit = limit.unwrap_or(codex_tools::TOOL_SEARCH_DEFAULT_LIMIT as u32); + if limit == 0 { + return Err(invalid_request("limit must be greater than zero")); + } + + let (_, thread) = self.load_thread(&thread_id).await?; + let result = thread + .search_tool_search(query, limit as usize) + .await + .map_err(|err| internal_error(format!("failed to search tool_search: {err}")))?; + tool_search_result_to_response(result) + } + async fn mcp_server_oauth_login_response( &self, params: McpServerOauthLoginParams, @@ -475,6 +510,59 @@ impl McpRequestProcessor { } } +fn tool_search_result_to_response( + result: codex_core::ToolSearchDebugResult, +) -> Result { + Ok(ToolSearchSearchResponse { + indexed_tool_count: usize_to_u32(result.indexed_tool_count, "indexedToolCount")?, + matching_tool_count: usize_to_u32(result.matching_tool_count, "matchingToolCount")?, + requested_limit: usize_to_u32(result.requested_limit, "requestedLimit")?, + effective_limit: usize_to_u32(result.effective_limit, "effectiveLimit")?, + top_k_truncated: result.top_k_truncated, + tools: serialize_tool_specs(result.tools)?, + results: result + .results + .into_iter() + .map(tool_search_result_entry_to_response) + .collect::, _>>()?, + }) +} + +fn tool_search_result_entry_to_response( + result: codex_core::ToolSearchDebugResultEntry, +) -> Result { + Ok(ToolSearchSearchResult { + rank: usize_to_u32(result.rank, "rank")?, + index: usize_to_u32(result.index, "index")?, + score: result.score, + source_name: result.source_name, + source_description: result.source_description, + searchable_text: result.searchable_text, + tools: serialize_tool_specs(result.tools)?, + }) +} + +fn serialize_tool_specs( + tools: Vec, +) -> Result, JSONRPCErrorError> { + tools + .into_iter() + .map(|tool| { + serde_json::to_value(tool).map_err(|err| { + internal_error(format!("failed to serialize tool_search tool: {err}")) + }) + }) + .collect() +} + +fn usize_to_u32(value: usize, field_name: &str) -> Result { + u32::try_from(value).map_err(|err| { + internal_error(format!( + "tool_search {field_name} value {value} does not fit in u32: {err}" + )) + }) +} + fn with_mcp_tool_call_thread_id_meta( meta: Option, thread_id: &str, diff --git a/codex-rs/core/src/codex_thread.rs b/codex-rs/core/src/codex_thread.rs index c80268e2f9..3fdd53d93e 100644 --- a/codex-rs/core/src/codex_thread.rs +++ b/codex-rs/core/src/codex_thread.rs @@ -658,6 +658,14 @@ impl CodexThread { .await } + pub async fn search_tool_search( + &self, + query: &str, + limit: usize, + ) -> CodexResult { + self.codex.session.search_tool_search(query, limit).await + } + pub fn enabled(&self, feature: Feature) -> bool { self.codex.enabled(feature) } diff --git a/codex-rs/core/src/lib.rs b/codex-rs/core/src/lib.rs index 91ccbd6e63..61e39b8395 100644 --- a/codex-rs/core/src/lib.rs +++ b/codex-rs/core/src/lib.rs @@ -104,6 +104,9 @@ pub(crate) use skills::skills_load_input_from_config; mod stream_events_utils; pub use stream_events_utils::image_generation_artifact_path; pub mod test_support; +mod tool_search_debug; +pub use tool_search_debug::ToolSearchDebugResult; +pub use tool_search_debug::ToolSearchDebugResultEntry; mod unified_exec; pub mod windows_sandbox; pub use client::X_RESPONSESAPI_INCLUDE_TIMING_METRICS_HEADER; diff --git a/codex-rs/core/src/session/mod.rs b/codex-rs/core/src/session/mod.rs index d4cfd9e05d..d79f870248 100644 --- a/codex-rs/core/src/session/mod.rs +++ b/codex-rs/core/src/session/mod.rs @@ -45,6 +45,7 @@ use crate::session::turn_context::TurnEnvironment; use crate::session_prefix::format_inter_agent_completion_message; use crate::skills::SkillRenderSideEffects; use crate::skills_load_input_from_config; +use crate::tool_search_debug::ToolSearchDebugResult; use crate::turn_metadata::TurnMetadataState; use crate::turn_timing::now_unix_timestamp_ms; use async_channel::Receiver; @@ -1993,6 +1994,29 @@ impl Session { .map(|task| Arc::clone(&task.turn_context)) } + pub async fn search_tool_search( + self: &Arc, + query: &str, + limit: usize, + ) -> CodexResult { + let active = self.active_turn_context_and_cancellation_token().await; + let (turn_context, cancellation_token) = match active { + Some(active) => active, + None => (self.new_default_turn().await, CancellationToken::new()), + }; + let step_context = self.capture_step_context(turn_context).await; + let router = + crate::session::turn::built_tools(self, step_context.as_ref(), &cancellation_token) + .await?; + + router + .search_tool_search(query, limit) + .map_err(|err| CodexErr::InvalidRequest(err.to_string()))? + .ok_or_else(|| { + CodexErr::InvalidRequest("tool_search is not available for this thread".to_string()) + }) + } + async fn active_turn_context_and_cancellation_token( &self, ) -> Option<(Arc, CancellationToken)> { diff --git a/codex-rs/core/src/tool_search_debug.rs b/codex-rs/core/src/tool_search_debug.rs new file mode 100644 index 0000000000..77e542e2ce --- /dev/null +++ b/codex-rs/core/src/tool_search_debug.rs @@ -0,0 +1,23 @@ +use codex_tools::LoadableToolSpec; + +#[derive(Debug, Clone, PartialEq)] +pub struct ToolSearchDebugResult { + pub indexed_tool_count: usize, + pub matching_tool_count: usize, + pub requested_limit: usize, + pub effective_limit: usize, + pub top_k_truncated: bool, + pub tools: Vec, + pub results: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +pub struct ToolSearchDebugResultEntry { + pub rank: usize, + pub index: usize, + pub score: Option, + pub source_name: Option, + pub source_description: Option, + pub searchable_text: String, + pub tools: Vec, +} diff --git a/codex-rs/core/src/tools/handlers/tool_search.rs b/codex-rs/core/src/tools/handlers/tool_search.rs index 272da2f226..b1ce6683c0 100644 --- a/codex-rs/core/src/tools/handlers/tool_search.rs +++ b/codex-rs/core/src/tools/handlers/tool_search.rs @@ -1,4 +1,6 @@ use crate::function_tool::FunctionCallError; +use crate::tool_search_debug::ToolSearchDebugResult; +use crate::tool_search_debug::ToolSearchDebugResultEntry; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; use crate::tools::context::ToolSearchOutput; @@ -152,7 +154,15 @@ impl ToolSearchHandler { } } -impl CoreToolRuntime for ToolSearchHandler {} +impl CoreToolRuntime for ToolSearchHandler { + fn search_tool_search( + &self, + query: &str, + limit: usize, + ) -> Result, FunctionCallError> { + self.search_with_debug(query, limit).map(Some) + } +} impl ToolSearchHandler { fn search( @@ -178,6 +188,50 @@ impl ToolSearchHandler { results.into_iter().map(|entry| entry.output.clone()), )) } + + fn search_with_debug( + &self, + query: &str, + limit: usize, + ) -> Result { + let results = self.search_engine.search(query, None); + let matching_tool_count = results.len(); + let effective_limit = limit.min(matching_tool_count); + let result_entries = results + .iter() + .take(effective_limit) + .enumerate() + .filter_map(|(rank, result)| { + let search_info = self.search_infos.get(result.document.id)?; + Some(ToolSearchDebugResultEntry { + rank: rank + 1, + index: result.document.id, + score: result.score.is_finite().then_some(result.score), + source_name: search_info + .source_info + .as_ref() + .map(|source| source.name.clone()), + source_description: search_info + .source_info + .as_ref() + .and_then(|source| source.description.clone()), + searchable_text: search_info.entry.search_text.clone(), + tools: vec![search_info.entry.output.clone()], + }) + }) + .collect::>(); + let tools = self.search(query, limit)?; + + Ok(ToolSearchDebugResult { + indexed_tool_count: self.search_infos.len(), + matching_tool_count, + requested_limit: limit, + effective_limit, + top_k_truncated: matching_tool_count > limit, + tools, + results: result_entries, + }) + } } #[cfg(test)] @@ -325,7 +379,121 @@ mod tests { ); } + #[test] + fn debug_search_returns_current_tool_search_output_and_truncation_metadata() { + let search_infos = (0..10) + .map(|index| { + McpHandler::new(tool_info( + "calendar", + &format!("list_events_{index}"), + "Calendar events", + )) + .expect("MCP tool should convert") + .search_info() + .expect("MCP handler should return search info") + }) + .collect::>(); + let handler = ToolSearchHandler::new(search_infos); + + let debug = handler + .search_with_debug("Calendar events", 8) + .expect("debug search should succeed"); + let current_tools = handler + .search("Calendar events", 8) + .expect("tool_search should succeed"); + + assert_eq!(debug.tools, current_tools); + assert_eq!(debug.indexed_tool_count, 10); + assert_eq!(debug.matching_tool_count, 10); + assert_eq!(debug.requested_limit, 8); + assert_eq!(debug.effective_limit, 8); + assert!(debug.top_k_truncated); + assert_eq!(debug.results.len(), 8); + assert!(debug.results.iter().all(|result| result.score.is_some())); + assert_eq!(debug.tools.len(), 1); + assert_eq!(canonical_names(&debug.tools).len(), 8); + } + + #[test] + fn debug_search_recovers_exact_named_tool() { + let search_infos = vec![ + McpHandler::new(tool_info("calendar", "list_events", "Calendar events")) + .expect("MCP tool should convert") + .search_info() + .expect("MCP handler should return search info"), + McpHandler::new(tool_info( + "openai_topics", + "list_topics", + "List OpenAI topics", + )) + .expect("MCP tool should convert") + .search_info() + .expect("MCP handler should return search info"), + ]; + let handler = ToolSearchHandler::new(search_infos); + + let debug = handler + .search_with_debug("openai_topics.list_topics", 8) + .expect("debug search should succeed"); + + assert_eq!( + canonical_names(&debug.results[0].tools), + vec!["mcp__openai_topics.list_topics"] + ); + assert_eq!(debug.results[0].rank, 1); + } + + #[test] + fn debug_search_exposes_searchable_text_without_tool_arguments_or_results() { + let search_infos = vec![ + McpHandler::new(tool_info_with_schema( + "secrets", + "lookup", + "Lookup metadata", + serde_json::json!({ + "type": "object", + "properties": { + "api_token": { "type": "string" } + }, + "additionalProperties": false, + }), + )) + .expect("MCP tool should convert") + .search_info() + .expect("MCP handler should return search info"), + ]; + let handler = ToolSearchHandler::new(search_infos); + + let debug = handler + .search_with_debug("api_token", 8) + .expect("debug search should succeed"); + let serialized = format!("{debug:?}"); + + assert!(debug.results[0].searchable_text.contains("api_token")); + assert!(!serialized.contains("arguments:")); + assert!(!serialized.contains("tool_input")); + assert!(!serialized.contains("tool_response")); + } + fn tool_info(server_name: &str, tool_name: &str, description_prefix: &str) -> ToolInfo { + tool_info_with_schema( + server_name, + tool_name, + description_prefix, + serde_json::json!({ + "type": "object", + "properties": {}, + "additionalProperties": false, + }), + ) + } + + fn tool_info_with_schema( + server_name: &str, + tool_name: &str, + description_prefix: &str, + input_schema: serde_json::Value, + ) -> ToolInfo { ToolInfo { server_name: server_name.to_string(), supports_parallel_tool_calls: false, @@ -336,15 +504,28 @@ mod tests { tool: Tool::new( tool_name.to_string(), format!("{description_prefix} desktop tool"), - Arc::new(rmcp::model::object(serde_json::json!({ - "type": "object", - "properties": {}, - "additionalProperties": false, - }))), + Arc::new(rmcp::model::object(input_schema)), ), connector_id: None, connector_name: None, plugin_display_names: Vec::new(), } } + + fn canonical_names(tools: &[LoadableToolSpec]) -> Vec { + tools + .iter() + .flat_map(|tool| match tool { + LoadableToolSpec::Function(tool) => vec![tool.name.clone()], + LoadableToolSpec::Namespace(namespace) => namespace + .tools + .iter() + .map(|tool| { + let ResponsesApiNamespaceTool::Function(tool) = tool; + format!("{}.{}", namespace.name, tool.name) + }) + .collect(), + }) + .collect() + } } diff --git a/codex-rs/core/src/tools/registry.rs b/codex-rs/core/src/tools/registry.rs index 2220d4244a..cf77fd5e03 100644 --- a/codex-rs/core/src/tools/registry.rs +++ b/codex-rs/core/src/tools/registry.rs @@ -13,6 +13,7 @@ use crate::memory_usage::emit_metric_for_tool_read; use crate::sandbox_tags::permission_profile_policy_tag; use crate::sandbox_tags::permission_profile_sandbox_tag; use crate::session::turn_context::TurnContext; +use crate::tool_search_debug::ToolSearchDebugResult; use crate::tools::context::FunctionToolOutput; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolOutput; @@ -142,6 +143,14 @@ pub(crate) trait CoreToolRuntime: ToolExecutor { fn create_diff_consumer(&self) -> Option> { None } + + fn search_tool_search( + &self, + _query: &str, + _limit: usize, + ) -> Result, FunctionCallError> { + Ok(None) + } } /// Consumes streamed argument diffs for a tool call and emits protocol events @@ -389,6 +398,18 @@ impl ToolRegistry { Some(tool.waits_for_runtime_cancellation()) } + pub(crate) fn search_tool_search( + &self, + name: &ToolName, + query: &str, + limit: usize, + ) -> Result, FunctionCallError> { + self.tool(name) + .map(|tool| tool.search_tool_search(query, limit)) + .transpose() + .map(Option::flatten) + } + #[allow(dead_code)] pub(crate) async fn dispatch_any( &self, diff --git a/codex-rs/core/src/tools/router.rs b/codex-rs/core/src/tools/router.rs index aca86b124b..a0e6746392 100644 --- a/codex-rs/core/src/tools/router.rs +++ b/codex-rs/core/src/tools/router.rs @@ -1,6 +1,7 @@ use crate::function_tool::FunctionCallError; use crate::session::session::Session; use crate::session::step_context::StepContext; +use crate::tool_search_debug::ToolSearchDebugResult; use crate::tools::context::SharedTurnDiffTracker; use crate::tools::context::ToolInvocation; use crate::tools::context::ToolPayload; @@ -14,6 +15,7 @@ use codex_protocol::dynamic_tools::DynamicToolSpec; use codex_protocol::models::ResponseItem; use codex_protocol::models::SearchToolCallParams; use codex_tools::DiscoverableTool; +use codex_tools::TOOL_SEARCH_TOOL_NAME; use codex_tools::ToolCall as ExtensionToolCall; use codex_tools::ToolExecutor; use codex_tools::ToolName; @@ -109,6 +111,15 @@ impl ToolRouter { .unwrap_or(false) } + pub(crate) fn search_tool_search( + &self, + query: &str, + limit: usize, + ) -> Result, FunctionCallError> { + self.registry + .search_tool_search(&ToolName::plain(TOOL_SEARCH_TOOL_NAME), query, limit) + } + #[instrument(level = "trace", skip_all, err)] pub fn build_tool_call(item: ResponseItem) -> Result, FunctionCallError> { match item {