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}