mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Move tool search metadata onto ToolExecutor (#25684)
Deferred tools need to be searchable even when they are not implemented inside `codex-core`. Extension-provided tools can be registered for later discovery, but the search metadata path was still owned by core-specific runtime hooks, which meant the shared `ToolExecutor` abstraction could not describe how a deferred extension tool should appear in `tool_search`. ## Changes - Move `ToolSearchEntry` and `ToolSearchInfo` into `codex-tools` and re-export them from the shared tools crate. - Add a default `ToolExecutor::search_info` implementation that derives loadable tool-search metadata from function and namespace specs. - Forward search metadata through extension adapters and exposure overrides while keeping custom search text/source metadata for dynamic, MCP, and multi-agent tools. - Remove the old core-local `tool_search_entry` module now that search metadata lives with the shared executor APIs. ## Testing - Added `deferred_extension_tools_are_discoverable_with_tool_search` coverage in `core/src/tools/spec_plan_tests.rs`.
This commit is contained in:
committed by
GitHub
Unverified
parent
8ee49a2f74
commit
8d720feb69
@@ -17,6 +17,7 @@ mod tool_discovery;
|
||||
mod tool_executor;
|
||||
mod tool_output;
|
||||
mod tool_payload;
|
||||
mod tool_search;
|
||||
mod tool_spec;
|
||||
|
||||
pub use code_mode::augment_tool_spec_for_code_mode;
|
||||
@@ -97,6 +98,9 @@ pub use tool_executor::ToolExposure;
|
||||
pub use tool_output::JsonToolOutput;
|
||||
pub use tool_output::ToolOutput;
|
||||
pub use tool_payload::ToolPayload;
|
||||
pub use tool_search::ToolSearchEntry;
|
||||
pub use tool_search::ToolSearchInfo;
|
||||
pub use tool_search::default_tool_search_text;
|
||||
pub use tool_spec::ResponsesApiWebSearchFilters;
|
||||
pub use tool_spec::ResponsesApiWebSearchUserLocation;
|
||||
pub use tool_spec::ToolSpec;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::FunctionCallError;
|
||||
use crate::ToolName;
|
||||
use crate::ToolOutput;
|
||||
use crate::ToolSearchInfo;
|
||||
use crate::ToolSpec;
|
||||
|
||||
/// Controls where a tool is exposed to the model.
|
||||
@@ -13,7 +14,9 @@ pub enum ToolExposure {
|
||||
Direct,
|
||||
|
||||
/// Register this tool for later discovery, but omit it from the initial
|
||||
/// model-visible tool list.
|
||||
/// model-visible tool list. Deferred tools must provide search metadata via
|
||||
/// [`ToolExecutor::search_info`]. The default implementation derives
|
||||
/// metadata from function and namespace specs.
|
||||
Deferred,
|
||||
|
||||
/// Include this tool in the initial model-visible tool list only.
|
||||
@@ -48,6 +51,11 @@ pub trait ToolExecutor<Invocation>: Send + Sync {
|
||||
ToolExposure::Direct
|
||||
}
|
||||
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
let spec = self.spec();
|
||||
ToolSearchInfo::from_tool_spec(&self.tool_name(), spec, /*source_info*/ None)
|
||||
}
|
||||
|
||||
fn supports_parallel_tool_calls(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
use crate::JsonSchema;
|
||||
use crate::LoadableToolSpec;
|
||||
use crate::ResponsesApiNamespaceTool;
|
||||
use crate::ResponsesApiTool;
|
||||
use crate::ToolName;
|
||||
use crate::ToolSearchSourceInfo;
|
||||
use crate::ToolSpec;
|
||||
use crate::default_namespace_description;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToolSearchEntry {
|
||||
pub search_text: String,
|
||||
pub output: LoadableToolSpec,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToolSearchInfo {
|
||||
pub entry: ToolSearchEntry,
|
||||
pub source_info: Option<ToolSearchSourceInfo>,
|
||||
}
|
||||
|
||||
impl ToolSearchInfo {
|
||||
pub fn from_tool_spec(
|
||||
tool_name: &ToolName,
|
||||
spec: ToolSpec,
|
||||
source_info: Option<ToolSearchSourceInfo>,
|
||||
) -> Option<Self> {
|
||||
let search_text = default_tool_search_text(tool_name, &spec);
|
||||
Self::from_spec(search_text, spec, source_info)
|
||||
}
|
||||
|
||||
pub fn from_spec(
|
||||
search_text: String,
|
||||
spec: ToolSpec,
|
||||
source_info: Option<ToolSearchSourceInfo>,
|
||||
) -> Option<Self> {
|
||||
let output = match spec {
|
||||
ToolSpec::Function(mut tool) => {
|
||||
tool.defer_loading = Some(true);
|
||||
tool.output_schema = None;
|
||||
LoadableToolSpec::Function(tool)
|
||||
}
|
||||
ToolSpec::Namespace(mut namespace) => {
|
||||
if namespace.description.trim().is_empty() {
|
||||
namespace.description = default_namespace_description(&namespace.name);
|
||||
}
|
||||
for tool in &mut namespace.tools {
|
||||
let ResponsesApiNamespaceTool::Function(tool) = tool;
|
||||
tool.defer_loading = Some(true);
|
||||
tool.output_schema = None;
|
||||
}
|
||||
LoadableToolSpec::Namespace(namespace)
|
||||
}
|
||||
ToolSpec::ToolSearch { .. }
|
||||
| ToolSpec::ImageGeneration { .. }
|
||||
| ToolSpec::WebSearch { .. }
|
||||
| ToolSpec::Freeform(_) => return None,
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
entry: ToolSearchEntry {
|
||||
search_text,
|
||||
output,
|
||||
},
|
||||
source_info,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_tool_search_text(tool_name: &ToolName, spec: &ToolSpec) -> String {
|
||||
let mut parts = Vec::new();
|
||||
push_search_part(&mut parts, tool_name.to_string());
|
||||
push_search_part(&mut parts, tool_name.name.replace('_', " "));
|
||||
if let Some(namespace) = &tool_name.namespace {
|
||||
push_search_part(&mut parts, namespace.clone());
|
||||
}
|
||||
|
||||
match spec {
|
||||
ToolSpec::Function(tool) => append_function_search_text(tool, &mut parts),
|
||||
ToolSpec::Namespace(namespace) => {
|
||||
push_search_part(&mut parts, namespace.name.clone());
|
||||
push_search_part(&mut parts, namespace.description.clone());
|
||||
for tool in &namespace.tools {
|
||||
let ResponsesApiNamespaceTool::Function(tool) = tool;
|
||||
append_function_search_text(tool, &mut parts);
|
||||
}
|
||||
}
|
||||
ToolSpec::ToolSearch { description, .. } => {
|
||||
push_search_part(&mut parts, description.clone());
|
||||
}
|
||||
ToolSpec::ImageGeneration { .. } => {
|
||||
push_search_part(&mut parts, "image generation".to_string());
|
||||
}
|
||||
ToolSpec::WebSearch { .. } => {
|
||||
push_search_part(&mut parts, "web search".to_string());
|
||||
}
|
||||
ToolSpec::Freeform(tool) => {
|
||||
push_search_part(&mut parts, tool.name.clone());
|
||||
push_search_part(&mut parts, tool.description.clone());
|
||||
push_search_part(&mut parts, tool.format.syntax.clone());
|
||||
}
|
||||
}
|
||||
|
||||
parts.join(" ")
|
||||
}
|
||||
|
||||
fn append_function_search_text(tool: &ResponsesApiTool, parts: &mut Vec<String>) {
|
||||
push_search_part(parts, tool.name.clone());
|
||||
push_search_part(parts, tool.name.replace('_', " "));
|
||||
push_search_part(parts, tool.description.clone());
|
||||
append_schema_search_text(&tool.parameters, parts);
|
||||
}
|
||||
|
||||
fn append_schema_search_text(schema: &JsonSchema, parts: &mut Vec<String>) {
|
||||
if let Some(description) = &schema.description {
|
||||
push_search_part(parts, description.clone());
|
||||
}
|
||||
if let Some(properties) = &schema.properties {
|
||||
for (name, schema) in properties {
|
||||
push_search_part(parts, name.clone());
|
||||
append_schema_search_text(schema, parts);
|
||||
}
|
||||
}
|
||||
if let Some(items) = &schema.items {
|
||||
append_schema_search_text(items, parts);
|
||||
}
|
||||
if let Some(variants) = &schema.any_of {
|
||||
for variant in variants {
|
||||
append_schema_search_text(variant, parts);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn push_search_part(parts: &mut Vec<String>, part: String) {
|
||||
let part = part.trim();
|
||||
if !part.is_empty() {
|
||||
parts.push(part.to_string());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user