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