1use std::time::{Duration, Instant};
4
5use reqwest::Client;
6use serde_json::{Value, json};
7use tracing::{debug, info, warn};
8
9use crate::error::AgentError;
10use crate::provider::{
11 AgentConfig, AgentOutput, AgentProvider, DebugMessage, DebugToolCall, DebugToolResult,
12 InvokeFuture,
13};
14use crate::providers::http::sse::{SseDelta, collect_sse_stream};
15use crate::providers::http::tools::ToolRegistry;
16use crate::providers::http::tools::routing::route_tool_call;
17
18#[derive(Debug)]
20pub struct TurnResult {
21 pub text: Option<String>,
23 #[allow(dead_code)]
25 pub tool_calls: Vec<HttpToolCall>,
26 pub is_final: bool,
28 pub structured_value: Option<Value>,
30 pub usage: HttpUsage,
32 pub model: Option<String>,
34}
35
36#[derive(Debug, Clone)]
38#[allow(dead_code)]
39pub struct HttpToolCall {
40 pub id: String,
42 pub name: String,
44 pub input: Value,
46}
47
48#[derive(Debug, Default)]
50pub struct HttpUsage {
51 pub input_tokens: Option<u64>,
53 pub output_tokens: Option<u64>,
55}
56
57pub trait HttpAgentAdapter: Send + Sync + 'static {
63 fn provider_name(&self) -> &'static str;
65
66 fn endpoint_url(&self, model: &str) -> String;
68
69 fn auth_headers(&self) -> Vec<(String, String)>;
71
72 fn build_request(&self, config: &AgentConfig) -> Result<Value, AgentError>;
74
75 fn parse_response(&self, body: &Value, config: &AgentConfig) -> Result<TurnResult, AgentError>;
77
78 fn parse_sse_line(&self, line: &str) -> Option<SseDelta>;
80
81 fn fold_sse_deltas(
83 &self,
84 deltas: Vec<SseDelta>,
85 config: &AgentConfig,
86 ) -> Result<TurnResult, AgentError>;
87
88 fn compute_cost(&self, model: &str, input_tokens: u64, output_tokens: u64) -> Option<f64>;
90
91 fn resolve_model(&self, model: &str) -> String;
93}
94
95const DEFAULT_TIMEOUT: Duration = Duration::from_secs(120);
97
98pub struct HttpAgentProvider<A: HttpAgentAdapter> {
111 adapter: A,
112 client: Client,
113 timeout: Duration,
114 tool_registry: Option<ToolRegistry>,
115}
116
117impl<A: HttpAgentAdapter> HttpAgentProvider<A> {
118 pub fn new(adapter: A) -> Self {
120 let client = Client::builder()
121 .timeout(DEFAULT_TIMEOUT)
122 .build()
123 .expect("failed to build reqwest client");
124 Self {
125 adapter,
126 client,
127 timeout: DEFAULT_TIMEOUT,
128 tool_registry: None,
129 }
130 }
131
132 pub fn with_tools(mut self, registry: ToolRegistry) -> Self {
139 self.tool_registry = Some(registry);
140 self
141 }
142
143 pub fn with_timeout(mut self, timeout: Duration) -> Self {
145 self.timeout = timeout;
146 self.client = Client::builder()
147 .timeout(timeout)
148 .build()
149 .expect("failed to build reqwest client");
150 self
151 }
152
153 async fn execute_turn(
154 &self,
155 request_body: &Value,
156 config: &AgentConfig,
157 ) -> Result<TurnResult, AgentError> {
158 let model = self.adapter.resolve_model(&config.model);
159 let url = self.adapter.endpoint_url(&model);
160 let headers = self.adapter.auth_headers();
161
162 let mut req = self.client.post(&url).json(request_body);
163 for (key, value) in &headers {
164 req = req.header(key, value);
165 }
166
167 let response = tokio::time::timeout(self.timeout, req.send())
168 .await
169 .map_err(|_| AgentError::Timeout {
170 limit: self.timeout,
171 })?
172 .map_err(|e| {
173 if e.is_timeout() {
174 AgentError::Timeout {
175 limit: self.timeout,
176 }
177 } else {
178 AgentError::HttpProvider {
179 provider: self.adapter.provider_name().to_string(),
180 status_code: 0,
181 message: format!("connection failed: {e}"),
182 }
183 }
184 })?;
185
186 let status = response.status().as_u16();
187
188 if status == 429 {
189 let retry_after = response
190 .headers()
191 .get("retry-after")
192 .and_then(|v| v.to_str().ok())
193 .and_then(|v| v.parse::<u64>().ok());
194 return Err(AgentError::RateLimited {
195 provider: self.adapter.provider_name().to_string(),
196 retry_after_secs: retry_after,
197 });
198 }
199
200 if status >= 400 {
201 let body_text = response.text().await.unwrap_or_default();
202 let message = serde_json::from_str::<Value>(&body_text)
203 .ok()
204 .and_then(|v| {
205 v.get("error")
206 .and_then(|e| e.get("message"))
207 .and_then(|m| m.as_str())
208 .map(String::from)
209 })
210 .unwrap_or(body_text);
211 return Err(AgentError::HttpProvider {
212 provider: self.adapter.provider_name().to_string(),
213 status_code: status,
214 message,
215 });
216 }
217
218 if config.verbose {
219 let deltas = collect_sse_stream(&self.adapter, response, self.timeout).await?;
220 self.adapter.fold_sse_deltas(deltas, config)
221 } else {
222 let body: Value = response
223 .json()
224 .await
225 .map_err(|e| AgentError::HttpProvider {
226 provider: self.adapter.provider_name().to_string(),
227 status_code: 0,
228 message: format!("failed to parse response JSON: {e}"),
229 })?;
230 self.adapter.parse_response(&body, config)
231 }
232 }
233}
234
235struct LoopState {
237 start: Instant,
238 total_input_tokens: u64,
239 total_output_tokens: u64,
240 total_cost: f64,
241 model_name: Option<String>,
242 debug_messages: Vec<DebugMessage>,
243 verbose: bool,
244}
245
246impl LoopState {
247 fn new(start: Instant, verbose: bool) -> Self {
248 Self {
249 start,
250 total_input_tokens: 0,
251 total_output_tokens: 0,
252 total_cost: 0.0,
253 model_name: None,
254 debug_messages: Vec::new(),
255 verbose,
256 }
257 }
258
259 fn into_output(self, value: Value) -> AgentOutput {
260 AgentOutput {
261 value,
262 session_id: None,
263 cost_usd: if self.total_cost > 0.0 {
264 Some(self.total_cost)
265 } else {
266 None
267 },
268 input_tokens: Some(self.total_input_tokens),
269 output_tokens: Some(self.total_output_tokens),
270 model: self.model_name,
271 duration_ms: self.start.elapsed().as_millis() as u64,
272 debug_messages: if self.verbose {
273 Some(self.debug_messages)
274 } else {
275 None
276 },
277 }
278 }
279}
280
281fn extract_value(turn_result: &TurnResult) -> Value {
283 if let Some(ref structured) = turn_result.structured_value {
284 structured.clone()
285 } else {
286 turn_result
287 .text
288 .as_ref()
289 .map(|t| Value::String(t.clone()))
290 .unwrap_or(Value::String(String::new()))
291 }
292}
293
294fn extract_text_value(turn_result: &TurnResult) -> Value {
296 turn_result
297 .text
298 .as_ref()
299 .map(|t| Value::String(t.clone()))
300 .unwrap_or(Value::String(String::new()))
301}
302
303impl<A: HttpAgentAdapter> AgentProvider for HttpAgentProvider<A> {
304 fn invoke<'a>(&'a self, config: &'a AgentConfig) -> InvokeFuture<'a> {
305 Box::pin(async move {
306 let mut request_body = self.adapter.build_request(config)?;
307
308 if let Some(ref registry) = self.tool_registry
310 && !registry.is_empty()
311 {
312 let tools_array = registry.to_openai_tools();
313 request_body["tools"] = Value::Array(tools_array);
314 }
315
316 let max_turns = config.max_turns.unwrap_or(25) as usize;
317 let max_budget = config.max_budget_usd.unwrap_or(f64::MAX);
318 let mut state = LoopState::new(Instant::now(), config.verbose);
319
320 let mut messages: Vec<Value> = request_body
322 .get("messages")
323 .and_then(|m| m.as_array())
324 .cloned()
325 .unwrap_or_default();
326
327 for turn in 0..max_turns {
328 request_body["messages"] = Value::Array(messages.clone());
329 let turn_result = self.execute_turn(&request_body, config).await?;
330
331 let turn_input = turn_result.usage.input_tokens.unwrap_or(0);
333 let turn_output = turn_result.usage.output_tokens.unwrap_or(0);
334 state.total_input_tokens += turn_input;
335 state.total_output_tokens += turn_output;
336
337 if state.model_name.is_none() {
338 state.model_name = turn_result.model.clone();
339 }
340
341 if let Some(ref model) = state.model_name
342 && let Some(turn_cost) =
343 self.adapter.compute_cost(model, turn_input, turn_output)
344 {
345 state.total_cost += turn_cost;
346 }
347
348 if config.verbose {
350 let tool_calls_debug: Vec<DebugToolCall> = turn_result
351 .tool_calls
352 .iter()
353 .map(|tc| DebugToolCall {
354 id: Some(tc.id.clone()),
355 name: tc.name.clone(),
356 input: tc.input.clone(),
357 })
358 .collect();
359
360 state.debug_messages.push(DebugMessage {
361 text: turn_result.text.clone(),
362 thinking: None,
363 thinking_redacted: false,
364 tool_calls: tool_calls_debug,
365 tool_results: Vec::new(),
366 stop_reason: if turn_result.is_final {
367 Some("end_turn".to_string())
368 } else {
369 Some("tool_use".to_string())
370 },
371 input_tokens: Some(turn_input),
372 output_tokens: Some(turn_output),
373 });
374 }
375
376 if turn_result.is_final || turn_result.tool_calls.is_empty() {
378 info!(
379 provider = self.adapter.provider_name(),
380 turns = turn + 1,
381 duration_ms = state.start.elapsed().as_millis() as u64,
382 input_tokens = state.total_input_tokens,
383 output_tokens = state.total_output_tokens,
384 "invocation complete"
385 );
386 return Ok(state.into_output(extract_value(&turn_result)));
387 }
388
389 let registry = match self.tool_registry {
391 Some(ref r) => r,
392 None => {
393 warn!(
394 provider = self.adapter.provider_name(),
395 tool_calls = turn_result.tool_calls.len(),
396 "model requested tool calls but no registry attached, returning text"
397 );
398 return Ok(state.into_output(extract_text_value(&turn_result)));
399 }
400 };
401
402 if state.total_cost >= max_budget {
404 warn!(
405 provider = self.adapter.provider_name(),
406 cost = state.total_cost,
407 budget = max_budget,
408 "budget exceeded, stopping agentic loop"
409 );
410 return Ok(state.into_output(extract_text_value(&turn_result)));
411 }
412
413 let assistant_tool_calls: Vec<Value> = turn_result
415 .tool_calls
416 .iter()
417 .map(|tc| {
418 json!({
419 "id": tc.id,
420 "type": "function",
421 "function": {
422 "name": tc.name,
423 "arguments": tc.input.to_string()
424 }
425 })
426 })
427 .collect();
428
429 let mut assistant_msg = json!({"role": "assistant"});
430 if let Some(ref text) = turn_result.text {
431 assistant_msg["content"] = Value::String(text.clone());
432 } else {
433 assistant_msg["content"] = Value::Null;
434 }
435 assistant_msg["tool_calls"] = Value::Array(assistant_tool_calls);
436 messages.push(assistant_msg);
437
438 let mut tool_results_debug: Vec<DebugToolResult> = Vec::new();
440
441 for tc in &turn_result.tool_calls {
442 debug!(
443 provider = self.adapter.provider_name(),
444 tool = %tc.name,
445 call_id = %tc.id,
446 "executing tool call"
447 );
448
449 let connectors = registry.connectors();
450 let (content, is_error) = if !connectors.is_empty() {
451 match route_tool_call(&tc.name, connectors) {
452 Ok(routed) => {
453 match registry
454 .execute(&routed.registry_key, tc.input.clone())
455 .await
456 {
457 Some(Ok(output)) => (output.content, output.is_error),
458 Some(Err(err)) => {
459 (format!("Tool execution error: {err}"), true)
460 }
461 None => (format!("Unknown tool: {}", tc.name), true),
462 }
463 }
464 Err(routing_err) => (routing_err.to_string(), true),
465 }
466 } else {
467 match registry.execute(&tc.name, tc.input.clone()).await {
468 Some(Ok(output)) => (output.content, output.is_error),
469 Some(Err(err)) => (format!("Tool execution error: {err}"), true),
470 None => (format!("Unknown tool: {}", tc.name), true),
471 }
472 };
473
474 messages.push(json!({
475 "role": "tool",
476 "tool_call_id": tc.id,
477 "content": content
478 }));
479
480 if config.verbose {
481 tool_results_debug.push(DebugToolResult {
482 tool_use_id: Some(tc.id.clone()),
483 content: Value::String(content.clone()),
484 is_error,
485 });
486 }
487 }
488
489 if config.verbose
490 && let Some(last_msg) = state.debug_messages.last_mut()
491 {
492 last_msg.tool_results = tool_results_debug;
493 }
494
495 info!(
496 provider = self.adapter.provider_name(),
497 turn = turn + 1,
498 tools_executed = turn_result.tool_calls.len(),
499 "turn complete, continuing loop"
500 );
501 }
502
503 warn!(
504 provider = self.adapter.provider_name(),
505 max_turns, "max turns reached, returning last state"
506 );
507 Ok(state.into_output(Value::String(String::new())))
508 })
509 }
510}