lmm_agent/runtime.rs
1// Copyright 2026 Mahmoud Harmouch.
2//
3// Licensed under the MIT license
4// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
5// option. This file may not be copied, modified, or distributed
6// except according to those terms.
7
8//! # `AutoAgent` - the async orchestrator.
9//!
10//! `AutoAgent` owns a collection of agents (as `Arc<Mutex<Box<dyn AgentFunctions>>>`)
11//! and runs them concurrently on a Tokio runtime.
12//!
13//! ## Usage
14//!
15//! ```rust
16//! use lmm_agent::prelude::*;
17//!
18//! #[derive(Debug, Default, Auto)]
19//! pub struct MyAgent {
20//! pub persona: Cow<'static, str>,
21//! pub behavior: Cow<'static, str>,
22//! pub status: Status,
23//! pub agent: LmmAgent,
24//! pub memory: Vec<Message>,
25//! }
26//! #[async_trait]
27//! impl Executor for MyAgent {
28//! async fn execute<'a>(&'a mut self, _: &'a mut Task, _: bool, _: bool, _: u64) -> Result<()> { Ok(()) }
29//! }
30//! #[tokio::main]
31//! async fn main() {
32//! let my_agent = MyAgent { agent: LmmAgent::new("p".into(), "t".into()), ..Default::default() };
33//! let autogpt = AutoAgent::default()
34//! .with(agents![my_agent])
35//! .max_tries(3)
36//! .build()
37//! .unwrap();
38//!
39//! autogpt.run().await.unwrap();
40//! }
41//! ```
42//!
43//! ## Attribution
44//!
45//! Adapted from the `autogpt` project's `prelude.rs` (`AutoGPT` struct):
46//! <https://github.com/wiseaidotdev/autogpt/blob/main/autogpt/src/prelude.rs>
47
48use crate::traits::composite::AgentFunctions;
49use crate::types::{Scope, Task};
50use anyhow::{Result, anyhow};
51use futures::future::join_all;
52use std::borrow::Cow;
53use std::sync::Arc;
54use tokio::{sync::Mutex, task};
55use tracing::{debug, error};
56use uuid::Uuid;
57
58/// Wraps any [`AgentFunctions`] implementor in the type-erased pointer form
59/// expected by [`AutoAgent::with`].
60///
61/// # Example
62///
63/// ```rust
64/// use lmm_agent::prelude::*;
65///
66/// #[derive(Debug, Default, Auto)]
67/// pub struct MyAgent {
68/// pub persona: Cow<'static, str>,
69/// pub behavior: Cow<'static, str>,
70/// pub status: Status,
71/// pub agent: LmmAgent,
72/// pub memory: Vec<Message>,
73/// }
74/// #[async_trait]
75/// impl Executor for MyAgent {
76/// async fn execute<'a>(&'a mut self, _: &'a mut Task, _: bool, _: bool, _: u64) -> Result<()> { Ok(()) }
77/// }
78/// let my_agent_a = MyAgent { agent: LmmAgent::new("p".into(), "t".into()), ..Default::default() };
79/// let my_agent_b = MyAgent { agent: LmmAgent::new("p".into(), "t".into()), ..Default::default() };
80/// let wrapped = agents![my_agent_a, my_agent_b];
81/// ```
82#[macro_export]
83macro_rules! agents {
84 ( $($agent:expr),* $(,)? ) => {
85 vec![
86 $(
87 ::std::sync::Arc::new(
88 ::tokio::sync::Mutex::new(
89 Box::new($agent) as Box<dyn $crate::traits::composite::AgentFunctions>
90 )
91 )
92 ),*
93 ]
94 };
95}
96
97/// Type alias for a thread-safe, heap-allocated, type-erased agent.
98pub type BoxedAgent = Arc<Mutex<Box<dyn AgentFunctions>>>;
99
100/// Orchestrates a pool of agents, running them concurrently on separate Tokio
101/// tasks and collecting results.
102///
103/// # Examples
104///
105/// ```rust
106/// use lmm_agent::prelude::*;
107///
108/// #[derive(Debug, Default, Auto)]
109/// pub struct MyAgent {
110/// pub persona: Cow<'static, str>,
111/// pub behavior: Cow<'static, str>,
112/// pub status: Status,
113/// pub agent: LmmAgent,
114/// pub memory: Vec<Message>,
115/// }
116/// #[async_trait]
117/// impl Executor for MyAgent {
118/// async fn execute<'a>(&'a mut self, _: &'a mut Task, _: bool, _: bool, _: u64) -> Result<()> { Ok(()) }
119/// }
120/// #[tokio::main]
121/// async fn main() {
122/// let agent = MyAgent {
123/// persona: "Researcher".into(),
124/// behavior: "Research Rust.".into(),
125/// agent: LmmAgent::new("Researcher".into(), "Research Rust.".into()),
126/// ..Default::default()
127/// };
128///
129/// AutoAgent::default()
130/// .with(agents![agent])
131/// .build()
132/// .unwrap()
133/// .run()
134/// .await
135/// .unwrap();
136/// }
137/// ```
138pub struct AutoAgent {
139 /// Unique ID for this orchestrator instance.
140 pub id: Uuid,
141
142 /// Pool of agents to run.
143 pub agents: Vec<BoxedAgent>,
144
145 /// Whether agents should execute generated artefacts.
146 pub execute: bool,
147
148 /// Whether agents may open browser tabs.
149 pub browse: bool,
150
151 /// Maximum task-execution attempts per agent.
152 pub max_tries: u64,
153
154 /// CRUD permission scope passed to each agent task.
155 pub crud: bool,
156
157 /// Auth permission scope.
158 pub auth: bool,
159
160 /// External-access permission scope.
161 pub external: bool,
162}
163
164impl Default for AutoAgent {
165 fn default() -> Self {
166 Self {
167 id: Uuid::new_v4(),
168 agents: vec![],
169 execute: true,
170 browse: false,
171 max_tries: 1,
172 crud: true,
173 auth: false,
174 external: true,
175 }
176 }
177}
178
179impl AutoAgent {
180 /// Creates a new [`AutoAgent`] with default settings.
181 pub fn new() -> Self {
182 Self::default()
183 }
184
185 /// Sets the orchestrator's unique identifier.
186 pub fn id(mut self, id: Uuid) -> Self {
187 self.id = id;
188 self
189 }
190
191 /// Provides the agent pool.
192 ///
193 /// Use the [`agents!`] macro to construct the pool.
194 pub fn with<A>(mut self, agents: A) -> Self
195 where
196 A: Into<Vec<BoxedAgent>>,
197 {
198 self.agents = agents.into();
199 self
200 }
201
202 /// Sets whether agents should execute generated artefacts (default: `true`).
203 pub fn execute(mut self, execute: bool) -> Self {
204 self.execute = execute;
205 self
206 }
207
208 /// Sets whether agents may open browser tabs (default: `false`).
209 pub fn browse(mut self, browse: bool) -> Self {
210 self.browse = browse;
211 self
212 }
213
214 /// Sets the maximum retry count per agent (default: `1`).
215 pub fn max_tries(mut self, max_tries: u64) -> Self {
216 self.max_tries = max_tries;
217 self
218 }
219
220 /// Enables/disables CRUD scope for all agents (default: `true`).
221 pub fn crud(mut self, enabled: bool) -> Self {
222 self.crud = enabled;
223 self
224 }
225
226 /// Enables/disables auth scope for all agents (default: `false`).
227 pub fn auth(mut self, enabled: bool) -> Self {
228 self.auth = enabled;
229 self
230 }
231
232 /// Enables/disables external-access scope for all agents (default: `true`).
233 pub fn external(mut self, enabled: bool) -> Self {
234 self.external = enabled;
235 self
236 }
237
238 /// Finalises the builder, returning `Err` when no agents are registered.
239 pub fn build(self) -> Result<Self> {
240 if self.agents.is_empty() {
241 return Err(anyhow!(
242 "No agents registered. Call `.with(agents![...])` first."
243 ));
244 }
245 Ok(self)
246 }
247
248 /// Runs all agents concurrently and waits for every one to finish.
249 ///
250 /// Returns `Ok("All agents executed successfully.")` when every agent
251 /// completes without error, or an aggregated error listing all failures.
252 pub async fn run(&self) -> Result<String> {
253 if self.agents.is_empty() {
254 return Err(anyhow!("No agents to run."));
255 }
256
257 let execute = self.execute;
258 let browse = self.browse;
259 let max_tries = self.max_tries;
260 let crud = self.crud;
261 let auth = self.auth;
262 let external = self.external;
263
264 let mut handles = Vec::with_capacity(self.agents.len());
265
266 for (i, agent_arc) in self.agents.iter().cloned().enumerate() {
267 let agent_clone = Arc::clone(&agent_arc);
268 let agent_persona = agent_arc.lock().await.get_agent().persona.clone();
269
270 let task_payload = Arc::new(Mutex::new(Task {
271 description: Cow::Owned(agent_persona.clone()),
272 scope: Some(Scope {
273 crud,
274 auth,
275 external,
276 }),
277 urls: None,
278 frontend_code: None,
279 backend_code: None,
280 api_schema: None,
281 }));
282
283 let handle = task::spawn(async move {
284 let mut locked_task = task_payload.lock().await;
285 let mut agent = agent_clone.lock().await;
286
287 match agent
288 .execute(&mut locked_task, execute, browse, max_tries)
289 .await
290 {
291 Ok(_) => {
292 debug!("Agent {i} ({agent_persona}) completed successfully.");
293 Ok::<(), anyhow::Error>(())
294 }
295 Err(err) => {
296 error!("Agent {i} ({agent_persona}) failed: {err}");
297 Err(anyhow!("Agent {i} failed: {err}"))
298 }
299 }
300 });
301
302 handles.push(handle);
303 }
304
305 let results = join_all(handles).await;
306
307 let failures: Vec<String> = results
308 .into_iter()
309 .enumerate()
310 .filter_map(|(i, res)| match res {
311 Ok(Err(e)) => Some(format!("Agent {i}: {e}")),
312 Err(join_err) => Some(format!("Agent {i} panicked: {join_err}")),
313 _ => None,
314 })
315 .collect();
316
317 if !failures.is_empty() {
318 return Err(anyhow!("Some agents failed:\n{}", failures.join("\n")));
319 }
320
321 Ok("All agents executed successfully.".to_string())
322 }
323}