Extract tool-search output helpers into codex-tools (#16497)

## Why

This is the next straight-refactor step in the `codex-tools` migration
that follows #16493.

`codex-rs/core` still owned a chunk of pure tool-discovery metadata and
response shaping even though the corresponding `tool_search` /
`tool_suggest` specs already live in `codex-rs/tools`. Per the guidance
in `AGENTS.md`, this moves that crate-agnostic logic out of `codex-core`
so the handler crate keeps only the BM25 ranking/orchestration and
runtime glue.

## What changed

- Moved the canonical `tool_search` / `tool_suggest` tool names and the
`tool_search` default limit into `codex-rs/tools/src/tool_discovery.rs`.
- Added `ToolSearchResultSource` and
`collect_tool_search_output_tools()` in `codex-tools` so namespace
grouping and deferred Responses API tool serialization happen outside
`codex-core`.
- Rewired `ToolSearchHandler`, `ToolSuggestHandler`, and
`core/src/tools/spec.rs` to consume those exports directly from
`codex-tools`.
- Ported the existing `tool_search` serializer tests from
`core/src/tools/handlers/tool_search_tests.rs` to
`tools/src/tool_discovery_tests.rs` without adding new behavior
coverage.

## Validation

```shell
cargo test -p codex-tools
cargo test -p codex-core tools::spec::tests
just argument-comment-lint
```
This commit is contained in:
Michael Bolin
2026-04-01 20:16:21 -07:00
committed by GitHub
parent 74d7149130
commit 0b856a4757
8 changed files with 233 additions and 275 deletions
-3
View File
@@ -47,10 +47,7 @@ pub use request_user_input::RequestUserInputHandler;
pub use shell::ShellCommandHandler;
pub use shell::ShellHandler;
pub use test_sync::TestSyncHandler;
pub(crate) use tool_search::DEFAULT_LIMIT as TOOL_SEARCH_DEFAULT_LIMIT;
pub(crate) use tool_search::TOOL_SEARCH_TOOL_NAME;
pub use tool_search::ToolSearchHandler;
pub(crate) use tool_suggest::TOOL_SUGGEST_TOOL_NAME;
pub use tool_suggest::ToolSuggestHandler;
pub use unified_exec::UnifiedExecHandler;
pub use view_image::ViewImageHandler;
+21 -66
View File
@@ -9,20 +9,16 @@ use async_trait::async_trait;
use bm25::Document;
use bm25::Language;
use bm25::SearchEngineBuilder;
use codex_tools::ResponsesApiNamespace;
use codex_tools::ResponsesApiNamespaceTool;
use codex_tools::ToolSearchOutputTool;
use codex_tools::mcp_tool_to_deferred_responses_api_tool;
use std::collections::BTreeMap;
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
use codex_tools::TOOL_SEARCH_TOOL_NAME;
use codex_tools::ToolSearchResultSource;
use codex_tools::collect_tool_search_output_tools;
use std::collections::HashMap;
pub struct ToolSearchHandler {
tools: HashMap<String, ToolInfo>,
}
pub(crate) const TOOL_SEARCH_TOOL_NAME: &str = "tool_search";
pub(crate) const DEFAULT_LIMIT: usize = 8;
impl ToolSearchHandler {
pub fn new(tools: HashMap<String, ToolInfo>) -> Self {
Self { tools }
@@ -58,7 +54,7 @@ impl ToolHandler for ToolSearchHandler {
"query must not be empty".to_string(),
));
}
let limit = args.limit.unwrap_or(DEFAULT_LIMIT);
let limit = args.limit.unwrap_or(TOOL_SEARCH_DEFAULT_LIMIT);
if limit == 0 {
return Err(FunctionCallError::RespondToModel(
@@ -82,65 +78,28 @@ impl ToolHandler for ToolSearchHandler {
SearchEngineBuilder::<usize>::with_documents(Language::English, documents).build();
let results = search_engine.search(query, limit);
let matched_entries = results
.into_iter()
.filter_map(|result| entries.get(result.document.id))
.collect::<Vec<_>>();
let tools = serialize_tool_search_output_tools(&matched_entries).map_err(|err| {
FunctionCallError::Fatal(format!("failed to encode tool_search output: {err}"))
let tools = collect_tool_search_output_tools(
results
.into_iter()
.filter_map(|result| entries.get(result.document.id))
.map(|(_name, tool)| ToolSearchResultSource {
tool_namespace: tool.tool_namespace.as_str(),
tool_name: tool.tool_name.as_str(),
tool: &tool.tool,
connector_name: tool.connector_name.as_deref(),
connector_description: tool.connector_description.as_deref(),
}),
)
.map_err(|err| {
FunctionCallError::Fatal(format!(
"failed to encode {TOOL_SEARCH_TOOL_NAME} output: {err}"
))
})?;
Ok(ToolSearchOutput { tools })
}
}
fn serialize_tool_search_output_tools(
matched_entries: &[&(String, ToolInfo)],
) -> Result<Vec<ToolSearchOutputTool>, serde_json::Error> {
let grouped: BTreeMap<String, Vec<ToolInfo>> =
matched_entries
.iter()
.fold(BTreeMap::new(), |mut acc, (_name, tool)| {
acc.entry(tool.tool_namespace.clone())
.or_default()
.push(tool.clone());
acc
});
let mut results = Vec::with_capacity(grouped.len());
for (namespace, tools) in grouped {
let Some(first_tool) = tools.first() else {
continue;
};
let description = first_tool.connector_description.clone().or_else(|| {
first_tool
.connector_name
.as_deref()
.map(str::trim)
.filter(|connector_name| !connector_name.is_empty())
.map(|connector_name| format!("Tools for working with {connector_name}."))
});
let tools = tools
.iter()
.map(|tool| {
mcp_tool_to_deferred_responses_api_tool(tool.tool_name.clone(), &tool.tool)
.map(ResponsesApiNamespaceTool::Function)
})
.collect::<Result<Vec<_>, _>>()?;
results.push(ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
name: namespace,
description: description.unwrap_or_default(),
tools,
}));
}
Ok(results)
}
fn build_search_text(name: &str, info: &ToolInfo) -> String {
let mut parts = vec![
name.to_string(),
@@ -183,7 +142,3 @@ fn build_search_text(name: &str, info: &ToolInfo) -> String {
parts.join(" ")
}
#[cfg(test)]
#[path = "tool_search_tests.rs"]
mod tests;
@@ -1,199 +0,0 @@
use super::*;
use crate::mcp::CODEX_APPS_MCP_SERVER_NAME;
use codex_tools::ResponsesApiTool;
use pretty_assertions::assert_eq;
use rmcp::model::JsonObject;
use rmcp::model::Tool;
use serde_json::json;
use std::sync::Arc;
#[test]
fn serialize_tool_search_output_tools_groups_results_by_namespace() {
let entries = [
(
"mcp__codex_apps__calendar_create_event".to_string(),
ToolInfo {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool_name: "_create_event".to_string(),
tool_namespace: "mcp__codex_apps__calendar".to_string(),
tool: Tool {
name: "calendar-create-event".to_string().into(),
title: None,
description: Some("Create a calendar event.".into()),
input_schema: Arc::new(JsonObject::from_iter([(
"type".to_string(),
json!("object"),
)])),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
},
connector_id: Some("calendar".to_string()),
connector_name: Some("Calendar".to_string()),
plugin_display_names: Vec::new(),
connector_description: Some("Plan events".to_string()),
},
),
(
"mcp__codex_apps__gmail_read_email".to_string(),
ToolInfo {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool_name: "_read_email".to_string(),
tool_namespace: "mcp__codex_apps__gmail".to_string(),
tool: Tool {
name: "gmail-read-email".to_string().into(),
title: None,
description: Some("Read an email.".into()),
input_schema: Arc::new(JsonObject::from_iter([(
"type".to_string(),
json!("object"),
)])),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
},
connector_id: Some("gmail".to_string()),
connector_name: Some("Gmail".to_string()),
plugin_display_names: Vec::new(),
connector_description: Some("Read mail".to_string()),
},
),
(
"mcp__codex_apps__calendar_list_events".to_string(),
ToolInfo {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool_name: "_list_events".to_string(),
tool_namespace: "mcp__codex_apps__calendar".to_string(),
tool: Tool {
name: "calendar-list-events".to_string().into(),
title: None,
description: Some("List calendar events.".into()),
input_schema: Arc::new(JsonObject::from_iter([(
"type".to_string(),
json!("object"),
)])),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
},
connector_id: Some("calendar".to_string()),
connector_name: Some("Calendar".to_string()),
plugin_display_names: Vec::new(),
connector_description: Some("Plan events".to_string()),
},
),
];
let tools = serialize_tool_search_output_tools(&[&entries[0], &entries[1], &entries[2]])
.expect("serialize tool search output");
assert_eq!(
tools,
vec![
ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
name: "mcp__codex_apps__calendar".to_string(),
description: "Plan events".to_string(),
tools: vec![
ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "_create_event".to_string(),
description: "Create a calendar event.".to_string(),
strict: false,
defer_loading: Some(true),
parameters: codex_tools::JsonSchema::Object {
properties: Default::default(),
required: None,
additional_properties: None,
},
output_schema: None,
}),
ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "_list_events".to_string(),
description: "List calendar events.".to_string(),
strict: false,
defer_loading: Some(true),
parameters: codex_tools::JsonSchema::Object {
properties: Default::default(),
required: None,
additional_properties: None,
},
output_schema: None,
}),
],
}),
ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
name: "mcp__codex_apps__gmail".to_string(),
description: "Read mail".to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "_read_email".to_string(),
description: "Read an email.".to_string(),
strict: false,
defer_loading: Some(true),
parameters: codex_tools::JsonSchema::Object {
properties: Default::default(),
required: None,
additional_properties: None,
},
output_schema: None,
})],
})
]
);
}
#[test]
fn serialize_tool_search_output_tools_falls_back_to_connector_name_description() {
let entries = [(
"mcp__codex_apps__gmail_batch_read_email".to_string(),
ToolInfo {
server_name: CODEX_APPS_MCP_SERVER_NAME.to_string(),
tool_name: "_batch_read_email".to_string(),
tool_namespace: "mcp__codex_apps__gmail".to_string(),
tool: Tool {
name: "gmail-batch-read-email".to_string().into(),
title: None,
description: Some("Read multiple emails.".into()),
input_schema: Arc::new(JsonObject::from_iter([(
"type".to_string(),
json!("object"),
)])),
output_schema: None,
annotations: None,
execution: None,
icons: None,
meta: None,
},
connector_id: Some("connector_gmail_456".to_string()),
connector_name: Some("Gmail".to_string()),
plugin_display_names: Vec::new(),
connector_description: None,
},
)];
let tools = serialize_tool_search_output_tools(&[&entries[0]]).expect("serialize");
assert_eq!(
tools,
vec![ToolSearchOutputTool::Namespace(ResponsesApiNamespace {
name: "mcp__codex_apps__gmail".to_string(),
description: "Tools for working with Gmail.".to_string(),
tools: vec![ResponsesApiNamespaceTool::Function(ResponsesApiTool {
name: "_batch_read_email".to_string(),
description: "Read multiple emails.".to_string(),
strict: false,
defer_loading: Some(true),
parameters: codex_tools::JsonSchema::Object {
properties: Default::default(),
required: None,
additional_properties: None,
},
output_schema: None,
})],
})]
);
}
@@ -11,6 +11,7 @@ use codex_rmcp_client::ElicitationAction;
use codex_tools::DiscoverableTool;
use codex_tools::DiscoverableToolAction;
use codex_tools::DiscoverableToolType;
use codex_tools::TOOL_SUGGEST_TOOL_NAME;
use codex_tools::filter_tool_suggest_discoverable_tools_for_client;
use rmcp::model::RequestId;
use serde::Deserialize;
@@ -30,7 +31,6 @@ use crate::tools::registry::ToolKind;
pub struct ToolSuggestHandler;
pub(crate) const TOOL_SUGGEST_TOOL_NAME: &str = "tool_suggest";
const TOOL_SUGGEST_APPROVAL_KIND_VALUE: &str = "tool_suggestion";
#[derive(Debug, Deserialize)]
+3 -3
View File
@@ -4,9 +4,6 @@ use crate::shell::Shell;
use crate::shell::ShellType;
use crate::tools::code_mode::PUBLIC_TOOL_NAME;
use crate::tools::code_mode::WAIT_TOOL_NAME;
use crate::tools::handlers::TOOL_SEARCH_DEFAULT_LIMIT;
use crate::tools::handlers::TOOL_SEARCH_TOOL_NAME;
use crate::tools::handlers::TOOL_SUGGEST_TOOL_NAME;
use crate::tools::handlers::agent_jobs::BatchJobHandler;
use crate::tools::handlers::multi_agents_common::DEFAULT_WAIT_TIMEOUT_MS;
use crate::tools::handlers::multi_agents_common::MAX_WAIT_TIMEOUT_MS;
@@ -20,6 +17,9 @@ use codex_tools::CommandToolOptions;
use codex_tools::DiscoverableTool;
use codex_tools::ShellToolOptions;
use codex_tools::SpawnAgentToolOptions;
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
use codex_tools::TOOL_SEARCH_TOOL_NAME;
use codex_tools::TOOL_SUGGEST_TOOL_NAME;
use codex_tools::ToolSearchAppSource;
use codex_tools::ToolSpec;
use codex_tools::ToolUserShellType;