use serde_json::Value;
use crate::driver_registry::LlmCallConfig;
use crate::error::{AgentLoopError, Result};
use std::collections::BTreeMap;
use crate::driver_registry::{HostedToolCall, HostedToolCallStatus, LlmStreamEvent};
use crate::openai_hosted_tools::{
OPENAI_MCP_APPROVAL_TOOL, OpenAiHostedTools, count_hosted_tool_calls, hosted_call_tool,
};
use super::OpenResponsesProtocolChatDriver;
use super::wire::{ResponsesContent, ResponsesInputItem};
impl OpenResponsesProtocolChatDriver {
pub(crate) fn hosted_tools_for(&self, config: &LlmCallConfig) -> Result<Vec<Value>> {
let requested =
OpenAiHostedTools::from_driver_options(&config.driver_options).map_err(|error| {
AgentLoopError::Configuration(format!("invalid OpenAI hosted tools: {error}"))
})?;
let native_computer = self.native_computer_for(config);
let Some(mut requested) = requested.or_else(|| native_computer.map(|_| Default::default()))
else {
return Ok(Vec::new());
};
requested.computer = native_computer;
if !self.hosted_tools {
return Err(AgentLoopError::Configuration(
"OpenAI hosted tools need the OpenAI or Azure OpenAI Responses API; \
this provider does not run them"
.to_string(),
));
}
if let Some(server) = requested
.mcp_servers
.iter()
.find(|s| s.server_url.is_empty())
{
return Err(AgentLoopError::Configuration(format!(
"MCP server {} needs a registered connection this host cannot resolve",
server.server_label
)));
}
Ok(requested.wire_tools())
}
}
pub(crate) fn hosted_tool_calls(response: &Value) -> BTreeMap<String, u32> {
response
.get("output")
.and_then(Value::as_array)
.map(|output| count_hosted_tool_calls(output))
.unwrap_or_default()
}
pub(crate) fn hosted_call_event(event_data: &str) -> Option<LlmStreamEvent> {
if !event_data.contains("_call") || !event_data.contains("response.output_item.") {
return None;
}
let frame: Value = serde_json::from_str(event_data).ok()?;
let done = match frame.get("type").and_then(Value::as_str)? {
"response.output_item.added" => false,
"response.output_item.done" => true,
_ => return None,
};
let item = frame.get("item")?;
let tool = hosted_call_tool(item.get("type").and_then(Value::as_str)?)?;
let failed = matches!(
item.get("status").and_then(Value::as_str),
Some("failed" | "incomplete")
);
let status = match (done, failed) {
(_, true) => HostedToolCallStatus::Failed,
(true, false) => HostedToolCallStatus::Completed,
(false, false) => HostedToolCallStatus::InProgress,
};
let summary = call_summary(item).map(|detail| detail.chars().take(200).collect());
Some(LlmStreamEvent::HostedToolCall(HostedToolCall {
id: item
.get("id")
.and_then(Value::as_str)
.unwrap_or_default()
.to_string(),
tool: tool.to_string(),
status,
summary,
}))
}
pub(crate) fn mcp_approval_call(item: &Value) -> Option<(String, String)> {
crate::openai_hosted_tools::mcp_approval_call(item).map(|(id, args)| (id, args.to_string()))
}
pub(crate) fn replay_mcp_approvals(items: Vec<ResponsesInputItem>) -> Vec<ResponsesInputItem> {
let mut approval_ids = std::collections::HashSet::new();
items
.into_iter()
.map(|item| match item {
ResponsesInputItem::FunctionCall {
call_id,
name,
arguments,
..
} if name == OPENAI_MCP_APPROVAL_TOOL => {
let args: Value = serde_json::from_str(&arguments).unwrap_or_default();
let field = |key: &str| args.get(key).and_then(Value::as_str).unwrap_or_default();
let request = serde_json::json!({
"type": "mcp_approval_request",
"id": call_id,
"server_label": field("server_label"),
"name": field("name"),
"arguments": field("arguments"),
});
approval_ids.insert(call_id);
ResponsesInputItem::ProviderItem(request)
}
ResponsesInputItem::FunctionCallOutput {
call_id, output, ..
} if approval_ids.contains(&call_id) => {
let text = match &output {
ResponsesContent::Text(text) => text.as_str(),
ResponsesContent::Parts(_) => "",
};
let approve = serde_json::from_str::<Value>(text)
.ok()
.and_then(|result| result.get("approve").and_then(Value::as_bool))
.unwrap_or(false);
ResponsesInputItem::ProviderItem(serde_json::json!({
"type": "mcp_approval_response",
"approval_request_id": call_id,
"approve": approve,
}))
}
other => other,
})
.collect()
}
fn call_summary(item: &Value) -> Option<String> {
let action = item.get("action");
let field = |key: &str| action.and_then(|a| a.get(key)).and_then(Value::as_str);
let first_line = |text: &str| {
text.lines()
.find(|line| !line.trim().is_empty())
.map(str::trim)
.map(str::to_string)
};
match item.get("type").and_then(Value::as_str)? {
"web_search_call" => field("query").or_else(|| field("url")).map(str::to_string),
"code_interpreter_call" => item
.get("code")
.and_then(Value::as_str)
.and_then(first_line),
"shell_call" => action
.and_then(|a| a.get("commands"))
.and_then(Value::as_array)
.map(|commands| {
commands
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>()
.join("; ")
})
.filter(|commands| !commands.is_empty()),
"mcp_call" => Some(format!(
"{}: {}",
item.get("server_label")
.and_then(Value::as_str)
.unwrap_or("mcp"),
item.get("name").and_then(Value::as_str)?
)),
"file_search_call" => item
.get("queries")
.and_then(Value::as_array)
.and_then(|queries| queries.first())
.and_then(Value::as_str)
.map(str::to_string),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn event(frame: Value) -> Option<HostedToolCall> {
match hosted_call_event(&frame.to_string())? {
LlmStreamEvent::HostedToolCall(call) => Some(call),
other => panic!("unexpected {other:?}"),
}
}
#[test]
fn web_search_item_frames_become_hosted_call_events() {
let started = event(json!({
"type": "response.output_item.added", "output_index": 0,
"item": { "type": "web_search_call", "id": "ws_1", "status": "in_progress" }
}))
.unwrap();
assert_eq!(started.status, HostedToolCallStatus::InProgress);
assert_eq!(started.tool, "web_search");
assert_eq!(started.summary, None);
let done = event(json!({
"type": "response.output_item.done", "output_index": 0,
"item": { "type": "web_search_call", "id": "ws_1", "status": "completed",
"action": { "type": "search", "query": "everruns release" } }
}))
.unwrap();
assert_eq!(done.id, "ws_1");
assert_eq!(done.status, HostedToolCallStatus::Completed);
assert_eq!(done.summary.as_deref(), Some("everruns release"));
let failed = event(json!({
"type": "response.output_item.done",
"item": { "type": "web_search_call", "id": "ws_2", "status": "failed" }
}))
.unwrap();
assert_eq!(failed.status, HostedToolCallStatus::Failed);
}
#[test]
fn container_and_file_calls_carry_their_detail() {
let done = |item: Value| {
event(json!({ "type": "response.output_item.done", "item": item })).unwrap()
};
let code = done(json!({ "type": "code_interpreter_call", "id": "ci_1",
"status": "completed", "code": "\nimport math\nmath.pi\n" }));
assert_eq!(
(code.tool.as_str(), code.summary.as_deref()),
("code_interpreter", Some("import math"))
);
let shell = done(
json!({ "type": "shell_call", "id": "sh_1", "status": "completed",
"action": { "commands": ["ls", "uname -s"] } }),
);
assert_eq!(
(shell.tool.as_str(), shell.summary.as_deref()),
("shell", Some("ls; uname -s"))
);
let files = done(
json!({ "type": "file_search_call", "id": "fs_1", "status": "completed",
"queries": ["refund policy"] }),
);
assert_eq!(
(files.tool.as_str(), files.summary.as_deref()),
("file_search", Some("refund policy"))
);
assert!(
event(json!({ "type": "response.output_item.done",
"item": { "type": "shell_call_output", "id": "sho_1" } }))
.is_none()
);
}
#[test]
fn mcp_calls_are_hosted_calls_and_approvals_are_not() {
let call = event(json!({ "type": "response.output_item.done", "item": {
"type": "mcp_call", "id": "mcp_1", "status": "completed",
"server_label": "deepwiki", "name": "ask_question" } }))
.unwrap();
assert_eq!(
(call.tool.as_str(), call.summary.as_deref()),
("mcp", Some("deepwiki: ask_question"))
);
assert!(
event(json!({ "type": "response.output_item.done", "item": {
"type": "mcp_approval_request", "id": "mcpr_1" } }))
.is_none()
);
}
#[test]
fn approval_replays_as_request_and_response() {
let args = json!({ "server_label": "deepwiki", "name": "ask", "arguments": "{}" });
let call = |id: &str| ResponsesInputItem::FunctionCall {
r#type: "function_call".into(),
call_id: id.into(),
name: OPENAI_MCP_APPROVAL_TOOL.into(),
arguments: args.to_string(),
};
let output = |id: &str, output: &str| ResponsesInputItem::FunctionCallOutput {
r#type: "function_call_output".into(),
call_id: id.into(),
output: output.to_string().into(),
};
let items = replay_mcp_approvals(vec![
call("mcpr_1"),
output("mcpr_1", r#"{"approve":true}"#),
call("mcpr_2"),
output("mcpr_2", "denied by user"),
output("fc_1", r#"{"approve":true}"#),
]);
let wire: Vec<Value> = items
.iter()
.map(|i| serde_json::to_value(i).unwrap())
.collect();
assert_eq!(
wire[0],
json!({ "type": "mcp_approval_request", "id": "mcpr_1", "server_label": "deepwiki",
"name": "ask", "arguments": "{}" })
);
assert_eq!(
wire[1],
json!({ "type": "mcp_approval_response", "approval_request_id": "mcpr_1", "approve": true })
);
assert_eq!(wire[3]["approve"], false);
assert_eq!(wire[4]["type"], "function_call_output");
}
#[test]
fn other_frames_are_not_hosted_calls() {
for frame in [
json!({ "type": "response.output_item.done",
"item": { "type": "function_call", "id": "fc_1", "call_id": "c", "name": "f", "arguments": "{}" } }),
json!({ "type": "response.web_search_call.searching", "item_id": "ws_1" }),
json!({ "type": "response.output_text.delta", "delta": "a web_search_call" }),
] {
assert!(event(frame).is_none());
}
}
#[test]
fn terminal_response_counts_hosted_calls() {
let counts = hosted_tool_calls(&json!({ "output": [
{ "type": "web_search_call", "id": "ws_1" },
{ "type": "message", "id": "m" },
]}));
assert_eq!(counts.get("web_search_call"), Some(&1));
assert!(hosted_tool_calls(&json!({})).is_empty());
}
}