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    tool_sources: Vec<Arc<dyn ToolSource>>,
1043}
1044
1045impl AgentBuilder {
1046    fn new(client: impl ChatClient + 'static) -> Self {
1047        Self {
1048            id: None,
1049            name: None,
1050            description: None,
1051            instructions: None,
1052            client: Arc::new(client),
1053            chat_options: ChatOptions::new(),
1054            context_providers: Vec::new(),
1055            agent_middleware: Vec::new(),
1056            chat_middleware: Vec::new(),
1057            function_middleware: Vec::new(),
1058            tool_sources: Vec::new(),
1059        }
1060    }
1061
1062    pub fn id(mut self, id: impl Into<String>) -> Self {
1063        self.id = Some(id.into());
1064        self
1065    }
1066    pub fn name(mut self, name: impl Into<String>) -> Self {
1067        self.name = Some(name.into());
1068        self
1069    }
1070    pub fn description(mut self, description: impl Into<String>) -> Self {
1071        self.description = Some(description.into());
1072        self
1073    }
1074    pub fn instructions(mut self, instructions: impl Into<String>) -> Self {
1075        self.instructions = Some(instructions.into());
1076        self
1077    }
1078    pub fn model(mut self, model: impl Into<String>) -> Self {
1079        self.chat_options.model = Some(model.into());
1080        self
1081    }
1082    pub fn temperature(mut self, temperature: f32) -> Self {
1083        self.chat_options.temperature = Some(temperature);
1084        self
1085    }
1086    pub fn max_tokens(mut self, max_tokens: u32) -> Self {
1087        self.chat_options.max_tokens = Some(max_tokens);
1088        self
1089    }
1090    /// Request a structured-output response format (e.g. a JSON schema).
1091    pub fn response_format(mut self, format: ResponseFormat) -> Self {
1092        self.chat_options.response_format = Some(format);
1093        self
1094    }
1095    /// Add a tool available to the agent.
1096    pub fn tool(mut self, tool: ToolDefinition) -> Self {
1097        self.chat_options.tools.push(tool);
1098        self
1099    }
1100    /// Add multiple tools.
1101    pub fn tools(mut self, tools: impl IntoIterator<Item = ToolDefinition>) -> Self {
1102        self.chat_options.tools.extend(tools);
1103        self
1104    }
1105    /// Register a dynamic tool source (e.g. an MCP server wrapper), resolved
1106    /// fresh on every run and appended after the agent's static/context/
1107    /// per-run tools (dedup by name against those and any earlier-registered
1108    /// source; first registrant wins). Call repeatedly to register more than
1109    /// one source. See [`ToolSource`], resolved internally by every
1110    /// [`Agent`] run.
1111    pub fn tool_source(mut self, source: Arc<dyn ToolSource>) -> Self {
1112        self.tool_sources.push(source);
1113        self
1114    }
1115    /// Add a single context provider (repeatable; providers run in
1116    /// registration order, agent providers after any thread-level ones).
1117    pub fn context_provider(mut self, provider: Arc<dyn ContextProvider>) -> Self {
1118        self.context_providers.push(provider);
1119        self
1120    }
1121    /// Set the context provider list, replacing any previously registered.
1122    pub fn context_providers(mut self, providers: Vec<Arc<dyn ContextProvider>>) -> Self {
1123        self.context_providers = providers;
1124        self
1125    }
1126    /// Attach conversation-history compaction, via a
1127    /// [`CompactionProvider`] wrapping
1128    /// `strategy` (with the default `ApproxTokenizer`; use
1129    /// [`AgentBuilder::context_provider`] with
1130    /// [`CompactionProvider::with_tokenizer`](crate::compaction::CompactionProvider::with_tokenizer)
1131    /// for a custom tokenizer).
1132    ///
1133    /// Registered as one of the agent's own context providers, which
1134    /// `Agent::combined_providers` always runs *after* the session's —
1135    /// including the auto-attached (or explicitly attached)
1136    /// [`HistoryProvider`](crate::history::HistoryProvider), which lives on
1137    /// the session — so compaction sees, and can shrink, the full
1138    /// history-prepended message list for the run. Not calling this leaves
1139    /// the default behavior unchanged: the full history is sent every run.
1140    pub fn with_compaction(self, strategy: impl CompactionStrategy + 'static) -> Self {
1141        self.context_provider(Arc::new(CompactionProvider::new(strategy)))
1142    }
1143    /// Add an agent middleware.
1144    pub fn middleware(mut self, mw: Arc<crate::middleware::AgentMiddleware>) -> Self {
1145        self.agent_middleware.push(mw);
1146        self
1147    }
1148    /// Add a chat middleware, run around the underlying chat-client call on
1149    /// every request (repeatable, like [`AgentBuilder::middleware`]).
1150    /// See the chat-client call pipeline for exactly what it can observe
1151    /// and mutate.
1152    pub fn chat_middleware(mut self, mw: Arc<crate::middleware::ChatMiddleware>) -> Self {
1153        self.chat_middleware.push(mw);
1154        self
1155    }
1156    /// Add a function-invocation middleware, run around every local tool call
1157    /// (repeatable). Plumbed down into the [`FunctionInvokingChatClient`]
1158    /// this builder wraps the underlying client with.
1159    pub fn function_middleware(mut self, mw: Arc<crate::middleware::FunctionMiddleware>) -> Self {
1160        self.function_middleware.push(mw);
1161        self
1162    }
1163    /// Override the whole chat options object (advanced).
1164    pub fn chat_options(mut self, options: ChatOptions) -> Self {
1165        // Preserve tools/instructions collected so far by merging.
1166        self.chat_options = options.merge(self.chat_options);
1167        self
1168    }
1169
1170    /// Build the agent.
1171    pub fn build(mut self) -> Agent {
1172        if let Some(instr) = self.instructions.take() {
1173            self.chat_options.instructions = Some(match self.chat_options.instructions.take() {
1174                Some(existing) => format!("{instr}\n{existing}"),
1175                None => instr,
1176            });
1177        }
1178        if self.chat_options.model.is_none() {
1179            self.chat_options.model = self.client.model().map(str::to_string);
1180        }
1181        // Wrap the raw client in `FunctionInvokingChatClient` now that all
1182        // builder-collected function middleware is known.
1183        let client: Arc<dyn ChatClient> = Arc::new(
1184            FunctionInvokingChatClient::new(self.client)
1185                .with_function_middleware(self.function_middleware),
1186        );
1187        Agent {
1188            id: self.id.unwrap_or_else(|| Uuid::new_v4().to_string()),
1189            name: self.name,
1190            description: self.description,
1191            client,
1192            chat_options: self.chat_options,
1193            context_providers: self.context_providers,
1194            agent_middleware: MiddlewarePipeline::new(self.agent_middleware),
1195            chat_middleware: MiddlewarePipeline::new(self.chat_middleware),
1196            tool_sources: self.tool_sources,
1197        }
1198    }
1199}
1200
1201impl Agent {
1202    /// The agent description, if any.
1203    pub fn description(&self) -> Option<&str> {
1204        self.description.as_deref()
1205    }
1206
1207    /// Create a new **service-managed** session bound to `service_session_id`,
1208    /// mirroring Python's `get_new_thread(service_thread_id=…)`
1209    /// (`_agents.py:1078-1082`).
1210    ///
1211    /// The agent's own context providers are NOT copied onto the returned
1212    /// session; see the note on [`Agent::create_session`].
1213    pub fn create_session_with_service_id(
1214        &self,
1215        service_session_id: impl Into<String>,
1216    ) -> AgentSession {
1217        AgentSession::service(service_session_id)
1218    }
1219
1220    /// Reconstruct a session from state (as produced by
1221    /// [`AgentSession::to_dict`]), mirroring Python's
1222    /// `BaseAgent.deserialize_thread` (`_agents.py:378-392`).
1223    ///
1224    /// Conversation history is **not** part of this state (see
1225    /// [`AgentSession::to_dict`]); reattach a [`crate::history::HistoryProvider`]
1226    /// (e.g. via [`crate::history::InMemoryHistoryProvider::from_dict`]) to
1227    /// `context_providers` separately when restoring a conversation. The
1228    /// agent's own context providers are NOT copied onto the returned
1229    /// session; see the note on [`Agent::create_session`].
1230    pub fn session_from_dict(&self, state: &Value) -> Result<AgentSession> {
1231        AgentSession::from_dict(state)
1232    }
1233
1234    /// Wrap this agent as a [`ToolDefinition`] usable by another agent's
1235    /// `.tool(...)`. Mirrors Python `BaseAgent.as_tool`.
1236    ///
1237    /// The tool takes a single string argument (default name `"task"`) and,
1238    /// on each call, runs this agent and returns the response text. By
1239    /// default each call runs **statelessly** (a fresh session per call);
1240    /// with [`AsToolOptions::propagate_session`] the parent agent's session
1241    /// is forwarded instead (as an [`AgentSession::child`]). Set
1242    /// [`AsToolOptions::stream_callback`] to observe the sub-agent's
1243    /// streamed updates, and [`AsToolOptions::approval_mode`] to gate calls
1244    /// behind human approval.
1245    ///
1246    /// A run that ends with pending user-input requests (function-approval
1247    /// requests from the sub-agent's own tools) cannot be satisfied from
1248    /// within a tool call and surfaces as a tool error — mirroring
1249    /// upstream's `UserInputRequiredException`.
1250    ///
1251    /// ```no_run
1252    /// # use agent_framework_core::prelude::*;
1253    /// # use agent_framework_core::agent::AsToolOptions;
1254    /// # fn demo(researcher: Agent, coordinator_client: impl ChatClient + 'static) {
1255    /// let research_tool = researcher.as_tool(AsToolOptions::new().name("research"));
1256    /// let coordinator = Agent::builder(coordinator_client)
1257    ///     .tool(research_tool)
1258    ///     .build();
1259    /// # let _ = coordinator;
1260    /// # }
1261    /// ```
1262    pub fn as_tool(&self, options: AsToolOptions) -> ToolDefinition {
1263        // Mirror Python `name or _sanitize_agent_name(self.name)`: an explicit
1264        // name is used verbatim; a derived name is sanitized into a valid tool
1265        // identifier. Falls back to the agent id when no name is available.
1266        let tool_name = options
1267            .name
1268            .or_else(|| sanitize_agent_name(self.name.as_deref()))
1269            .unwrap_or_else(|| self.id.clone());
1270        let description = options
1271            .description
1272            .or_else(|| self.description.clone())
1273            .unwrap_or_default();
1274        let arg_name = options.arg_name.unwrap_or_else(|| "task".to_string());
1275        let arg_description = options
1276            .arg_description
1277            .unwrap_or_else(|| format!("Task for {tool_name}"));
1278        let schema = serde_json::json!({
1279            "type": "object",
1280            "properties": {
1281                arg_name.clone(): { "type": "string", "description": arg_description }
1282            },
1283            "required": [arg_name.clone()],
1284            "additionalProperties": false,
1285        });
1286        ToolDefinition {
1287            name: tool_name.clone(),
1288            description: description.clone(),
1289            parameters: schema.clone(),
1290            kind: crate::tools::ToolKind::Function,
1291            approval_mode: options.approval_mode,
1292            executor: Some(Arc::new(AgentAsTool {
1293                agent: Arc::new(self.clone()),
1294                name: tool_name,
1295                description,
1296                parameters: schema,
1297                arg_key: arg_name,
1298                propagate_session: options.propagate_session,
1299                stream_callback: options.stream_callback,
1300            })),
1301        }
1302    }
1303}
1304
1305/// The [`Tool`] behind [`Agent::as_tool`]: delegates each call to the wrapped
1306/// agent. Reads the parent run's session from the invocation context (via
1307/// [`Tool::invoke_in_context`]) when `propagate_session` is enabled.
1308///
1309/// [`Tool`]: crate::tools::Tool
1310struct AgentAsTool {
1311    agent: Arc<Agent>,
1312    name: String,
1313    description: String,
1314    parameters: Value,
1315    arg_key: String,
1316    propagate_session: bool,
1317    stream_callback: Option<AgentToolStreamCallback>,
1318}
1319
1320impl AgentAsTool {
1321    async fn run_task(
1322        &self,
1323        arguments: Value,
1324        parent_session: Option<&AgentSession>,
1325    ) -> Result<Value> {
1326        let task = arguments
1327            .get(&self.arg_key)
1328            .and_then(Value::as_str)
1329            .unwrap_or_default()
1330            .to_string();
1331
1332        // With `propagate_session`, run the sub-agent on a *child* of the
1333        // parent's session: shared identity + state, isolated server-side
1334        // conversation pointer (see `AgentSession::child`). Without it (or
1335        // when the call arrives without a session — e.g. a direct
1336        // `Tool::invoke`), the sub-agent runs on a fresh session per call.
1337        let mut child = if self.propagate_session {
1338            parent_session.map(AgentSession::child)
1339        } else {
1340            None
1341        };
1342
1343        let response = match &self.stream_callback {
1344            Some(callback) => {
1345                let mut stream = SupportsAgentRun::run_stream(
1346                    self.agent.as_ref(),
1347                    task.into_messages(),
1348                    child.clone(),
1349                    None,
1350                )
1351                .await?;
1352                let mut updates = Vec::new();
1353                while let Some(update) = stream.next().await {
1354                    let update = update?;
1355                    callback(&update);
1356                    updates.push(update);
1357                }
1358                AgentResponse::from_updates(updates)
1359            }
1360            None => {
1361                SupportsAgentRun::run(self.agent.as_ref(), task.into_messages(), child.as_mut())
1362                    .await?
1363            }
1364        };
1365
1366        // Pending user-input (approval) requests cannot be answered from
1367        // within a tool call; surface them as a tool error (upstream raises
1368        // `UserInputRequiredException` here).
1369        if !response.user_input_requests().is_empty() {
1370            return Err(Error::tool(format!(
1371                "agent tool '{}' ended its run with pending user-input requests, \
1372                 which cannot be satisfied from within a tool call",
1373                self.name
1374            )));
1375        }
1376        Ok(Value::String(response.text()))
1377    }
1378}
1379
1380#[async_trait]
1381impl crate::tools::Tool for AgentAsTool {
1382    fn name(&self) -> &str {
1383        &self.name
1384    }
1385    fn description(&self) -> &str {
1386        &self.description
1387    }
1388    fn parameters_schema(&self) -> Value {
1389        self.parameters.clone()
1390    }
1391
1392    async fn invoke(&self, arguments: Value) -> Result<Value> {
1393        self.run_task(arguments, None).await
1394    }
1395
1396    async fn invoke_in_context(
1397        &self,
1398        arguments: Value,
1399        ctx: &crate::middleware::FunctionInvocationContext,
1400    ) -> Result<Value> {
1401        self.run_task(arguments, ctx.session.as_ref()).await
1402    }
1403}
1404
1405#[cfg(test)]
1406mod tests {
1407    use super::*;
1408    use crate::client::ChatStream;
1409    use crate::compaction::Truncation;
1410    use crate::types::{ChatResponse, ChatResponseUpdate};
1411    use futures::stream;
1412    use std::sync::Mutex;
1413
1414    /// A chat client that records the full message list of every request it
1415    /// receives and always replies with the same canned text.
1416    #[derive(Clone, Default)]
1417    struct RecordingClient {
1418        received: Arc<Mutex<Vec<Vec<Message>>>>,
1419    }
1420
1421    impl RecordingClient {
1422        fn requests(&self) -> Vec<Vec<Message>> {
1423            self.received.lock().unwrap().clone()
1424        }
1425    }
1426
1427    #[async_trait]
1428    impl ChatClient for RecordingClient {
1429        async fn get_response(
1430            &self,
1431            messages: Vec<Message>,
1432            _options: ChatOptions,
1433        ) -> Result<ChatResponse> {
1434            self.received.lock().unwrap().push(messages);
1435            Ok(ChatResponse::from_text("ok"))
1436        }
1437
1438        async fn get_streaming_response(
1439            &self,
1440            messages: Vec<Message>,
1441            options: ChatOptions,
1442        ) -> Result<ChatStream> {
1443            let resp = self.get_response(messages, options).await?;
1444            let updates: Vec<Result<ChatResponseUpdate>> = resp
1445                .messages
1446                .into_iter()
1447                .map(|m| {
1448                    Ok(ChatResponseUpdate {
1449                        contents: m.contents,
1450                        role: Some(m.role),
1451                        ..Default::default()
1452                    })
1453                })
1454                .collect();
1455            Ok(Box::pin(stream::iter(updates)))
1456        }
1457    }
1458
1459    #[tokio::test]
1460    async fn without_compaction_sends_the_full_accumulated_history() {
1461        let client = RecordingClient::default();
1462        let agent = Agent::builder(client.clone()).build();
1463        let mut session = agent.create_session();
1464
1465        agent
1466            .run(vec![Message::user("turn 1")], Some(&mut session))
1467            .await
1468            .unwrap();
1469        agent
1470            .run(vec![Message::user("turn 2")], Some(&mut session))
1471            .await
1472            .unwrap();
1473        agent
1474            .run(vec![Message::user("turn 3")], Some(&mut session))
1475            .await
1476            .unwrap();
1477
1478        let requests = client.requests();
1479        assert_eq!(requests.len(), 3);
1480
1481        // Third request: the full history accumulated by turns 1 and 2 (2
1482        // user + 2 assistant messages) plus this turn's own input.
1483        let last = requests.last().unwrap();
1484        assert_eq!(last.len(), 5);
1485        assert_eq!(last[0].text(), "turn 1");
1486        assert_eq!(last[2].text(), "turn 2");
1487        assert_eq!(last.last().unwrap().text(), "turn 3");
1488    }
1489
1490    #[tokio::test]
1491    async fn with_compaction_sends_only_the_compacted_message_set() {
1492        let client = RecordingClient::default();
1493        let agent = Agent::builder(client.clone())
1494            .with_compaction(Truncation::new(2))
1495            .build();
1496        let mut session = agent.create_session();
1497
1498        agent
1499            .run(vec![Message::user("turn 1")], Some(&mut session))
1500            .await
1501            .unwrap();
1502        agent
1503            .run(vec![Message::user("turn 2")], Some(&mut session))
1504            .await
1505            .unwrap();
1506        agent
1507            .run(vec![Message::user("turn 3")], Some(&mut session))
1508            .await
1509            .unwrap();
1510
1511        let requests = client.requests();
1512        assert_eq!(requests.len(), 3);
1513
1514        // Third request: compaction caps the *stored history* (4 messages
1515        // by then) at 2 before this turn's own input is appended, so the
1516        // outgoing request is 3 messages, and the oldest turn is gone.
1517        let last = requests.last().unwrap();
1518        assert_eq!(last.len(), 3);
1519        assert!(last.iter().all(|m| m.text() != "turn 1"));
1520        assert_eq!(last[0].text(), "turn 2");
1521        assert_eq!(last.last().unwrap().text(), "turn 3");
1522    }
1523
1524    #[tokio::test]
1525    async fn with_compaction_runs_after_the_history_provider_in_combined_providers() {
1526        // Direct check on `combined_providers` ordering: the agent-level
1527        // provider attached by `with_compaction` must come after the
1528        // session's auto-attached history provider.
1529        let client = RecordingClient::default();
1530        let agent = Agent::builder(client)
1531            .with_compaction(Truncation::new(2))
1532            .build();
1533        let session = agent.create_session();
1534
1535        let providers = agent.combined_providers(&session);
1536        assert_eq!(providers.len(), 2);
1537        assert!(providers[0].is_history_provider());
1538        assert!(!providers[1].is_history_provider());
1539    }
1540}