mirror of
https://github.com/openai/codex.git
synced 2026-11-02 00:18:52 +00:00
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:
@@ -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;
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user