mirror of
https://github.com/openai/codex.git
synced 2026-11-01 16:17:58 +00:00
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`.
139 lines
4.3 KiB
Rust
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
|
|
}
|