Skip to main content

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}