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 auto_memory;
31pub mod capability;
32pub mod code;
33mod code_prompts;
34pub mod contracts;
35pub mod custom_agents;
36pub mod driver;
37pub mod guard;
38pub mod hooks;
39pub mod instructions;
40pub mod manifest;
41#[cfg(feature = "agents-mcp")]
42pub mod mcp_json;
43pub mod memory;
44pub mod org_policy;
45pub mod permission;
46pub mod phase;
47pub mod pool;
48pub mod repl;
49mod repl_directives;
50mod repl_display;
51pub mod result;
52pub mod runtime;
53mod runtime_helpers;
54pub mod session;
55pub mod settings;
56pub mod signing;
57pub mod skill;
58pub mod status_line;
59pub mod task_tool;
60pub mod tool;
61pub mod tui;
62pub mod worktree;
63
64// Re-export key types for convenience.
65pub use capability::{capability_matches, Capability};
66pub use guard::{LoopGuard, LoopVerdict};
67pub use manifest::{AgentManifest, AutoPullError, ModelConfig, ResourceQuota};
68pub use memory::InMemorySubstrate;
69pub use phase::LoopPhase;
70pub use pool::{AgentId, AgentMessage, AgentPool, MessageRouter, SpawnConfig, ToolBuilder};
71pub use result::{AgentError, AgentLoopResult, DriverError, StopReason, TokenUsage};
72
73use driver::{LlmDriver, StreamEvent};
74use memory::MemorySubstrate;
75use tokio::sync::mpsc;
76use tool::ToolRegistry;
77
78/// Ergonomic builder for constructing and running agent loops.
79///
80/// ```rust,ignore
81/// let result = AgentBuilder::new(manifest)
82///     .driver(&my_driver)
83///     .tool(Box::new(rag_tool))
84///     .memory(&substrate)
85///     .run("What is SIMD?")
86///     .await?;
87/// ```
88pub struct AgentBuilder<'a> {
89    manifest: &'a AgentManifest,
90    driver: Option<&'a dyn LlmDriver>,
91    tools: ToolRegistry,
92    memory: Option<&'a dyn MemorySubstrate>,
93    stream_tx: Option<mpsc::Sender<StreamEvent>>,
94}
95
96impl<'a> AgentBuilder<'a> {
97    /// Create a new builder from an agent manifest.
98    pub fn new(manifest: &'a AgentManifest) -> Self {
99        Self { manifest, driver: None, tools: ToolRegistry::new(), memory: None, stream_tx: None }
100    }
101
102    /// Set the LLM driver for inference.
103    #[must_use]
104    pub fn driver(mut self, driver: &'a dyn LlmDriver) -> Self {
105        self.driver = Some(driver);
106        self
107    }
108
109    /// Register a tool in the tool registry.
110    #[must_use]
111    pub fn tool(mut self, tool: Box<dyn tool::Tool>) -> Self {
112        self.tools.register(tool);
113        self
114    }
115
116    /// Set the memory substrate.
117    #[must_use]
118    pub fn memory(mut self, memory: &'a dyn MemorySubstrate) -> Self {
119        self.memory = Some(memory);
120        self
121    }
122
123    /// Set the stream event channel for real-time events.
124    #[must_use]
125    pub fn stream(mut self, tx: mpsc::Sender<StreamEvent>) -> Self {
126        self.stream_tx = Some(tx);
127        self
128    }
129
130    /// Run the agent loop with the given query.
131    ///
132    /// Uses `InMemorySubstrate` if no memory was provided.
133    pub async fn run(self, query: &str) -> Result<AgentLoopResult, AgentError> {
134        let driver = self
135            .driver
136            .ok_or_else(|| AgentError::ManifestError("no LLM driver configured".into()))?;
137
138        let default_memory = InMemorySubstrate::new();
139        let memory = self.memory.unwrap_or(&default_memory);
140
141        runtime::run_agent_loop(self.manifest, query, driver, &self.tools, memory, self.stream_tx)
142            .await
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use driver::mock::MockDriver;
150
151    #[tokio::test]
152    async fn test_builder_minimal() {
153        let manifest = AgentManifest::default();
154        let driver = MockDriver::single_response("built!");
155
156        let result = AgentBuilder::new(&manifest)
157            .driver(&driver)
158            .run("hello")
159            .await
160            .expect("builder run failed");
161
162        assert_eq!(result.text, "built!");
163    }
164
165    #[tokio::test]
166    async fn test_builder_no_driver_errors() {
167        let manifest = AgentManifest::default();
168
169        let err = AgentBuilder::new(&manifest).run("hello").await.unwrap_err();
170
171        assert!(matches!(err, AgentError::ManifestError(_)), "expected ManifestError, got: {err}");
172    }
173
174    #[tokio::test]
175    async fn test_builder_with_memory() {
176        let manifest = AgentManifest::default();
177        let driver = MockDriver::single_response("remembered");
178        let memory = InMemorySubstrate::new();
179
180        let result = AgentBuilder::new(&manifest)
181            .driver(&driver)
182            .memory(&memory)
183            .run("test")
184            .await
185            .expect("builder run failed");
186
187        assert_eq!(result.text, "remembered");
188    }
189
190    #[tokio::test]
191    async fn test_builder_with_stream() {
192        let manifest = AgentManifest::default();
193        let driver = MockDriver::single_response("streamed");
194        let (tx, mut rx) = mpsc::channel(32);
195
196        let result = AgentBuilder::new(&manifest)
197            .driver(&driver)
198            .stream(tx)
199            .run("test")
200            .await
201            .expect("builder run failed");
202
203        assert_eq!(result.text, "streamed");
204
205        let mut got_events = false;
206        while let Ok(_event) = rx.try_recv() {
207            got_events = true;
208        }
209        assert!(got_events, "expected stream events");
210    }
211
212    #[tokio::test]
213    async fn test_builder_with_tool() {
214        use crate::agent::driver::ToolDefinition;
215        use crate::agent::tool::ToolResult as TResult;
216
217        struct DummyTool;
218
219        #[async_trait::async_trait]
220        impl tool::Tool for DummyTool {
221            fn name(&self) -> &'static str {
222                "dummy"
223            }
224            fn definition(&self) -> ToolDefinition {
225                ToolDefinition {
226                    name: "dummy".into(),
227                    description: "Dummy tool".into(),
228                    input_schema: serde_json::json!(
229                        {"type": "object"}
230                    ),
231                }
232            }
233            async fn execute(&self, _input: serde_json::Value) -> TResult {
234                TResult::success("dummy result")
235            }
236            fn required_capability(&self) -> capability::Capability {
237                capability::Capability::Memory
238            }
239        }
240
241        let manifest = AgentManifest::default();
242        let driver = MockDriver::single_response("with tool");
243
244        let result = AgentBuilder::new(&manifest)
245            .driver(&driver)
246            .tool(Box::new(DummyTool))
247            .run("test")
248            .await
249            .expect("builder run with tool failed");
250
251        assert_eq!(result.text, "with tool");
252    }
253}