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:
parent
578be807e7
commit
27ca3c058d
8 changed files with 60 additions and 80 deletions
|
|
@ -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,14 +116,17 @@ 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:") {
|
||||||
"seen_current" => format_key_list(memory_keys),
|
state.get(key).cloned().unwrap_or_else(|| "(not set)".to_string())
|
||||||
"walked" => format_key_list(walked),
|
} else {
|
||||||
_ => {
|
match name {
|
||||||
result.push_str("{{");
|
"seen_current" => format_key_list(memory_keys),
|
||||||
result.push_str(&after[..end + 2]);
|
_ => {
|
||||||
rest = &after[end + 2..];
|
result.push_str("{{");
|
||||||
continue;
|
result.push_str(&after[..end + 2]);
|
||||||
|
rest = &after[end + 2..];
|
||||||
|
continue;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
result.push_str(&replacement);
|
result.push_str(&replacement);
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -241,14 +241,18 @@ impl SubconsciousScreen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !snap.walked.is_empty() {
|
if let Some(walked_str) = snap.state.get("walked") {
|
||||||
lines.push(Line::raw(""));
|
let walked: Vec<&str> = walked_str.lines()
|
||||||
lines.push(Line::styled(
|
.map(|l| l.trim()).filter(|l| !l.is_empty()).collect();
|
||||||
format!(" walked ({}):", snap.walked.len()),
|
if !walked.is_empty() {
|
||||||
Style::default().fg(Color::Cyan),
|
lines.push(Line::raw(""));
|
||||||
));
|
lines.push(Line::styled(
|
||||||
for key in &snap.walked {
|
format!(" walked ({}):", walked.len()),
|
||||||
lines.push(Line::styled(format!(" {}", key), dim));
|
Style::default().fg(Color::Cyan),
|
||||||
|
));
|
||||||
|
for key in &walked {
|
||||||
|
lines.push(Line::styled(format!(" {}", key), dim));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue