Skip to main content

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