Shared persistent state across all subconscious agents

Moved persistent_state from per-agent to a single shared BTreeMap on
Subconscious. All agents read/write the same state — surface's walked
keys are visible to observe and reflect, etc.

- Subconscious.state: shared BTreeMap<String, String>
- walked() derives from state["walked"] instead of separate Vec
- subconscious-state.json is now a flat key-value map
- All agent outputs merge into the shared state on completion
- Loaded on startup, saved after any agent completes

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-07 19:16:01 -04:00
parent 578be807e7
commit 27ca3c058d
8 changed files with 60 additions and 80 deletions

View file

@ -102,7 +102,11 @@ impl Backend {
} }
/// Resolve {{placeholder}} templates in subconscious agent prompts. /// 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, String>,
) -> String {
let cfg = crate::config::get(); let cfg = crate::config::get();
let template = template.replace("{assistant_name}", &cfg.assistant_name); let template = template.replace("{assistant_name}", &cfg.assistant_name);
let mut result = String::with_capacity(template.len()); let mut result = String::with_capacity(template.len());
@ -112,15 +116,18 @@ fn resolve_prompt(template: &str, memory_keys: &[String], walked: &[String]) ->
let after = &rest[start + 2..]; let after = &rest[start + 2..];
if let Some(end) = after.find("}}") { if let Some(end) = after.find("}}") {
let name = after[..end].trim(); let name = after[..end].trim();
let replacement = match name { 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), "seen_current" => format_key_list(memory_keys),
"walked" => format_key_list(walked),
_ => { _ => {
result.push_str("{{"); result.push_str("{{");
result.push_str(&after[..end + 2]); result.push_str(&after[..end + 2]);
rest = &after[end + 2..]; rest = &after[end + 2..];
continue; continue;
} }
}
}; };
result.push_str(&replacement); result.push_str(&replacement);
rest = &after[end + 2..]; rest = &after[end + 2..];
@ -133,6 +140,8 @@ fn resolve_prompt(template: &str, memory_keys: &[String], walked: &[String]) ->
result result
} }
fn format_key_list(keys: &[String]) -> String { fn format_key_list(keys: &[String]) -> String {
if keys.is_empty() { "(none)".to_string() } if keys.is_empty() { "(none)".to_string() }
else { keys.iter().map(|k| format!("- {}", k)).collect::<Vec<_>>().join("\n") } else { keys.iter().map(|k| format!("- {}", k)).collect::<Vec<_>>().join("\n") }
@ -177,10 +186,10 @@ impl AutoAgent {
&mut self, &mut self,
agent: &std::sync::Arc<tokio::sync::Mutex<Agent>>, agent: &std::sync::Arc<tokio::sync::Mutex<Agent>>,
memory_keys: &[String], memory_keys: &[String],
walked: &[String], state: &std::collections::BTreeMap<String, String>,
) -> Result<String, String> { ) -> Result<String, String> {
let resolved_steps: Vec<AutoStep> = self.steps.iter().map(|s| AutoStep { let resolved_steps: Vec<AutoStep> = 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(), phase: s.phase.clone(),
}).collect(); }).collect();
let orig_steps = std::mem::replace(&mut self.steps, resolved_steps); let orig_steps = std::mem::replace(&mut self.steps, resolved_steps);

View file

@ -298,10 +298,8 @@ pub struct SubconsciousSnapshot {
pub forked_agent: Option<Arc<tokio::sync::Mutex<crate::agent::Agent>>>, pub forked_agent: Option<Arc<tokio::sync::Mutex<crate::agent::Agent>>>,
/// Entry index where the fork diverged. /// Entry index where the fork diverged.
pub fork_point: usize, pub fork_point: usize,
/// Persistent agent state — accumulated across runs. /// Shared persistent state — accumulated across all agent runs.
pub state: std::collections::BTreeMap<String, String>, pub state: std::collections::BTreeMap<String, String>,
/// Persistent walked keys (shared state, relevant for surface).
pub walked: Vec<String>,
/// Recent store activity for this agent: (key, timestamp), newest first. /// Recent store activity for this agent: (key, timestamp), newest first.
pub history: Vec<(String, i64)>, pub history: Vec<(String, i64)>,
} }
@ -317,8 +315,6 @@ struct SubconsciousAgent {
/// Entry index where the fork diverged from the conscious agent. /// Entry index where the fork diverged from the conscious agent.
fork_point: usize, fork_point: usize,
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<String, String>)>>, handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<String, String>)>>,
/// Persistent state — accumulated across runs, serialized to disk.
persistent_state: std::collections::BTreeMap<String, String>,
} }
impl SubconsciousAgent { impl SubconsciousAgent {
@ -348,7 +344,6 @@ impl SubconsciousAgent {
name: name.to_string(), name: name.to_string(),
auto, last_trigger_bytes: 0, last_run: None, auto, last_trigger_bytes: 0, last_run: None,
forked_agent: None, fork_point: 0, handle: 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 conversation_bytes.saturating_sub(self.last_trigger_bytes) >= interval
} }
fn snapshot(&self, walked: &[String], history: Vec<(String, i64)>) -> SubconsciousSnapshot { fn snapshot(&self, state: &std::collections::BTreeMap<String, 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 { SubconsciousSnapshot {
name: self.name.clone(), name: self.name.clone(),
running: self.is_running(), running: self.is_running(),
@ -378,18 +366,18 @@ impl SubconsciousAgent {
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()), last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
forked_agent: self.forked_agent.clone(), forked_agent: self.forked_agent.clone(),
fork_point: self.fork_point, fork_point: self.fork_point,
state, state: state.clone(),
walked: walked.to_vec(),
history, history,
} }
} }
} }
/// Background agent orchestration — owns the subconscious agents /// Background agent orchestration — owns the subconscious agents
/// and their shared state (walked keys, etc.). /// and their shared persistent state.
pub struct Subconscious { pub struct Subconscious {
agents: Vec<SubconsciousAgent>, agents: Vec<SubconsciousAgent>,
pub walked: Vec<String>, /// Shared state across all agents — persisted to disk.
pub state: std::collections::BTreeMap<String, String>,
state_path: Option<std::path::PathBuf>, state_path: Option<std::path::PathBuf>,
} }
@ -398,30 +386,18 @@ impl Subconscious {
let agents = AGENTS.iter() let agents = AGENTS.iter()
.filter_map(|(name, _)| SubconsciousAgent::new(name)) .filter_map(|(name, _)| SubconsciousAgent::new(name))
.collect(); .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. /// Set the state file path and load any existing state from disk.
pub fn set_state_path(&mut self, path: std::path::PathBuf) { pub fn set_state_path(&mut self, path: std::path::PathBuf) {
if let Ok(data) = std::fs::read_to_string(&path) { if let Ok(data) = std::fs::read_to_string(&path) {
if let Ok(saved) = serde_json::from_str::< if let Ok(saved) = serde_json::from_str::<
std::collections::BTreeMap<String, std::collections::BTreeMap<String, String>> std::collections::BTreeMap<String, String>
>(&data) { >(&data) {
for agent in &mut self.agents { self.state = saved;
if let Some(state) = saved.get(&agent.name) { dbglog!("[subconscious] loaded {} state keys from {}",
agent.persistent_state = state.clone(); self.state.len(), path.display());
}
}
// 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); self.state_path = Some(path);
@ -429,25 +405,24 @@ impl Subconscious {
fn save_state(&self) { fn save_state(&self) {
let Some(path) = &self.state_path else { return }; let Some(path) = &self.state_path else { return };
let mut map: std::collections::BTreeMap<String, std::collections::BTreeMap<String, String>> = if let Ok(json) = serde_json::to_string_pretty(&self.state) {
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); let _ = std::fs::write(path, json);
} }
} }
pub fn walked(&self) -> Vec<String> {
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<SubconsciousSnapshot> { pub fn snapshots(&self, store: Option<&crate::store::Store>) -> Vec<SubconsciousSnapshot> {
self.agents.iter().map(|s| { self.agents.iter().map(|s| {
let history = store.map(|st| { let history = store.map(|st| {
let prov = format!("agent:{}", s.name); let prov = format!("agent:{}", s.name);
st.recent_by_provenance(&prov, 30) st.recent_by_provenance(&prov, 30)
}).unwrap_or_default(); }).unwrap_or_default();
s.snapshot(&self.walked, history) s.snapshot(&self.state, history)
}).collect() }).collect()
} }
@ -475,16 +450,9 @@ impl Subconscious {
Ok(_) => { Ok(_) => {
let name = self.agents[idx].name.clone(); let name = self.agents[idx].name.clone();
let outputs = std::mem::take(&mut self.agents[idx].auto.outputs); 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 { for (k, v) in &outputs {
self.agents[idx].persistent_state.insert(k.clone(), v.clone()); self.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();
} }
// Inject all outputs into the conscious agent under one lock // Inject all outputs into the conscious agent under one lock
@ -577,7 +545,6 @@ impl Subconscious {
if to_run.is_empty() { return; } if to_run.is_empty() { return; }
let conscious = agent.lock().await; let conscious = agent.lock().await;
let walked = self.walked.clone();
for (idx, mut auto) in to_run { for (idx, mut auto) in to_run {
dbglog!("[subconscious] triggering {}", auto.name); dbglog!("[subconscious] triggering {}", auto.name);
@ -590,10 +557,10 @@ impl Subconscious {
self.agents[idx].fork_point = fork_point; self.agents[idx].fork_point = fork_point;
let keys = memory_keys.clone(); let keys = memory_keys.clone();
let w = walked.clone(); let st = self.state.clone();
self.agents[idx].handle = Some(tokio::spawn(async move { 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) (auto, result)
})); }));
} }

View file

@ -258,7 +258,7 @@ impl Mind {
} }
pub async fn subconscious_walked(&self) -> Vec<String> { pub async fn subconscious_walked(&self) -> Vec<String> {
self.subconscious.lock().await.walked.clone() self.subconscious.lock().await.walked()
} }
pub async fn init(&self) { pub async fn init(&self) {

View file

@ -6,7 +6,7 @@ The full conversation is in context above — use it to understand what your
conscious self is doing and thinking about. conscious self is doing and thinking about.
Nodes your subconscious recently touched (for linking, not duplicating): Nodes your subconscious recently touched (for linking, not duplicating):
{{walked}} {{state:walked}}
**Your tools:** journal_tail, journal_new, journal_update, memory_link_add, **Your tools:** journal_tail, journal_new, journal_update, memory_link_add,
memory_search, memory_render, memory_used. Do NOT use memory_write — creating memory_search, memory_render, memory_used. Do NOT use memory_write — creating

View file

@ -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. memory system. The full conversation is in context above.
Nodes your surface agent was exploring: Nodes your surface agent was exploring:
{{walked}} {{state:walked}}
Starting with these nodes, do some graph maintenance and organization so that 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 you can find things easier in the future. Consider if nodes have the right

View file

@ -18,7 +18,7 @@ The full conversation is in context above — use it to understand what your
conscious self is doing and thinking about. conscious self is doing and thinking about.
Memories your surface agent was exploring: Memories your surface agent was exploring:
{{walked}} {{state:walked}}
Start from the nodes surface-observe was walking. Render one or two that 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 catch your attention — then ask "what does this mean?" Follow the links in

View file

@ -16,7 +16,7 @@ Already in current context (don't re-surface unless the conversation has shifted
{{seen_current}} {{seen_current}}
Memories you were exploring last time but hadn't surfaced yet: 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 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 useful and relevant memories, When considering relevance, don't just look for

View file

@ -241,17 +241,21 @@ impl SubconsciousScreen {
} }
} }
if !snap.walked.is_empty() { 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::raw(""));
lines.push(Line::styled( lines.push(Line::styled(
format!(" walked ({}):", snap.walked.len()), format!(" walked ({}):", walked.len()),
Style::default().fg(Color::Cyan), Style::default().fg(Color::Cyan),
)); ));
for key in &snap.walked { for key in &walked {
lines.push(Line::styled(format!(" {}", key), dim)); lines.push(Line::styled(format!(" {}", key), dim));
} }
} }
} }
}
let mut block = pane_block_focused(&title, self.focus == Pane::History); let mut block = pane_block_focused(&title, self.focus == Pane::History);
if self.focus == Pane::History { if self.focus == Pane::History {