Skip to main content

batuta/agent/
mod.rs

1//! Autonomous Agent Runtime (perceive-reason-act loop).
2//!
3//! Implements a sovereign agent that uses local LLM inference
4//! (realizar), RAG retrieval (trueno-rag), and persistent memory
5//! (trueno-db) — all running locally with zero API dependencies.
6//!
7//! # Architecture
8//!
9//! ```text
10//! AgentManifest (TOML)
11//!   → PERCEIVE: recall memories
12//!   → REASON:   LlmDriver.complete()
13//!   → ACT:      Tool.execute()
14//!   → repeat until Done or guard triggers
15//! ```
16//!
17//! # Toyota Production System Principles
18//!
19//! - **Jidoka**: `LoopGuard` stops on ping-pong, budget, max iterations
20//! - **Poka-Yoke**: Capability system prevents unauthorized tool access
21//! - **Muda**: `CostCircuitBreaker` prevents runaway spend
22//! - **Genchi Genbutsu**: Default sovereign — local hardware, no proxies
23//!
24//! # References
25//!
26//! - arXiv:2512.10350 — Geometric dynamics of agentic loops
27//! - arXiv:2501.09136 — Agentic RAG survey
28//! - arXiv:2406.09187 — `GuardAgent` safety
29
30pub mod capability;
31pub mod code;
32mod code_prompts;
33pub mod contracts;
34pub mod custom_agents;
35pub mod driver;
36pub mod guard;
37pub mod hooks;
38pub mod manifest;
39pub mod memory;
40pub mod org_policy;
41pub mod permission;
42pub mod phase;
43pub mod pool;
44pub mod repl;
45mod repl_display;
46pub mod result;
47pub mod runtime;
48mod runtime_helpers;
49pub mod session;
50pub mod signing;
51pub mod skill;
52pub mod status_line;
53pub mod task_tool;
54pub mod tool;
55pub mod tui;
56pub mod worktree;
57
58// Re-export key types for convenience.
59pub use capability::{capability_matches, Capability};
60pub use guard::{LoopGuard, LoopVerdict};
61pub use manifest::{AgentManifest, AutoPullError, ModelConfig, ResourceQuota};
62pub use memory::InMemorySubstrate;
63pub use phase::LoopPhase;
64pub use pool::{AgentId, AgentMessage, AgentPool, MessageRouter, SpawnConfig, ToolBuilder};
65pub use result::{AgentError, AgentLoopResult, DriverError, StopReason, TokenUsage};
66
67use driver::{LlmDriver, StreamEvent};
68use memory::MemorySubstrate;
69use tokio::sync::mpsc;
70use tool::ToolRegistry;
71
72/// Ergonomic builder for constructing and running agent loops.
73///
74/// ```rust,ignore
75/// let result = AgentBuilder::new(manifest)
76///     .driver(&my_driver)
77///     .tool(Box::new(rag_tool))
78///     .memory(&substrate)
79///     .run("What is SIMD?")
80///     .await?;
81/// ```
82pub struct AgentBuilder<'a> {
83    manifest: &'a AgentManifest,
84    driver: Option<&'a dyn LlmDriver>,
85    tools: ToolRegistry,
86    memory: Option<&'a dyn MemorySubstrate>,
87    stream_tx: Option<mpsc::Sender<StreamEvent>>,
88}
89
90impl<'a> AgentBuilder<'a> {
91    /// Create a new builder from an agent manifest.
92    pub fn new(manifest: &'a AgentManifest) -> Self {
93        Self { manifest, driver: None, tools: ToolRegistry::new(), memory: None, stream_tx: None }
94    }
95
96    /// Set the LLM driver for inference.
97    #[must_use]
98    pub fn driver(mut self, driver: &'a dyn LlmDriver) -> Self {
99        self.driver = Some(driver);
100        self
101    }
102
103    /// Register a tool in the tool registry.
104    #[must_use]
105    pub fn tool(mut self, tool: Box<dyn tool::Tool>) -> Self {
106        self.tools.register(tool);
107        self
108    }
109
110    /// Set the memory substrate.
111    #[must_use]
112    pub fn memory(mut self, memory: &'a dyn MemorySubstrate) -> Self {
113        self.memory = Some(memory);
114        self
115    }
116
117    /// Set the stream event channel for real-time events.
118    #[must_use]
119    pub fn stream(mut self, tx: mpsc::Sender<StreamEvent>) -> Self {
120        self.stream_tx = Some(tx);
121        self
122    }
123
124    /// Run the agent loop with the given query.
125    ///
126    /// Uses `InMemorySubstrate` if no memory was provided.
127    pub async fn run(self, query: &str) -> Result<AgentLoopResult, AgentError> {
128        let driver = self
129            .driver
130            .ok_or_else(|| AgentError::ManifestError("no LLM driver configured".into()))?;
131
132        let default_memory = InMemorySubstrate::new();
133        let memory = self.memory.unwrap_or(&default_memory);
134
135        runtime::run_agent_loop(self.manifest, query, driver, &self.tools, memory, self.stream_tx)
136            .await
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use driver::mock::MockDriver;
144
145    #[tokio::test]
146    async fn test_builder_minimal() {
147        let manifest = AgentManifest::default();
148        let driver = MockDriver::single_response("built!");
149
150        let result = AgentBuilder::new(&manifest)
151            .driver(&driver)
152            .run("hello")
153            .await
154            .expect("builder run failed");
155
156        assert_eq!(result.text, "built!");
157    }
158
159    #[tokio::test]
160    async fn test_builder_no_driver_errors() {
161        let manifest = AgentManifest::default();
162
163        let err = AgentBuilder::new(&manifest).run("hello").await.unwrap_err();
164
165        assert!(matches!(err, AgentError::ManifestError(_)), "expected ManifestError, got: {err}");
166    }
167
168    #[tokio::test]
169    async fn test_builder_with_memory() {
170        let manifest = AgentManifest::default();
171        let driver = MockDriver::single_response("remembered");
172        let memory = InMemorySubstrate::new();
173
174        let result = AgentBuilder::new(&manifest)
175            .driver(&driver)
176            .memory(&memory)
177            .run("test")
178            .await
179            .expect("builder run failed");
180
181        assert_eq!(result.text, "remembered");
182    }
183
184    #[tokio::test]
185    async fn test_builder_with_stream() {
186        let manifest = AgentManifest::default();
187        let driver = MockDriver::single_response("streamed");
188        let (tx, mut rx) = mpsc::channel(32);
189
190        let result = AgentBuilder::new(&manifest)
191            .driver(&driver)
192            .stream(tx)
193            .run("test")
194            .await
195            .expect("builder run failed");
196
197        assert_eq!(result.text, "streamed");
198
199        let mut got_events = false;
200        while let Ok(_event) = rx.try_recv() {
201            got_events = true;
202        }
203        assert!(got_events, "expected stream events");
204    }
205
206    #[tokio::test]
207    async fn test_builder_with_tool() {
208        use crate::agent::driver::ToolDefinition;
209        use crate::agent::tool::ToolResult as TResult;
210
211        struct DummyTool;
212
213        #[async_trait::async_trait]
214        impl tool::Tool for DummyTool {
215            fn name(&self) -> &'static str {
216                "dummy"
217            }
218            fn definition(&self) -> ToolDefinition {
219                ToolDefinition {
220                    name: "dummy".into(),
221                    description: "Dummy tool".into(),
222                    input_schema: serde_json::json!(
223                        {"type": "object"}
224                    ),
225                }
226            }
227            async fn execute(&self, _input: serde_json::Value) -> TResult {
228                TResult::success("dummy result")
229            }
230            fn required_capability(&self) -> capability::Capability {
231                capability::Capability::Memory
232            }
233        }
234
235        let manifest = AgentManifest::default();
236        let driver = MockDriver::single_response("with tool");
237
238        let result = AgentBuilder::new(&manifest)
239            .driver(&driver)
240            .tool(Box::new(DummyTool))
241            .run("test")
242            .await
243            .expect("builder run with tool failed");
244
245        assert_eq!(result.text, "with tool");
246    }
247}