Skip to main content

agent_framework_core/
client.rs

1//! The chat client trait and the automatic function-invocation loop.
2//!
3//! Rust equivalent of `agent_framework._clients` plus the tool loop from
4//! `_tools.use_function_invocation`.
5
6use async_trait::async_trait;
7use futures::stream::{self, Stream, StreamExt};
8use serde_json::Value;
9use std::collections::HashMap;
10use std::pin::Pin;
11use std::sync::Arc;
12use std::time::{Duration, SystemTime, UNIX_EPOCH};
13use tracing::Instrument;
14
15use crate::error::{Error, Result};
16use crate::middleware::{FunctionInvocationContext, LiveToolList, MiddlewarePipeline, Terminal};
17use crate::tools::{FunctionInvocationConfig, ToolDefinition, ToolKind};
18use crate::types::{
19    ChatOptions, ChatResponse, ChatResponseUpdate, Content, EmbeddingGenerationOptions,
20    FunctionApprovalRequestContent, FunctionApprovalResponseContent, FunctionCallContent,
21    FunctionResultContent, GeneratedEmbeddings, Message, Role, ToolMode, UsageContent,
22};
23
24/// A boxed stream of streaming chat updates.
25pub type ChatStream = Pin<Box<dyn Stream<Item = Result<ChatResponseUpdate>> + Send>>;
26
27/// The interface every chat client implements.
28///
29/// Implementors provide [`ChatClient::get_response`] and
30/// [`ChatClient::get_streaming_response`]; the framework layers tool invocation
31/// and middleware on top via [`FunctionInvokingChatClient`].
32#[async_trait]
33pub trait ChatClient: Send + Sync {
34    /// Get a complete (non-streaming) response.
35    async fn get_response(
36        &self,
37        messages: Vec<Message>,
38        options: ChatOptions,
39    ) -> Result<ChatResponse>;
40
41    /// Get a streaming response as a sequence of updates.
42    async fn get_streaming_response(
43        &self,
44        messages: Vec<Message>,
45        options: ChatOptions,
46    ) -> Result<ChatStream>;
47
48    /// The default model id for this client, if any.
49    fn model(&self) -> Option<&str> {
50        None
51    }
52}
53
54/// Blanket impl so `Arc<dyn ChatClient>` and wrappers are usable as clients.
55#[async_trait]
56impl<T: ChatClient + ?Sized> ChatClient for Arc<T> {
57    async fn get_response(
58        &self,
59        messages: Vec<Message>,
60        options: ChatOptions,
61    ) -> Result<ChatResponse> {
62        (**self).get_response(messages, options).await
63    }
64    async fn get_streaming_response(
65        &self,
66        messages: Vec<Message>,
67        options: ChatOptions,
68    ) -> Result<ChatStream> {
69        (**self).get_streaming_response(messages, options).await
70    }
71    fn model(&self) -> Option<&str> {
72        (**self).model()
73    }
74}
75
76/// The interface every embedding client implements.
77///
78/// Rust equivalent of upstream's `SupportsGetEmbeddings` protocol /
79/// `BaseEmbeddingClient` (`_clients.py`): generate one embedding per input
80/// string, batched in a single request. Vectors are `Vec<f32>` — see the
81/// note on [`crate::types::Embedding`] about upstream's genericity.
82#[async_trait]
83pub trait EmbeddingClient: Send + Sync {
84    /// Generate embeddings for the given values (one per value, in order).
85    async fn get_embeddings(
86        &self,
87        values: Vec<String>,
88        options: Option<EmbeddingGenerationOptions>,
89    ) -> Result<GeneratedEmbeddings>;
90
91    /// The default embedding model id for this client, if any.
92    fn model(&self) -> Option<&str> {
93        None
94    }
95}
96
97/// Blanket impl so `Arc<dyn EmbeddingClient>` and wrappers are usable as
98/// clients.
99#[async_trait]
100impl<T: EmbeddingClient + ?Sized> EmbeddingClient for Arc<T> {
101    async fn get_embeddings(
102        &self,
103        values: Vec<String>,
104        options: Option<EmbeddingGenerationOptions>,
105    ) -> Result<GeneratedEmbeddings> {
106        (**self).get_embeddings(values, options).await
107    }
108    fn model(&self) -> Option<&str> {
109        (**self).model()
110    }
111}
112
113/// Wraps a [`ChatClient`] to automatically execute local tool calls in a loop,
114/// mirroring `use_function_invocation`.
115pub struct FunctionInvokingChatClient<C: ChatClient> {
116    inner: C,
117    config: FunctionInvocationConfig,
118    /// Middleware run around every individual tool call (mirrors Python's
119    /// function-middleware pipeline, driven here instead of by a
120    /// `use_function_invocation` decorator).
121    function_middleware: MiddlewarePipeline<FunctionInvocationContext>,
122}
123
124impl<C: ChatClient> FunctionInvokingChatClient<C> {
125    pub fn new(inner: C) -> Self {
126        Self {
127            inner,
128            config: FunctionInvocationConfig::default(),
129            function_middleware: MiddlewarePipeline::default(),
130        }
131    }
132
133    /// Override the function-invocation configuration.
134    pub fn with_config(mut self, config: FunctionInvocationConfig) -> Self {
135        self.config = config;
136        self
137    }
138
139    /// Configure the function-invocation middleware pipeline run around every
140    /// tool call: middleware may inspect/rewrite
141    /// [`FunctionInvocationContext::arguments`], short-circuit execution by
142    /// setting [`FunctionInvocationContext::result`] (and either not calling
143    /// `next`, or setting `terminate = true`), or observe a propagated
144    /// execution error by matching on the `Result` returned from their own
145    /// `next.run(...)` call. Replaces any previously configured middleware.
146    pub fn with_function_middleware(
147        mut self,
148        middleware: Vec<Arc<crate::middleware::FunctionMiddleware>>,
149    ) -> Self {
150        self.function_middleware = MiddlewarePipeline::new(middleware);
151        self
152    }
153
154    /// A reference to the wrapped client.
155    pub fn inner(&self) -> &C {
156        &self.inner
157    }
158
159    async fn inner_get_response(
160        &self,
161        messages: Vec<Message>,
162        options: ChatOptions,
163    ) -> Result<ChatResponse> {
164        self.inner.get_response(messages, options).await
165    }
166}
167
168/// Extract the executable tools from the options into a name→tool map.
169fn executable_tools(options: &ChatOptions) -> Vec<ToolDefinition> {
170    options
171        .tools
172        .iter()
173        .filter(|t| t.is_executable())
174        .cloned()
175        .collect()
176}
177
178/// Whether `tool` is a *declaration-only* function tool: a known function with
179/// no local executor. Mirrors Python's `AIFunction.declaration_only`. A call to
180/// such a tool is returned to the caller unexecuted (the frontend-tool pattern
181/// that makes AG-UI client-side tools work). Hosted tools (web search, MCP, …)
182/// are deliberately excluded — they are not function tools and a call whose
183/// name matches none of the local function tools is treated as unknown, not
184/// declaration-only, exactly as Python's `_get_tool_map` omits them.
185fn is_declaration_only(tool: &ToolDefinition) -> bool {
186    tool.kind == ToolKind::Function && tool.executor.is_none()
187}
188
189/// The exact rejection payload Python emits for a denied tool call.
190const REJECTION_MESSAGE: &str = "Error: Tool call invocation was rejected by user.";
191
192/// Execute a single requested tool call through the function-middleware
193/// pipeline, with the actual invocation (wrapped in an `execute_tool` span)
194/// as the pipeline's terminal handler.
195///
196/// Returns `(is_error, result)`. `terminate_on_unknown` turns an unknown-tool
197/// call into a hard error (propagated) rather than an error result. Unknown
198/// tools and unparseable arguments are rejected before middleware ever sees
199/// them (there is no function to hand the pipeline in that case); once a
200/// [`FunctionInvocationContext`] is built, middleware can rewrite
201/// `arguments`, short-circuit by setting `result` (without calling `next`, or
202/// with `terminate = true`), or observe an execution error by matching on the
203/// `Result` their own `next.run(...)` call returns. A propagated error is
204/// converted to the same `(true, FunctionResultContent { exception: .. })`
205/// shape the direct-error path used before middleware existed, so
206/// `include_detailed_errors` behaves identically either way.
207async fn execute_tool_call(
208    tool: Option<ToolDefinition>,
209    call: &FunctionCallContent,
210    include_detailed_errors: bool,
211    terminate_on_unknown: bool,
212    function_middleware: &MiddlewarePipeline<FunctionInvocationContext>,
213    session: Option<&crate::session::AgentSession>,
214    live_tools: Option<&LiveToolList>,
215) -> Result<(bool, FunctionResultContent)> {
216    match tool {
217        None => {
218            if terminate_on_unknown {
219                return Err(Error::tool(format!("unknown tool: {}", call.name)));
220            }
221            Ok((
222                true,
223                FunctionResultContent {
224                    call_id: call.call_id.clone(),
225                    result: None,
226                    exception: Some(format!("tool '{}' not found", call.name)),
227                },
228            ))
229        }
230        Some(def) => {
231            // Reject unparseable arguments rather than silently invoking the tool
232            // with null/default input.
233            let args = match call.parse_arguments() {
234                Ok(m) => Value::Object(m.into_iter().collect()),
235                Err(e) => {
236                    let msg = if include_detailed_errors {
237                        format!("invalid tool arguments: {e}")
238                    } else {
239                        "invalid tool arguments".to_string()
240                    };
241                    return Ok((
242                        true,
243                        FunctionResultContent {
244                            call_id: call.call_id.clone(),
245                            result: None,
246                            exception: Some(msg),
247                        },
248                    ));
249                }
250            };
251            let exec = def.executor.as_ref().unwrap().clone();
252            let tool_name = def.name.clone();
253            let description = def.description.clone();
254            let call_id = call.call_id.clone();
255            let terminal: Terminal<FunctionInvocationContext> = Box::new(move |mut ctx| {
256                Box::pin(async move {
257                    if ctx.terminate {
258                        return Ok(ctx);
259                    }
260                    let span = crate::observability::tool_span_ex(
261                        &tool_name,
262                        &call_id,
263                        Some(&description),
264                    );
265                    let capture =
266                        crate::observability::ObservabilityConfig::from_env().enable_sensitive_data;
267                    crate::observability::record_tool_arguments(&span, &ctx.arguments, capture);
268                    #[cfg(feature = "otel-metrics")]
269                    let started = std::time::Instant::now();
270                    let outcome = async {
271                        let result = exec.invoke_in_context(ctx.arguments.clone(), &ctx).await;
272                        if let Err(e) = &result {
273                            crate::observability::record_error(&tracing::Span::current(), e);
274                        }
275                        result
276                    }
277                    .instrument(span.clone())
278                    .await;
279                    #[cfg(feature = "otel-metrics")]
280                    crate::observability::metrics::record_function_invocation_duration(
281                        &tool_name,
282                        started.elapsed(),
283                        outcome
284                            .as_ref()
285                            .err()
286                            .map(crate::observability::error_type)
287                            .as_deref(),
288                    );
289                    if let Ok(value) = &outcome {
290                        crate::observability::record_tool_result(&span, value, capture);
291                    }
292                    ctx.result = Some(outcome?);
293                    Ok(ctx)
294                }) as crate::tools::BoxFuture<Result<FunctionInvocationContext>>
295            });
296
297            let ctx = FunctionInvocationContext::new(call.name.clone(), args)
298                .with_session(session.cloned())
299                .with_tools(live_tools.cloned());
300            match function_middleware.execute(ctx, terminal).await {
301                Ok(ctx) => Ok((
302                    false,
303                    FunctionResultContent {
304                        call_id: call.call_id.clone(),
305                        result: Some(ctx.result.unwrap_or(Value::Null)),
306                        exception: None,
307                    },
308                )),
309                Err(e) => {
310                    let msg = if include_detailed_errors {
311                        format!("{e}")
312                    } else {
313                        "tool execution failed".to_string()
314                    };
315                    Ok((
316                        true,
317                        FunctionResultContent {
318                            call_id: call.call_id.clone(),
319                            result: None,
320                            exception: Some(msg),
321                        },
322                    ))
323                }
324            }
325        }
326    }
327}
328
329/// Collect all function-approval responses present in a conversation.
330fn collect_approval_responses(messages: &[Message]) -> Vec<FunctionApprovalResponseContent> {
331    let mut out = Vec::new();
332    for msg in messages {
333        for content in &msg.contents {
334            if let Content::FunctionApprovalResponse(resp) = content {
335                out.push(resp.clone());
336            }
337        }
338    }
339    out
340}
341
342/// Rewrite approval request/response contents in place, mirroring Python's
343/// `_replace_approval_contents_with_results`.
344///
345/// * A [`FunctionApprovalRequestContent`] becomes its embedded
346///   [`FunctionCallContent`], unless that call already exists in the same
347///   message (a duplicate), in which case the request is removed.
348/// * An approved [`FunctionApprovalResponseContent`] becomes the corresponding
349///   result (correlated strictly by call id) and the message role becomes
350///   `tool`.
351/// * A rejected response becomes a [`FunctionResultContent`] carrying the
352///   rejection payload, and the message role becomes `tool`.
353fn replace_approval_contents_with_results(
354    messages: &mut [Message],
355    approved_results: &HashMap<String, FunctionResultContent>,
356) {
357    for msg in messages.iter_mut() {
358        let existing_call_ids: std::collections::HashSet<String> = msg
359            .contents
360            .iter()
361            .filter_map(Content::as_function_call)
362            .filter(|fc| !fc.call_id.is_empty())
363            .map(|fc| fc.call_id.clone())
364            .collect();
365
366        let mut to_remove: Vec<usize> = Vec::new();
367        let mut set_role_tool = false;
368
369        for (idx, content) in msg.contents.iter_mut().enumerate() {
370            match content {
371                Content::FunctionApprovalRequest(req) => {
372                    if existing_call_ids.contains(&req.function_call.call_id) {
373                        to_remove.push(idx);
374                    } else {
375                        *content = Content::FunctionCall(req.function_call.clone());
376                    }
377                }
378                Content::FunctionApprovalResponse(resp) => {
379                    let call_id = resp.function_call.call_id.clone();
380                    if resp.approved {
381                        if let Some(result) = approved_results.get(&call_id) {
382                            *content = Content::FunctionResult(result.clone());
383                            set_role_tool = true;
384                        }
385                    } else {
386                        *content = Content::FunctionResult(FunctionResultContent {
387                            call_id,
388                            result: Some(Value::String(REJECTION_MESSAGE.to_string())),
389                            exception: None,
390                        });
391                        set_role_tool = true;
392                    }
393                }
394                _ => {}
395            }
396        }
397
398        for idx in to_remove.into_iter().rev() {
399            msg.contents.remove(idx);
400        }
401        if set_role_tool {
402            msg.role = Role::tool();
403        }
404    }
405}
406
407#[async_trait]
408impl<C: ChatClient> ChatClient for FunctionInvokingChatClient<C> {
409    async fn get_response(
410        &self,
411        messages: Vec<Message>,
412        mut options: ChatOptions,
413    ) -> Result<ChatResponse> {
414        // After the tool loop settles, auto-populate `ChatResponse.value` from
415        // the final text when a structured `response_format` was requested
416        // (mirrors Python `try_parse_value`). This is the central non-streaming
417        // fill point: it covers a bare `FunctionInvokingChatClient` and every
418        // `Agent` run (whose client is always wrapped in one). The tool
419        // loop is run inside an `async move` block so its interior `return`s
420        // funnel through this single fill/return path.
421        let response_format = options.response_format.clone();
422        let mut response: ChatResponse = async move {
423            self.config.validate()?;
424            // Pop the agent-session side channel before the inner provider
425            // client ever sees the options (mirrors upstream's
426            // `effective_client_kwargs.pop("session")`); it is handed to
427            // invoked tools via `FunctionInvocationContext::session`.
428            let session = options.session.take();
429
430            // Default tool choice to auto when tools are present and unset.
431            if !options.tools.is_empty() && options.tool_choice.is_none() {
432                options.tool_choice = Some(ToolMode::Auto);
433            }
434
435            if executable_tools(&options).is_empty() || !self.config.enabled {
436                return self.inner_get_response(messages, options).await;
437            }
438
439            // The run's live tool list (progressive tool exposure): handed to
440            // every invocation via `FunctionInvocationContext::tools`, and
441            // re-snapshotted into the wire options at the top of every model
442            // iteration — so `add_tools`/`remove_tools` from middleware or
443            // tools take effect on the NEXT iteration, never the in-flight
444            // batch (mirrors upstream `_middleware.py` add_tools/remove_tools
445            // semantics).
446            let live_tools = LiveToolList::new(std::mem::take(&mut options.tools));
447
448            let mut conversation = messages;
449            let mut carried: Vec<Message> = Vec::new();
450            let mut consecutive_errors = 0usize;
451
452            for _ in 0..self.config.max_iterations {
453                options.tools = live_tools.snapshot();
454                let tools = executable_tools(&options);
455                // Process any function-approval responses supplied in the input:
456                // execute the approved calls and splice their results into the
457                // conversation (mirrors Python's `_collect_approval_responses` +
458                // `_replace_approval_contents_with_results`).
459                let approval_responses = collect_approval_responses(&conversation);
460                if !approval_responses.is_empty() {
461                    let mut approved_results: HashMap<String, FunctionResultContent> =
462                        HashMap::new();
463                    let mut had_error = false;
464                    for resp in &approval_responses {
465                        if !resp.approved {
466                            continue;
467                        }
468                        let call = &resp.function_call;
469                        let tool = tools.iter().find(|t| t.name == call.name).cloned();
470                        let (is_error, content) = execute_tool_call(
471                            tool,
472                            call,
473                            self.config.include_detailed_errors,
474                            self.config.terminate_on_unknown_calls,
475                            &self.function_middleware,
476                            session.as_ref(),
477                            Some(&live_tools),
478                        )
479                        .await?;
480                        had_error |= is_error;
481                        approved_results.insert(content.call_id.clone(), content);
482                    }
483                    replace_approval_contents_with_results(&mut conversation, &approved_results);
484                    if had_error {
485                        consecutive_errors += 1;
486                        if consecutive_errors > self.config.max_consecutive_errors_per_request {
487                            options.tool_choice = Some(ToolMode::None);
488                        }
489                    }
490                }
491
492                let response = self
493                    .inner_get_response(conversation.clone(), options.clone())
494                    .await?;
495
496                // A call whose result is already present in the same response
497                // was executed by the provider (e.g. Anthropic server-side
498                // web-search/code-execution/MCP `server_tool_use` blocks,
499                // which arrive paired with their `*_tool_result`). Executing
500                // it locally would produce a bogus "tool not found" — only
501                // unresolved calls enter the local tool loop.
502                let resolved_call_ids: std::collections::HashSet<&str> = response
503                    .messages
504                    .iter()
505                    .flat_map(|m| m.contents.iter())
506                    .filter_map(Content::as_function_result)
507                    .map(|fr| fr.call_id.as_str())
508                    .collect();
509                let calls: Vec<_> = response
510                    .messages
511                    .iter()
512                    .flat_map(|m| m.contents.iter())
513                    .filter_map(Content::as_function_call)
514                    .filter(|fc| !resolved_call_ids.contains(fc.call_id.as_str()))
515                    .cloned()
516                    .collect();
517
518                if calls.is_empty() {
519                    // Prepend the accumulated tool-interaction messages so the final
520                    // assistant message stays last.
521                    let mut final_resp = response;
522                    let mut msgs = std::mem::take(&mut carried);
523                    msgs.append(&mut final_resp.messages);
524                    final_resp.messages = msgs;
525                    return Ok(final_resp);
526                }
527
528                // Human-in-the-loop gate: if *any* requested tool requires approval,
529                // defer *all* calls (matching Python) and return an assistant message
530                // that carries the original calls plus one approval request each.
531                let needs_approval = calls.iter().any(|c| {
532                    tools
533                        .iter()
534                        .find(|t| t.name == c.name)
535                        .map(ToolDefinition::requires_approval)
536                        .unwrap_or(false)
537                });
538                if needs_approval {
539                    let mut resp = response;
540                    let approval_contents: Vec<Content> = calls
541                        .iter()
542                        .map(|c| {
543                            Content::FunctionApprovalRequest(FunctionApprovalRequestContent {
544                                id: c.call_id.clone(),
545                                function_call: c.clone(),
546                            })
547                        })
548                        .collect();
549                    if let Some(m) = resp
550                        .messages
551                        .iter_mut()
552                        .rev()
553                        .find(|m| m.role == Role::assistant())
554                    {
555                        m.contents.extend(approval_contents);
556                    } else {
557                        resp.messages
558                            .push(Message::with_contents(Role::assistant(), approval_contents));
559                    }
560                    let mut msgs = std::mem::take(&mut carried);
561                    msgs.append(&mut resp.messages);
562                    resp.messages = msgs;
563                    return Ok(resp);
564                }
565
566                // Declaration-only calls: a call targeting a KNOWN tool that has
567                // no local executor (declaration-only — e.g. an AG-UI frontend
568                // tool, or a per-run `additional_tools` entry) terminates the
569                // loop and returns the response with the `FunctionCallContent`
570                // intact, so the caller can execute it. Mirrors Python's
571                // `_try_execute_function_calls` `declaration_only` branch
572                // (`_tools.py:1396-1420`): if *any* requested call is
573                // declaration-only, the whole response is returned unexecuted.
574                // A genuinely unknown tool name is NOT declaration-only and
575                // keeps today's not-found handling in `execute_tool_call`.
576                let has_declaration_only = calls.iter().any(|c| {
577                    options
578                        .tools
579                        .iter()
580                        .any(|t| t.name == c.name && is_declaration_only(t))
581                });
582                if has_declaration_only {
583                    let mut resp = response;
584                    let mut msgs = std::mem::take(&mut carried);
585                    msgs.append(&mut resp.messages);
586                    resp.messages = msgs;
587                    return Ok(resp);
588                }
589
590                // Record the assistant message(s) that requested the calls.
591                carried.extend(response.messages.iter().cloned());
592                let response_conversation_id = response.conversation_id.clone();
593
594                // Execute all calls concurrently: the model may emit several
595                // parallel tool calls, and I/O-bound tools should not be serialized.
596                let invocations = calls.iter().map(|call| {
597                    let tool = tools.iter().find(|t| t.name == call.name).cloned();
598                    let call = call.clone();
599                    let include_detailed_errors = self.config.include_detailed_errors;
600                    let terminate_on_unknown = self.config.terminate_on_unknown_calls;
601                    let function_middleware = self.function_middleware.clone();
602                    let session = session.clone();
603                    let live_tools = live_tools.clone();
604                    async move {
605                        execute_tool_call(
606                            tool,
607                            &call,
608                            include_detailed_errors,
609                            terminate_on_unknown,
610                            &function_middleware,
611                            session.as_ref(),
612                            Some(&live_tools),
613                        )
614                        .await
615                    }
616                });
617
618                let outcomes = futures::future::try_join_all(invocations).await?;
619                let mut result_contents: Vec<Content> = Vec::with_capacity(outcomes.len());
620                let mut had_error = false;
621                for (is_error, content) in outcomes {
622                    had_error |= is_error;
623                    result_contents.push(Content::FunctionResult(content));
624                }
625
626                if had_error {
627                    consecutive_errors += 1;
628                    if consecutive_errors > self.config.max_consecutive_errors_per_request {
629                        // Give up on tools and let the model answer directly.
630                        options.tool_choice = Some(ToolMode::None);
631                    }
632                } else {
633                    consecutive_errors = 0;
634                }
635
636                let tool_message = Message::with_contents(Role::tool(), result_contents);
637                carried.push(tool_message.clone());
638                match response_conversation_id {
639                    // A service-managed client that created (or continued) the
640                    // conversation now holds the history server-side. Propagate
641                    // its id so the follow-up tool-output submission targets the
642                    // right thread — without this, Assistants / Azure AI reject
643                    // the submission because `conversation_id` is still `None` —
644                    // and send ONLY the new tool results next turn rather than
645                    // re-sending the whole history (mirrors Python
646                    // `_tools.py:1635-1637, 1695-1699`).
647                    Some(cid) => {
648                        options.conversation_id = Some(cid);
649                        conversation = vec![tool_message];
650                    }
651                    // Stateless client (e.g. Chat Completions): accumulate and
652                    // re-send the full history each turn.
653                    None => {
654                        conversation.extend(response.messages);
655                        conversation.push(tool_message);
656                    }
657                }
658            }
659
660            // Failsafe: one final call with tools disabled.
661            options.tool_choice = Some(ToolMode::None);
662            let mut final_resp = self.inner_get_response(conversation, options).await?;
663            let mut msgs = std::mem::take(&mut carried);
664            msgs.append(&mut final_resp.messages);
665            final_resp.messages = msgs;
666            Ok(final_resp)
667        }
668        .await?;
669        response.try_parse_value(response_format.as_ref());
670        Ok(response)
671    }
672
673    async fn get_streaming_response(
674        &self,
675        messages: Vec<Message>,
676        options: ChatOptions,
677    ) -> Result<ChatStream> {
678        let tools = executable_tools(&options);
679        if tools.is_empty() || !self.config.enabled {
680            return self.inner.get_streaming_response(messages, options).await;
681        }
682        // With tools, run the full loop then stream the aggregated result.
683        // Each message is replayed as its own update with a stable, distinct
684        // `message_id` so that consumers re-aggregating via
685        // `ChatResponse::from_updates` keep the messages separate rather than
686        // merging the tool-call and final assistant messages by role.
687        let response = self.get_response(messages, options).await?;
688        // Response-level metadata must survive the replay so re-aggregation
689        // (and the agent's thread adoption) sees it: ids on every update,
690        // and usage/finish-reason on the final one (usage rides as a
691        // `Content::Usage` item, which `absorb_update` folds into
692        // `usage_details` rather than the message contents — the same shape
693        // providers use for their terminal stream chunk).
694        let conversation_id = response.conversation_id.clone();
695        let response_id = response.response_id.clone();
696        let finish_reason = response.finish_reason.clone();
697        let usage_details = response.usage_details.clone();
698        let last = response.messages.len().saturating_sub(1);
699        // Keep the provider message ids only when they're all present and
700        // distinct; otherwise use positional ids for every message. A service
701        // (e.g. Assistants) can reuse one run id for both the tool-call turn
702        // and the final assistant turn, and `ChatResponse::from_updates` keys
703        // messages by id — a duplicate would merge the final answer into the
704        // tool-call message, ahead of the tool result.
705        let keep_provider_ids = {
706            let mut seen = std::collections::HashSet::new();
707            response.messages.iter().all(|m| {
708                m.message_id
709                    .as_ref()
710                    .is_some_and(|id| !id.is_empty() && seen.insert(id.as_str()))
711            })
712        };
713        let mut updates: Vec<Result<ChatResponseUpdate>> = response
714            .messages
715            .into_iter()
716            .enumerate()
717            .map(|(i, m)| {
718                let message_id = if keep_provider_ids {
719                    m.message_id.clone()
720                } else {
721                    Some(format!("replay-{i}"))
722                };
723                let mut contents = m.contents;
724                let is_last = i == last;
725                if is_last {
726                    if let Some(usage) = usage_details.clone() {
727                        contents.push(Content::Usage(UsageContent { details: usage }));
728                    }
729                }
730                Ok(ChatResponseUpdate {
731                    contents,
732                    role: Some(m.role),
733                    author_name: m.author_name,
734                    message_id,
735                    conversation_id: conversation_id.clone(),
736                    response_id: response_id.clone(),
737                    finish_reason: is_last.then(|| finish_reason.clone()).flatten(),
738                    ..Default::default()
739                })
740            })
741            .collect();
742        // A messageless response (unusual, but possible) still carries its
743        // terminal metadata in one trailing update.
744        if updates.is_empty() && (usage_details.is_some() || finish_reason.is_some()) {
745            let contents = usage_details
746                .map(|u| vec![Content::Usage(UsageContent { details: u })])
747                .unwrap_or_default();
748            updates.push(Ok(ChatResponseUpdate {
749                contents,
750                role: Some(Role::assistant()),
751                conversation_id,
752                response_id,
753                finish_reason,
754                ..Default::default()
755            }));
756        }
757        Ok(stream::iter(updates).boxed())
758    }
759
760    fn model(&self) -> Option<&str> {
761        self.inner.model()
762    }
763}
764
765// ---------------------------------------------------------------------------
766// Retry / backoff layer
767// ---------------------------------------------------------------------------
768
769/// Which errors a [`RetryPolicy`] considers retryable.
770#[derive(Clone)]
771pub enum RetryOn {
772    /// The built-in default predicate (see [`RetryPolicy`] docs for the exact
773    /// rule): retries HTTP `408`/`429`/`5xx` ([`Error::ServiceStatus`]) and
774    /// transport-ish [`Error::Service`] failures (timeouts / connection
775    /// errors). Never retries [`Error::ServiceInvalidAuth`],
776    /// [`Error::ServiceInvalidRequest`], or [`Error::ServiceContentFilter`] —
777    /// authentication/authorization failures, malformed requests, and
778    /// content-filter refusals are non-transient, so retrying would just
779    /// repeat the same rejection.
780    Default,
781    /// A fully custom predicate deciding, per error, whether to retry.
782    Predicate(Arc<dyn Fn(&Error) -> bool + Send + Sync>),
783}
784
785impl std::fmt::Debug for RetryOn {
786    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
787        match self {
788            RetryOn::Default => f.write_str("RetryOn::Default"),
789            RetryOn::Predicate(_) => f.write_str("RetryOn::Predicate(..)"),
790        }
791    }
792}
793
794impl RetryOn {
795    /// A custom retry predicate.
796    pub fn predicate<F>(f: F) -> Self
797    where
798        F: Fn(&Error) -> bool + Send + Sync + 'static,
799    {
800        RetryOn::Predicate(Arc::new(f))
801    }
802
803    fn should_retry(&self, err: &Error) -> bool {
804        match self {
805            RetryOn::Default => default_should_retry(err),
806            RetryOn::Predicate(p) => p(err),
807        }
808    }
809}
810
811/// The default retryability rule used by [`RetryOn::Default`].
812///
813/// Retries when either:
814/// * the error is an [`Error::ServiceStatus`] whose status is `408`
815///   (Request Timeout), `429` (Too Many Requests), or any `5xx`; or
816/// * the error is an [`Error::Service`] whose (lowercased) message contains one
817///   of the transport-failure markers the provider clients emit — `"request
818///   failed"` (the prefix wrapping every `reqwest` send error: DNS, connect,
819///   timeout, reset), `"timed out"`, `"timeout"`, `"connection"`, or `"stream
820///   error"`.
821///
822/// Everything else (4xx other than 408/429, parse errors, tool/workflow errors,
823/// non-transport service errors) is treated as non-retryable. This explicitly
824/// includes [`Error::ServiceInvalidAuth`], [`Error::ServiceInvalidRequest`],
825/// and [`Error::ServiceContentFilter`] — authentication/authorization
826/// failures, malformed requests, and content-filter refusals are
827/// non-transient, so retrying would just repeat the same rejection. None of
828/// the three carry a status via [`Error::status`], so they fall through to
829/// the final `_ => false` below (there's no dedicated match arm for them:
830/// merging one in would just duplicate that `false`, which `clippy` flags as
831/// `match_same_arms`).
832fn default_should_retry(err: &Error) -> bool {
833    if let Some(status) = err.status() {
834        return status == 408 || status == 429 || (500..600).contains(&status);
835    }
836    match err {
837        Error::Service(msg) => {
838            let m = msg.to_lowercase();
839            m.contains("request failed")
840                || m.contains("timed out")
841                || m.contains("timeout")
842                || m.contains("connection")
843                || m.contains("stream error")
844        }
845        _ => false,
846    }
847}
848
849/// Policy controlling [`RetryingChatClient`] backoff.
850///
851/// Delays grow exponentially from [`initial_delay`](Self::initial_delay) by
852/// [`backoff_multiplier`](Self::backoff_multiplier) per attempt, are capped at
853/// [`max_delay`](Self::max_delay), and are then reduced by up to
854/// [`jitter`](Self::jitter) (a fraction of the delay). When the failing error
855/// carries a server `Retry-After` (see [`Error::retry_after`]) that value is
856/// used instead of the computed backoff (still capped by `max_delay`, and not
857/// jittered — it is an explicit server instruction).
858#[derive(Clone, Debug)]
859pub struct RetryPolicy {
860    /// Maximum number of *retries* after the initial attempt (default `3`, so
861    /// up to four total attempts).
862    pub max_retries: usize,
863    /// Base delay before the first retry (default `500ms`).
864    pub initial_delay: Duration,
865    /// Upper bound on any single delay, also capping a server `Retry-After`
866    /// (default `30s`).
867    pub max_delay: Duration,
868    /// Exponential growth factor applied per retry (default `2.0`).
869    pub backoff_multiplier: f64,
870    /// Jitter as a fraction in `0.0..=1.0` (default `0.3`): the computed delay
871    /// is multiplied by `1 - jitter * r` for a per-attempt random `r` in
872    /// `[0, 1)`. `0.0` disables jitter (fully deterministic delays).
873    pub jitter: f64,
874    /// Which errors to retry (default [`RetryOn::Default`]).
875    pub retry_on: RetryOn,
876}
877
878impl Default for RetryPolicy {
879    fn default() -> Self {
880        Self {
881            max_retries: 3,
882            initial_delay: Duration::from_millis(500),
883            max_delay: Duration::from_secs(30),
884            backoff_multiplier: 2.0,
885            jitter: 0.3,
886            retry_on: RetryOn::Default,
887        }
888    }
889}
890
891impl RetryPolicy {
892    /// A policy with the given retry count and otherwise-default backoff.
893    pub fn with_max_retries(max_retries: usize) -> Self {
894        Self {
895            max_retries,
896            ..Self::default()
897        }
898    }
899
900    /// Set the base delay before the first retry.
901    pub fn initial_delay(mut self, delay: Duration) -> Self {
902        self.initial_delay = delay;
903        self
904    }
905
906    /// Set the per-delay cap (also caps a server `Retry-After`).
907    pub fn max_delay(mut self, delay: Duration) -> Self {
908        self.max_delay = delay;
909        self
910    }
911
912    /// Set the exponential growth factor.
913    pub fn backoff_multiplier(mut self, multiplier: f64) -> Self {
914        self.backoff_multiplier = multiplier;
915        self
916    }
917
918    /// Set the jitter fraction (clamped to `0.0..=1.0`).
919    pub fn jitter(mut self, jitter: f64) -> Self {
920        self.jitter = jitter.clamp(0.0, 1.0);
921        self
922    }
923
924    /// Set the retryability rule.
925    pub fn retry_on(mut self, retry_on: RetryOn) -> Self {
926        self.retry_on = retry_on;
927        self
928    }
929
930    /// The delay to wait before a retry, given the 1-based `attempt` number
931    /// (attempt `1` is the first retry) and the error that triggered it.
932    fn delay_for(&self, attempt: usize, err: &Error) -> Duration {
933        // A server-advised `Retry-After` wins over computed backoff (capped by
934        // `max_delay`, not jittered — it is an explicit instruction).
935        if let Some(secs) = err.retry_after() {
936            let capped = secs.min(self.max_delay.as_secs_f64()).max(0.0);
937            return Duration::from_secs_f64(capped);
938        }
939        let exp = self.backoff_multiplier.powi((attempt - 1) as i32);
940        let base = self.initial_delay.as_secs_f64() * exp;
941        let capped = base.min(self.max_delay.as_secs_f64());
942        let jittered = capped * jitter_factor(self.jitter);
943        Duration::from_secs_f64(jittered.max(0.0))
944    }
945}
946
947/// A cheap jitter multiplier in `[1 - jitter, 1.0]`, without a `rand`
948/// dependency: entropy comes from the current wall-clock nanoseconds mixed
949/// with a process-lifetime counter (so repeated calls within the same
950/// nanosecond still differ). `jitter <= 0` returns `1.0` (no jitter).
951fn jitter_factor(jitter: f64) -> f64 {
952    let jitter = jitter.clamp(0.0, 1.0);
953    if jitter == 0.0 {
954        return 1.0;
955    }
956    use std::sync::atomic::{AtomicU64, Ordering};
957    static COUNTER: AtomicU64 = AtomicU64::new(0);
958    let nanos = SystemTime::now()
959        .duration_since(UNIX_EPOCH)
960        .map(|d| d.as_nanos() as u64)
961        .unwrap_or(0);
962    let mixed = nanos ^ COUNTER.fetch_add(0x9E37_79B9_7F4A_7C15, Ordering::Relaxed);
963    // Map to [0, 1) via the top 53 bits (f64 mantissa width).
964    let r = (mixed >> 11) as f64 / ((1u64 << 53) as f64);
965    1.0 - jitter * r
966}
967
968/// A [`ChatClient`] decorator that retries transient failures with exponential
969/// backoff, honoring a server `Retry-After` when present.
970///
971/// Wraps any inner [`ChatClient`] and re-issues the request per its
972/// [`RetryPolicy`]. For streaming, only the *initial connection* is retried:
973/// if establishing the stream (or its very first item, before anything is
974/// yielded to the consumer) fails with a retryable error, the connection is
975/// re-attempted; once the first update flows, later stream errors propagate
976/// unchanged.
977///
978/// ```no_run
979/// # use std::time::Duration;
980/// # use agent_framework_core::client::{RetryingChatClient, RetryPolicy};
981/// # use agent_framework_core::prelude::*;
982/// # fn demo(inner: impl ChatClient + 'static) {
983/// let client = RetryingChatClient::new(inner)
984///     .with_policy(RetryPolicy::with_max_retries(5).initial_delay(Duration::from_millis(200)));
985/// # let _ = client;
986/// # }
987/// ```
988pub struct RetryingChatClient<C: ChatClient> {
989    inner: C,
990    policy: RetryPolicy,
991}
992
993impl<C: ChatClient> RetryingChatClient<C> {
994    /// Wrap `inner` with the default [`RetryPolicy`].
995    pub fn new(inner: C) -> Self {
996        Self {
997            inner,
998            policy: RetryPolicy::default(),
999        }
1000    }
1001
1002    /// Set the retry policy (builder-style).
1003    pub fn with_policy(mut self, policy: RetryPolicy) -> Self {
1004        self.policy = policy;
1005        self
1006    }
1007
1008    /// A reference to the wrapped client.
1009    pub fn inner(&self) -> &C {
1010        &self.inner
1011    }
1012
1013    /// A reference to the active retry policy.
1014    pub fn policy(&self) -> &RetryPolicy {
1015        &self.policy
1016    }
1017
1018    /// Sleep before a retry, emitting a tracing warning describing the attempt.
1019    async fn backoff(&self, attempt: usize, err: &Error) {
1020        let delay = self.policy.delay_for(attempt, err);
1021        tracing::warn!(
1022            attempt,
1023            max_retries = self.policy.max_retries,
1024            delay_ms = delay.as_millis() as u64,
1025            retry_after = err.retry_after(),
1026            status = err.status(),
1027            error = %err,
1028            "retrying chat request after transient error"
1029        );
1030        tokio::time::sleep(delay).await;
1031    }
1032}
1033
1034#[async_trait]
1035impl<C: ChatClient> ChatClient for RetryingChatClient<C> {
1036    async fn get_response(
1037        &self,
1038        messages: Vec<Message>,
1039        options: ChatOptions,
1040    ) -> Result<ChatResponse> {
1041        let mut attempt = 0usize;
1042        loop {
1043            match self
1044                .inner
1045                .get_response(messages.clone(), options.clone())
1046                .await
1047            {
1048                Ok(resp) => return Ok(resp),
1049                Err(e) => {
1050                    if attempt >= self.policy.max_retries || !self.policy.retry_on.should_retry(&e)
1051                    {
1052                        return Err(e);
1053                    }
1054                    attempt += 1;
1055                    self.backoff(attempt, &e).await;
1056                }
1057            }
1058        }
1059    }
1060
1061    async fn get_streaming_response(
1062        &self,
1063        messages: Vec<Message>,
1064        options: ChatOptions,
1065    ) -> Result<ChatStream> {
1066        let mut attempt = 0usize;
1067        loop {
1068            let established = self
1069                .inner
1070                .get_streaming_response(messages.clone(), options.clone())
1071                .await;
1072            match established {
1073                // The stream opened: peek its first item. An error there (with
1074                // nothing yet yielded to the consumer) is still an
1075                // initial-connection failure and is eligible for retry; any Ok
1076                // item — or a non-retryable / retries-exhausted error — is
1077                // handed back with the rest of the stream chained after it.
1078                Ok(mut stream) => match stream.next().await {
1079                    Some(Err(e))
1080                        if attempt < self.policy.max_retries
1081                            && self.policy.retry_on.should_retry(&e) =>
1082                    {
1083                        attempt += 1;
1084                        self.backoff(attempt, &e).await;
1085                        continue;
1086                    }
1087                    Some(first) => {
1088                        let head = stream::once(async move { first });
1089                        return Ok(head.chain(stream).boxed());
1090                    }
1091                    None => return Ok(stream::empty().boxed()),
1092                },
1093                // The stream never opened (e.g. a non-success HTTP status).
1094                Err(e) => {
1095                    if attempt >= self.policy.max_retries || !self.policy.retry_on.should_retry(&e)
1096                    {
1097                        return Err(e);
1098                    }
1099                    attempt += 1;
1100                    self.backoff(attempt, &e).await;
1101                }
1102            }
1103        }
1104    }
1105
1106    fn model(&self) -> Option<&str> {
1107        self.inner.model()
1108    }
1109}