mirror of
https://github.com/openai/codex.git
synced 2026-04-24 14:45:27 +00:00
## Summary Add dynamic tool injection to thread startup in API v2, wire dynamic tool calls through the app server to clients, and plumb responses back into the model tool pipeline. ### Flow (high level) - Thread start injects `dynamic_tools` into the model tool list for that thread (validation is done here). - When the model emits a tool call for one of those names, core raises a `DynamicToolCallRequest` event. - The app server forwards it to the client as `item/tool/call`, waits for the client’s response, then submits a `DynamicToolResponse` back to core. - Core turns that into a `function_call_output` in the next model request so the model can continue. ### What changed - Added dynamic tool specs to v2 thread start params and protocol types; introduced `item/tool/call` (request/response) for dynamic tool execution. - Core now registers dynamic tool specs at request time and routes those calls via a new dynamic tool handler. - App server validates tool names/schemas, forwards dynamic tool call requests to clients, and publishes tool outputs back into the session. - Integration tests
99 lines
3.0 KiB
Rust
99 lines
3.0 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::protocol::EventMsg;
|
|
use serde_json::Value;
|
|
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(),
|
|
)
|
|
})?;
|
|
|
|
Ok(ToolOutput::Function {
|
|
content: response.output,
|
|
content_items: None,
|
|
success: Some(response.success),
|
|
})
|
|
}
|
|
}
|
|
|
|
async fn request_dynamic_tool(
|
|
session: &Session,
|
|
turn_context: &TurnContext,
|
|
call_id: String,
|
|
tool: String,
|
|
arguments: Value,
|
|
) -> Option<DynamicToolResponse> {
|
|
let _sub_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 event = EventMsg::DynamicToolCallRequest(DynamicToolCallRequest {
|
|
call_id,
|
|
turn_id: turn_context.sub_id.clone(),
|
|
tool,
|
|
arguments,
|
|
});
|
|
session.send_event(turn_context, event).await;
|
|
rx_response.await.ok()
|
|
}
|