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}