Encapsulate tool search entries in handlers (#22261)

## Why

This builds on the handler-owned spec refactor by moving deferred
tool-search metadata to the same handlers that already own tool specs.
The registry builder no longer needs a separate prebuilt
`tool_search_entries` path; it can collect searchable entries from
deferred handlers directly.

## What changed

- Added `search_info()` to tool handlers and implemented it for MCP and
dynamic handlers.
- Reused handler `spec()` output when constructing tool-search entries,
adapting it into the deferred `LoadableToolSpec` shape expected by
`tool_search`.
- Simplified `build_tool_registry_builder(...)` so `tool_search`
registration is based on deferred handlers with search info.
- Removed the old standalone search-entry builders and now-unused
`codex-tools` discovery helper exports.

## Verification

- `cargo test -p codex-core tools::handlers::tool_search::tests:: --
--nocapture`
- `cargo test -p codex-core tools::spec_plan::tests::search_tool --
--nocapture`
- `cargo test -p codex-core tools::spec::tests:: -- --nocapture`
- `cargo test -p codex-core tools::spec_plan::tests:: -- --nocapture`
- `cargo test -p codex-tools`
- `just fix -p codex-core`
- `just fix -p codex-tools`
This commit is contained in:
pakrym-oai
2026-05-12 20:48:02 -07:00
committed by GitHub
parent 67c8486462
commit 104fc14956
15 changed files with 345 additions and 460 deletions

View File

@@ -47,7 +47,6 @@ pub use responses_api::ResponsesApiNamespaceTool;
pub use responses_api::ResponsesApiTool;
pub use responses_api::coalesce_loadable_tool_specs;
pub use responses_api::default_namespace_description;
pub use responses_api::dynamic_tool_to_loadable_tool_spec;
pub use responses_api::dynamic_tool_to_responses_api_tool;
pub use responses_api::mcp_tool_to_deferred_responses_api_tool;
pub use responses_api::mcp_tool_to_responses_api_tool;
@@ -69,13 +68,9 @@ pub use tool_discovery::REQUEST_PLUGIN_INSTALL_TOOL_NAME;
pub use tool_discovery::RequestPluginInstallEntry;
pub use tool_discovery::TOOL_SEARCH_DEFAULT_LIMIT;
pub use tool_discovery::TOOL_SEARCH_TOOL_NAME;
pub use tool_discovery::ToolSearchResultSource;
pub use tool_discovery::ToolSearchSource;
pub use tool_discovery::ToolSearchSourceInfo;
pub use tool_discovery::collect_request_plugin_install_entries;
pub use tool_discovery::collect_tool_search_source_infos;
pub use tool_discovery::filter_request_plugin_install_discoverable_tools_for_client;
pub use tool_discovery::tool_search_result_source_to_loadable_tool_spec;
pub use tool_spec::ResponsesApiWebSearchFilters;
pub use tool_spec::ResponsesApiWebSearchUserLocation;
pub use tool_spec::ToolSpec;

View File

@@ -74,21 +74,6 @@ pub fn dynamic_tool_to_responses_api_tool(
)?))
}
pub fn dynamic_tool_to_loadable_tool_spec(
tool: &DynamicToolSpec,
) -> Result<LoadableToolSpec, serde_json::Error> {
let output_tool = dynamic_tool_to_responses_api_tool(tool)?;
Ok(match tool.namespace.as_ref() {
Some(namespace) => LoadableToolSpec::Namespace(ResponsesApiNamespace {
name: namespace.clone(),
// the user doesn't provide a description for dynamic tools, so we use the default
description: default_namespace_description(namespace),
tools: vec![ResponsesApiNamespaceTool::Function(output_tool)],
}),
None => LoadableToolSpec::Function(output_tool),
})
}
pub fn coalesce_loadable_tool_specs(
specs: impl IntoIterator<Item = LoadableToolSpec>,
) -> Vec<LoadableToolSpec> {

View File

@@ -1,9 +1,3 @@
use crate::LoadableToolSpec;
use crate::ResponsesApiNamespace;
use crate::ResponsesApiNamespaceTool;
use crate::ToolName;
use crate::default_namespace_description;
use crate::mcp_tool_to_deferred_responses_api_tool;
use codex_app_server_protocol::AppInfo;
use serde::Deserialize;
use serde::Serialize;
@@ -19,23 +13,6 @@ pub struct ToolSearchSourceInfo {
pub description: Option<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ToolSearchSource<'a> {
pub server_name: &'a str,
pub connector_name: Option<&'a str>,
pub description: Option<&'a str>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ToolSearchResultSource<'a> {
pub server_name: &'a str,
pub tool_namespace: &'a str,
pub tool_name: &'a str,
pub tool: &'a rmcp::model::Tool,
pub connector_name: Option<&'a str>,
pub description: Option<&'a str>,
}
#[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum DiscoverableToolType {
@@ -133,78 +110,6 @@ pub struct RequestPluginInstallEntry {
pub app_connector_ids: Vec<String>,
}
pub fn tool_search_result_source_to_loadable_tool_spec(
source: ToolSearchResultSource<'_>,
) -> Result<LoadableToolSpec, serde_json::Error> {
Ok(LoadableToolSpec::Namespace(ResponsesApiNamespace {
name: source.tool_namespace.to_string(),
description: tool_search_result_source_namespace_description(source),
tools: vec![tool_search_result_source_to_namespace_tool(source)?],
}))
}
fn tool_search_result_source_namespace_description(source: ToolSearchResultSource<'_>) -> String {
source
.description
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string)
.or_else(|| {
source
.connector_name
.map(str::trim)
.filter(|connector_name| !connector_name.is_empty())
.map(|connector_name| format!("Tools for working with {connector_name}."))
})
.unwrap_or_else(|| default_namespace_description(source.tool_namespace))
}
fn tool_search_result_source_to_namespace_tool(
source: ToolSearchResultSource<'_>,
) -> Result<ResponsesApiNamespaceTool, serde_json::Error> {
let tool_name = ToolName::namespaced(source.tool_namespace, source.tool_name);
mcp_tool_to_deferred_responses_api_tool(&tool_name, source.tool)
.map(ResponsesApiNamespaceTool::Function)
}
pub fn collect_tool_search_source_infos<'a>(
searchable_tools: impl IntoIterator<Item = ToolSearchSource<'a>>,
) -> Vec<ToolSearchSourceInfo> {
searchable_tools
.into_iter()
.filter_map(|tool| {
if let Some(name) = tool
.connector_name
.map(str::trim)
.filter(|connector_name| !connector_name.is_empty())
{
return Some(ToolSearchSourceInfo {
name: name.to_string(),
description: tool
.description
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string),
});
}
let name = tool.server_name.trim();
if name.is_empty() {
return None;
}
Some(ToolSearchSourceInfo {
name: name.to_string(),
description: tool
.description
.map(str::trim)
.filter(|description| !description.is_empty())
.map(str::to_string),
})
})
.collect()
}
pub fn collect_request_plugin_install_entries(
discoverable_tools: &[DiscoverableTool],
) -> Vec<RequestPluginInstallEntry> {