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