Skip to main content

agent_framework_core/
agent.rs

1//! Agents: the [`SupportsAgentRun`] trait and the concrete [`Agent`].
2//!
3//! Rust equivalent of `agent_framework._agents`.
4
5use async_trait::async_trait;
6use futures::stream::{Stream, StreamExt};
7use serde_json::Value;
8use std::pin::Pin;
9use std::sync::Arc;
10use tracing::Instrument;
11use uuid::Uuid;
12
13use crate::client::{ChatClient, FunctionInvokingChatClient};
14use crate::compaction::{CompactionProvider, CompactionStrategy};
15use crate::error::{Error, Result};
16use crate::history::ensure_history_provider;
17use crate::memory::{ContextProvider, SessionContext};
18use crate::middleware::{AgentContext, ChatContext, MiddlewarePipeline, Terminal};
19use crate::session::AgentSession;
20use crate::tools::{ToolDefinition, ToolSource};
21use crate::types::{
22    prepare_messages, AgentResponse, AgentResponseUpdate, ChatOptions, ChatResponse, IntoMessages,
23    Message, ResponseFormat,
24};
25
26/// A boxed stream of agent run updates.
27pub type AgentRunStream = Pin<Box<dyn Stream<Item = Result<AgentResponseUpdate>> + Send>>;
28
29/// Per-run option overrides for a single [`SupportsAgentRun::run_with_options`] /
30/// [`SupportsAgentRun::run_stream`] call, merged over the agent's build-time defaults.
31///
32/// Mirrors upstream `run`/`run_stream` per-call keyword arguments and .NET
33/// `AgentRunOptions`: the per-run [`ChatOptions`] take precedence over the
34/// agent's defaults (via [`ChatOptions::merge`], matching Python's
35/// `run_chat_options & ChatOptions(...)`), and
36/// [`additional_tools`](Self::additional_tools) are appended to the tool list
37/// for that call only.
38#[derive(Debug, Clone, Default)]
39pub struct AgentRunOptions {
40    /// Chat-option overrides merged over the agent's defaults (per-run wins).
41    pub chat_options: Option<ChatOptions>,
42    /// Extra tools available only for this run, appended to the agent's tools.
43    ///
44    /// Declaration-only tools (no executor) surface their calls back to the
45    /// caller instead of being executed locally — this is how a hosting
46    /// frontend injects client-side tools (see the AG-UI router).
47    pub additional_tools: Vec<ToolDefinition>,
48}
49
50impl AgentRunOptions {
51    /// Empty options (no overrides).
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    /// Set the per-run chat-option overrides (merged over the agent defaults,
57    /// per-run winning).
58    pub fn with_chat_options(mut self, options: ChatOptions) -> Self {
59        self.chat_options = Some(options);
60        self
61    }
62
63    /// Append a tool available only for this run.
64    pub fn with_tool(mut self, tool: ToolDefinition) -> Self {
65        self.additional_tools.push(tool);
66        self
67    }
68
69    /// Append multiple tools available only for this run.
70    pub fn with_tools(mut self, tools: impl IntoIterator<Item = ToolDefinition>) -> Self {
71        self.additional_tools.extend(tools);
72        self
73    }
74
75    /// Whether these options carry no overrides at all. Used by the default
76    /// [`SupportsAgentRun::run_with_options`] to decide whether to warn about ignoring
77    /// options it cannot honor.
78    pub fn is_empty(&self) -> bool {
79        self.chat_options.is_none() && self.additional_tools.is_empty()
80    }
81}
82
83/// Map a completed run into buffered agent updates — one update per message,
84/// each with a distinct `message_id` so that re-aggregation via
85/// [`AgentResponse::from_updates`] keeps the message boundaries. Shared by
86/// the default [`SupportsAgentRun::run_stream`] and [`Agent`]'s middleware-path
87/// replay.
88///
89/// Response-level metadata survives the replay: `response_id` and
90/// `conversation_id` ride on every update, and `usage_details` rides the
91/// final update as a [`Content::Usage`] item (aggregation folds it back into
92/// [`AgentResponse::usage_details`], never into message contents) — the
93/// same contract as the tool-loop replay in `FunctionInvokingChatClient`.
94pub(crate) fn response_to_updates(response: AgentResponse) -> Vec<Result<AgentResponseUpdate>> {
95    let AgentResponse {
96        messages,
97        response_id,
98        conversation_id,
99        usage_details,
100        ..
101    } = response;
102    let last = messages.len().saturating_sub(1);
103    // Keep provider message ids only when all present and distinct; otherwise
104    // positional ids for every message. A service (e.g. Assistants) can reuse
105    // one run id across the tool-call and final assistant messages, and
106    // `AgentResponse::from_updates` keys by id — a duplicate would merge
107    // the final answer into the tool-call message, so streamed and
108    // non-streamed responses would differ.
109    let keep_provider_ids = {
110        let mut seen = std::collections::HashSet::new();
111        messages.iter().all(|m| {
112            m.message_id
113                .as_ref()
114                .is_some_and(|id| !id.is_empty() && seen.insert(id.as_str()))
115        })
116    };
117    let mut updates: Vec<Result<AgentResponseUpdate>> = messages
118        .into_iter()
119        .enumerate()
120        .map(|(i, m)| {
121            let message_id = if keep_provider_ids {
122                m.message_id.clone()
123            } else {
124                Some(format!("msg-{i}"))
125            };
126            let mut contents = m.contents;
127            if i == last {
128                if let Some(usage) = usage_details.clone() {
129                    contents.push(crate::types::Content::Usage(crate::types::UsageContent {
130                        details: usage,
131                    }));
132                }
133            }
134            Ok(AgentResponseUpdate {
135                contents,
136                role: Some(m.role),
137                author_name: m.author_name,
138                message_id,
139                response_id: response_id.clone(),
140                conversation_id: conversation_id.clone(),
141                ..Default::default()
142            })
143        })
144        .collect();
145    if updates.is_empty() && (usage_details.is_some() || response_id.is_some()) {
146        let contents = usage_details
147            .map(|u| {
148                vec![crate::types::Content::Usage(crate::types::UsageContent {
149                    details: u,
150                })]
151            })
152            .unwrap_or_default();
153        updates.push(Ok(AgentResponseUpdate {
154            contents,
155            role: Some(crate::types::Role::assistant()),
156            response_id,
157            conversation_id,
158            ..Default::default()
159        }));
160    }
161    updates
162}
163
164/// Sanitize an agent name into a valid tool/function identifier, mirroring
165/// Python's `_sanitize_agent_name` (`_agents.py:53-87`).
166///
167/// Every character that is not ASCII alphanumeric or `_` is replaced with `_`;
168/// runs of `_` are collapsed to one; leading/trailing `_` are trimmed. An
169/// all-invalid name (e.g. `"@@@"`) becomes `"agent"`, and a name that would
170/// start with a digit is prefixed with `_`. `None` maps to `None`.
171fn sanitize_agent_name(agent_name: Option<&str>) -> Option<String> {
172    let name = agent_name?;
173    let replaced: String = name
174        .chars()
175        .map(|c| {
176            if c.is_ascii_alphanumeric() || c == '_' {
177                c
178            } else {
179                '_'
180            }
181        })
182        .collect();
183    // Collapse consecutive underscores into one.
184    let mut collapsed = String::with_capacity(replaced.len());
185    let mut prev_underscore = false;
186    for c in replaced.chars() {
187        if c == '_' {
188            if !prev_underscore {
189                collapsed.push('_');
190            }
191            prev_underscore = true;
192        } else {
193            collapsed.push(c);
194            prev_underscore = false;
195        }
196    }
197    let trimmed = collapsed.trim_matches('_');
198    if trimmed.is_empty() {
199        return Some("agent".to_string());
200    }
201    let mut result = trimmed.to_string();
202    if result.starts_with(|c: char| c.is_ascii_digit()) {
203        result.insert(0, '_');
204    }
205    Some(result)
206}
207
208/// The common interface implemented by all agents.
209#[async_trait]
210pub trait SupportsAgentRun: Send + Sync {
211    /// Run the agent to completion.
212    async fn run(
213        &self,
214        messages: Vec<Message>,
215        session: Option<&mut AgentSession>,
216    ) -> Result<AgentResponse>;
217
218    /// Run the agent to completion, applying per-run [`AgentRunOptions`] over
219    /// the agent's build-time defaults.
220    ///
221    /// The default implementation ignores `options` and delegates to
222    /// [`SupportsAgentRun::run`], emitting a `tracing::warn!` when non-empty options are
223    /// supplied — mirroring upstream agents that silently drop kwargs they do
224    /// not understand. Agents that support per-run overrides (notably
225    /// [`Agent`]) override this.
226    async fn run_with_options(
227        &self,
228        messages: Vec<Message>,
229        session: Option<&mut AgentSession>,
230        options: AgentRunOptions,
231    ) -> Result<AgentResponse> {
232        if !options.is_empty() {
233            tracing::warn!(
234                agent = %self.id(),
235                "agent does not support per-run options; ignoring them"
236            );
237        }
238        self.run(messages, session).await
239    }
240
241    /// Run the agent and stream incremental [`AgentResponseUpdate`]s.
242    ///
243    /// The default implementation is a **buffered fallback**: it runs to
244    /// completion via [`SupportsAgentRun::run_with_options`] and yields the response's
245    /// messages as updates. Agents with a real streaming backend (notably
246    /// [`Agent`], [`WorkflowAgent`](crate::workflow::WorkflowAgent), and the
247    /// A2A client agent) override this to stream incrementally.
248    ///
249    /// `session` is taken **by value**: the returned stream owns it and
250    /// drives its context providers (including any history provider) once
251    /// the stream is fully consumed. When a provider's storage is shared (as
252    /// [`InMemoryHistoryProvider`](crate::history::InMemoryHistoryProvider)'s
253    /// is, via `Arc`), the write-back is
254    /// observable through a clone taken before streaming.
255    async fn run_stream(
256        &self,
257        messages: Vec<Message>,
258        session: Option<AgentSession>,
259        options: Option<AgentRunOptions>,
260    ) -> Result<AgentRunStream> {
261        let mut owned = session;
262        let response = self
263            .run_with_options(messages, owned.as_mut(), options.unwrap_or_default())
264            .await?;
265        Ok(futures::stream::iter(response_to_updates(response)).boxed())
266    }
267
268    /// A stable identifier for this agent.
269    fn id(&self) -> &str;
270
271    /// The optional human-readable name.
272    fn name(&self) -> Option<&str> {
273        None
274    }
275
276    /// The display name: `name` if set, else `id`.
277    fn display_name(&self) -> String {
278        self.name()
279            .map(str::to_string)
280            .unwrap_or_else(|| self.id().to_string())
281    }
282
283    /// A fresh session for a new conversation.
284    fn create_session(&self) -> AgentSession {
285        AgentSession::new()
286    }
287}
288
289/// The primary concrete agent: pairs a chat client with instructions, default
290/// options, tools, context providers, and middleware.
291///
292/// Cheaply cloneable (the client, context providers, and middleware are shared
293/// via `Arc`), which is what makes [`Agent::as_tool`] possible.
294#[derive(Clone)]
295pub struct Agent {
296    id: String,
297    name: Option<String>,
298    description: Option<String>,
299    client: Arc<dyn ChatClient>,
300    chat_options: ChatOptions,
301    context_providers: Vec<Arc<dyn ContextProvider>>,
302    agent_middleware: MiddlewarePipeline<AgentContext>,
303    /// Middleware run around the underlying chat-client call (mirrors
304    /// Python's `use_chat_middleware`). See [`Agent::call_chat_client`].
305    chat_middleware: MiddlewarePipeline<ChatContext>,
306    /// Dynamic tool sources (e.g. MCP servers), resolved fresh on every run
307    /// and appended after the agent's static/context/per-run tools. See
308    /// [`ToolSource`] and [`Agent::prepare_request`].
309    tool_sources: Vec<Arc<dyn ToolSource>>,
310}
311
312/// A callback receiving each [`AgentResponseUpdate`] streamed by an agent
313/// running as a tool — see [`AsToolOptions::stream_callback`].
314pub type AgentToolStreamCallback = Arc<dyn Fn(&AgentResponseUpdate) + Send + Sync>;
315
316/// Options for [`Agent::as_tool`].
317#[derive(Clone, Default)]
318pub struct AsToolOptions {
319    /// The tool name. Defaults to the agent's name (else its id).
320    pub name: Option<String>,
321    /// The tool description. Defaults to the agent's description (else empty).
322    pub description: Option<String>,
323    /// The single string argument's name. Defaults to `"task"`.
324    pub arg_name: Option<String>,
325    /// The argument's description. Defaults to `"Task for {tool_name}"`.
326    pub arg_description: Option<String>,
327    /// Whether calls to this delegated tool require human approval before
328    /// executing (default: no). Mirrors upstream `as_tool(approval_mode=…)`.
329    pub approval_mode: crate::tools::ApprovalMode,
330    /// Observe the sub-agent's streamed updates as they arrive. When set,
331    /// the wrapper runs the sub-agent via `run_stream` and invokes the
332    /// callback on every update before aggregating the final response.
333    /// Mirrors upstream `as_tool(stream_callback=…)`.
334    pub stream_callback: Option<AgentToolStreamCallback>,
335    /// Forward the **parent** agent's session to the sub-agent, so both
336    /// share the same session identity and state bag.
337    ///
338    /// The sub-agent receives a [`AgentSession::child`] of the parent's
339    /// session: same `session_id`, shared `state`, but an **isolated**
340    /// `service_session_id` — the parent's server-side conversation pointer
341    /// (whose tool call is still pending mid-run) must not leak into the
342    /// sub-agent's own service calls. Mirrors upstream
343    /// `as_tool(propagate_session=True)` with the child-session isolation
344    /// fix (microsoft/agent-framework#5875). Defaults to `false` (a fresh
345    /// session per call).
346    pub propagate_session: bool,
347}
348
349impl std::fmt::Debug for AsToolOptions {
350    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
351        f.debug_struct("AsToolOptions")
352            .field("name", &self.name)
353            .field("description", &self.description)
354            .field("arg_name", &self.arg_name)
355            .field("arg_description", &self.arg_description)
356            .field("approval_mode", &self.approval_mode)
357            .field("stream_callback", &self.stream_callback.is_some())
358            .field("propagate_session", &self.propagate_session)
359            .finish()
360    }
361}
362
363impl AsToolOptions {
364    pub fn new() -> Self {
365        Self::default()
366    }
367    /// Set the tool name.
368    pub fn name(mut self, name: impl Into<String>) -> Self {
369        self.name = Some(name.into());
370        self
371    }
372    /// Set the tool description.
373    pub fn description(mut self, description: impl Into<String>) -> Self {
374        self.description = Some(description.into());
375        self
376    }
377    /// Set the argument name (default `"task"`).
378    pub fn arg_name(mut self, arg_name: impl Into<String>) -> Self {
379        self.arg_name = Some(arg_name.into());
380        self
381    }
382    /// Set the argument description.
383    pub fn arg_description(mut self, arg_description: impl Into<String>) -> Self {
384        self.arg_description = Some(arg_description.into());
385        self
386    }
387    /// Require human approval before every call to the delegated tool.
388    pub fn approval_mode(mut self, mode: crate::tools::ApprovalMode) -> Self {
389        self.approval_mode = mode;
390        self
391    }
392    /// Observe the sub-agent's streamed updates (see
393    /// [`AsToolOptions::stream_callback`]).
394    pub fn stream_callback(mut self, callback: AgentToolStreamCallback) -> Self {
395        self.stream_callback = Some(callback);
396        self
397    }
398    /// Forward the parent agent's session to the sub-agent (see
399    /// [`AsToolOptions::propagate_session`]).
400    pub fn propagate_session(mut self, propagate: bool) -> Self {
401        self.propagate_session = propagate;
402        self
403    }
404}
405
406impl Agent {
407    /// Start building an agent from a chat client. The client is automatically
408    /// wrapped with [`FunctionInvokingChatClient`] so local tools are executed.
409    pub fn builder(client: impl ChatClient + 'static) -> AgentBuilder {
410        AgentBuilder::new(client)
411    }
412
413    /// The agent's default instructions.
414    pub fn instructions(&self) -> Option<&str> {
415        self.chat_options.instructions.as_deref()
416    }
417
418    /// Run and stream incremental updates — an ergonomic wrapper over the
419    /// object-safe [`SupportsAgentRun::run_stream`] trait method (the real streaming
420    /// implementation), accepting `impl IntoMessages`.
421    ///
422    /// The session's context providers (including any history provider) are
423    /// driven when the stream completes; because provider storage is shared
424    /// via `Arc`, updates are visible on the original session once the
425    /// returned stream is fully consumed. Pass per-run [`AgentRunOptions`] to
426    /// override the agent's defaults for this call only.
427    pub async fn run_stream(
428        &self,
429        messages: impl IntoMessages,
430        session: Option<AgentSession>,
431        options: Option<AgentRunOptions>,
432    ) -> Result<AgentRunStream> {
433        SupportsAgentRun::run_stream(self, messages.into_messages(), session, options).await
434    }
435
436    /// Ergonomic streaming run with a fresh session and no per-run options
437    /// (mirrors [`Agent::run_once`]).
438    pub async fn run_stream_once(&self, messages: impl IntoMessages) -> Result<AgentRunStream> {
439        SupportsAgentRun::run_stream(self, messages.into_messages(), None, None).await
440    }
441
442    /// Ergonomic run without an explicit session.
443    pub async fn run_once(&self, messages: impl IntoMessages) -> Result<AgentResponse> {
444        self.run(messages.into_messages(), None).await
445    }
446
447    /// The real streaming implementation, shared by the [`SupportsAgentRun::run_stream`]
448    /// trait impl. Kept as an inherent helper so the trait method stays a thin
449    /// forwarder.
450    async fn run_stream_impl(
451        &self,
452        input: Vec<Message>,
453        session: Option<AgentSession>,
454        run_options: AgentRunOptions,
455    ) -> Result<AgentRunStream> {
456        let mut session = session.unwrap_or_else(|| self.create_session());
457        let (final_messages, options) = self
458            .prepare_request(&input, &mut session, &run_options)
459            .await?;
460
461        // When agent middleware is configured, route the run through the same
462        // pipeline as `run` (so guardrails/rewrites/termination apply) and then
463        // emit the resulting messages as updates. Token-level streaming is only
464        // used when there is no agent middleware to honor.
465        if self.has_middleware() {
466            let response = match self.run_core(final_messages, options, true).await {
467                Ok(r) => r,
468                Err(e) => {
469                    for cp in self.combined_providers(&session) {
470                        let _ = cp.after_run(&input, &[], Some(&e)).await;
471                    }
472                    return Err(e);
473                }
474            };
475            self.update_session_conversation_id(&mut session, response.conversation_id.as_deref())?;
476            for cp in self.combined_providers(&session) {
477                cp.after_run(&input, &response.messages, None).await?;
478            }
479            // Distinct message ids keep boundaries when re-aggregated; the
480            // response's conversation/response ids and usage ride along so
481            // service-managed continuity survives the middleware replay.
482            return Ok(futures::stream::iter(response_to_updates(response)).boxed());
483        }
484
485        let (final_messages, options) = self
486            .apply_chat_middleware_pre_call(final_messages, options)
487            .await?;
488        // Capture the structured-output format before `options` is consumed, so
489        // the stream's terminal aggregation can auto-populate `value` (task 3).
490        let response_format = options.response_format.clone();
491        let agent_name = self.name.clone();
492        let providers = self.combined_providers(&session);
493        let inner = match self
494            .client
495            .get_streaming_response(final_messages, options)
496            .await
497        {
498            Ok(s) => s,
499            Err(e) => {
500                // Failure before the stream opens: let providers observe it.
501                for cp in &providers {
502                    let _ = cp.after_run(&input, &[], Some(&e)).await;
503                }
504                return Err(e);
505            }
506        };
507
508        // Wrap the inner stream: forward mapped updates, then update the session.
509        let stream = async_stream_forward(
510            inner,
511            agent_name,
512            session,
513            input,
514            providers,
515            response_format,
516        );
517        Ok(stream.boxed())
518    }
519
520    /// Assemble the final message list and options for a request, applying
521    /// context providers (including history) over the session.
522    async fn prepare_request(
523        &self,
524        input: &[Message],
525        session: &mut AgentSession,
526        run_options: &AgentRunOptions,
527    ) -> Result<(Vec<Message>, ChatOptions)> {
528        // Auto-attach a fresh `InMemoryHistoryProvider` to a non-service-managed
529        // session that doesn't already carry a history provider, so local
530        // multi-turn conversations keep accumulating history the way the old
531        // `AgentThread` message store used to.
532        ensure_history_provider(session);
533        let service_session_id = session.service_session_id().map(str::to_string);
534
535        // Merge per-run chat-option overrides over the agent's defaults, with
536        // the per-run side winning (mirrors Python's `run_chat_options &
537        // ChatOptions(...)`, whose right-hand side takes precedence — see
538        // `ChatOptions::merge`).
539        let mut options = match &run_options.chat_options {
540            Some(overrides) => self.chat_options.clone().merge(overrides.clone()),
541            None => self.chat_options.clone(),
542        };
543        // A service-managed session's id drives continuity and wins; on a
544        // local session, a per-run / agent-default `conversation_id` override
545        // survives (previously it was unconditionally cleared here, silently
546        // starting a new service conversation despite the documented per-run
547        // precedence).
548        options.conversation_id = service_session_id
549            .clone()
550            .or(options.conversation_id.take());
551
552        // Context provider injection: run every provider's `before_run` over a
553        // shared `SessionContext`, then fold the result into the request
554        // (provider instructions AFTER the agent's own; provider messages —
555        // including, via the auto-attached/explicit `HistoryProvider`, thread
556        // history — PREPENDED ahead of the run's own input; provider tools
557        // appended, deduplicated by name).
558        let providers = self.combined_providers(session);
559        let mut ctx = SessionContext::new(input.to_vec());
560        ctx.session_id = Some(session.session_id().to_string());
561        ctx.service_session_id = service_session_id.clone();
562        for provider in &providers {
563            provider.before_run(&mut ctx).await?;
564        }
565        if let Some(instr) = ctx.instructions {
566            options.instructions = Some(match options.instructions.take() {
567                Some(base) => format!("{base}\n{instr}"),
568                None => instr,
569            });
570        }
571        let mut history = ctx.messages;
572
573        // Deduplicate by name: a tool may be defined on the agent and also
574        // injected by a context provider. Providers rejecting duplicate
575        // tool names would otherwise fail the request.
576        for t in ctx.tools {
577            if !options.tools.iter().any(|existing| existing.name == t.name) {
578                options.tools.push(t);
579            }
580        }
581
582        // Append per-run additional tools (deduplicated by name), available for
583        // this call only. Declaration-only tools (no executor) surface their
584        // calls back to the caller (frontend-tool pattern) via the
585        // function-invocation loop's declaration-only handling in `client.rs`.
586        for t in &run_options.additional_tools {
587            if !options.tools.iter().any(|existing| existing.name == t.name) {
588                options.tools.push(t.clone());
589            }
590        }
591
592        // Resolve dynamic tool sources (e.g. MCP servers) fresh for this run,
593        // appended after every tool assembled above (dedup by name against
594        // those tools plus any earlier source already appended in this same
595        // loop; first registrant wins) — mirrors the Python reference's
596        // `existing_names` skip when (re)loading MCP tools/prompts
597        // (`_mcp.py:654,696`). A source's failure propagates out of the whole
598        // run; see [`ToolSource::resolve_tools`].
599        for source in &self.tool_sources {
600            let resolved = source.resolve_tools().await?;
601            for t in resolved {
602                if options.tools.iter().any(|existing| existing.name == t.name) {
603                    tracing::warn!(
604                        source = source.source_name(),
605                        tool = %t.name,
606                        "tool source produced a tool whose name collides with an existing \
607                         tool; skipping"
608                    );
609                    continue;
610                }
611                options.tools.push(t);
612            }
613        }
614
615        history.extend(input.iter().cloned());
616        let instructions = options.instructions.take();
617        let final_messages = prepare_messages(history, instructions.as_deref());
618
619        // Hand the run's session to the function-invocation loop (which pops
620        // it before the wire client sees the options), so invoked tools can
621        // read it from `FunctionInvocationContext::session` — the channel
622        // behind `as_tool` + `propagate_session`. The clone shares the
623        // session's state bag by reference (see `SessionState`).
624        options.session = Some(session.clone());
625        Ok((final_messages, options))
626    }
627
628    /// Run the agent middleware pipeline with a terminal that calls the chat
629    /// client, returning the aggregated response. Shared by `run` and the
630    /// middleware path of `run_stream` (thread updates are handled by callers).
631    ///
632    /// Wrapped in an `invoke_agent` span (OTel GenAI semconv). The plain
633    /// token-streaming path does not go through here; that path is observed at
634    /// the chat-client decorator level (see [`crate::observability`]).
635    async fn run_core(
636        &self,
637        final_messages: Vec<Message>,
638        options: ChatOptions,
639        is_streaming: bool,
640    ) -> Result<AgentResponse> {
641        let span = crate::observability::agent_span(
642            self.name.as_deref().unwrap_or(self.id.as_str()),
643            &self.id,
644        );
645        async move {
646            let result = self
647                .run_core_inner(final_messages, options, is_streaming)
648                .await;
649            let span = tracing::Span::current();
650            match &result {
651                Ok(response) => {
652                    if let Some(usage) = &response.usage_details {
653                        if let Some(input) = usage.input_token_count {
654                            span.record(crate::observability::attr::INPUT_TOKENS, input);
655                        }
656                        if let Some(output) = usage.output_token_count {
657                            span.record(crate::observability::attr::OUTPUT_TOKENS, output);
658                        }
659                    }
660                }
661                Err(err) => {
662                    crate::observability::record_error(&span, err);
663                }
664            }
665            result
666        }
667        .instrument(span)
668        .await
669    }
670
671    async fn run_core_inner(
672        &self,
673        final_messages: Vec<Message>,
674        options: ChatOptions,
675        is_streaming: bool,
676    ) -> Result<AgentResponse> {
677        let client = self.client.clone();
678        let chat_middleware = self.chat_middleware.clone();
679        let terminal: Terminal<AgentContext> = Box::new(move |mut ctx: AgentContext| {
680            let client = client.clone();
681            let options = options.clone();
682            let chat_middleware = chat_middleware.clone();
683            Box::pin(async move {
684                if ctx.terminate {
685                    return Ok(ctx);
686                }
687                let response = Self::call_chat_client(
688                    &client,
689                    &chat_middleware,
690                    ctx.messages.clone(),
691                    options,
692                    ctx.is_streaming,
693                )
694                .await?;
695                ctx.result = Some(AgentResponse::from_chat_response(response));
696                Ok(ctx)
697            }) as crate::tools::BoxFuture<Result<AgentContext>>
698        });
699
700        let ctx = AgentContext::new(final_messages, is_streaming);
701        let ctx = self.agent_middleware.execute(ctx, terminal).await?;
702        let mut response = ctx.result.ok_or_else(|| {
703            crate::error::Error::AgentExecution("agent produced no result".into())
704        })?;
705
706        if let Some(name) = &self.name {
707            for m in &mut response.messages {
708                if m.author_name.is_none() {
709                    m.author_name = Some(name.clone());
710                }
711            }
712        }
713        Ok(response)
714    }
715
716    /// Invoke the chat client once, routed through the chat-middleware
717    /// pipeline (mirrors Python's `use_chat_middleware`).
718    ///
719    /// Middleware may mutate `messages`/`chat_options` before the call, then
720    /// observe (or override, via [`ChatContext::result`]) the response after
721    /// calling `next.run(...)`. A middleware that sets `terminate = true`
722    /// without invoking `next` short-circuits the call entirely: the
723    /// underlying client is never invoked, and [`ChatContext::result`] (if
724    /// set) becomes the returned response.
725    async fn call_chat_client(
726        client: &Arc<dyn ChatClient>,
727        chat_middleware: &MiddlewarePipeline<ChatContext>,
728        messages: Vec<Message>,
729        options: ChatOptions,
730        is_streaming: bool,
731    ) -> Result<ChatResponse> {
732        if chat_middleware.is_empty() {
733            return client.get_response(messages, options).await;
734        }
735        let client = client.clone();
736        let terminal: Terminal<ChatContext> = Box::new(move |mut ctx: ChatContext| {
737            let client = client.clone();
738            Box::pin(async move {
739                if ctx.terminate {
740                    return Ok(ctx);
741                }
742                let response = client
743                    .get_response(ctx.messages.clone(), ctx.chat_options.clone())
744                    .await?;
745                ctx.result = Some(response);
746                Ok(ctx)
747            }) as crate::tools::BoxFuture<Result<ChatContext>>
748        });
749        let ctx = ChatContext::new(messages, options, is_streaming);
750        let ctx = chat_middleware.execute(ctx, terminal).await?;
751        ctx.result.ok_or_else(|| {
752            crate::error::Error::AgentExecution("chat middleware produced no result".into())
753        })
754    }
755
756    /// Apply chat middleware to a *streaming* call's `messages`/`chat_options`
757    /// before the real network call.
758    ///
759    /// Unlike [`Agent::call_chat_client`], this only honors *pre-call*
760    /// mutation: a real token stream can't flow back through
761    /// [`ChatContext::result`] (typed for a complete [`ChatResponse`]), so any
762    /// middleware logic placed *after* `next.run(...)` observes
763    /// `ctx.result == None` and cannot post-process individual streamed
764    /// tokens, and `terminate`/`result` short-circuiting is not honored here.
765    /// This mirrors upstream Python's `use_chat_middleware`, whose streaming
766    /// path likewise hands middleware an unconsumed async generator rather
767    /// than driving it through the pipeline. Full interception (including
768    /// short-circuiting) for chat middleware is available via
769    /// [`Agent::run`]/[`Agent::run_once`], and via `run_stream` too
770    /// when at least one agent middleware is also configured (that path
771    /// funnels through [`Agent::run_core`] and replays the result as
772    /// updates).
773    async fn apply_chat_middleware_pre_call(
774        &self,
775        messages: Vec<Message>,
776        options: ChatOptions,
777    ) -> Result<(Vec<Message>, ChatOptions)> {
778        if self.chat_middleware.is_empty() {
779            return Ok((messages, options));
780        }
781        let terminal: Terminal<ChatContext> = Box::new(|ctx| Box::pin(async move { Ok(ctx) }));
782        let ctx = ChatContext::new(messages, options, true);
783        let ctx = self.chat_middleware.execute(ctx, terminal).await?;
784        Ok((ctx.messages, ctx.chat_options))
785    }
786
787    /// Whether this agent has any agent-level middleware configured.
788    fn has_middleware(&self) -> bool {
789        !self.agent_middleware.is_empty()
790    }
791
792    /// The effective context providers for a run: the session's, combined
793    /// with the agent's own. There is no aggregate wrapper any more — callers
794    /// iterate this list directly.
795    fn combined_providers(&self, session: &AgentSession) -> Vec<Arc<dyn ContextProvider>> {
796        let mut providers = session.context_providers.clone();
797        providers.extend(self.context_providers.iter().cloned());
798        providers
799    }
800
801    /// Reconcile a run's conversation id with the session, mirroring Python's
802    /// `_update_thread_with_type_and_conversation_id` (`_agents.py:1204-1234`).
803    ///
804    /// * No id returned while the session *is* service-managed → the service
805    ///   doesn't support service-managed sessions for this request, so surface
806    ///   an [`Error::AgentExecution`] (matches Python raising
807    ///   `AgentExecutionException`, GAP item 14).
808    /// * An id returned that the session newly adopts is simply recorded on
809    ///   the session; there is no `thread_created` hook any more (upstream
810    ///   removed it — see [`crate::memory::ContextProvider`]).
811    fn update_session_conversation_id(
812        &self,
813        session: &mut AgentSession,
814        response_conversation_id: Option<&str>,
815    ) -> Result<()> {
816        match response_conversation_id {
817            None => {
818                if session.service_session_id().is_some() {
819                    return Err(Error::AgentExecution(
820                        "Service did not return a valid conversation id when using a service \
821                         managed thread."
822                            .into(),
823                    ));
824                }
825                Ok(())
826            }
827            Some(cid) => {
828                session.try_adopt_service_session_id(cid);
829                Ok(())
830            }
831        }
832    }
833}
834
835/// State carried while forwarding a chat stream as agent updates.
836type ForwardFinish = Option<(
837    AgentSession,
838    Vec<Message>,
839    Vec<Arc<dyn ContextProvider>>,
840    Option<ResponseFormat>,
841)>;
842
843/// Forward an inner chat stream as agent updates and update the session on end.
844fn async_stream_forward(
845    inner: crate::client::ChatStream,
846    agent_name: Option<String>,
847    session: AgentSession,
848    input: Vec<Message>,
849    providers: Vec<Arc<dyn ContextProvider>>,
850    response_format: Option<ResponseFormat>,
851) -> impl Stream<Item = Result<AgentResponseUpdate>> + Send {
852    let finish: ForwardFinish = Some((session, input, providers, response_format));
853    futures::stream::unfold(
854        (
855            inner,
856            Vec::<crate::types::ChatResponseUpdate>::new(),
857            false,
858            finish,
859        ),
860        move |(mut inner, mut collected, done, mut finish)| {
861            let agent_name = agent_name.clone();
862            async move {
863                if done {
864                    return None;
865                }
866                match inner.next().await {
867                    Some(Ok(update)) => {
868                        collected.push(update.clone());
869                        let mut au = AgentResponseUpdate::from_chat_update(&update);
870                        if au.author_name.is_none() {
871                            au.author_name = agent_name.clone();
872                        }
873                        Some((Ok(au), (inner, collected, false, finish)))
874                    }
875                    Some(Err(e)) => {
876                        // Failure mid-stream: let context providers observe the
877                        // error before surfacing it. The stream error takes
878                        // precedence, so the hooks' results are discarded.
879                        if let Some((_session, input, providers, _rf)) = finish.take() {
880                            for cp in &providers {
881                                let _ = cp.after_run(&input, &[], Some(&e)).await;
882                            }
883                        }
884                        Some((Err(e), (inner, collected, true, None)))
885                    }
886                    None => {
887                        // Stream finished: reconcile the conversation id and fire
888                        // the context providers' completion hook (which records
889                        // history, for any attached `HistoryProvider`). Surface
890                        // any failure as the final item rather than dropping it.
891                        if let Some((mut session, input, providers, response_format)) =
892                            finish.take()
893                        {
894                            let response = ChatResponse::from_updates_with_format(
895                                collected.clone(),
896                                response_format.as_ref(),
897                            );
898                            match response.conversation_id.as_deref() {
899                                None => {
900                                    if session.service_session_id().is_some() {
901                                        return Some((
902                                            Err(Error::AgentExecution(
903                                                "Service did not return a valid conversation id \
904                                                 when using a service managed thread."
905                                                    .into(),
906                                            )),
907                                            (inner, collected, true, None),
908                                        ));
909                                    }
910                                }
911                                Some(cid) => {
912                                    session.try_adopt_service_session_id(cid);
913                                }
914                            }
915                            for cp in providers {
916                                if let Err(e) = cp.after_run(&input, &response.messages, None).await
917                                {
918                                    return Some((Err(e), (inner, collected, true, None)));
919                                }
920                            }
921                        }
922                        None
923                    }
924                }
925            }
926        },
927    )
928}
929
930#[async_trait]
931impl SupportsAgentRun for Agent {
932    async fn run(
933        &self,
934        messages: Vec<Message>,
935        session: Option<&mut AgentSession>,
936    ) -> Result<AgentResponse> {
937        self.run_with_options(messages, session, AgentRunOptions::default())
938            .await
939    }
940
941    async fn run_with_options(
942        &self,
943        messages: Vec<Message>,
944        session: Option<&mut AgentSession>,
945        options: AgentRunOptions,
946    ) -> Result<AgentResponse> {
947        let mut owned_session;
948        let session: &mut AgentSession = match session {
949            Some(s) => s,
950            None => {
951                owned_session = self.create_session();
952                &mut owned_session
953            }
954        };
955
956        let (final_messages, chat_options) =
957            self.prepare_request(&messages, session, &options).await?;
958        let response = match self.run_core(final_messages, chat_options, false).await {
959            Ok(r) => r,
960            Err(e) => {
961                // Failure path: let context providers observe the error.
962                // The run's error takes precedence over any hook failure, so
963                // the hook result is intentionally discarded.
964                for cp in self.combined_providers(session) {
965                    let _ = cp.after_run(&messages, &[], Some(&e)).await;
966                }
967                return Err(e);
968            }
969        };
970
971        // Persist / validate the service-managed conversation id before
972        // firing the completion hooks (tasks: service-session adoption +
973        // missing-id error).
974        self.update_session_conversation_id(session, response.conversation_id.as_deref())?;
975
976        // Fire the context providers' success completion hook (this is what
977        // records history, for any attached `HistoryProvider`).
978        for cp in self.combined_providers(session) {
979            cp.after_run(&messages, &response.messages, None).await?;
980        }
981
982        Ok(response)
983    }
984
985    async fn run_stream(
986        &self,
987        messages: Vec<Message>,
988        session: Option<AgentSession>,
989        options: Option<AgentRunOptions>,
990    ) -> Result<AgentRunStream> {
991        self.run_stream_impl(messages, session, options.unwrap_or_default())
992            .await
993    }
994
995    fn id(&self) -> &str {
996        &self.id
997    }
998    fn name(&self) -> Option<&str> {
999        self.name.as_deref()
1000    }
1001
1002    fn create_session(&self) -> AgentSession {
1003        // A service-managed conversation id yields a service session;
1004        // otherwise eagerly attach a fresh `InMemoryHistoryProvider` (rather
1005        // than relying solely on `prepare_request`'s auto-attach) so history
1006        // is observable across clones (e.g. during streaming): the only way a
1007        // caller observes the post-stream write-back through a clone taken
1008        // beforehand is if the provider (and therefore its `Arc`) already
1009        // exists at clone time.
1010        //
1011        // The agent's own context providers are NOT copied onto the session
1012        // here: [`Agent::combined_providers`] merges the session's providers
1013        // with the agent's own at request time, so copying them here would
1014        // double-invoke the agent's providers for every run against this
1015        // session.
1016        match &self.chat_options.conversation_id {
1017            Some(id) => AgentSession::service(id.clone()),
1018            None => {
1019                let mut session = AgentSession::new();
1020                ensure_history_provider(&mut session);
1021                session
1022            }
1023        }
1024    }
1025}
1026
1027/// Builder for [`Agent`].
1028pub struct AgentBuilder {
1029    id: Option<String>,
1030    name: Option<String>,
1031    description: Option<String>,
1032    instructions: Option<String>,
1033    /// The raw, caller-supplied client. Wrapping in [`FunctionInvokingChatClient`]
1034    /// is deferred to [`AgentBuilder::build`] so that builder-collected
1035    /// function middleware can be threaded into the wrapper's constructor.
1036    client: Arc<dyn ChatClient>,
1037    chat_options: ChatOptions,
1038    context_providers: Vec<Arc<dyn ContextProvider>>,
1039    agent_middleware: Vec<Arc<crate::middleware::AgentMiddleware>>,
1040    chat_middleware: Vec<Arc<crate::middleware::ChatMiddleware>>,
1041    function_middleware: Vec<Arc<crate::middleware::FunctionMiddleware>>,
1042    /// Governs the tool-loop spans; `None` leaves the wrapper reading the
1043    /// environment. See [`AgentBuilder::observability_config`].
1044    observability: Option<crate::observability::ObservabilityConfig>,
1045    tool_sources: Vec<Arc<dyn ToolSource>>,
1046}
1047
1048impl AgentBuilder {
1049    fn new(client: impl ChatClient + 'static) -> Self {
1050        Self {
1051            id: None,
1052            name: None,
1053            description: None,
1054            instructions: None,
1055            client: Arc::new(client),
1056            chat_options: ChatOptions::new(),
1057            context_providers: Vec::new(),
1058            agent_middleware: Vec::new(),
1059            chat_middleware: Vec::new(),
1060            function_middleware: Vec::new(),
1061            observability: None,
1062            tool_sources: Vec::new(),
1063        }
1064    }
1065
1066    pub fn id(mut self, id: impl Into<String>) -> Self {
1067        self.id = Some(id.into());
1068        self
1069    }
1070    pub fn name(mut self, name: impl Into<String>) -> Self {
1071        self.name = Some(name.into());
1072        self
1073    }
1074    pub fn description(mut self, description: impl Into<String>) -> Self {
1075        self.description = Some(description.into());
1076        self
1077    }
1078    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
1079        self.instructions = Some(instructions.into());
1080        self
1081    }
1082    pub fn model(mut self, model: impl Into<String>) -> Self {
1083        self.chat_options.model = Some(model.into());
1084        self
1085    }
1086    pub fn temperature(mut self, temperature: f32) -> Self {
1087        self.chat_options.temperature = Some(temperature);
1088        self
1089    }
1090    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
1091        self.chat_options.max_tokens = Some(max_tokens);
1092        self
1093    }
1094    /// Request a structured-output response format (e.g. a JSON schema).
1095    pub fn response_format(mut self, format: ResponseFormat) -> Self {
1096        self.chat_options.response_format = Some(format);
1097        self
1098    }
1099    /// Add a tool available to the agent.
1100    pub fn tool(mut self, tool: ToolDefinition) -> Self {
1101        self.chat_options.tools.push(tool);
1102        self
1103    }
1104    /// Add multiple tools.
1105    pub fn tools(mut self, tools: impl IntoIterator<Item = ToolDefinition>) -> Self {
1106        self.chat_options.tools.extend(tools);
1107        self
1108    }
1109    /// Register a dynamic tool source (e.g. an MCP server wrapper), resolved
1110    /// fresh on every run and appended after the agent's static/context/
1111    /// per-run tools (dedup by name against those and any earlier-registered
1112    /// source; first registrant wins). Call repeatedly to register more than
1113    /// one source. See [`ToolSource`], resolved internally by every
1114    /// [`Agent`] run.
1115    pub fn tool_source(mut self, source: Arc<dyn ToolSource>) -> Self {
1116        self.tool_sources.push(source);
1117        self
1118    }
1119    /// Add a single context provider (repeatable; providers run in
1120    /// registration order, agent providers after any thread-level ones).
1121    pub fn context_provider(mut self, provider: Arc<dyn ContextProvider>) -> Self {
1122        self.context_providers.push(provider);
1123        self
1124    }
1125    /// Set the context provider list, replacing any previously registered.
1126    pub fn context_providers(mut self, providers: Vec<Arc<dyn ContextProvider>>) -> Self {
1127        self.context_providers = providers;
1128        self
1129    }
1130    /// Attach conversation-history compaction, via a
1131    /// [`CompactionProvider`] wrapping
1132    /// `strategy` (with the default `ApproxTokenizer`; use
1133    /// [`AgentBuilder::context_provider`] with
1134    /// [`CompactionProvider::with_tokenizer`](crate::compaction::CompactionProvider::with_tokenizer)
1135    /// for a custom tokenizer).
1136    ///
1137    /// Registered as one of the agent's own context providers, which
1138    /// `Agent::combined_providers` always runs *after* the session's —
1139    /// including the auto-attached (or explicitly attached)
1140    /// [`HistoryProvider`](crate::history::HistoryProvider), which lives on
1141    /// the session — so compaction sees, and can shrink, the full
1142    /// history-prepended message list for the run. Not calling this leaves
1143    /// the default behavior unchanged: the full history is sent every run.
1144    pub fn with_compaction(self, strategy: impl CompactionStrategy + 'static) -> Self {
1145        self.context_provider(Arc::new(CompactionProvider::new(strategy)))
1146    }
1147    /// Add an agent middleware.
1148    pub fn middleware(mut self, mw: Arc<crate::middleware::AgentMiddleware>) -> Self {
1149        self.agent_middleware.push(mw);
1150        self
1151    }
1152    /// Add a chat middleware, run around the underlying chat-client call on
1153    /// every request (repeatable, like [`AgentBuilder::middleware`]).
1154    /// See the chat-client call pipeline for exactly what it can observe
1155    /// and mutate.
1156    pub fn chat_middleware(mut self, mw: Arc<crate::middleware::ChatMiddleware>) -> Self {
1157        self.chat_middleware.push(mw);
1158        self
1159    }
1160    /// Add a function-invocation middleware, run around every local tool call
1161    /// (repeatable). Plumbed down into the [`FunctionInvokingChatClient`]
1162    /// this builder wraps the underlying client with.
1163    pub fn function_middleware(mut self, mw: Arc<crate::middleware::FunctionMiddleware>) -> Self {
1164        self.function_middleware.push(mw);
1165        self
1166    }
1167    /// Set the [`ObservabilityConfig`](crate::observability::ObservabilityConfig)
1168    /// for the `execute_tool` spans this agent's tool loop emits — content
1169    /// capture and the GenAI semantic-convention version.
1170    ///
1171    /// The builder wraps the caller's client in a
1172    /// [`FunctionInvokingChatClient`] itself, so this is the only way to reach
1173    /// that wrapper's own config. Pass the same value given to an
1174    /// [`ObservableChatClient`](crate::observability::ObservableChatClient)
1175    /// around the same client, so one trace reports one convention version
1176    /// across its chat and tool spans. Left unset, the tool loop reads the
1177    /// environment, as it always has.
1178    pub fn observability_config(
1179        mut self,
1180        config: crate::observability::ObservabilityConfig,
1181    ) -> Self {
1182        self.observability = Some(config);
1183        self
1184    }
1185
1186    /// Override the whole chat options object (advanced).
1187    pub fn chat_options(mut self, options: ChatOptions) -> Self {
1188        // Preserve tools/instructions collected so far by merging.
1189        self.chat_options = options.merge(self.chat_options);
1190        self
1191    }
1192
1193    /// Build the agent.
1194    pub fn build(mut self) -> Agent {
1195        if let Some(instr) = self.instructions.take() {
1196            self.chat_options.instructions = Some(match self.chat_options.instructions.take() {
1197                Some(existing) => format!("{instr}\n{existing}"),
1198                None => instr,
1199            });
1200        }
1201        if self.chat_options.model.is_none() {
1202            self.chat_options.model = self.client.model().map(str::to_string);
1203        }
1204        // Wrap the raw client in `FunctionInvokingChatClient` now that all
1205        // builder-collected function middleware is known.
1206        let mut invoking = FunctionInvokingChatClient::new(self.client)
1207            .with_function_middleware(self.function_middleware);
1208        if let Some(config) = self.observability {
1209            invoking = invoking.with_observability_config(config);
1210        }
1211        let client: Arc<dyn ChatClient> = Arc::new(invoking);
1212        Agent {
1213            id: self.id.unwrap_or_else(|| Uuid::new_v4().to_string()),
1214            name: self.name,
1215            description: self.description,
1216            client,
1217            chat_options: self.chat_options,
1218            context_providers: self.context_providers,
1219            agent_middleware: MiddlewarePipeline::new(self.agent_middleware),
1220            chat_middleware: MiddlewarePipeline::new(self.chat_middleware),
1221            tool_sources: self.tool_sources,
1222        }
1223    }
1224}
1225
1226impl Agent {
1227    /// The agent description, if any.
1228    pub fn description(&self) -> Option<&str> {
1229        self.description.as_deref()
1230    }
1231
1232    /// Create a new **service-managed** session bound to `service_session_id`,
1233    /// mirroring Python's `get_new_thread(service_thread_id=…)`
1234    /// (`_agents.py:1078-1082`).
1235    ///
1236    /// The agent's own context providers are NOT copied onto the returned
1237    /// session; see the note on [`Agent::create_session`].
1238    pub fn create_session_with_service_id(
1239        &self,
1240        service_session_id: impl Into<String>,
1241    ) -> AgentSession {
1242        AgentSession::service(service_session_id)
1243    }
1244
1245    /// Reconstruct a session from state (as produced by
1246    /// [`AgentSession::to_dict`]), mirroring Python's
1247    /// `BaseAgent.deserialize_thread` (`_agents.py:378-392`).
1248    ///
1249    /// Conversation history is **not** part of this state (see
1250    /// [`AgentSession::to_dict`]); reattach a [`crate::history::HistoryProvider`]
1251    /// (e.g. via [`crate::history::InMemoryHistoryProvider::from_dict`]) to
1252    /// `context_providers` separately when restoring a conversation. The
1253    /// agent's own context providers are NOT copied onto the returned
1254    /// session; see the note on [`Agent::create_session`].
1255    pub fn session_from_dict(&self, state: &Value) -> Result<AgentSession> {
1256        AgentSession::from_dict(state)
1257    }
1258
1259    /// Wrap this agent as a [`ToolDefinition`] usable by another agent's
1260    /// `.tool(...)`. Mirrors Python `BaseAgent.as_tool`.
1261    ///
1262    /// The tool takes a single string argument (default name `"task"`) and,
1263    /// on each call, runs this agent and returns the response text. By
1264    /// default each call runs **statelessly** (a fresh session per call);
1265    /// with [`AsToolOptions::propagate_session`] the parent agent's session
1266    /// is forwarded instead (as an [`AgentSession::child`]). Set
1267    /// [`AsToolOptions::stream_callback`] to observe the sub-agent's
1268    /// streamed updates, and [`AsToolOptions::approval_mode`] to gate calls
1269    /// behind human approval.
1270    ///
1271    /// A run that ends with pending user-input requests (function-approval
1272    /// requests from the sub-agent's own tools) cannot be satisfied from
1273    /// within a tool call and surfaces as a tool error — mirroring
1274    /// upstream's `UserInputRequiredException`.
1275    ///
1276    /// ```no_run
1277    /// # use agent_framework_core::prelude::*;
1278    /// # use agent_framework_core::agent::AsToolOptions;
1279    /// # fn demo(researcher: Agent, coordinator_client: impl ChatClient + 'static) {
1280    /// let research_tool = researcher.as_tool(AsToolOptions::new().name("research"));
1281    /// let coordinator = Agent::builder(coordinator_client)
1282    ///     .tool(research_tool)
1283    ///     .build();
1284    /// # let _ = coordinator;
1285    /// # }
1286    /// ```
1287    pub fn as_tool(&self, options: AsToolOptions) -> ToolDefinition {
1288        // Mirror Python `name or _sanitize_agent_name(self.name)`: an explicit
1289        // name is used verbatim; a derived name is sanitized into a valid tool
1290        // identifier. Falls back to the agent id when no name is available.
1291        let tool_name = options
1292            .name
1293            .or_else(|| sanitize_agent_name(self.name.as_deref()))
1294            .unwrap_or_else(|| self.id.clone());
1295        let description = options
1296            .description
1297            .or_else(|| self.description.clone())
1298            .unwrap_or_default();
1299        let arg_name = options.arg_name.unwrap_or_else(|| "task".to_string());
1300        let arg_description = options
1301            .arg_description
1302            .unwrap_or_else(|| format!("Task for {tool_name}"));
1303        let schema = serde_json::json!({
1304            "type": "object",
1305            "properties": {
1306                arg_name.clone(): { "type": "string", "description": arg_description }
1307            },
1308            "required": [arg_name.clone()],
1309            "additionalProperties": false,
1310        });
1311        ToolDefinition {
1312            name: tool_name.clone(),
1313            description: description.clone(),
1314            parameters: schema.clone(),
1315            kind: crate::tools::ToolKind::Function,
1316            approval_mode: options.approval_mode,
1317            executor: Some(Arc::new(AgentAsTool {
1318                agent: Arc::new(self.clone()),
1319                name: tool_name,
1320                description,
1321                parameters: schema,
1322                arg_key: arg_name,
1323                propagate_session: options.propagate_session,
1324                stream_callback: options.stream_callback,
1325            })),
1326        }
1327    }
1328}
1329
1330/// The [`Tool`] behind [`Agent::as_tool`]: delegates each call to the wrapped
1331/// agent. Reads the parent run's session from the invocation context (via
1332/// [`Tool::invoke_in_context`]) when `propagate_session` is enabled.
1333///
1334/// [`Tool`]: crate::tools::Tool
1335struct AgentAsTool {
1336    agent: Arc<Agent>,
1337    name: String,
1338    description: String,
1339    parameters: Value,
1340    arg_key: String,
1341    propagate_session: bool,
1342    stream_callback: Option<AgentToolStreamCallback>,
1343}
1344
1345impl AgentAsTool {
1346    async fn run_task(
1347        &self,
1348        arguments: Value,
1349        parent_session: Option<&AgentSession>,
1350    ) -> Result<Value> {
1351        let task = arguments
1352            .get(&self.arg_key)
1353            .and_then(Value::as_str)
1354            .unwrap_or_default()
1355            .to_string();
1356
1357        // With `propagate_session`, run the sub-agent on a *child* of the
1358        // parent's session: shared identity + state, isolated server-side
1359        // conversation pointer (see `AgentSession::child`). Without it (or
1360        // when the call arrives without a session — e.g. a direct
1361        // `Tool::invoke`), the sub-agent runs on a fresh session per call.
1362        let mut child = if self.propagate_session {
1363            parent_session.map(AgentSession::child)
1364        } else {
1365            None
1366        };
1367
1368        let response = match &self.stream_callback {
1369            Some(callback) => {
1370                let mut stream = SupportsAgentRun::run_stream(
1371                    self.agent.as_ref(),
1372                    task.into_messages(),
1373                    child.clone(),
1374                    None,
1375                )
1376                .await?;
1377                let mut updates = Vec::new();
1378                while let Some(update) = stream.next().await {
1379                    let update = update?;
1380                    callback(&update);
1381                    updates.push(update);
1382                }
1383                AgentResponse::from_updates(updates)
1384            }
1385            None => {
1386                SupportsAgentRun::run(self.agent.as_ref(), task.into_messages(), child.as_mut())
1387                    .await?
1388            }
1389        };
1390
1391        // Pending user-input (approval) requests cannot be answered from
1392        // within a tool call; surface them as a tool error (upstream raises
1393        // `UserInputRequiredException` here).
1394        if !response.user_input_requests().is_empty() {
1395            return Err(Error::tool(format!(
1396                "agent tool '{}' ended its run with pending user-input requests, \
1397                 which cannot be satisfied from within a tool call",
1398                self.name
1399            )));
1400        }
1401        Ok(Value::String(response.text()))
1402    }
1403}
1404
1405#[async_trait]
1406impl crate::tools::Tool for AgentAsTool {
1407    fn name(&self) -> &str {
1408        &self.name
1409    }
1410    fn description(&self) -> &str {
1411        &self.description
1412    }
1413    fn parameters_schema(&self) -> Value {
1414        self.parameters.clone()
1415    }
1416
1417    async fn invoke(&self, arguments: Value) -> Result<Value> {
1418        self.run_task(arguments, None).await
1419    }
1420
1421    async fn invoke_in_context(
1422        &self,
1423        arguments: Value,
1424        ctx: &crate::middleware::FunctionInvocationContext,
1425    ) -> Result<Value> {
1426        self.run_task(arguments, ctx.session.as_ref()).await
1427    }
1428}
1429
1430#[cfg(test)]
1431mod tests {
1432    use super::*;
1433    use crate::client::ChatStream;
1434    use crate::compaction::Truncation;
1435    use crate::types::{ChatResponse, ChatResponseUpdate};
1436    use futures::stream;
1437    use std::sync::Mutex;
1438
1439    /// A chat client that records the full message list of every request it
1440    /// receives and always replies with the same canned text.
1441    #[derive(Clone, Default)]
1442    struct RecordingClient {
1443        received: Arc<Mutex<Vec<Vec<Message>>>>,
1444    }
1445
1446    impl RecordingClient {
1447        fn requests(&self) -> Vec<Vec<Message>> {
1448            self.received.lock().unwrap().clone()
1449        }
1450    }
1451
1452    #[async_trait]
1453    impl ChatClient for RecordingClient {
1454        async fn get_response(
1455            &self,
1456            messages: Vec<Message>,
1457            _options: ChatOptions,
1458        ) -> Result<ChatResponse> {
1459            self.received.lock().unwrap().push(messages);
1460            Ok(ChatResponse::from_text("ok"))
1461        }
1462
1463        async fn get_streaming_response(
1464            &self,
1465            messages: Vec<Message>,
1466            options: ChatOptions,
1467        ) -> Result<ChatStream> {
1468            let resp = self.get_response(messages, options).await?;
1469            let updates: Vec<Result<ChatResponseUpdate>> = resp
1470                .messages
1471                .into_iter()
1472                .map(|m| {
1473                    Ok(ChatResponseUpdate {
1474                        contents: m.contents,
1475                        role: Some(m.role),
1476                        ..Default::default()
1477                    })
1478                })
1479                .collect();
1480            Ok(Box::pin(stream::iter(updates)))
1481        }
1482    }
1483
1484    #[tokio::test]
1485    async fn without_compaction_sends_the_full_accumulated_history() {
1486        let client = RecordingClient::default();
1487        let agent = Agent::builder(client.clone()).build();
1488        let mut session = agent.create_session();
1489
1490        agent
1491            .run(vec![Message::user("turn 1")], Some(&mut session))
1492            .await
1493            .unwrap();
1494        agent
1495            .run(vec![Message::user("turn 2")], Some(&mut session))
1496            .await
1497            .unwrap();
1498        agent
1499            .run(vec![Message::user("turn 3")], Some(&mut session))
1500            .await
1501            .unwrap();
1502
1503        let requests = client.requests();
1504        assert_eq!(requests.len(), 3);
1505
1506        // Third request: the full history accumulated by turns 1 and 2 (2
1507        // user + 2 assistant messages) plus this turn's own input.
1508        let last = requests.last().unwrap();
1509        assert_eq!(last.len(), 5);
1510        assert_eq!(last[0].text(), "turn 1");
1511        assert_eq!(last[2].text(), "turn 2");
1512        assert_eq!(last.last().unwrap().text(), "turn 3");
1513    }
1514
1515    #[tokio::test]
1516    async fn with_compaction_sends_only_the_compacted_message_set() {
1517        let client = RecordingClient::default();
1518        let agent = Agent::builder(client.clone())
1519            .with_compaction(Truncation::new(2))
1520            .build();
1521        let mut session = agent.create_session();
1522
1523        agent
1524            .run(vec![Message::user("turn 1")], Some(&mut session))
1525            .await
1526            .unwrap();
1527        agent
1528            .run(vec![Message::user("turn 2")], Some(&mut session))
1529            .await
1530            .unwrap();
1531        agent
1532            .run(vec![Message::user("turn 3")], Some(&mut session))
1533            .await
1534            .unwrap();
1535
1536        let requests = client.requests();
1537        assert_eq!(requests.len(), 3);
1538
1539        // Third request: compaction caps the *stored history* (4 messages
1540        // by then) at 2 before this turn's own input is appended, so the
1541        // outgoing request is 3 messages, and the oldest turn is gone.
1542        let last = requests.last().unwrap();
1543        assert_eq!(last.len(), 3);
1544        assert!(last.iter().all(|m| m.text() != "turn 1"));
1545        assert_eq!(last[0].text(), "turn 2");
1546        assert_eq!(last.last().unwrap().text(), "turn 3");
1547    }
1548
1549    #[tokio::test]
1550    async fn with_compaction_runs_after_the_history_provider_in_combined_providers() {
1551        // Direct check on `combined_providers` ordering: the agent-level
1552        // provider attached by `with_compaction` must come after the
1553        // session's auto-attached history provider.
1554        let client = RecordingClient::default();
1555        let agent = Agent::builder(client)
1556            .with_compaction(Truncation::new(2))
1557            .build();
1558        let session = agent.create_session();
1559
1560        let providers = agent.combined_providers(&session);
1561        assert_eq!(providers.len(), 2);
1562        assert!(providers[0].is_history_provider());
1563        assert!(!providers[1].is_history_provider());
1564    }
1565}