Files
codex/codex-rs/core/src/tools/handlers/dynamic.rs
T
Owen Lin a0fd94bde6 feat(app-server): add ThreadItem::DynamicToolCall (#12732)
Previously, clients would call `thread/start` with dynamic_tools set,
and when a model invokes a dynamic tool, it would just make the
server->client `item/tool/call` request and wait for the client's
response to complete the tool call. This works, but it doesn't have an
`item/started` or `item/completed` event.

Now we are doing this:
- [new] emit `item/started` with `DynamicToolCall` populated with the
call arguments
- send an `item/tool/call` server request
- [new] once the client responds, emit `item/completed` with
`DynamicToolCall` populated with the response.

Also, with `persistExtendedHistory: true`, dynamic tool calls are now
reconstructable in `thread/read` and `thread/resume` as
`ThreadItem::DynamicToolCall`.
2026-02-25 12:00:10 -08:00

139 lines
4.3 KiB
Rust

use crate::codex::Session;
use crate::codex::TurnContext;
use crate::function_tool::FunctionCallError;
use crate::tools::context::ToolInvocation;
use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::handlers::parse_arguments;
use crate::tools::registry::ToolHandler;
use crate::tools::registry::ToolKind;
use async_trait::async_trait;
use codex_protocol::dynamic_tools::DynamicToolCallRequest;
use codex_protocol::dynamic_tools::DynamicToolResponse;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::FunctionCallOutputContentItem;
use codex_protocol::protocol::DynamicToolCallResponseEvent;
use codex_protocol::protocol::EventMsg;
use serde_json::Value;
use std::time::Instant;
use tokio::sync::oneshot;
use tracing::warn;
pub struct DynamicToolHandler;
#[async_trait]
impl ToolHandler for DynamicToolHandler {
fn kind(&self) -> ToolKind {
ToolKind::Function
}
async fn is_mutating(&self, _invocation: &ToolInvocation) -> bool {
true
}
async fn handle(&self, invocation: ToolInvocation) -> Result<ToolOutput, 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<_>>();
let body = FunctionCallOutputBody::ContentItems(body);
Ok(ToolOutput::Function {
body,
success: Some(success),
})
}
}
async fn request_dynamic_tool(
session: &Session,
turn_context: &TurnContext,
call_id: String,
tool: String,
arguments: Value,
) -> Option<DynamicToolResponse> {
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(),
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,
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,
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
}