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