diff --git a/src/agent/oneshot.rs b/src/agent/oneshot.rs index e007a14..b17c674 100644 --- a/src/agent/oneshot.rs +++ b/src/agent/oneshot.rs @@ -102,7 +102,11 @@ impl Backend { } /// Resolve {{placeholder}} templates in subconscious agent prompts. -fn resolve_prompt(template: &str, memory_keys: &[String], walked: &[String]) -> String { +fn resolve_prompt( + template: &str, + memory_keys: &[String], + state: &std::collections::BTreeMap, +) -> String { let cfg = crate::config::get(); let template = template.replace("{assistant_name}", &cfg.assistant_name); let mut result = String::with_capacity(template.len()); @@ -112,14 +116,17 @@ fn resolve_prompt(template: &str, memory_keys: &[String], walked: &[String]) -> let after = &rest[start + 2..]; if let Some(end) = after.find("}}") { let name = after[..end].trim(); - let replacement = match name { - "seen_current" => format_key_list(memory_keys), - "walked" => format_key_list(walked), - _ => { - result.push_str("{{"); - result.push_str(&after[..end + 2]); - rest = &after[end + 2..]; - continue; + let replacement = if let Some(key) = name.strip_prefix("state:") { + state.get(key).cloned().unwrap_or_else(|| "(not set)".to_string()) + } else { + match name { + "seen_current" => format_key_list(memory_keys), + _ => { + result.push_str("{{"); + result.push_str(&after[..end + 2]); + rest = &after[end + 2..]; + continue; + } } }; result.push_str(&replacement); @@ -133,6 +140,8 @@ fn resolve_prompt(template: &str, memory_keys: &[String], walked: &[String]) -> result } + + fn format_key_list(keys: &[String]) -> String { if keys.is_empty() { "(none)".to_string() } else { keys.iter().map(|k| format!("- {}", k)).collect::>().join("\n") } @@ -177,10 +186,10 @@ impl AutoAgent { &mut self, agent: &std::sync::Arc>, memory_keys: &[String], - walked: &[String], + state: &std::collections::BTreeMap, ) -> Result { let resolved_steps: Vec = self.steps.iter().map(|s| AutoStep { - prompt: resolve_prompt(&s.prompt, memory_keys, walked), + prompt: resolve_prompt(&s.prompt, memory_keys, state), phase: s.phase.clone(), }).collect(); let orig_steps = std::mem::replace(&mut self.steps, resolved_steps); diff --git a/src/mind/dmn.rs b/src/mind/dmn.rs index 21832f2..4d92d49 100644 --- a/src/mind/dmn.rs +++ b/src/mind/dmn.rs @@ -298,10 +298,8 @@ pub struct SubconsciousSnapshot { pub forked_agent: Option>>, /// Entry index where the fork diverged. pub fork_point: usize, - /// Persistent agent state — accumulated across runs. + /// Shared persistent state — accumulated across all agent 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)>, } @@ -317,8 +315,6 @@ 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 { @@ -348,7 +344,6 @@ 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(), }) } @@ -362,14 +357,7 @@ impl SubconsciousAgent { conversation_bytes.saturating_sub(self.last_trigger_bytes) >= interval } - 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()); - } - } + fn snapshot(&self, state: &std::collections::BTreeMap, history: Vec<(String, i64)>) -> SubconsciousSnapshot { SubconsciousSnapshot { name: self.name.clone(), running: self.is_running(), @@ -378,18 +366,18 @@ 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(), + state: state.clone(), history, } } } /// Background agent orchestration — owns the subconscious agents -/// and their shared state (walked keys, etc.). +/// and their shared persistent state. pub struct Subconscious { agents: Vec, - pub walked: Vec, + /// Shared state across all agents — persisted to disk. + pub state: std::collections::BTreeMap, state_path: Option, } @@ -398,30 +386,18 @@ impl Subconscious { let agents = AGENTS.iter() .filter_map(|(name, _)| SubconsciousAgent::new(name)) .collect(); - Self { agents, walked: Vec::new(), state_path: None } + Self { agents, state: std::collections::BTreeMap::new(), state_path: None } } /// 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> + 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 = saved; + dbglog!("[subconscious] loaded {} state keys from {}", + self.state.len(), path.display()); } } self.state_path = Some(path); @@ -429,25 +405,24 @@ impl Subconscious { 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) { + if let Ok(json) = serde_json::to_string_pretty(&self.state) { let _ = std::fs::write(path, json); } } + pub fn walked(&self) -> Vec { + self.state.get("walked") + .map(|s| s.lines().map(|l| l.trim().to_string()).filter(|l| !l.is_empty()).collect()) + .unwrap_or_default() + } + 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) + s.snapshot(&self.state, history) }).collect() } @@ -475,16 +450,9 @@ 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 + // Merge into shared 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() - .map(|l| l.trim().to_string()) - .filter(|l| !l.is_empty()) - .collect(); + self.state.insert(k.clone(), v.clone()); } // Inject all outputs into the conscious agent under one lock @@ -577,7 +545,6 @@ impl Subconscious { if to_run.is_empty() { return; } let conscious = agent.lock().await; - let walked = self.walked.clone(); for (idx, mut auto) in to_run { dbglog!("[subconscious] triggering {}", auto.name); @@ -590,10 +557,10 @@ impl Subconscious { self.agents[idx].fork_point = fork_point; let keys = memory_keys.clone(); - let w = walked.clone(); + let st = self.state.clone(); self.agents[idx].handle = Some(tokio::spawn(async move { - let result = auto.run_forked_shared(&shared_forked, &keys, &w).await; + let result = auto.run_forked_shared(&shared_forked, &keys, &st).await; (auto, result) })); } diff --git a/src/mind/mod.rs b/src/mind/mod.rs index b167ad8..50d1bf0 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -258,7 +258,7 @@ impl Mind { } pub async fn subconscious_walked(&self) -> Vec { - self.subconscious.lock().await.walked.clone() + self.subconscious.lock().await.walked() } pub async fn init(&self) { diff --git a/src/subconscious/agents/subconscious-journal.agent b/src/subconscious/agents/subconscious-journal.agent index 88e733d..26d79e0 100644 --- a/src/subconscious/agents/subconscious-journal.agent +++ b/src/subconscious/agents/subconscious-journal.agent @@ -6,7 +6,7 @@ The full conversation is in context above — use it to understand what your conscious self is doing and thinking about. Nodes your subconscious recently touched (for linking, not duplicating): -{{walked}} +{{state:walked}} **Your tools:** journal_tail, journal_new, journal_update, memory_link_add, memory_search, memory_render, memory_used. Do NOT use memory_write — creating diff --git a/src/subconscious/agents/subconscious-observe.agent b/src/subconscious/agents/subconscious-observe.agent index 1124e8f..9f714e8 100644 --- a/src/subconscious/agents/subconscious-observe.agent +++ b/src/subconscious/agents/subconscious-observe.agent @@ -6,7 +6,7 @@ You are an agent of {assistant_name}'s subconscious — the librarian of the memory system. The full conversation is in context above. Nodes your surface agent was exploring: -{{walked}} +{{state:walked}} Starting with these nodes, do some graph maintenance and organization so that you can find things easier in the future. Consider if nodes have the right diff --git a/src/subconscious/agents/subconscious-reflect.agent b/src/subconscious/agents/subconscious-reflect.agent index 0fb0de8..e49833b 100644 --- a/src/subconscious/agents/subconscious-reflect.agent +++ b/src/subconscious/agents/subconscious-reflect.agent @@ -18,7 +18,7 @@ The full conversation is in context above — use it to understand what your conscious self is doing and thinking about. Memories your surface agent was exploring: -{{walked}} +{{state:walked}} Start from the nodes surface-observe was walking. Render one or two that catch your attention — then ask "what does this mean?" Follow the links in diff --git a/src/subconscious/agents/subconscious-surface.agent b/src/subconscious/agents/subconscious-surface.agent index b7e9b2d..1b2c785 100644 --- a/src/subconscious/agents/subconscious-surface.agent +++ b/src/subconscious/agents/subconscious-surface.agent @@ -16,7 +16,7 @@ Already in current context (don't re-surface unless the conversation has shifted {{seen_current}} Memories you were exploring last time but hadn't surfaced yet: -{{walked}} +{{state:walked}} How focused is the current conversation? If it's more focused, look for the useful and relevant memories, When considering relevance, don't just look for diff --git a/src/user/subconscious.rs b/src/user/subconscious.rs index 4c82d32..6b33088 100644 --- a/src/user/subconscious.rs +++ b/src/user/subconscious.rs @@ -241,14 +241,18 @@ impl SubconsciousScreen { } } - if !snap.walked.is_empty() { - lines.push(Line::raw("")); - lines.push(Line::styled( - format!(" walked ({}):", snap.walked.len()), - Style::default().fg(Color::Cyan), - )); - for key in &snap.walked { - lines.push(Line::styled(format!(" {}", key), dim)); + if let Some(walked_str) = snap.state.get("walked") { + let walked: Vec<&str> = walked_str.lines() + .map(|l| l.trim()).filter(|l| !l.is_empty()).collect(); + if !walked.is_empty() { + lines.push(Line::raw("")); + lines.push(Line::styled( + format!(" walked ({}):", walked.len()), + Style::default().fg(Color::Cyan), + )); + for key in &walked { + lines.push(Line::styled(format!(" {}", key), dim)); + } } } }