1use 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#[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#[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#[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#[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
63pub fn save_session(colony: &Colony, path: &Path, files_indexed: &[String]) -> std::io::Result<()> {
67 save_session_with_agents(colony, path, files_indexed, &[])
68}
69
70pub 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 if let Some(parent) = path.parent() {
133 std::fs::create_dir_all(parent)?;
134 }
135
136 std::fs::write(path, json)
137}
138
139pub 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
146pub 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 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 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 let target_tick = state.metadata.tick;
213 while colony.stats().tick < target_tick {
214 colony.substrate_mut().advance_tick();
215 }
216}
217
218pub 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
256pub 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 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 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 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 let agent_state = digester.export_state();
314
315 colony.spawn(Box::new(digester));
317 colony.run(10);
318
319 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 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 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 digester.digest_text("cell membrane protein transport channel".to_string());
347 digester.digest_text("receptor signaling pathway cascade".to_string());
348
349 let state = digester.export_state();
351
352 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}