codex-rs: add tool search inspector API

This commit is contained in:
Steven Lee
2026-06-25 19:37:17 +00:00
parent 322e33512b
commit dd64a9a6e6
20 changed files with 786 additions and 6 deletions
@@ -19660,6 +19660,111 @@
],
"type": "object"
},
"ToolSearchInspectOutputTool": {
"properties": {
"namespace": {
"type": [
"string",
"null"
]
},
"toolNames": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"toolNames"
],
"type": "object"
},
"ToolSearchInspectResult": {
"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"
},
"source": {
"anyOf": [
{
"$ref": "#/definitions/v2/ToolSearchInspectSource"
},
{
"type": "null"
}
]
},
"tools": {
"items": {
"$ref": "#/definitions/v2/ToolSearchInspectTool"
},
"type": "array"
}
},
"required": [
"index",
"rank",
"searchableText",
"tools"
],
"type": "object"
},
"ToolSearchInspectSource": {
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
},
"ToolSearchInspectTool": {
"properties": {
"canonicalName": {
"type": "string"
},
"name": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
}
},
"required": [
"canonicalName",
"name"
],
"type": "object"
},
"ToolsV2": {
"properties": {
"web_search": {
@@ -17439,6 +17439,111 @@
],
"type": "object"
},
"ToolSearchInspectOutputTool": {
"properties": {
"namespace": {
"type": [
"string",
"null"
]
},
"toolNames": {
"items": {
"type": "string"
},
"type": "array"
}
},
"required": [
"toolNames"
],
"type": "object"
},
"ToolSearchInspectResult": {
"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"
},
"source": {
"anyOf": [
{
"$ref": "#/definitions/ToolSearchInspectSource"
},
{
"type": "null"
}
]
},
"tools": {
"items": {
"$ref": "#/definitions/ToolSearchInspectTool"
},
"type": "array"
}
},
"required": [
"index",
"rank",
"searchableText",
"tools"
],
"type": "object"
},
"ToolSearchInspectSource": {
"properties": {
"description": {
"type": [
"string",
"null"
]
},
"name": {
"type": "string"
}
},
"required": [
"name"
],
"type": "object"
},
"ToolSearchInspectTool": {
"properties": {
"canonicalName": {
"type": "string"
},
"name": {
"type": "string"
},
"namespace": {
"type": [
"string",
"null"
]
}
},
"required": [
"canonicalName",
"name"
],
"type": "object"
},
"ToolsV2": {
"properties": {
"web_search": {
@@ -0,0 +1,5 @@
// 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.
export type ToolSearchInspectOutputTool = { namespace: string | null, toolNames: Array<string>, };
@@ -0,0 +1,7 @@
// 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 { ToolSearchInspectSource } from "./ToolSearchInspectSource";
import type { ToolSearchInspectTool } from "./ToolSearchInspectTool";
export type ToolSearchInspectResult = { rank: number, index: number, score: number | null, source: ToolSearchInspectSource | null, tools: Array<ToolSearchInspectTool>, searchableText: string, };
@@ -0,0 +1,5 @@
// 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.
export type ToolSearchInspectSource = { name: string, description: string | null, };
@@ -0,0 +1,5 @@
// 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.
export type ToolSearchInspectTool = { namespace: string | null, name: string, canonicalName: string, };
@@ -466,6 +466,10 @@ export type { ToolRequestUserInputOption } from "./ToolRequestUserInputOption";
export type { ToolRequestUserInputParams } from "./ToolRequestUserInputParams";
export type { ToolRequestUserInputQuestion } from "./ToolRequestUserInputQuestion";
export type { ToolRequestUserInputResponse } from "./ToolRequestUserInputResponse";
export type { ToolSearchInspectOutputTool } from "./ToolSearchInspectOutputTool";
export type { ToolSearchInspectResult } from "./ToolSearchInspectResult";
export type { ToolSearchInspectSource } from "./ToolSearchInspectSource";
export type { ToolSearchInspectTool } from "./ToolSearchInspectTool";
export type { ToolsV2 } from "./ToolsV2";
export type { Turn } from "./Turn";
export type { TurnCompletedNotification } from "./TurnCompletedNotification";
@@ -961,6 +961,12 @@ client_request_definitions! {
serialization: global("mcp-registry"),
response: v2::ListMcpServerStatusResponse,
},
#[experimental("toolSearch/inspect")]
ToolSearchInspect => "toolSearch/inspect" {
params: v2::ToolSearchInspectParams,
serialization: thread_id(params.thread_id),
response: v2::ToolSearchInspectResponse,
},
McpResourceRead => "mcpServer/resource/read" {
params: v2::McpResourceReadParams,
@@ -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::*;
@@ -0,0 +1,65 @@
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Serialize;
use ts_rs::TS;
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ToolSearchInspectParams {
pub thread_id: String,
pub query: String,
/// Optional result limit. Defaults to the runtime `tool_search` default.
#[ts(optional = nullable)]
pub limit: Option<u32>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ToolSearchInspectResponse {
pub indexed_tool_count: u32,
pub matching_tool_count: u32,
pub requested_limit: u32,
pub effective_limit: u32,
pub top_k_truncated: bool,
pub results: Vec<ToolSearchInspectResult>,
pub output_tools: Vec<ToolSearchInspectOutputTool>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ToolSearchInspectResult {
pub rank: u32,
pub index: u32,
pub score: Option<f32>,
pub source: Option<ToolSearchInspectSource>,
pub tools: Vec<ToolSearchInspectTool>,
pub searchable_text: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ToolSearchInspectSource {
pub name: String,
pub description: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ToolSearchInspectTool {
pub namespace: Option<String>,
pub name: String,
pub canonical_name: String,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct ToolSearchInspectOutputTool {
pub namespace: Option<String>,
pub tool_names: Vec<String>,
}
@@ -1382,6 +1382,9 @@ impl MessageProcessor {
.mcp_server_status_list(&request_id, params)
.await
}
ClientRequest::ToolSearchInspect { params, .. } => {
self.mcp_processor.tool_search_inspect(params).await
}
ClientRequest::McpResourceRead { params, .. } => {
self.mcp_processor
.mcp_resource_read(&request_id, params)
@@ -266,6 +266,12 @@ 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::ToolSearchInspectOutputTool;
use codex_app_server_protocol::ToolSearchInspectParams;
use codex_app_server_protocol::ToolSearchInspectResponse;
use codex_app_server_protocol::ToolSearchInspectResult;
use codex_app_server_protocol::ToolSearchInspectSource;
use codex_app_server_protocol::ToolSearchInspectTool;
use codex_app_server_protocol::Turn;
use codex_app_server_protocol::TurnEnvironmentParams;
use codex_app_server_protocol::TurnError;
@@ -449,6 +455,7 @@ use codex_thread_store::ThreadMetadataPatch as StoreThreadMetadataPatch;
use codex_thread_store::ThreadSortKey as StoreThreadSortKey;
use codex_thread_store::ThreadStore;
use codex_thread_store::ThreadStoreError;
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::DEFAULT_OUTPUT_BYTES_CAP;
use std::collections::BTreeMap;
@@ -53,6 +53,15 @@ impl McpRequestProcessor {
.map(|()| None)
}
pub(crate) async fn tool_search_inspect(
&self,
params: ToolSearchInspectParams,
) -> Result<Option<ClientResponsePayload>, JSONRPCErrorError> {
self.tool_search_inspect_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_inspect_response(
&self,
params: ToolSearchInspectParams,
) -> Result<ToolSearchInspectResponse, JSONRPCErrorError> {
let ToolSearchInspectParams {
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(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 inspection = thread
.inspect_tool_search(query, limit as usize)
.await
.map_err(|err| internal_error(format!("failed to inspect tool_search: {err}")))?;
tool_search_inspection_to_response(inspection)
}
async fn mcp_server_oauth_login_response(
&self,
params: McpServerOauthLoginParams,
@@ -466,3 +501,63 @@ fn with_mcp_tool_call_thread_id_meta(
other => other,
}
}
fn tool_search_inspection_to_response(
inspection: codex_core::ToolSearchInspection,
) -> Result<ToolSearchInspectResponse, JSONRPCErrorError> {
Ok(ToolSearchInspectResponse {
indexed_tool_count: usize_to_u32(inspection.indexed_tool_count, "indexed_tool_count")?,
matching_tool_count: usize_to_u32(inspection.matching_tool_count, "matching_tool_count")?,
requested_limit: usize_to_u32(inspection.requested_limit, "requested_limit")?,
effective_limit: usize_to_u32(inspection.effective_limit, "effective_limit")?,
top_k_truncated: inspection.top_k_truncated,
results: inspection
.results
.into_iter()
.map(tool_search_result_to_response)
.collect::<Result<Vec<_>, _>>()?,
output_tools: inspection
.output_tools
.into_iter()
.map(tool_search_output_tool_to_response)
.collect(),
})
}
fn tool_search_result_to_response(
result: codex_core::ToolSearchInspectionResult,
) -> Result<ToolSearchInspectResult, JSONRPCErrorError> {
Ok(ToolSearchInspectResult {
rank: usize_to_u32(result.rank, "rank")?,
index: usize_to_u32(result.index, "index")?,
score: result.score,
source: result.source.map(|source| ToolSearchInspectSource {
name: source.name,
description: source.description,
}),
tools: result
.tools
.into_iter()
.map(|tool| ToolSearchInspectTool {
namespace: tool.namespace,
name: tool.name,
canonical_name: tool.canonical_name,
})
.collect(),
searchable_text: result.searchable_text,
})
}
fn tool_search_output_tool_to_response(
output_tool: codex_core::ToolSearchInspectionOutputTool,
) -> ToolSearchInspectOutputTool {
ToolSearchInspectOutputTool {
namespace: output_tool.namespace,
tool_names: output_tool.tool_names,
}
}
fn usize_to_u32(value: usize, field: &str) -> Result<u32, JSONRPCErrorError> {
u32::try_from(value)
.map_err(|_| internal_error(format!("tool_search inspection field {field} exceeds u32")))
}
+8
View File
@@ -635,6 +635,14 @@ impl CodexThread {
.await
}
pub async fn inspect_tool_search(
&self,
query: &str,
limit: usize,
) -> CodexResult<crate::ToolSearchInspection> {
self.codex.session.inspect_tool_search(query, limit).await
}
pub fn enabled(&self, feature: Feature) -> bool {
self.codex.enabled(feature)
}
+6
View File
@@ -101,6 +101,12 @@ pub(crate) use skills::maybe_emit_implicit_skill_invocation;
pub(crate) use skills::skills_load_input_from_config;
mod stream_events_utils;
pub use stream_events_utils::image_generation_artifact_path;
mod tool_search_debug;
pub use tool_search_debug::ToolSearchInspection;
pub use tool_search_debug::ToolSearchInspectionOutputTool;
pub use tool_search_debug::ToolSearchInspectionResult;
pub use tool_search_debug::ToolSearchInspectionSource;
pub use tool_search_debug::ToolSearchInspectionTool;
pub mod test_support;
mod unified_exec;
pub mod windows_sandbox;
+21
View File
@@ -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::ToolSearchInspection;
use crate::turn_metadata::TurnMetadataState;
use crate::turn_timing::now_unix_timestamp_ms;
use async_channel::Receiver;
@@ -1983,6 +1984,26 @@ impl Session {
.map(|task| Arc::clone(&task.turn_context))
}
pub async fn inspect_tool_search(
&self,
query: &str,
limit: usize,
) -> CodexResult<ToolSearchInspection> {
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 =
StepContext::new(Arc::clone(&turn_context), turn_context.environments.clone());
let router =
crate::session::turn::built_tools(self, &step_context, &cancellation_token).await?;
router.inspect_tool_search(query, limit).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<TurnContext>, CancellationToken)> {
+39
View File
@@ -0,0 +1,39 @@
#[derive(Debug, Clone, PartialEq)]
pub struct ToolSearchInspection {
pub indexed_tool_count: usize,
pub matching_tool_count: usize,
pub requested_limit: usize,
pub effective_limit: usize,
pub top_k_truncated: bool,
pub results: Vec<ToolSearchInspectionResult>,
pub output_tools: Vec<ToolSearchInspectionOutputTool>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ToolSearchInspectionResult {
pub rank: usize,
pub index: usize,
pub score: Option<f32>,
pub source: Option<ToolSearchInspectionSource>,
pub tools: Vec<ToolSearchInspectionTool>,
pub searchable_text: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolSearchInspectionSource {
pub name: String,
pub description: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolSearchInspectionTool {
pub namespace: Option<String>,
pub name: String,
pub canonical_name: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolSearchInspectionOutputTool {
pub namespace: Option<String>,
pub tool_names: Vec<String>,
}
+269 -6
View File
@@ -11,6 +11,7 @@ use bm25::Language;
use bm25::SearchEngine;
use bm25::SearchEngineBuilder;
use codex_tools::LoadableToolSpec;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
use codex_tools::TOOL_SEARCH_TOOL_NAME;
use codex_tools::ToolName;
@@ -22,6 +23,12 @@ use std::sync::Arc;
use std::sync::Mutex;
use tracing::instrument;
use crate::tool_search_debug::ToolSearchInspection;
use crate::tool_search_debug::ToolSearchInspectionOutputTool;
use crate::tool_search_debug::ToolSearchInspectionResult;
use crate::tool_search_debug::ToolSearchInspectionSource;
use crate::tool_search_debug::ToolSearchInspectionTool;
pub struct ToolSearchHandler {
search_infos: Vec<ToolSearchInfo>,
spec: ToolSpec,
@@ -152,7 +159,56 @@ impl ToolSearchHandler {
}
}
impl CoreToolRuntime for ToolSearchHandler {}
impl ToolSearchHandler {
fn inspect(&self, query: &str, limit: usize) -> ToolSearchInspection {
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(inspect_result(
rank + 1,
result.document.id,
result.score,
search_info,
))
})
.collect::<Vec<_>>();
let output_tools = self
.search_output_tools(
results
.iter()
.take(effective_limit)
.map(|result| result.document.id)
.filter_map(|id| self.search_infos.get(id))
.map(|search_info| &search_info.entry),
)
.unwrap_or_default()
.iter()
.map(inspect_output_tool)
.collect();
ToolSearchInspection {
indexed_tool_count: self.search_infos.len(),
matching_tool_count,
requested_limit: limit,
effective_limit,
top_k_truncated: matching_tool_count > limit,
results: result_entries,
output_tools,
}
}
}
impl CoreToolRuntime for ToolSearchHandler {
fn inspect_tool_search(&self, query: &str, limit: usize) -> Option<ToolSearchInspection> {
Some(self.inspect(query, limit))
}
}
impl ToolSearchHandler {
fn search(
@@ -180,6 +236,70 @@ impl ToolSearchHandler {
}
}
fn inspect_result(
rank: usize,
index: usize,
score: f32,
search_info: &ToolSearchInfo,
) -> ToolSearchInspectionResult {
ToolSearchInspectionResult {
rank,
index,
score: score.is_finite().then_some(score),
source: search_info
.source_info
.as_ref()
.map(|source_info| ToolSearchInspectionSource {
name: source_info.name.clone(),
description: source_info.description.clone(),
}),
tools: inspect_entry_tools(&search_info.entry.output),
searchable_text: search_info.entry.search_text.clone(),
}
}
fn inspect_entry_tools(output: &LoadableToolSpec) -> Vec<ToolSearchInspectionTool> {
match output {
LoadableToolSpec::Function(tool) => vec![ToolSearchInspectionTool {
namespace: None,
name: tool.name.clone(),
canonical_name: tool.name.clone(),
}],
LoadableToolSpec::Namespace(namespace) => namespace
.tools
.iter()
.map(|tool| {
let ResponsesApiNamespaceTool::Function(tool) = tool;
ToolSearchInspectionTool {
namespace: Some(namespace.name.clone()),
name: tool.name.clone(),
canonical_name: format!("{}.{}", namespace.name, tool.name),
}
})
.collect(),
}
}
fn inspect_output_tool(output: &LoadableToolSpec) -> ToolSearchInspectionOutputTool {
match output {
LoadableToolSpec::Function(tool) => ToolSearchInspectionOutputTool {
namespace: None,
tool_names: vec![tool.name.clone()],
},
LoadableToolSpec::Namespace(namespace) => ToolSearchInspectionOutputTool {
namespace: Some(namespace.name.clone()),
tool_names: namespace
.tools
.iter()
.map(|tool| {
let ResponsesApiNamespaceTool::Function(tool) = tool;
tool.name.clone()
})
.collect(),
},
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -325,7 +445,154 @@ mod tests {
);
}
#[test]
fn inspect_reports_broad_query_top_k_truncation_and_omission() {
let mut search_infos = (0..10)
.map(|index| {
McpHandler::new(tool_info(
&format!("distractor_{index}"),
"semantic_match",
"OpenAI Topics list topics",
))
.expect("MCP tool should convert")
.search_info()
.expect("MCP handler should return search info")
})
.collect::<Vec<_>>();
search_infos.push(
McpHandler::new(tool_info(
"openai_topics",
"list_topics",
"Connector metadata",
))
.expect("MCP tool should convert")
.search_info()
.expect("MCP handler should return search info"),
);
let handler = ToolSearchHandler::new(search_infos);
let inspection = handler.inspect("OpenAI Topics list topics", 8);
assert_eq!(inspection.indexed_tool_count, 11);
assert_eq!(inspection.matching_tool_count, 10);
assert_eq!(inspection.requested_limit, 8);
assert_eq!(inspection.effective_limit, 8);
assert!(inspection.top_k_truncated);
assert_eq!(inspection.results.len(), 8);
assert!(
inspection
.results
.iter()
.all(|result| result.tools[0].canonical_name != "mcp__openai_topics.list_topics")
);
}
#[test]
fn inspect_named_openai_topics_query_recovers_target_first() {
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 inspection = handler.inspect("list topics", 8);
assert_eq!(
inspection.results[0].tools[0].canonical_name,
"mcp__openai_topics.list_topics"
);
assert_eq!(inspection.results[0].rank, 1);
assert!(inspection.results[0].score.is_some());
}
#[test]
fn inspect_output_tools_match_namespace_coalescing() {
let search_infos = vec![
McpHandler::new(tool_info("calendar", "create_event", "events"))
.expect("MCP tool should convert")
.search_info()
.expect("MCP handler should return search info"),
McpHandler::new(tool_info("calendar", "list_events", "events"))
.expect("MCP tool should convert")
.search_info()
.expect("MCP handler should return search info"),
];
let handler = ToolSearchHandler::new(search_infos);
let inspection = handler.inspect("events", 2);
let mut tool_names = inspection.output_tools[0].tool_names.clone();
tool_names.sort();
assert_eq!(
inspection.output_tools[0].namespace,
Some("mcp__calendar".to_string())
);
assert_eq!(
tool_names,
vec!["create_event".to_string(), "list_events".to_string()]
);
}
#[test]
fn inspect_exposes_metadata_without_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 inspection = handler.inspect("api_token", 8);
let debug = format!("{inspection:?}");
assert!(inspection.results[0].searchable_text.contains("api_token"));
assert!(!debug.contains("arguments:"));
assert!(!debug.contains("tool_input"));
assert!(!debug.contains("tool_response"));
assert!(!debug.contains("secret-token-value"));
}
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,11 +603,7 @@ 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,
+18
View File
@@ -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::ToolSearchInspection;
use crate::tools::context::FunctionToolOutput;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
@@ -142,6 +143,10 @@ pub(crate) trait CoreToolRuntime: ToolExecutor<ToolInvocation> {
fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>> {
None
}
fn inspect_tool_search(&self, _query: &str, _limit: usize) -> Option<ToolSearchInspection> {
None
}
}
/// Consumes streamed argument diffs for a tool call and emits protocol events
@@ -317,6 +322,10 @@ impl CoreToolRuntime for ExposureOverride {
fn create_diff_consumer(&self) -> Option<Box<dyn ToolArgumentDiffConsumer>> {
self.handler.create_diff_consumer()
}
fn inspect_tool_search(&self, query: &str, limit: usize) -> Option<ToolSearchInspection> {
self.handler.inspect_tool_search(query, limit)
}
}
pub struct ToolRegistry {
@@ -389,6 +398,15 @@ impl ToolRegistry {
Some(tool.waits_for_runtime_cancellation())
}
pub(crate) fn inspect_tool_search(
&self,
name: &ToolName,
query: &str,
limit: usize,
) -> Option<ToolSearchInspection> {
self.tool(name)?.inspect_tool_search(query, limit)
}
#[allow(dead_code)]
pub(crate) async fn dispatch_any(
&self,
+11
View File
@@ -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::ToolSearchInspection;
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 inspect_tool_search(
&self,
query: &str,
limit: usize,
) -> Option<ToolSearchInspection> {
self.registry
.inspect_tool_search(&ToolName::plain(TOOL_SEARCH_TOOL_NAME), query, limit)
}
#[instrument(level = "trace", skip_all, err)]
pub fn build_tool_call(item: ResponseItem) -> Result<Option<ToolCall>, FunctionCallError> {
match item {