mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
codex-rs: add tool search inspector API
This commit is contained in:
+105
@@ -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": {
|
||||
|
||||
+105
@@ -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": {
|
||||
|
||||
+5
@@ -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")))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)> {
|
||||
|
||||
@@ -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>,
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user