Skip to main content

phago_runtime/
colony_builder.rs

1//! Colony builder with configurable persistence.
2//!
3//! Provides a builder pattern for creating colonies with optional
4//! SQLite-backed persistence for the knowledge graph.
5//!
6//! # Architecture
7//!
8//! The Colony uses PetTopologyGraph internally for simulation (required for
9//! reference-based operations). Persistence is handled by:
10//! - Loading initial state from SQLite on creation
11//! - Saving state to SQLite on explicit save or drop
12//!
13//! This gives the benefits of persistence without compromising simulation performance.
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! use phago_runtime::colony_builder::ColonyBuilder;
19//! use phago_runtime::backend::BackendConfig;
20//!
21//! // Create a colony with SQLite persistence
22//! let mut colony = ColonyBuilder::new()
23//!     .with_persistence("knowledge.db")
24//!     .auto_save(true)
25//!     .build()?;
26//!
27//! // Run simulation
28//! colony.run(100);
29//!
30//! // Explicitly save (also happens on drop if auto_save is enabled)
31//! colony.save()?;
32//! ```
33
34use crate::colony::{Colony, ColonyConfig};
35use crate::topology_impl::PetTopologyGraph;
36use phago_core::topology::TopologyGraph;
37use phago_core::types::*;
38use std::path::{Path, PathBuf};
39
40#[cfg(feature = "sqlite")]
41use crate::sqlite_topology::SqliteTopologyGraph;
42
43/// Error type for colony builder operations.
44#[derive(Debug)]
45pub enum BuilderError {
46    /// SQLite feature not enabled.
47    SqliteNotEnabled,
48    /// Failed to open or create database.
49    DatabaseError(String),
50    /// Failed to load or save state.
51    PersistenceError(String),
52}
53
54impl std::fmt::Display for BuilderError {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            BuilderError::SqliteNotEnabled => {
58                write!(f, "SQLite feature not enabled. Add features = [\"sqlite\"] to Cargo.toml")
59            }
60            BuilderError::DatabaseError(msg) => write!(f, "Database error: {}", msg),
61            BuilderError::PersistenceError(msg) => write!(f, "Persistence error: {}", msg),
62        }
63    }
64}
65
66impl std::error::Error for BuilderError {}
67
68/// Builder for creating colonies with optional persistence.
69pub struct ColonyBuilder {
70    persistence_path: Option<PathBuf>,
71    auto_save: bool,
72    cache_size: usize,
73    colony_config: ColonyConfig,
74}
75
76impl Default for ColonyBuilder {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82impl ColonyBuilder {
83    /// Create a new colony builder with default settings.
84    pub fn new() -> Self {
85        Self {
86            persistence_path: None,
87            auto_save: false,
88            cache_size: 1000,
89            colony_config: ColonyConfig::default(),
90        }
91    }
92
93    /// Set the colony configuration for simulation parameters.
94    ///
95    /// This allows customizing decay rates, pruning thresholds, and
96    /// semantic wiring settings.
97    pub fn with_config(mut self, config: ColonyConfig) -> Self {
98        self.colony_config = config;
99        self
100    }
101
102    /// Enable SQLite persistence at the given path.
103    ///
104    /// The database will be created if it doesn't exist.
105    /// If it exists, the graph state will be loaded on build.
106    #[cfg(feature = "sqlite")]
107    pub fn with_persistence<P: AsRef<Path>>(mut self, path: P) -> Self {
108        self.persistence_path = Some(path.as_ref().to_path_buf());
109        self
110    }
111
112    /// Stub for when SQLite is not enabled.
113    #[cfg(not(feature = "sqlite"))]
114    pub fn with_persistence<P: AsRef<Path>>(self, _path: P) -> Self {
115        // Will error on build
116        self
117    }
118
119    /// Enable automatic saving on colony drop.
120    ///
121    /// When enabled, the colony state will be persisted when the
122    /// `PersistentColony` is dropped.
123    pub fn auto_save(mut self, enabled: bool) -> Self {
124        self.auto_save = enabled;
125        self
126    }
127
128    /// Set the cache size for SQLite operations.
129    pub fn cache_size(mut self, size: usize) -> Self {
130        self.cache_size = size;
131        self
132    }
133
134    /// Build a standard Colony (no persistence).
135    pub fn build_simple(self) -> Colony {
136        Colony::from_config(self.colony_config)
137    }
138
139    /// Build a PersistentColony with SQLite backing.
140    #[cfg(feature = "sqlite")]
141    pub fn build(self) -> Result<PersistentColony, BuilderError> {
142        let mut colony = Colony::from_config(self.colony_config);
143
144        let persistence = if let Some(path) = self.persistence_path {
145            let db = SqliteTopologyGraph::open(&path)
146                .map_err(|e| BuilderError::DatabaseError(e.to_string()))?
147                .with_cache_size(self.cache_size);
148
149            // Load existing nodes into the colony's graph
150            load_from_sqlite(&db, colony.substrate_mut().graph_mut())?;
151
152            Some(PersistenceState {
153                db,
154                path,
155                auto_save: self.auto_save,
156            })
157        } else {
158            None
159        };
160
161        Ok(PersistentColony {
162            colony,
163            persistence,
164        })
165    }
166
167    /// Build without SQLite feature - returns error if persistence was requested.
168    #[cfg(not(feature = "sqlite"))]
169    pub fn build(self) -> Result<PersistentColony, BuilderError> {
170        if self.persistence_path.is_some() {
171            return Err(BuilderError::SqliteNotEnabled);
172        }
173        Ok(PersistentColony {
174            colony: Colony::from_config(self.colony_config),
175            persistence: None,
176        })
177    }
178}
179
180/// Internal state for persistence.
181#[cfg(feature = "sqlite")]
182struct PersistenceState {
183    db: SqliteTopologyGraph,
184    path: PathBuf,
185    auto_save: bool,
186}
187
188#[cfg(not(feature = "sqlite"))]
189struct PersistenceState;
190
191/// A Colony with optional SQLite persistence.
192///
193/// Wraps a standard Colony and provides automatic loading/saving
194/// of the knowledge graph to SQLite.
195pub struct PersistentColony {
196    colony: Colony,
197    #[cfg(feature = "sqlite")]
198    persistence: Option<PersistenceState>,
199    #[cfg(not(feature = "sqlite"))]
200    persistence: Option<PersistenceState>,
201}
202
203impl PersistentColony {
204    /// Get a reference to the inner colony.
205    pub fn colony(&self) -> &Colony {
206        &self.colony
207    }
208
209    /// Get a mutable reference to the inner colony.
210    pub fn colony_mut(&mut self) -> &mut Colony {
211        &mut self.colony
212    }
213
214    /// Consume self and return the inner colony.
215    ///
216    /// Note: This will NOT trigger auto-save. Call `save()` first if needed.
217    pub fn into_inner(mut self) -> Colony {
218        // Disable auto_save to prevent save on drop
219        #[cfg(feature = "sqlite")]
220        if let Some(ref mut state) = self.persistence {
221            state.auto_save = false;
222        }
223        // Use ManuallyDrop to prevent Drop from running
224        let colony = std::mem::replace(&mut self.colony, Colony::new());
225        std::mem::forget(self); // Don't run Drop
226        colony
227    }
228
229    /// Check if persistence is enabled.
230    pub fn has_persistence(&self) -> bool {
231        self.persistence.is_some()
232    }
233
234    /// Save the current graph state to SQLite.
235    #[cfg(feature = "sqlite")]
236    pub fn save(&mut self) -> Result<(), BuilderError> {
237        if let Some(ref mut state) = self.persistence {
238            save_to_sqlite(self.colony.substrate().graph(), &mut state.db)?;
239        }
240        Ok(())
241    }
242
243    #[cfg(not(feature = "sqlite"))]
244    pub fn save(&mut self) -> Result<(), BuilderError> {
245        Ok(())
246    }
247
248    /// Get the persistence path if configured.
249    #[cfg(feature = "sqlite")]
250    pub fn persistence_path(&self) -> Option<&Path> {
251        self.persistence.as_ref().map(|s| s.path.as_path())
252    }
253
254    #[cfg(not(feature = "sqlite"))]
255    pub fn persistence_path(&self) -> Option<&Path> {
256        None
257    }
258}
259
260// Delegate common Colony methods
261impl PersistentColony {
262    /// Run simulation for N ticks.
263    pub fn run(&mut self, ticks: u64) -> Vec<Vec<crate::colony::ColonyEvent>> {
264        self.colony.run(ticks)
265    }
266
267    /// Run a single tick.
268    pub fn tick(&mut self) -> Vec<crate::colony::ColonyEvent> {
269        self.colony.tick()
270    }
271
272    /// Ingest a document.
273    pub fn ingest_document(&mut self, title: &str, content: &str, position: Position) -> DocumentId {
274        self.colony.ingest_document(title, content, position)
275    }
276
277    /// Get colony statistics.
278    pub fn stats(&self) -> crate::colony::ColonyStats {
279        self.colony.stats()
280    }
281
282    /// Get a snapshot of the colony.
283    pub fn snapshot(&self) -> crate::colony::ColonySnapshot {
284        self.colony.snapshot()
285    }
286
287    /// Spawn an agent.
288    pub fn spawn(
289        &mut self,
290        agent: Box<dyn phago_core::agent::Agent<Input = String, Fragment = String, Presentation = Vec<String>>>,
291    ) -> AgentId {
292        self.colony.spawn(agent)
293    }
294
295    /// Number of alive agents.
296    pub fn alive_count(&self) -> usize {
297        self.colony.alive_count()
298    }
299}
300
301#[cfg(feature = "sqlite")]
302impl Drop for PersistentColony {
303    fn drop(&mut self) {
304        if let Some(ref state) = self.persistence {
305            if state.auto_save {
306                // Best-effort save on drop
307                let _ = save_to_sqlite(self.colony.substrate().graph(), &mut self.persistence.as_mut().unwrap().db);
308            }
309        }
310    }
311}
312
313/// Load nodes and edges from SQLite into PetTopologyGraph.
314#[cfg(feature = "sqlite")]
315fn load_from_sqlite(
316    source: &SqliteTopologyGraph,
317    target: &mut PetTopologyGraph,
318) -> Result<(), BuilderError> {
319    // Load all nodes using the iterator
320    let mut node_count = 0;
321    for node in source.iter_nodes() {
322        target.add_node(node);
323        node_count += 1;
324    }
325
326    // Load all edges
327    let mut edge_count = 0;
328    for (from, to, edge) in source.iter_edges() {
329        target.set_edge(from, to, edge);
330        edge_count += 1;
331    }
332
333    if node_count > 0 || edge_count > 0 {
334        eprintln!(
335            "Loaded {} nodes and {} edges from SQLite database.",
336            node_count, edge_count
337        );
338    }
339
340    Ok(())
341}
342
343/// Save nodes and edges from PetTopologyGraph to SQLite.
344#[cfg(feature = "sqlite")]
345fn save_to_sqlite(
346    source: &PetTopologyGraph,
347    target: &mut SqliteTopologyGraph,
348) -> Result<(), BuilderError> {
349    // Save all nodes
350    for node_id in source.all_nodes() {
351        if let Some(node) = source.get_node(&node_id) {
352            target.add_node(node.clone());
353        }
354    }
355
356    // Save all edges
357    for (from, to, edge) in source.all_edges() {
358        target.set_edge(from, to, edge.clone());
359    }
360
361    Ok(())
362}
363
364#[cfg(test)]
365mod tests {
366    use super::*;
367
368    #[test]
369    fn build_simple_colony() {
370        let colony = ColonyBuilder::new().build_simple();
371        assert_eq!(colony.alive_count(), 0);
372    }
373
374    #[test]
375    fn build_without_persistence() {
376        let colony = ColonyBuilder::new().build().unwrap();
377        assert!(!colony.has_persistence());
378    }
379
380    #[cfg(feature = "sqlite")]
381    #[test]
382    fn build_with_persistence() {
383        let tmp = std::env::temp_dir().join("phago_builder_test.db");
384        let _ = std::fs::remove_file(&tmp); // Clean up from previous runs
385
386        let mut colony = ColonyBuilder::new()
387            .with_persistence(&tmp)
388            .build()
389            .unwrap();
390
391        assert!(colony.has_persistence());
392        assert_eq!(colony.persistence_path(), Some(tmp.as_path()));
393
394        // Ingest a document and save
395        colony.ingest_document("Test", "Content", Position::new(0.0, 0.0));
396        colony.run(5);
397        colony.save().unwrap();
398
399        // Clean up
400        let _ = std::fs::remove_file(&tmp);
401    }
402
403    #[cfg(feature = "sqlite")]
404    #[test]
405    fn auto_save_on_drop() {
406        let tmp = std::env::temp_dir().join("phago_autosave_test.db");
407        let _ = std::fs::remove_file(&tmp);
408
409        {
410            let mut colony = ColonyBuilder::new()
411                .with_persistence(&tmp)
412                .auto_save(true)
413                .build()
414                .unwrap();
415
416            colony.ingest_document("Test", "Content", Position::new(0.0, 0.0));
417            colony.run(5);
418            // Drop should trigger save
419        }
420
421        // Verify file exists
422        assert!(tmp.exists());
423        let _ = std::fs::remove_file(&tmp);
424    }
425
426    #[cfg(feature = "sqlite")]
427    #[test]
428    fn roundtrip_save_load() {
429        use phago_agents::digester::Digester;
430
431        let tmp = std::env::temp_dir().join("phago_roundtrip_test.db");
432        let _ = std::fs::remove_file(&tmp);
433
434        // Create colony, add data, save
435        let (node_count, edge_count) = {
436            let mut colony = ColonyBuilder::new()
437                .with_persistence(&tmp)
438                .build()
439                .unwrap();
440
441            colony.ingest_document("Biology 101", "Cell membrane proteins transport molecules", Position::new(0.0, 0.0));
442            colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0)).with_max_idle(50)));
443            colony.run(15);
444
445            let stats = colony.stats();
446            colony.save().unwrap();
447            (stats.graph_nodes, stats.graph_edges)
448        };
449
450        // Load into new colony
451        let colony2 = ColonyBuilder::new()
452            .with_persistence(&tmp)
453            .build()
454            .unwrap();
455
456        let stats2 = colony2.stats();
457
458        // Verify data was loaded
459        assert_eq!(stats2.graph_nodes, node_count, "Node count should match after reload");
460        assert_eq!(stats2.graph_edges, edge_count, "Edge count should match after reload");
461
462        let _ = std::fs::remove_file(&tmp);
463    }
464}