agentic_core/executor/
messages_loop.rs1use std::time::Duration;
16
17use futures::future::join_all;
18use serde_json::{Value, json};
19
20use crate::executor::error::{ExecutorError, ExecutorResult};
21use crate::executor::inference::fetch_response_json_with_headers;
22use crate::executor::messages_request::{normalize_native_web_search, web_search_budget_exhausted_result};
23use crate::executor::request::ExecutionContext;
24use crate::tool::ToolRegistry;
25use crate::types::messages::tool_seam;
26use crate::utils::common::{deserialize_from_str, serialize_to_string};
27
28pub(super) const MAX_GATEWAY_TOOL_ROUNDS: usize = 10;
33
34pub(super) const GATEWAY_TOOL_TIMEOUT: Duration = Duration::from_secs(60);
38
39#[derive(Clone, Debug)]
41pub struct MessagesUpstream {
42 url: String,
43 headers: reqwest::header::HeaderMap,
44}
45
46impl MessagesUpstream {
47 #[must_use]
48 pub fn new(base_url: &str, query: Option<&str>, headers: reqwest::header::HeaderMap) -> Self {
49 let mut url = format!("{}/v1/messages", base_url.trim_end_matches('/'));
50 if let Some(query) = query.filter(|query| !query.is_empty()) {
51 url.push('?');
52 url.push_str(query);
53 }
54 Self { url, headers }
55 }
56
57 pub(super) fn url(&self) -> &str {
58 &self.url
59 }
60
61 pub(super) fn headers(&self) -> &reqwest::header::HeaderMap {
62 &self.headers
63 }
64}
65
66pub struct MessagesResponse<T> {
68 pub body: T,
70 pub headers: http::HeaderMap,
72}
73
74struct ResolvedCall {
78 tool_result_block: Value,
79}
80
81pub async fn run_messages_loop(
92 mut request: Value,
93 registry: &ToolRegistry,
94 exec_ctx: &ExecutionContext,
95 upstream: &MessagesUpstream,
96) -> ExecutorResult<MessagesResponse<Value>> {
97 let mut web_search_budget = normalize_native_web_search(&mut request)?;
98 request["stream"] = Value::Bool(false);
101
102 for _round in 0..MAX_GATEWAY_TOOL_ROUNDS {
103 let body = serialize_to_string(&request).map_err(ExecutorError::JsonError)?;
104 let (resp_text, response_headers) =
105 fetch_response_json_with_headers(body, &upstream.url, &exec_ctx.client, &upstream.headers).await?;
106 let message: Value = deserialize_from_str(&resp_text).map_err(ExecutorError::JsonError)?;
107
108 if message.get("type").and_then(Value::as_str) == Some("error") {
111 return Ok(MessagesResponse {
112 body: message,
113 headers: response_headers,
114 });
115 }
116
117 let content = message.get("content").and_then(Value::as_array);
118 let stop_reason = message.get("stop_reason").and_then(Value::as_str);
119
120 let Some(content) = content else {
124 return Ok(MessagesResponse {
125 body: message,
126 headers: response_headers,
127 });
128 };
129 let gateway_map = &exec_ctx.messages_gateway_tools;
130 let mut gateway_calls: Vec<Value> = Vec::new();
131 let mut has_client_tool_use = false;
132 for block in content {
133 if block.get("type").and_then(Value::as_str) == Some("tool_use") {
134 let name = block.get("name").and_then(Value::as_str).unwrap_or_default();
135 if gateway_map.is_gateway_owned(name) {
136 gateway_calls.push(block.clone());
137 } else {
138 has_client_tool_use = true;
139 }
140 }
141 }
142
143 if gateway_calls.is_empty() || stop_reason != Some("tool_use") {
148 return Ok(MessagesResponse {
149 body: message,
150 headers: response_headers,
151 });
152 }
153 if has_client_tool_use {
154 let stripped = tool_seam::strip_gateway_tool_use(content, gateway_map);
157 let mut message = message;
158 message["content"] = Value::Array(stripped);
159 return Ok(MessagesResponse {
160 body: message,
161 headers: response_headers,
162 });
163 }
164
165 let assistant_content = content.clone();
169 let allowed_searches = web_search_budget.reserve(gateway_calls.len());
170 let resolved = execute_gateway_calls(&gateway_calls, registry, gateway_map, allowed_searches).await;
171 append_round_to_history(&mut request, &assistant_content, &resolved);
172 }
173
174 Ok(MessagesResponse {
179 body: json!({
180 "type": "error",
181 "error": {
182 "type": "api_error",
183 "message": format!("gateway tool loop exceeded {MAX_GATEWAY_TOOL_ROUNDS} rounds")
184 }
185 }),
186 headers: http::HeaderMap::new(),
187 })
188}
189
190async fn execute_gateway_calls(
193 gateway_calls: &[Value],
194 registry: &ToolRegistry,
195 gateway_map: &tool_seam::GatewayToolMap,
196 allowed_searches: usize,
197) -> Vec<ResolvedCall> {
198 let futures = gateway_calls.iter().enumerate().map(|(index, block)| async move {
199 let id = block.get("id").and_then(Value::as_str).unwrap_or_default();
200 let name = block.get("name").and_then(Value::as_str).unwrap_or_default();
201
202 if index >= allowed_searches {
203 return ResolvedCall {
204 tool_result_block: web_search_budget_exhausted_result(id),
205 };
206 }
207
208 let input = block.get("input").cloned().unwrap_or(Value::Null);
212 let (output, is_error) = if input.is_object() {
213 let call = tool_seam::tool_use_to_call(id, name, &input, gateway_map);
214 match tokio::time::timeout(GATEWAY_TOOL_TIMEOUT, registry.dispatch(&call)).await {
215 Ok(Some(result)) => match result.output {
216 Ok(tool_output) => (tool_output.output, false),
217 Err(e) => (format!("tool execution failed: {e}"), true),
218 },
219 Ok(None) => (format!("no handler for tool '{name}'"), true),
220 Err(_) => (
221 format!("gateway tool '{name}' timed out after {GATEWAY_TOOL_TIMEOUT:?}"),
222 true,
223 ),
224 }
225 } else {
226 (
227 "invalid tool arguments (not a JSON object); tool was not run".to_owned(),
228 true,
229 )
230 };
231
232 ResolvedCall {
233 tool_result_block: tool_seam::tool_result_block(id, &output, is_error),
234 }
235 });
236 join_all(futures).await
237}
238
239fn append_round_to_history(request: &mut Value, assistant_content: &[Value], resolved: &[ResolvedCall]) {
244 let assistant = json!({ "role": "assistant", "content": assistant_content });
245 let user = json!({
246 "role": "user",
247 "content": resolved.iter().map(|r| r.tool_result_block.clone()).collect::<Vec<_>>()
248 });
249 if let Some(messages) = request.get_mut("messages").and_then(Value::as_array_mut) {
250 messages.push(assistant);
251 messages.push(user);
252 }
253}