diff --git a/src/agent/oneshot.rs b/src/agent/oneshot.rs index 613b52b..e007a14 100644 --- a/src/agent/oneshot.rs +++ b/src/agent/oneshot.rs @@ -55,7 +55,7 @@ pub struct AutoAgent { priority: i32, /// Named outputs from the agent's output() tool calls. /// Collected per-run, read by Mind after completion. - pub outputs: std::collections::HashMap, + pub outputs: std::collections::BTreeMap, // Observable status pub current_phase: String, pub turn: usize, @@ -152,7 +152,7 @@ impl AutoAgent { temperature, top_p: 0.95, top_k: 20, }, priority, - outputs: std::collections::HashMap::new(), + outputs: std::collections::BTreeMap::new(), current_phase: String::new(), turn: 0, } diff --git a/src/hippocampus/store/ops.rs b/src/hippocampus/store/ops.rs index 96e9d6a..11795d4 100644 --- a/src/hippocampus/store/ops.rs +++ b/src/hippocampus/store/ops.rs @@ -38,6 +38,17 @@ impl Store { Ok(()) } + /// Recent nodes by provenance, sorted newest-first. Returns (key, timestamp). + pub fn recent_by_provenance(&self, provenance: &str, limit: usize) -> Vec<(String, i64)> { + let mut nodes: Vec<_> = self.nodes.values() + .filter(|n| !n.deleted && n.provenance == provenance) + .map(|n| (n.key.clone(), n.timestamp)) + .collect(); + nodes.sort_by(|a, b| b.1.cmp(&a.1)); + nodes.truncate(limit); + nodes + } + /// Upsert a node: update if exists (and content changed), create if not. /// Returns: "created", "updated", or "unchanged". /// diff --git a/src/mind/dmn.rs b/src/mind/dmn.rs index 51da357..21832f2 100644 --- a/src/mind/dmn.rs +++ b/src/mind/dmn.rs @@ -298,6 +298,12 @@ pub struct SubconsciousSnapshot { pub forked_agent: Option>>, /// Entry index where the fork diverged. pub fork_point: usize, + /// Persistent agent state — accumulated across runs. + pub state: std::collections::BTreeMap, + /// Persistent walked keys (shared state, relevant for surface). + pub walked: Vec, + /// Recent store activity for this agent: (key, timestamp), newest first. + pub history: Vec<(String, i64)>, } struct SubconsciousAgent { @@ -311,6 +317,8 @@ struct SubconsciousAgent { /// Entry index where the fork diverged from the conscious agent. fork_point: usize, handle: Option)>>, + /// Persistent state — accumulated across runs, serialized to disk. + persistent_state: std::collections::BTreeMap, } impl SubconsciousAgent { @@ -340,6 +348,7 @@ impl SubconsciousAgent { name: name.to_string(), auto, last_trigger_bytes: 0, last_run: None, forked_agent: None, fork_point: 0, handle: None, + persistent_state: std::collections::BTreeMap::new(), }) } @@ -353,7 +362,14 @@ impl SubconsciousAgent { conversation_bytes.saturating_sub(self.last_trigger_bytes) >= interval } - fn snapshot(&self) -> SubconsciousSnapshot { + fn snapshot(&self, walked: &[String], history: Vec<(String, i64)>) -> SubconsciousSnapshot { + // Merge persistent state with any live outputs from a running agent + let mut state = self.persistent_state.clone(); + if self.is_running() { + for (k, v) in &self.auto.outputs { + state.insert(k.clone(), v.clone()); + } + } SubconsciousSnapshot { name: self.name.clone(), running: self.is_running(), @@ -362,6 +378,9 @@ impl SubconsciousAgent { last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()), forked_agent: self.forked_agent.clone(), fork_point: self.fork_point, + state, + walked: walked.to_vec(), + history, } } } @@ -371,6 +390,7 @@ impl SubconsciousAgent { pub struct Subconscious { agents: Vec, pub walked: Vec, + state_path: Option, } impl Subconscious { @@ -378,11 +398,57 @@ impl Subconscious { let agents = AGENTS.iter() .filter_map(|(name, _)| SubconsciousAgent::new(name)) .collect(); - Self { agents, walked: Vec::new() } + Self { agents, walked: Vec::new(), state_path: None } } - pub fn snapshots(&self) -> Vec { - self.agents.iter().map(|s| s.snapshot()).collect() + /// Set the state file path and load any existing state from disk. + pub fn set_state_path(&mut self, path: std::path::PathBuf) { + if let Ok(data) = std::fs::read_to_string(&path) { + if let Ok(saved) = serde_json::from_str::< + std::collections::BTreeMap> + >(&data) { + for agent in &mut self.agents { + if let Some(state) = saved.get(&agent.name) { + agent.persistent_state = state.clone(); + } + } + // Restore walked from surface agent if present + if let Some(surface) = self.agents.iter().find(|a| a.name == "subconscious-surface") { + if let Some(walked_str) = surface.persistent_state.get("walked") { + self.walked = walked_str.lines() + .map(|l| l.trim().to_string()) + .filter(|l| !l.is_empty()) + .collect(); + } + } + dbglog!("[subconscious] loaded state from {}", path.display()); + } + } + self.state_path = Some(path); + } + + fn save_state(&self) { + let Some(path) = &self.state_path else { return }; + let mut map: std::collections::BTreeMap> = + std::collections::BTreeMap::new(); + for agent in &self.agents { + if !agent.persistent_state.is_empty() { + map.insert(agent.name.clone(), agent.persistent_state.clone()); + } + } + if let Ok(json) = serde_json::to_string_pretty(&map) { + let _ = std::fs::write(path, json); + } + } + + pub fn snapshots(&self, store: Option<&crate::store::Store>) -> Vec { + self.agents.iter().map(|s| { + let history = store.map(|st| { + let prov = format!("agent:{}", s.name); + st.recent_by_provenance(&prov, 30) + }).unwrap_or_default(); + s.snapshot(&self.walked, history) + }).collect() } /// Collect results from finished agents, inject outputs into the @@ -397,6 +463,7 @@ impl Subconscious { None } }).collect(); + let had_finished = !finished.is_empty(); for (idx, handle) in finished { let (auto_back, result) = handle.await.unwrap_or_else( @@ -408,6 +475,10 @@ impl Subconscious { Ok(_) => { let name = self.agents[idx].name.clone(); let outputs = std::mem::take(&mut self.agents[idx].auto.outputs); + // Merge into persistent state + for (k, v) in &outputs { + self.agents[idx].persistent_state.insert(k.clone(), v.clone()); + } if let Some(walked_str) = outputs.get("walked") { self.walked = walked_str.lines() @@ -470,6 +541,9 @@ impl Subconscious { Err(e) => dbglog!("[subconscious] agent failed: {}", e), } } + if had_finished { + self.save_state(); + } } /// Trigger subconscious agents that are due to run.