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
@@ -9,7 +9,6 @@ use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::CoreToolRuntime;
|
||||
use crate::tools::registry::ToolExecutor;
|
||||
use crate::tools::registry::ToolExposure;
|
||||
use crate::tools::tool_search_entry::ToolSearchInfo;
|
||||
use crate::turn_timing::now_unix_timestamp_ms;
|
||||
use codex_protocol::dynamic_tools::DynamicToolCallRequest;
|
||||
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
||||
@@ -20,6 +19,7 @@ use codex_protocol::protocol::EventMsg;
|
||||
use codex_tools::ResponsesApiNamespace;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchInfo;
|
||||
use codex_tools::ToolSearchSourceInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::default_namespace_description;
|
||||
@@ -75,6 +75,17 @@ impl ToolExecutor<ToolInvocation> for DynamicToolHandler {
|
||||
self.exposure
|
||||
}
|
||||
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
ToolSearchInfo::from_spec(
|
||||
self.search_text.clone(),
|
||||
self.spec(),
|
||||
Some(ToolSearchSourceInfo {
|
||||
name: "Dynamic tools".to_string(),
|
||||
description: Some("Tools provided by the current Codex thread.".to_string()),
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
invocation: ToolInvocation,
|
||||
@@ -126,18 +137,7 @@ impl ToolExecutor<ToolInvocation> for DynamicToolHandler {
|
||||
}
|
||||
}
|
||||
|
||||
impl CoreToolRuntime for DynamicToolHandler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
ToolSearchInfo::from_spec(
|
||||
self.search_text.clone(),
|
||||
self.spec(),
|
||||
Some(ToolSearchSourceInfo {
|
||||
name: "Dynamic tools".to_string(),
|
||||
description: Some("Tools provided by the current Codex thread.".to_string()),
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
impl CoreToolRuntime for DynamicToolHandler {}
|
||||
|
||||
#[expect(
|
||||
clippy::await_holding_invalid_type,
|
||||
|
||||
@@ -7,6 +7,7 @@ use codex_tools::ExtensionTurnItem;
|
||||
use codex_tools::ImageGenerationCompletionFuture;
|
||||
use codex_tools::ToolCall as ExtensionToolCall;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::TurnItemEmissionFuture;
|
||||
use codex_tools::TurnItemEmitter;
|
||||
@@ -49,6 +50,10 @@ impl ToolExecutor<ToolInvocation> for ExtensionToolAdapter {
|
||||
self.0.supports_parallel_tool_calls()
|
||||
}
|
||||
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
self.0.search_info()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
invocation: ToolInvocation,
|
||||
|
||||
@@ -15,11 +15,11 @@ use crate::tools::registry::PostToolUsePayload;
|
||||
use crate::tools::registry::PreToolUsePayload;
|
||||
use crate::tools::registry::ToolExecutor;
|
||||
use crate::tools::registry::ToolTelemetryTags;
|
||||
use crate::tools::tool_search_entry::ToolSearchInfo;
|
||||
use codex_mcp::ToolInfo;
|
||||
use codex_tools::ResponsesApiNamespace;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchInfo;
|
||||
use codex_tools::ToolSearchSourceInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::mcp_tool_to_responses_api_tool;
|
||||
@@ -87,6 +87,32 @@ impl ToolExecutor<ToolInvocation> for McpHandler {
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
let source_name = self
|
||||
.tool_info
|
||||
.connector_name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|connector_name| !connector_name.is_empty())
|
||||
.unwrap_or_else(|| self.tool_info.server_name.trim());
|
||||
let source_info = (!source_name.is_empty()).then(|| ToolSearchSourceInfo {
|
||||
name: source_name.to_string(),
|
||||
description: self
|
||||
.tool_info
|
||||
.namespace_description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|description| !description.is_empty())
|
||||
.map(str::to_string),
|
||||
});
|
||||
|
||||
ToolSearchInfo::from_spec(
|
||||
build_mcp_search_text(&self.tool_info),
|
||||
self.spec(),
|
||||
source_info,
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
invocation: ToolInvocation,
|
||||
@@ -131,32 +157,6 @@ impl ToolExecutor<ToolInvocation> for McpHandler {
|
||||
}
|
||||
|
||||
impl CoreToolRuntime for McpHandler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
let source_name = self
|
||||
.tool_info
|
||||
.connector_name
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|connector_name| !connector_name.is_empty())
|
||||
.unwrap_or_else(|| self.tool_info.server_name.trim());
|
||||
let source_info = (!source_name.is_empty()).then(|| ToolSearchSourceInfo {
|
||||
name: source_name.to_string(),
|
||||
description: self
|
||||
.tool_info
|
||||
.namespace_description
|
||||
.as_deref()
|
||||
.map(str::trim)
|
||||
.filter(|description| !description.is_empty())
|
||||
.map(str::to_string),
|
||||
});
|
||||
|
||||
ToolSearchInfo::from_spec(
|
||||
build_mcp_search_text(&self.tool_info),
|
||||
self.spec(),
|
||||
source_info,
|
||||
)
|
||||
}
|
||||
|
||||
fn telemetry_tags<'a>(
|
||||
&'a self,
|
||||
_invocation: &'a ToolInvocation,
|
||||
|
||||
@@ -19,7 +19,6 @@ use crate::tools::handlers::multi_agents_spec::MULTI_AGENT_V1_NAMESPACE;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::CoreToolRuntime;
|
||||
use crate::tools::registry::ToolExecutor;
|
||||
use crate::tools::tool_search_entry::ToolSearchInfo;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
@@ -36,6 +35,7 @@ use codex_protocol::protocol::CollabWaitingBeginEvent;
|
||||
use codex_protocol::protocol::CollabWaitingEndEvent;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchInfo;
|
||||
use codex_tools::ToolSearchSourceInfo;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -16,6 +16,13 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
create_close_agent_tool_v1()
|
||||
}
|
||||
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"close_agent close shutdown stop agent subagent thread status target",
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
invocation: ToolInvocation,
|
||||
@@ -109,13 +116,6 @@ async fn handle_close_agent(
|
||||
}
|
||||
|
||||
impl CoreToolRuntime for Handler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"close_agent close shutdown stop agent subagent thread status target",
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
matches!(payload, ToolPayload::Function { .. })
|
||||
}
|
||||
|
||||
@@ -17,6 +17,13 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
create_resume_agent_tool()
|
||||
}
|
||||
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"resume_agent resume reopen closed agent subagent thread id target",
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
invocation: ToolInvocation,
|
||||
@@ -135,13 +142,6 @@ async fn handle_resume_agent(
|
||||
}
|
||||
|
||||
impl CoreToolRuntime for Handler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"resume_agent resume reopen closed agent subagent thread id target",
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
matches!(payload, ToolPayload::Function { .. })
|
||||
}
|
||||
|
||||
@@ -16,6 +16,13 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
create_send_input_tool_v1()
|
||||
}
|
||||
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"send_input send message existing agent subagent follow up interrupt redirect queue target",
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
invocation: ToolInvocation,
|
||||
@@ -91,13 +98,6 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
}
|
||||
|
||||
impl CoreToolRuntime for Handler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"send_input send message existing agent subagent follow up interrupt redirect queue target",
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
matches!(payload, ToolPayload::Function { .. })
|
||||
}
|
||||
|
||||
@@ -32,6 +32,13 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
create_spawn_agent_tool_v1(self.options.clone())
|
||||
}
|
||||
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"spawn_agent spawn agent subagent sub-agent delegate delegation parallel work worker explorer no-apps fork model reasoning",
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
invocation: ToolInvocation,
|
||||
@@ -204,13 +211,6 @@ async fn handle_spawn_agent(
|
||||
}
|
||||
|
||||
impl CoreToolRuntime for Handler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"spawn_agent spawn agent subagent sub-agent delegate delegation parallel work worker explorer no-apps fork model reasoning",
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
matches!(payload, ToolPayload::Function { .. })
|
||||
}
|
||||
|
||||
@@ -37,6 +37,13 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
create_wait_agent_tool_v1(self.options)
|
||||
}
|
||||
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"wait_agent wait agent subagent status final result complete timeout targets",
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
invocation: ToolInvocation,
|
||||
@@ -203,13 +210,6 @@ impl ToolExecutor<ToolInvocation> for Handler {
|
||||
}
|
||||
|
||||
impl CoreToolRuntime for Handler {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
multi_agent_tool_search_info(
|
||||
"wait_agent wait agent subagent status final result complete timeout targets",
|
||||
self.spec(),
|
||||
)
|
||||
}
|
||||
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
matches!(payload, ToolPayload::Function { .. })
|
||||
}
|
||||
|
||||
@@ -6,8 +6,6 @@ use crate::tools::context::boxed_tool_output;
|
||||
use crate::tools::handlers::tool_search_spec::create_tool_search_tool;
|
||||
use crate::tools::registry::CoreToolRuntime;
|
||||
use crate::tools::registry::ToolExecutor;
|
||||
use crate::tools::tool_search_entry::ToolSearchEntry;
|
||||
use crate::tools::tool_search_entry::ToolSearchInfo;
|
||||
use bm25::Document;
|
||||
use bm25::Language;
|
||||
use bm25::SearchEngine;
|
||||
@@ -16,6 +14,8 @@ use codex_tools::LoadableToolSpec;
|
||||
use codex_tools::TOOL_SEARCH_DEFAULT_LIMIT;
|
||||
use codex_tools::TOOL_SEARCH_TOOL_NAME;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchEntry;
|
||||
use codex_tools::ToolSearchInfo;
|
||||
use codex_tools::ToolSearchSourceInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::coalesce_loadable_tool_specs;
|
||||
|
||||
@@ -14,7 +14,6 @@ pub(crate) mod runtimes;
|
||||
pub(crate) mod sandboxing;
|
||||
pub(crate) mod spec_plan;
|
||||
pub(crate) mod tool_dispatch_trace;
|
||||
pub(crate) mod tool_search_entry;
|
||||
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
||||
@@ -24,13 +24,13 @@ use crate::tools::hook_names::HookToolName;
|
||||
use crate::tools::lifecycle::notify_tool_finish;
|
||||
use crate::tools::lifecycle::notify_tool_start;
|
||||
use crate::tools::tool_dispatch_trace::ToolDispatchTrace;
|
||||
use crate::tools::tool_search_entry::ToolSearchInfo;
|
||||
use crate::util::error_or_panic;
|
||||
use codex_extension_api::ToolCallOutcome;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::models::ResponseInputItem;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolSearchInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use futures::future::BoxFuture;
|
||||
use serde_json::Value;
|
||||
@@ -46,10 +46,6 @@ pub use codex_tools::ToolExposure;
|
||||
/// Implementers provide the shared `ToolExecutor` behavior plus optional
|
||||
/// core-owned metadata for hooks, telemetry, tool search, and argument diffs.
|
||||
pub(crate) trait CoreToolRuntime: ToolExecutor<ToolInvocation> {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
None
|
||||
}
|
||||
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
matches!(
|
||||
payload,
|
||||
@@ -273,6 +269,10 @@ impl ToolExecutor<ToolInvocation> for ExposureOverride {
|
||||
self.exposure != ToolExposure::Hidden && self.handler.supports_parallel_tool_calls()
|
||||
}
|
||||
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
self.handler.search_info()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
invocation: ToolInvocation,
|
||||
@@ -282,10 +282,6 @@ impl ToolExecutor<ToolInvocation> for ExposureOverride {
|
||||
}
|
||||
|
||||
impl CoreToolRuntime for ExposureOverride {
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
self.handler.search_info()
|
||||
}
|
||||
|
||||
fn matches_kind(&self, payload: &ToolPayload) -> bool {
|
||||
self.handler.matches_kind(payload)
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ use codex_tools::ToolEnvironmentMode;
|
||||
use codex_tools::ToolExecutor;
|
||||
use codex_tools::ToolName;
|
||||
use codex_tools::ToolOutput;
|
||||
use codex_tools::ToolSearchInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::UnifiedExecShellMode;
|
||||
use codex_tools::can_request_original_image_detail;
|
||||
@@ -937,6 +938,10 @@ impl ToolExecutor<ToolInvocation> for MultiAgentV2NamespaceOverride {
|
||||
self.handler.supports_parallel_tool_calls()
|
||||
}
|
||||
|
||||
fn search_info(&self) -> Option<ToolSearchInfo> {
|
||||
self.handler.search_info()
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
invocation: ToolInvocation,
|
||||
@@ -950,10 +955,6 @@ impl CoreToolRuntime for MultiAgentV2NamespaceOverride {
|
||||
self.handler.matches_kind(payload)
|
||||
}
|
||||
|
||||
fn search_info(&self) -> Option<crate::tools::tool_search_entry::ToolSearchInfo> {
|
||||
self.handler.search_info()
|
||||
}
|
||||
|
||||
fn create_diff_consumer(
|
||||
&self,
|
||||
) -> Option<Box<dyn crate::tools::registry::ToolArgumentDiffConsumer>> {
|
||||
|
||||
@@ -313,6 +313,44 @@ impl ToolExecutor<ExtensionToolCall> for WebRunExtensionTool {
|
||||
}
|
||||
}
|
||||
|
||||
struct DeferredExtensionTool;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ToolExecutor<ExtensionToolCall> for DeferredExtensionTool {
|
||||
fn tool_name(&self) -> ToolName {
|
||||
ToolName::plain("extension_echo")
|
||||
}
|
||||
|
||||
fn spec(&self) -> ToolSpec {
|
||||
ToolSpec::Function(ResponsesApiTool {
|
||||
name: "extension_echo".to_string(),
|
||||
description: "Echoes arguments through an extension tool.".to_string(),
|
||||
strict: true,
|
||||
defer_loading: None,
|
||||
parameters: codex_tools::JsonSchema::object(
|
||||
BTreeMap::from([(
|
||||
"message".to_string(),
|
||||
codex_tools::JsonSchema::string(/*description*/ None),
|
||||
)]),
|
||||
Some(vec!["message".to_string()]),
|
||||
Some(false.into()),
|
||||
),
|
||||
output_schema: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn exposure(&self) -> ToolExposure {
|
||||
ToolExposure::Deferred
|
||||
}
|
||||
|
||||
async fn handle(
|
||||
&self,
|
||||
_call: ExtensionToolCall,
|
||||
) -> Result<Box<dyn ToolOutput>, codex_tools::FunctionCallError> {
|
||||
panic!("spec planning should not execute extension tools")
|
||||
}
|
||||
}
|
||||
|
||||
fn duplicate_primary_environment(turn: &mut TurnContext) {
|
||||
let mut second_environment = turn.environments.turn_environments[0].clone();
|
||||
second_environment.environment_id = "secondary".to_string();
|
||||
@@ -702,6 +740,25 @@ async fn mcp_and_tool_search_follow_direct_and_deferred_tool_exposure() {
|
||||
]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn deferred_extension_tools_are_discoverable_with_tool_search() {
|
||||
let plan = probe_with(
|
||||
|turn| {
|
||||
turn.model_info.supports_search_tool = true;
|
||||
},
|
||||
ToolPlanInputs {
|
||||
extension_tool_executors: vec![Arc::new(DeferredExtensionTool)],
|
||||
..ToolPlanInputs::default()
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
plan.assert_visible_contains(&["tool_search"]);
|
||||
plan.assert_visible_lacks(&["extension_echo"]);
|
||||
plan.assert_registered_contains(&["extension_echo"]);
|
||||
assert_eq!(plan.exposure("extension_echo"), ToolExposure::Deferred);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_mcp_tools_are_not_registered() {
|
||||
let plan = probe_with(
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
use codex_tools::LoadableToolSpec;
|
||||
use codex_tools::ResponsesApiNamespaceTool;
|
||||
use codex_tools::ToolSearchSourceInfo;
|
||||
use codex_tools::ToolSpec;
|
||||
use codex_tools::default_namespace_description;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ToolSearchEntry {
|
||||
pub(crate) search_text: String,
|
||||
pub(crate) output: LoadableToolSpec,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct ToolSearchInfo {
|
||||
pub(crate) entry: ToolSearchEntry,
|
||||
pub(crate) source_info: Option<ToolSearchSourceInfo>,
|
||||
}
|
||||
|
||||
impl ToolSearchInfo {
|
||||
pub(crate) 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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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