mirror of
https://github.com/openai/codex.git
synced 2026-11-02 00:18:52 +00:00
Deferred dynamic tools need to round-trip a namespace so a tool returned by `tool_search` can be called through the same registry key that core uses for dispatch. This change adds namespace support for dynamic tool specs/calls, persists it through app-server thread state, and routes dynamic tool calls by full `ToolName` while still sending the app the leaf tool name. Deferred dynamic tools must provide a namespace; non-deferred dynamic tools may remain top-level. It also introduces `LoadableToolSpec` as the shared function-or-namespace Responses shape used by both `tool_search` output and dynamic tool registration, so dynamic tools use the same wrapping logic in both paths. Validation: - `cargo test -p codex-tools` - `cargo test -p codex-core tool_search` --------- Co-authored-by: Sayan Sisodiya <sayan@openai.com>
143 lines
4.5 KiB
Rust
143 lines
4.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::handlers::parse_arguments;
|
|
use crate::tools::registry::ToolHandler;
|
|
use crate::tools::registry::ToolKind;
|
|
use codex_protocol::dynamic_tools::DynamicToolCallRequest;
|
|
use codex_protocol::dynamic_tools::DynamicToolResponse;
|
|
use codex_protocol::models::FunctionCallOutputContentItem;
|
|
use codex_protocol::protocol::DynamicToolCallResponseEvent;
|
|
use codex_protocol::protocol::EventMsg;
|
|
use codex_tools::ToolName;
|
|
use serde_json::Value;
|
|
use std::time::Instant;
|
|
use tokio::sync::oneshot;
|
|
use tracing::warn;
|
|
|
|
pub struct DynamicToolHandler;
|
|
|
|
impl ToolHandler for DynamicToolHandler {
|
|
type Output = FunctionToolOutput;
|
|
|
|
fn kind(&self) -> ToolKind {
|
|
ToolKind::Function
|
|
}
|
|
|
|
async fn is_mutating(&self, _invocation: &ToolInvocation) -> bool {
|
|
true
|
|
}
|
|
|
|
async fn handle(&self, invocation: ToolInvocation) -> Result<Self::Output, FunctionCallError> {
|
|
let ToolInvocation {
|
|
session,
|
|
turn,
|
|
call_id,
|
|
tool_name,
|
|
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, tool_name, 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(FunctionToolOutput::from_content(body, Some(success)))
|
|
}
|
|
}
|
|
|
|
#[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 event = EventMsg::DynamicToolCallRequest(DynamicToolCallRequest {
|
|
call_id: call_id.clone(),
|
|
turn_id: turn_id.clone(),
|
|
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,
|
|
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,
|
|
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
|
|
}
|