use std::time::Duration;
use futures::future::join_all;
use serde_json::{Value, json};
use crate::executor::error::{ExecutorError, ExecutorResult};
use crate::executor::inference::fetch_response_json_with_headers;
use crate::executor::messages_request::{normalize_native_web_search, web_search_budget_exhausted_result};
use crate::executor::request::ExecutionContext;
use crate::tool::ToolRegistry;
use crate::types::messages::tool_seam;
use crate::utils::common::{deserialize_from_str, serialize_to_string};
pub(super) const MAX_GATEWAY_TOOL_ROUNDS: usize = 10;
pub(super) const GATEWAY_TOOL_TIMEOUT: Duration = Duration::from_secs(60);
#[derive(Clone, Debug)]
pub struct MessagesUpstream {
url: String,
headers: reqwest::header::HeaderMap,
}
impl MessagesUpstream {
#[must_use]
pub fn new(base_url: &str, query: Option<&str>, headers: reqwest::header::HeaderMap) -> Self {
let mut url = format!("{}/v1/messages", base_url.trim_end_matches('/'));
if let Some(query) = query.filter(|query| !query.is_empty()) {
url.push('?');
url.push_str(query);
}
Self { url, headers }
}
pub(super) fn url(&self) -> &str {
&self.url
}
pub(super) fn headers(&self) -> &reqwest::header::HeaderMap {
&self.headers
}
}
pub struct MessagesResponse<T> {
pub body: T,
pub headers: http::HeaderMap,
}
struct ResolvedCall {
tool_result_block: Value,
}
pub async fn run_messages_loop(
mut request: Value,
registry: &ToolRegistry,
exec_ctx: &ExecutionContext,
upstream: &MessagesUpstream,
) -> ExecutorResult<MessagesResponse<Value>> {
let mut web_search_budget = normalize_native_web_search(&mut request)?;
request["stream"] = Value::Bool(false);
for _round in 0..MAX_GATEWAY_TOOL_ROUNDS {
let body = serialize_to_string(&request).map_err(ExecutorError::JsonError)?;
let (resp_text, response_headers) =
fetch_response_json_with_headers(body, &upstream.url, &exec_ctx.client, &upstream.headers).await?;
let message: Value = deserialize_from_str(&resp_text).map_err(ExecutorError::JsonError)?;
if message.get("type").and_then(Value::as_str) == Some("error") {
return Ok(MessagesResponse {
body: message,
headers: response_headers,
});
}
let content = message.get("content").and_then(Value::as_array);
let stop_reason = message.get("stop_reason").and_then(Value::as_str);
let Some(content) = content else {
return Ok(MessagesResponse {
body: message,
headers: response_headers,
});
};
let gateway_map = &exec_ctx.messages_gateway_tools;
let mut gateway_calls: Vec<Value> = Vec::new();
let mut has_client_tool_use = false;
for block in content {
if block.get("type").and_then(Value::as_str) == Some("tool_use") {
let name = block.get("name").and_then(Value::as_str).unwrap_or_default();
if gateway_map.is_gateway_owned(name) {
gateway_calls.push(block.clone());
} else {
has_client_tool_use = true;
}
}
}
if gateway_calls.is_empty() || stop_reason != Some("tool_use") {
return Ok(MessagesResponse {
body: message,
headers: response_headers,
});
}
if has_client_tool_use {
let stripped = tool_seam::strip_gateway_tool_use(content, gateway_map);
let mut message = message;
message["content"] = Value::Array(stripped);
return Ok(MessagesResponse {
body: message,
headers: response_headers,
});
}
let assistant_content = content.clone();
let allowed_searches = web_search_budget.reserve(gateway_calls.len());
let resolved = execute_gateway_calls(&gateway_calls, registry, gateway_map, allowed_searches).await;
append_round_to_history(&mut request, &assistant_content, &resolved);
}
Ok(MessagesResponse {
body: json!({
"type": "error",
"error": {
"type": "api_error",
"message": format!("gateway tool loop exceeded {MAX_GATEWAY_TOOL_ROUNDS} rounds")
}
}),
headers: http::HeaderMap::new(),
})
}
async fn execute_gateway_calls(
gateway_calls: &[Value],
registry: &ToolRegistry,
gateway_map: &tool_seam::GatewayToolMap,
allowed_searches: usize,
) -> Vec<ResolvedCall> {
let futures = gateway_calls.iter().enumerate().map(|(index, block)| async move {
let id = block.get("id").and_then(Value::as_str).unwrap_or_default();
let name = block.get("name").and_then(Value::as_str).unwrap_or_default();
if index >= allowed_searches {
return ResolvedCall {
tool_result_block: web_search_budget_exhausted_result(id),
};
}
let input = block.get("input").cloned().unwrap_or(Value::Null);
let (output, is_error) = if input.is_object() {
let call = tool_seam::tool_use_to_call(id, name, &input, gateway_map);
match tokio::time::timeout(GATEWAY_TOOL_TIMEOUT, registry.dispatch(&call)).await {
Ok(Some(result)) => match result.output {
Ok(tool_output) => (tool_output.output, false),
Err(e) => (format!("tool execution failed: {e}"), true),
},
Ok(None) => (format!("no handler for tool '{name}'"), true),
Err(_) => (
format!("gateway tool '{name}' timed out after {GATEWAY_TOOL_TIMEOUT:?}"),
true,
),
}
} else {
(
"invalid tool arguments (not a JSON object); tool was not run".to_owned(),
true,
)
};
ResolvedCall {
tool_result_block: tool_seam::tool_result_block(id, &output, is_error),
}
});
join_all(futures).await
}
fn append_round_to_history(request: &mut Value, assistant_content: &[Value], resolved: &[ResolvedCall]) {
let assistant = json!({ "role": "assistant", "content": assistant_content });
let user = json!({
"role": "user",
"content": resolved.iter().map(|r| r.tool_result_block.clone()).collect::<Vec<_>>()
});
if let Some(messages) = request.get_mut("messages").and_then(Value::as_array_mut) {
messages.push(assistant);
messages.push(user);
}
}