Skip to main content

phago_runtime/
session.rs

1//! Session persistence — save/load colony graph and agent state.
2//!
3//! Serializes the knowledge graph (nodes + edges) and agent state to JSON
4//! for persistence across sessions. Agents can be fully restored with their
5//! vocabulary, fitness history, and other internal state.
6
7use crate::colony::Colony;
8use phago_agents::serialize::SerializedAgent;
9use phago_core::topology::TopologyGraph;
10use phago_core::types::*;
11use serde::{Deserialize, Serialize};
12use std::path::Path;
13
14/// Serializable snapshot of the knowledge graph and agent state.
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct GraphState {
17    pub nodes: Vec<SerializedNode>,
18    pub edges: Vec<SerializedEdge>,
19    #[serde(default)]
20    pub agents: Vec<SerializedAgent>,
21    pub metadata: SessionMetadata,
22}
23
24/// Serializable node.
25#[derive(Debug, Clone, Serialize, Deserialize)]
26pub struct SerializedNode {
27    pub label: String,
28    pub node_type: String,
29    pub access_count: u64,
30    pub position_x: f64,
31    pub position_y: f64,
32    #[serde(default)]
33    pub created_tick: u64,
34    #[serde(default, skip_serializing_if = "Option::is_none")]
35    pub embedding: Option<Vec<f32>>,
36}
37
38/// Serializable edge.
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct SerializedEdge {
41    pub from_label: String,
42    pub to_label: String,
43    pub weight: f64,
44    pub co_activations: u64,
45    #[serde(default)]
46    pub created_tick: u64,
47    #[serde(default)]
48    pub last_activated_tick: u64,
49}
50
51/// Session metadata.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct SessionMetadata {
54    pub session_id: String,
55    pub tick: u64,
56    pub node_count: usize,
57    pub edge_count: usize,
58    #[serde(default)]
59    pub agent_count: usize,
60    pub files_indexed: Vec<String>,
61}
62
63/// Save the colony's knowledge graph to a JSON file.
64///
65/// To include agent state, use `save_session_with_agents` instead.
66pub fn save_session(colony: &Colony, path: &Path, files_indexed: &[String]) -> std::io::Result<()> {
67    save_session_with_agents(colony, path, files_indexed, &[])
68}
69
70/// Save the colony's knowledge graph and agent state to a JSON file.
71///
72/// # Arguments
73/// * `colony` - The colony to save
74/// * `path` - Path to the output JSON file
75/// * `files_indexed` - List of files that were indexed
76/// * `agents` - Serialized agent states (use SerializableAgent::export_state())
77pub fn save_session_with_agents(
78    colony: &Colony,
79    path: &Path,
80    files_indexed: &[String],
81    agents: &[SerializedAgent],
82) -> std::io::Result<()> {
83    let graph = colony.substrate().graph();
84    let all_nodes = graph.all_nodes();
85
86    let nodes: Vec<SerializedNode> = all_nodes.iter()
87        .filter_map(|nid| graph.get_node(nid))
88        .map(|n| SerializedNode {
89            label: n.label.clone(),
90            node_type: format!("{:?}", n.node_type),
91            access_count: n.access_count,
92            position_x: n.position.x,
93            position_y: n.position.y,
94            created_tick: n.created_tick,
95            embedding: n.embedding.clone(),
96        })
97        .collect();
98
99    let edges: Vec<SerializedEdge> = graph.all_edges().iter()
100        .filter_map(|(from, to, edge)| {
101            let from_label = graph.get_node(from)?.label.clone();
102            let to_label = graph.get_node(to)?.label.clone();
103            Some(SerializedEdge {
104                from_label,
105                to_label,
106                weight: edge.weight,
107                co_activations: edge.co_activations,
108                created_tick: edge.created_tick,
109                last_activated_tick: edge.last_activated_tick,
110            })
111        })
112        .collect();
113
114    let state = GraphState {
115        metadata: SessionMetadata {
116            session_id: uuid::Uuid::new_v4().to_string(),
117            tick: colony.stats().tick,
118            node_count: nodes.len(),
119            edge_count: edges.len(),
120            agent_count: agents.len(),
121            files_indexed: files_indexed.to_vec(),
122        },
123        nodes,
124        edges,
125        agents: agents.to_vec(),
126    };
127
128    let json = serde_json::to_string_pretty(&state)
129        .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
130
131    // Create parent directory if needed
132    if let Some(parent) = path.parent() {
133        std::fs::create_dir_all(parent)?;
134    }
135
136    std::fs::write(path, json)
137}
138
139/// Load a saved session from JSON.
140pub fn load_session(path: &Path) -> std::io::Result<GraphState> {
141    let json = std::fs::read_to_string(path)?;
142    serde_json::from_str(&json)
143        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
144}
145
146/// Restore a graph state into a colony.
147/// Adds all nodes and edges from the saved state.
148///
149/// Note: Agents must be restored separately using `state.agents` and
150/// `SerializableAgent::from_state()` for each agent type.
151///
152/// # Example
153/// ```ignore
154/// use phago_agents::serialize::SerializableAgent;
155/// use phago_agents::digester::Digester;
156///
157/// let state = load_session(&path)?;
158/// restore_into_colony(&mut colony, &state);
159///
160/// // Restore agents
161/// for agent_state in &state.agents {
162///     if let Some(digester) = Digester::from_state(agent_state) {
163///         colony.spawn(Box::new(digester));
164///     }
165/// }
166/// ```
167pub fn restore_into_colony(colony: &mut Colony, state: &GraphState) {
168    use phago_core::substrate::Substrate;
169    use std::collections::HashMap;
170
171    let mut label_to_id: HashMap<String, NodeId> = HashMap::new();
172
173    // Add nodes
174    for node in &state.nodes {
175        let node_type = match node.node_type.as_str() {
176            "Concept" => NodeType::Concept,
177            "Insight" => NodeType::Insight,
178            "Anomaly" => NodeType::Anomaly,
179            _ => NodeType::Concept,
180        };
181
182        let data = NodeData {
183            id: NodeId::new(),
184            label: node.label.clone(),
185            node_type,
186            position: Position::new(node.position_x, node.position_y),
187            access_count: node.access_count,
188            created_tick: node.created_tick,
189            embedding: node.embedding.clone(),
190        };
191        let id = colony.substrate_mut().add_node(data);
192        label_to_id.insert(node.label.clone(), id);
193    }
194
195    // Add edges with full temporal state
196    for edge in &state.edges {
197        if let (Some(&from_id), Some(&to_id)) = (
198            label_to_id.get(&edge.from_label),
199            label_to_id.get(&edge.to_label),
200        ) {
201            colony.substrate_mut().set_edge(from_id, to_id, EdgeData {
202                weight: edge.weight,
203                co_activations: edge.co_activations,
204                created_tick: edge.created_tick,
205                last_activated_tick: edge.last_activated_tick,
206            });
207        }
208    }
209
210    // Advance colony tick to match the saved session
211    // so that maturation/staleness calculations remain correct
212    let target_tick = state.metadata.tick;
213    while colony.stats().tick < target_tick {
214        colony.substrate_mut().advance_tick();
215    }
216}
217
218/// Restore agents from a GraphState into a colony.
219///
220/// This is a convenience function that handles all built-in agent types.
221/// Returns the number of agents successfully restored.
222pub fn restore_agents(colony: &mut Colony, state: &GraphState) -> usize {
223    use phago_agents::digester::Digester;
224    use phago_agents::serialize::SerializableAgent;
225    use phago_agents::sentinel::Sentinel;
226    use phago_agents::synthesizer::Synthesizer;
227
228    let mut restored = 0;
229
230    for agent_state in &state.agents {
231        match agent_state {
232            SerializedAgent::Digester(_) => {
233                if let Some(digester) = Digester::from_state(agent_state) {
234                    colony.spawn(Box::new(digester));
235                    restored += 1;
236                }
237            }
238            SerializedAgent::Synthesizer(_) => {
239                if let Some(synthesizer) = Synthesizer::from_state(agent_state) {
240                    colony.spawn(Box::new(synthesizer));
241                    restored += 1;
242                }
243            }
244            SerializedAgent::Sentinel(_) => {
245                if let Some(sentinel) = Sentinel::from_state(agent_state) {
246                    colony.spawn(Box::new(sentinel));
247                    restored += 1;
248                }
249            }
250        }
251    }
252
253    restored
254}
255
256/// Check if save/load preserves node and edge counts.
257pub fn verify_fidelity(original: &Colony, restored: &Colony) -> (bool, usize, usize, usize, usize) {
258    let orig_nodes = original.substrate().graph().node_count();
259    let orig_edges = original.substrate().graph().edge_count();
260    let rest_nodes = restored.substrate().graph().node_count();
261    let rest_edges = restored.substrate().graph().edge_count();
262    let identical = orig_nodes == rest_nodes && orig_edges == rest_edges;
263    (identical, orig_nodes, orig_edges, rest_nodes, rest_edges)
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269    use crate::colony::Colony;
270    use phago_core::agent::Agent;
271
272    #[test]
273    fn save_load_roundtrip() {
274        let mut colony = Colony::new();
275        colony.ingest_document("test", "cell membrane protein", Position::new(0.0, 0.0));
276
277        use phago_agents::digester::Digester;
278        colony.spawn(Box::new(Digester::new(Position::new(0.0, 0.0)).with_max_idle(80)));
279        colony.run(15);
280
281        let tmp = std::env::temp_dir().join("phago_session_test.json");
282        save_session(&colony, &tmp, &["test.rs".to_string()]).unwrap();
283
284        let state = load_session(&tmp).unwrap();
285        assert!(!state.nodes.is_empty());
286        assert!(state.metadata.node_count > 0);
287
288        // Restore into new colony
289        let mut restored = Colony::new();
290        restore_into_colony(&mut restored, &state);
291
292        let (_identical, orig_n, _orig_e, rest_n, rest_e) = verify_fidelity(&colony, &restored);
293        assert_eq!(orig_n, rest_n, "Node count should match");
294        // Edge count may differ slightly due to label collisions
295        assert!(rest_e > 0, "Restored colony should have edges");
296
297        std::fs::remove_file(&tmp).ok();
298    }
299
300    #[test]
301    fn save_load_with_agent_state() {
302        use phago_agents::digester::Digester;
303        use phago_agents::serialize::SerializableAgent;
304
305        let mut colony = Colony::new();
306        colony.ingest_document("test", "cell membrane protein biology", Position::new(0.0, 0.0));
307
308        // Create a digester and let it process
309        let mut digester = Digester::new(Position::new(0.0, 0.0)).with_max_idle(100);
310        let _ = digester.digest_text("cell membrane protein biology structure".to_string());
311
312        // Export agent state
313        let agent_state = digester.export_state();
314
315        // Spawn the digester
316        colony.spawn(Box::new(digester));
317        colony.run(10);
318
319        // Save with agent state
320        let tmp = std::env::temp_dir().join("phago_agent_state_test.json");
321        save_session_with_agents(&colony, &tmp, &["test.rs".to_string()], &[agent_state]).unwrap();
322
323        // Load and verify agent state is present
324        let state = load_session(&tmp).unwrap();
325        assert_eq!(state.agents.len(), 1, "Should have saved one agent");
326        assert_eq!(state.metadata.agent_count, 1);
327
328        // Restore into new colony
329        let mut restored = Colony::new();
330        restore_into_colony(&mut restored, &state);
331        let agents_restored = restore_agents(&mut restored, &state);
332        assert_eq!(agents_restored, 1, "Should restore one agent");
333        assert_eq!(restored.alive_count(), 1, "Colony should have one agent");
334
335        std::fs::remove_file(&tmp).ok();
336    }
337
338    #[test]
339    fn digester_state_preserves_vocabulary() {
340        use phago_agents::digester::Digester;
341        use phago_agents::serialize::SerializableAgent;
342
343        let mut digester = Digester::new(Position::new(1.0, 2.0)).with_max_idle(50);
344
345        // Process some text to build vocabulary
346        digester.digest_text("cell membrane protein transport channel".to_string());
347        digester.digest_text("receptor signaling pathway cascade".to_string());
348
349        // Export state
350        let state = digester.export_state();
351
352        // Restore and verify
353        let restored = Digester::from_state(&state).expect("Should restore digester");
354
355        assert_eq!(restored.position().x, 1.0);
356        assert_eq!(restored.position().y, 2.0);
357        assert!(restored.total_fragments() > 0, "Vocabulary should be preserved");
358    }
359}