Skip to main content

molo/agent/
mod.rs

1//! Agents: reasoning loops.
2//!
3//! An Agent is expressed as a trait (consistent with the rest of the
4//! library: Provider / Tool / Memory); the concrete reasoning loops (ReAct /
5//! Plan & Execute / ...) are provided by each implementation.
6//!
7//! This module provides:
8//! - Interfaces: [`Agent`] reasoning-loop trait, [`CancellableAgent`]
9//!   optional cooperative cancellation, [`AgentEvent`] application-level
10//!   event interface, [`AgentError`] run-failure reasons;
11//! - The classic assembly: [`ReActAgent`] generic ReAct loop and its
12//!   convenience macro [`react_agent!`](crate::react_agent);
13//! - Sub-agent parts: [`SubAgentTool`] sub-agent as a tool, [`SubAgentPool`]
14//!   named sub-agent pool (the main loop delegates sub-loops via tools);
15//! - Structured output: [`TypedAgent`] typed-output interface,
16//!   [`StructuredValidator`] validation component (validation / feedback
17//!   messages / retry budget in one);
18//! - Message chunks and summaries: [`MessageChunk`] / [`RunSummary`];
19//! - Optional behavior configuration: [`AgentConfig`].
20//!
21//! Execution state such as goal / plan / step does not belong to the
22//! [`Agent`] trait; each concrete loop manages it itself.
23//!
24//! # Examples
25//!
26//! Assemble an agent in one shot with [`react_agent!`](crate::react_agent)
27//! and run a round of conversation:
28//!
29//! ```
30//! # #[tokio::main]
31//! # async fn main() -> Result<(), molo::AgentError> {
32//! use molo::{react_agent, Agent, FakeProvider, FakeReply};
33//!
34//! let mut agent = react_agent!(
35//!     FakeProvider::new([FakeReply::Text("Hello".into())]),
36//!     "You are a helpful assistant",
37//! );
38//! let answer = agent.run("hi").await?;
39//! assert_eq!(answer, "Hello");
40//! # Ok(())
41//! # }
42//! ```
43
44mod config;
45mod events;
46mod react;
47mod structured;
48mod sub_agent;
49
50pub use config::AgentConfig;
51pub use events::ReActEvent;
52pub use react::ReActAgent;
53pub use structured::{
54    StructuredOutcome, StructuredValidator, structured_retry_message, validate_structured,
55};
56pub use sub_agent::{PoolError, SubAgentPool, SubAgentTool};
57
58use crate::memory::MemoryError;
59use crate::provider::{ProviderError, Usage};
60use futures::stream::BoxStream;
61use std::fmt;
62use tokio_util::sync::CancellationToken;
63
64/// Reasoning-loop interface: one `run` takes the user input, drives the
65/// reasoning loop, and returns the final answer.
66///
67/// Every reasoning loop (the built-in [`ReActAgent`] and custom
68/// implementations) implements this trait; implementations that want
69/// cooperative cancellation additionally implement [`CancellableAgent`].
70///
71/// The streaming and non-streaming entry points share the same semantics:
72/// the reply is either given whole ([`run`](Agent::run)) or returned as a
73/// [`MessageChunk`] stream ([`run_stream`](Agent::run_stream), ending with
74/// [`MessageChunk::Done`]).
75#[async_trait::async_trait]
76pub trait Agent {
77    /// One run: record the user input, drive the reasoning loop, and return
78    /// the model's final answer as text.
79    ///
80    /// # Errors
81    ///
82    /// Returns [`AgentError::Memory`] when context access fails;
83    /// [`AgentError::Provider`] when communicating with the LLM fails;
84    /// [`AgentError::TooManyToolRounds`] when the model keeps requesting
85    /// tools beyond [`AgentConfig::max_tool_rounds`] without a final answer;
86    /// [`AgentError::Cancelled`] when the run is cooperatively cancelled
87    /// (the passed [`CancellationToken`] is requested).
88    async fn run(&mut self, input: &str) -> Result<String, AgentError>;
89
90    /// Streaming run: same semantics as [`run`](Agent::run), with the reply
91    /// returned as a stream of message chunks (see [`MessageChunk`]), ending
92    /// with [`MessageChunk::Done`]; errors are produced as `Err` items and
93    /// terminate the stream (no Done afterwards).
94    ///
95    /// The default implementation is not truly streaming — the whole answer
96    /// is given as a single [`MessageChunk::Delta`] chunk; implementations
97    /// that need per-character streaming (including tool progress) should
98    /// override this method.
99    /// The default implementation calls [`run`](Agent::run) and only gets
100    /// text, so it can't count rounds / usage, and `Done` carries a
101    /// zero-valued summary; implementations that need a real summary should
102    /// override it.
103    async fn run_stream<'a>(
104        &'a mut self,
105        input: &'a str,
106    ) -> Result<BoxStream<'a, Result<MessageChunk, AgentError>>, AgentError> {
107        let answer = self.run(input).await?;
108        Ok(Box::pin(futures::stream::iter([
109            Ok(MessageChunk::Delta(answer)),
110            Ok(MessageChunk::Done(RunSummary::default())),
111        ])))
112    }
113}
114
115/// Optional capability: cooperative cancellation.
116///
117/// opt-in — implementations that don't need cancellation don't implement
118/// this trait (the methods don't even exist at compile time, so there's no
119/// fake cancellation where "the default implementation ignores the token");
120/// callers that need cancellation (such as interactive apps) call
121/// [`run_cancellable`](CancellableAgent::run_cancellable)
122/// / [`run_stream_cancellable`](CancellableAgent::run_stream_cancellable)
123/// directly on the concrete type.
124///
125/// Each run carries a [`CancellationToken`], the cooperative cancellation
126/// source for this run — any holder can cancel the same token (UI button /
127/// timeout / external signal); implementations should check at safe points
128/// and terminate promptly: `run_cancellable` returns
129/// `Err([`AgentError::Cancelled`])`, while `run_stream_cancellable`
130/// terminates with a [`MessageChunk::Cancelled`] terminal chunk (no `Done`).
131/// Messages already recorded are kept, not rolled back.
132///
133/// # Examples
134///
135/// An already-cancelled token makes the run fail immediately; a fresh token
136/// lets it proceed:
137///
138/// ```
139/// # #[tokio::main]
140/// # async fn main() -> Result<(), molo::AgentError> {
141/// use molo::agent::{CancellableAgent, ReActAgent};
142/// use molo::provider::{FakeProvider, FakeReply};
143/// use molo::tool::ToolRegistry;
144/// use molo::CancellationToken;
145///
146/// let mut agent = ReActAgent::new(
147///     FakeProvider::new([FakeReply::Text("Hello".into())]),
148///     ToolRegistry::new(),
149///     "",
150/// );
151///
152/// let cancelled = CancellationToken::new();
153/// cancelled.cancel();
154/// // Cancelled token: run returns Err(AgentError::Cancelled) immediately
155/// assert!(agent.run_cancellable("hi", &cancelled).await.is_err());
156///
157/// // Fresh token: completes normally
158/// let fresh = CancellationToken::new();
159/// assert_eq!(agent.run_cancellable("hi", &fresh).await?, "Hello");
160/// # Ok(())
161/// # }
162/// ```
163#[async_trait::async_trait]
164pub trait CancellableAgent: Agent {
165    /// Run with a cancellation source. Returns `Err(AgentError::Cancelled)`
166    /// on cancellation.
167    async fn run_cancellable(
168        &mut self,
169        input: &str,
170        token: &CancellationToken,
171    ) -> Result<String, AgentError>;
172
173    /// Streaming run with a cancellation source: same semantics as
174    /// [`run_cancellable`](CancellableAgent::run_cancellable); when
175    /// cancelled, terminates with a [`MessageChunk::Cancelled`] terminal
176    /// chunk (no `Done`).
177    ///
178    /// The default implementation is not truly streaming — the whole answer
179    /// is given as a single [`MessageChunk::Delta`] chunk; implementations
180    /// that need per-character streaming should override this method.
181    async fn run_stream_cancellable<'a>(
182        &'a mut self,
183        input: &'a str,
184        token: &CancellationToken,
185    ) -> Result<BoxStream<'a, Result<MessageChunk, AgentError>>, AgentError> {
186        // Cancellation (before or during the run) always terminates with an
187        // in-stream Cancelled terminal chunk — no Done, no Err — distinct
188        // from run_cancellable's Err(Cancelled) shape.
189        let answer = match self.run_cancellable(input, token).await {
190            Ok(answer) => answer,
191            Err(AgentError::Cancelled) => {
192                return Ok(Box::pin(futures::stream::iter([Ok(
193                    MessageChunk::Cancelled,
194                )])));
195            }
196            Err(e) => return Err(e),
197        };
198        Ok(Box::pin(futures::stream::iter([
199            Ok(MessageChunk::Delta(answer)),
200            Ok(MessageChunk::Done(RunSummary::default())),
201        ])))
202    }
203}
204
205/// Optional capability: typed output (opt-in — implementations that don't
206/// need it don't implement it; the method doesn't even exist at compile
207/// time, the same pattern as [`CancellableAgent`]).
208///
209/// [`run_typed`](TypedAgent::run_typed) has the same semantics as
210/// [`Agent::run`] (records input, drives the reasoning loop), but
211/// deserializes the final answer into the type parameter `U` once it passes
212/// validation — this run auto-generates a JSON Schema from `U`
213/// (`schemars`-derived), feeds validation failures back to the model for
214/// retry, and reports [`AgentError::StructuredRetriesExhausted`] when the
215/// budget is exhausted.
216///
217/// **Why separate from [`Agent`]**: trait generic methods are not
218/// object-safe — putting it in `Agent` would immediately break
219/// `Box<dyn Agent>` (sub-agent delegation, etc.); a separate trait leaves
220/// `Box<dyn Agent>` unaffected, and code with the generic bound
221/// `A: TypedAgent` can call it on any implementation.
222///
223/// **No default implementation**: validation retries happen inside the
224/// reasoning loop (a failure is fed back to the model and the conversation
225/// continues), while `Agent::run` is a one-shot call — a default
226/// implementation couldn't retry within the loop; implementors assemble the
227/// public parts [`StructuredValidator`] (validation / feedback messages /
228/// retry budget in one) or the pure functions [`validate_structured`] /
229/// [`structured_retry_message`] inside their own loops (the built-in
230/// [`ReActAgent`] assembly is exactly this shape).
231#[async_trait::async_trait]
232pub trait TypedAgent: Agent {
233    /// Typed run: the final answer is deserialized into `U` once validation
234    /// passes.
235    ///
236    /// # Errors
237    ///
238    /// - [`AgentError::StructuredRetriesExhausted`][]: validation failures
239    ///   are fed back to the model for retry, with the budget defined by the
240    ///   implementation (see
241    ///   [`AgentConfig::max_structured_retries`](crate::agent::AgentConfig)
242    ///   for the built-in assembly); returned when the budget is exhausted
243    ///   without success;
244    /// - [`AgentError::StructuredParse`][]: validation passed but
245    ///   deserialization failed;
246    /// - otherwise the same as [`Agent::run`](Agent::run).
247    async fn run_typed<U>(&mut self, input: &str) -> Result<U, AgentError>
248    where
249        U: serde::de::DeserializeOwned + schemars::JsonSchema + Send + Sync;
250}
251
252/// Execution summary for one run (business-facing data; carried with the
253/// streaming [`MessageChunk::Done`] chunk).
254///
255/// - `rounds`: number of conversation rounds. One conversation plus the
256///   tool executions that follow counts as one round, matching the
257///   tool-round limit
258///   ([`AgentConfig::max_tool_rounds`](crate::agent::AgentConfig));
259///   error paths (cancellation, exceeding the round limit, etc.) don't
260///   produce a `Done`, so there is no summary;
261/// - `tool_calls`: total number of tool executions;
262/// - `usage`: sum of token usage across rounds; rounds where the streaming
263///   endpoint didn't return usage count as zero.
264///
265/// The summary is for business display ("how many rounds / how many
266/// tokens"); observability metrics like latency and call hierarchy don't
267/// enter the message-chunk stream. The non-streaming [`run`](Agent::run)
268/// returns plain text without a summary — use the streaming entry point
269/// when you need one.
270///
271/// # Examples
272///
273/// ```
274/// use molo::agent::RunSummary;
275///
276/// let summary = RunSummary { rounds: 3, tool_calls: 2, ..Default::default() };
277/// assert_eq!(summary.rounds, 3);
278/// ```
279#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
280pub struct RunSummary {
281    /// Number of conversation rounds.
282    pub rounds: usize,
283    /// Total number of tool executions.
284    pub tool_calls: usize,
285    /// Sum of token usage across rounds.
286    pub usage: Usage,
287}
288
289/// Message chunks for a streaming run — the streaming output of one run,
290/// sliced into pieces.
291///
292/// `Delta` / `ToolCall` / `ToolResult` are the streaming projection of the
293/// message record (text the model is generating, recorded Assistant tool
294/// requests, and returned ToolResult messages); `Done` / `Cancelled` are
295/// terminal markers. These are not "events" — real events are the
296/// application-level event abstraction
297/// [`AgentEvent`](trait, where each Agent implementation defines its own
298/// event variants (the framework doesn't anticipate them), flowing through
299/// an event pipeline.
300///
301/// # Reasoning
302///
303/// Reasoning produces no chunks: reasoning deltas from thinking models do
304/// not appear in this enum — matching on `MessageChunk::Reasoning` won't
305/// compile, and that's intentional. To surface reasoning, attach an
306/// [`EventChannel`](crate::event_channel::EventChannel) and subscribe to
307/// [`ReActEvent::Reasoning`], or consume
308/// [`StreamEvent::Reasoning`](crate::provider::StreamEvent::Reasoning) at
309/// the Provider layer.
310///
311/// The enum carries `#[non_exhaustive]` (reserved for extension): matches
312/// must include a wildcard arm.
313#[derive(Debug, Clone, PartialEq, Eq)]
314#[non_exhaustive]
315pub enum MessageChunk {
316    /// An increment of the reply text; increments within the same round are
317    /// concatenated in order.
318    Delta(String),
319    /// The model requested a tool call (the tool_calls of a recorded
320    /// Assistant message).
321    ToolCall {
322        /// The id of this call, matching [`ToolCall::id`](crate::ToolCall::id)
323        /// in the recorded memory; used to pair multiple calls of the same
324        /// tool within a round.
325        id: String,
326        /// The tool name.
327        name: String,
328        /// The arguments generated by the model (JSON text).
329        arguments: String,
330    },
331    /// A tool execution completed (records a ToolResult message; on failure
332    /// the content is the error text).
333    ToolResult {
334        /// The id of this execution, matching
335        /// [`Message::ToolResult`](crate::Message::ToolResult) in the
336        /// recorded memory; paired with the
337        /// [`ToolCall`](MessageChunk::ToolCall) id in the same round.
338        id: String,
339        /// The tool name.
340        name: String,
341        /// The execution result text (the error text on failure).
342        content: String,
343    },
344    /// The run ended normally; carries the execution summary for this run
345    /// ([`RunSummary`]); the stream produces no further chunks afterwards.
346    Done(RunSummary),
347    /// The run was cooperatively cancelled (via the CancellationToken passed
348    /// to run/run_stream); terminal chunk, the stream produces no further
349    /// chunks afterwards (no Done).
350    Cancelled,
351}
352
353/// Application-level event abstraction.
354///
355/// Each Agent implementation defines its own event types (tool lifecycle /
356/// plan steps / retrieval / sub-agents, etc.); the framework doesn't
357/// anticipate variants. Events are pushed through
358/// [`EventChannel`](crate::event_channel::EventChannel) for external
359/// subscription. Consumers downcast known types precisely via `as_any` (see
360/// [`impl dyn AgentEvent`](AgentEvent) below) and fall back to
361/// [`name`](AgentEvent::name) for unknown types.
362///
363/// Event payloads are uniformly `Arc<dyn AgentEvent>`: `Arc` covers the
364/// clone requirement of broadcast channels, the trait itself needs no
365/// `Clone`, and event types are zero-boilerplate.
366pub trait AgentEvent: std::any::Any + Send + Sync + fmt::Debug {
367    /// Event name: lets subscribers at least display a name for unknown
368    /// types. Default = the type's full path; override for a short name
369    /// (e.g. `"tool.started"`).
370    fn name(&self) -> &'static str {
371        std::any::type_name::<Self>()
372    }
373}
374
375impl dyn AgentEvent {
376    /// Typed access: `event.as_any().downcast_ref::<ToolStarted>()`.
377    ///
378    /// Declared as an inherent method rather than a trait default method:
379    /// `&dyn AgentEvent` → `&dyn Any` is a trait-object upcast (Rust 1.86+,
380    /// with `Any` as a supertrait), which can't be expressed directly in a
381    /// trait default method.
382    pub fn as_any(&self) -> &dyn std::any::Any {
383        self as &dyn std::any::Any
384    }
385}
386
387/// Reasons an Agent run can fail.
388///
389/// A tool execution failure is not an `AgentError` — it is fed back to the
390/// model as text, and the model decides what to do next. `#[non_exhaustive]`
391/// ensures future error categories won't be a breaking change.
392///
393/// # Examples
394///
395/// ```
396/// use molo::AgentError;
397///
398/// // The round-limit error carries the limit value, useful for prompting
399/// // the user to adjust the config
400/// let err = AgentError::TooManyToolRounds(10);
401/// assert!(err.to_string().contains("10"));
402/// ```
403#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
404#[non_exhaustive]
405pub enum AgentError {
406    /// Context access failed.
407    #[error("memory error: {0}")]
408    Memory(#[from] MemoryError),
409    /// Communication with the LLM failed.
410    #[error("provider error: {0}")]
411    Provider(#[from] ProviderError),
412    /// The model kept requesting tools past the implementation's maximum
413    /// number of rounds without giving a final answer. Increase via
414    /// [`AgentConfig::max_tool_rounds`](crate::agent::AgentConfig) (chained
415    /// `with_config(AgentConfig { max_tool_rounds: N, ..Default::default() })`).
416    #[error(
417        "model requested tools for more than {0} rounds; increase AgentConfig::max_tool_rounds (via with_config) if intended"
418    )]
419    TooManyToolRounds(usize),
420    /// The run was cooperatively cancelled (via the CancellationToken passed
421    /// to run/run_stream); already-recorded messages are kept, not rolled
422    /// back.
423    #[error("run cancelled")]
424    Cancelled,
425    /// Structured output: validation passed but deserializing into the
426    /// target type failed — triggered when the JSON the schema allows is
427    /// inconsistent with the serde representation of `run_typed`'s type
428    /// parameter `U` (auto-generated schemas agree with `U` by default;
429    /// conflicts come from `#[schemars(...)]` custom derives).
430    #[error("structured output failed to deserialize: {0}")]
431    StructuredParse(String),
432    /// Structured output: validation failed more times than the configured
433    /// limit. Increase via
434    /// [`AgentConfig::max_structured_retries`](crate::agent::AgentConfig)
435    /// (chained
436    /// `with_config(AgentConfig { max_structured_retries: N, ..Default::default() })`).
437    #[error(
438        "structured output failed validation for more than {0} attempts; increase AgentConfig::max_structured_retries (via with_config) if intended"
439    )]
440    StructuredRetriesExhausted(usize),
441}