mirror of
https://github.com/openai/codex.git
synced 2026-11-07 08:34:29 +00:00
## Why The tool runtime path still had a typed output associated type on `ToolExecutor`, plus a core-only `RegisteredTool` adapter and extension-only executor aliases. That made every new shared tool runtime carry extra adapter plumbing before it could participate in core dispatch, extension tools, hook payloads, telemetry, and model-visible spec generation. This PR moves output erasure to the shared executor boundary so core and extension tools can use the same execution contract directly. ## What Changed - Changed `codex_tools::ToolExecutor` to return `Box<dyn ToolOutput>` instead of an associated `Output` type. - Removed the extension-specific `ExtensionToolExecutor` / `ExtensionToolOutput` aliases and exposed `ToolExecutor<ToolCall>` plus `ToolOutput` through `codex-extension-api`. - Reworked core tool registration around `CoreToolRuntime` and `ToolRegistry::from_tools`, removing the extra `RegisteredTool` / `ToolRegistryBuilder` layer. - Consolidated model-visible spec planning and registry construction in `core/src/tools/spec_plan.rs`, including deferred tool search and code-mode-only filtering. - Added `ToolOutput` helpers for post-tool-use hook ids and inputs so MCP, unified exec, extension, and other boxed outputs preserve the same hook payload behavior. - Updated core handlers, memories tools, and the related registry/spec/router tests to use the simplified contract. ## Test Coverage - Updated coverage for tool spec planning, registry lookup, deferred tool search registration, extension tool routing, post-tool-use hook payloads, dispatch tracing, guardian output extraction, and memories extension tool execution.
239 lines
7.5 KiB
Rust
239 lines
7.5 KiB
Rust
use crate::function_tool::FunctionCallError;
|
|
use crate::session::session::Session;
|
|
use crate::session::turn_context::TurnContext;
|
|
use crate::tools::context::FunctionToolOutput;
|
|
use crate::tools::context::ToolInvocation;
|
|
use crate::tools::context::ToolPayload;
|
|
use crate::tools::context::boxed_tool_output;
|
|
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;
|
|
use codex_protocol::dynamic_tools::DynamicToolSpec;
|
|
use codex_protocol::models::FunctionCallOutputContentItem;
|
|
use codex_protocol::protocol::DynamicToolCallResponseEvent;
|
|
use codex_protocol::protocol::EventMsg;
|
|
use codex_tools::ResponsesApiNamespace;
|
|
use codex_tools::ResponsesApiNamespaceTool;
|
|
use codex_tools::ToolName;
|
|
use codex_tools::ToolSearchSourceInfo;
|
|
use codex_tools::ToolSpec;
|
|
use codex_tools::default_namespace_description;
|
|
use codex_tools::dynamic_tool_to_responses_api_tool;
|
|
use serde_json::Value;
|
|
use std::time::Instant;
|
|
use tokio::sync::oneshot;
|
|
use tracing::warn;
|
|
|
|
pub struct DynamicToolHandler {
|
|
tool_name: ToolName,
|
|
spec: Option<ToolSpec>,
|
|
exposure: ToolExposure,
|
|
search_text: String,
|
|
}
|
|
|
|
impl DynamicToolHandler {
|
|
pub fn new(tool: &DynamicToolSpec) -> Option<Self> {
|
|
let tool_name = ToolName::new(tool.namespace.clone(), tool.name.clone());
|
|
let output_tool = dynamic_tool_to_responses_api_tool(tool).ok()?;
|
|
let spec = match tool.namespace.as_ref() {
|
|
Some(namespace) => ToolSpec::Namespace(ResponsesApiNamespace {
|
|
name: namespace.clone(),
|
|
description: default_namespace_description(namespace),
|
|
tools: vec![ResponsesApiNamespaceTool::Function(output_tool)],
|
|
}),
|
|
None => ToolSpec::Function(output_tool),
|
|
};
|
|
Some(Self {
|
|
tool_name,
|
|
spec: Some(spec),
|
|
exposure: if tool.defer_loading {
|
|
ToolExposure::Deferred
|
|
} else {
|
|
ToolExposure::Direct
|
|
},
|
|
search_text: build_dynamic_search_text(tool),
|
|
})
|
|
}
|
|
}
|
|
|
|
#[async_trait::async_trait]
|
|
impl ToolExecutor<ToolInvocation> for DynamicToolHandler {
|
|
fn tool_name(&self) -> ToolName {
|
|
self.tool_name.clone()
|
|
}
|
|
|
|
fn spec(&self) -> Option<ToolSpec> {
|
|
self.spec.clone()
|
|
}
|
|
|
|
fn exposure(&self) -> ToolExposure {
|
|
self.exposure
|
|
}
|
|
|
|
async fn handle(
|
|
&self,
|
|
invocation: ToolInvocation,
|
|
) -> Result<Box<dyn crate::tools::context::ToolOutput>, FunctionCallError> {
|
|
let ToolInvocation {
|
|
session,
|
|
turn,
|
|
call_id,
|
|
payload,
|
|
..
|
|
} = invocation;
|
|
|
|
let arguments = match payload {
|
|
ToolPayload::Function { arguments } => arguments,
|
|
_ => {
|
|
return Err(FunctionCallError::RespondToModel(
|
|
"dynamic tool handler received unsupported payload".to_string(),
|
|
));
|
|
}
|
|
};
|
|
|
|
let args: Value = parse_arguments(&arguments)?;
|
|
let response = request_dynamic_tool(
|
|
&session,
|
|
turn.as_ref(),
|
|
call_id,
|
|
self.tool_name.clone(),
|
|
args,
|
|
)
|
|
.await
|
|
.ok_or_else(|| {
|
|
FunctionCallError::RespondToModel(
|
|
"dynamic tool call was cancelled before receiving a response".to_string(),
|
|
)
|
|
})?;
|
|
|
|
let DynamicToolResponse {
|
|
content_items,
|
|
success,
|
|
} = response;
|
|
let body = content_items
|
|
.into_iter()
|
|
.map(FunctionCallOutputContentItem::from)
|
|
.collect::<Vec<_>>();
|
|
Ok(boxed_tool_output(FunctionToolOutput::from_content(
|
|
body,
|
|
Some(success),
|
|
)))
|
|
}
|
|
}
|
|
|
|
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()),
|
|
}),
|
|
)
|
|
}
|
|
}
|
|
|
|
#[expect(
|
|
clippy::await_holding_invalid_type,
|
|
reason = "active turn checks and dynamic tool response registration must remain atomic"
|
|
)]
|
|
async fn request_dynamic_tool(
|
|
session: &Session,
|
|
turn_context: &TurnContext,
|
|
call_id: String,
|
|
tool_name: ToolName,
|
|
arguments: Value,
|
|
) -> Option<DynamicToolResponse> {
|
|
let namespace = tool_name.namespace;
|
|
let tool = tool_name.name;
|
|
let turn_id = turn_context.sub_id.clone();
|
|
let (tx_response, rx_response) = oneshot::channel();
|
|
let event_id = call_id.clone();
|
|
let prev_entry = {
|
|
let mut active = session.active_turn.lock().await;
|
|
match active.as_mut() {
|
|
Some(at) => {
|
|
let mut ts = at.turn_state.lock().await;
|
|
ts.insert_pending_dynamic_tool(call_id.clone(), tx_response)
|
|
}
|
|
None => None,
|
|
}
|
|
};
|
|
if prev_entry.is_some() {
|
|
warn!("Overwriting existing pending dynamic tool call for call_id: {event_id}");
|
|
}
|
|
|
|
let started_at = Instant::now();
|
|
let started_at_ms = now_unix_timestamp_ms();
|
|
let event = EventMsg::DynamicToolCallRequest(DynamicToolCallRequest {
|
|
call_id: call_id.clone(),
|
|
turn_id: turn_id.clone(),
|
|
started_at_ms,
|
|
namespace: namespace.clone(),
|
|
tool: tool.clone(),
|
|
arguments: arguments.clone(),
|
|
});
|
|
session.send_event(turn_context, event).await;
|
|
let response = rx_response.await.ok();
|
|
|
|
let response_event = match &response {
|
|
Some(response) => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
|
|
call_id,
|
|
turn_id,
|
|
completed_at_ms: now_unix_timestamp_ms(),
|
|
namespace,
|
|
tool,
|
|
arguments,
|
|
content_items: response.content_items.clone(),
|
|
success: response.success,
|
|
error: None,
|
|
duration: started_at.elapsed(),
|
|
}),
|
|
None => EventMsg::DynamicToolCallResponse(DynamicToolCallResponseEvent {
|
|
call_id,
|
|
turn_id,
|
|
completed_at_ms: now_unix_timestamp_ms(),
|
|
namespace,
|
|
tool,
|
|
arguments,
|
|
content_items: Vec::new(),
|
|
success: false,
|
|
error: Some("dynamic tool call was cancelled before receiving a response".to_string()),
|
|
duration: started_at.elapsed(),
|
|
}),
|
|
};
|
|
session.send_event(turn_context, response_event).await;
|
|
|
|
response
|
|
}
|
|
|
|
fn build_dynamic_search_text(tool: &DynamicToolSpec) -> String {
|
|
let mut schema_properties = tool
|
|
.input_schema
|
|
.get("properties")
|
|
.and_then(serde_json::Value::as_object)
|
|
.map(|map| map.keys().cloned().collect::<Vec<_>>())
|
|
.unwrap_or_default();
|
|
schema_properties.sort();
|
|
let mut parts = vec![
|
|
tool.name.clone(),
|
|
tool.name.replace('_', " "),
|
|
tool.description.clone(),
|
|
];
|
|
if let Some(namespace) = &tool.namespace {
|
|
parts.push(namespace.clone());
|
|
}
|
|
parts.extend(schema_properties);
|
|
parts.join(" ")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "dynamic_tests.rs"]
|
|
mod tests;
|