Shared subconscious state — walked keys are Mind-level, not per-agent

SubconsciousSharedState holds walked keys shared between all
subconscious agents. Enables splitting surface-observe into separate
surface and observe agents that share the same walked state.

Walked is passed to run_forked() at run time instead of living on
AutoAgent. UI shows walked count in the subconscious screen header.

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
Kent Overstreet 2026-04-07 02:13:06 -04:00
parent ef868cb98f
commit f3ba7e7097
4 changed files with 37 additions and 23 deletions

View file

@ -54,8 +54,6 @@ pub struct AutoAgent {
pub steps: Vec<AutoStep>,
sampling: super::api::SamplingParams,
priority: i32,
/// Memory keys the surface agent was exploring — persists between runs.
pub walked: Vec<String>,
/// Named outputs from the agent's output() tool calls.
/// Collected per-run, read by Mind after completion.
pub outputs: std::collections::HashMap<String, String>,
@ -164,7 +162,6 @@ impl AutoAgent {
temperature, top_p: 0.95, top_k: 20,
},
priority,
walked: Vec::new(),
outputs: std::collections::HashMap::new(),
last_run_entries: Vec::new(),
current_phase: String::new(),
@ -186,18 +183,18 @@ impl AutoAgent {
}
/// Run forked from a conscious agent's context. Each call gets a
/// fresh fork for KV cache sharing. Walked state persists between runs.
/// fresh fork for KV cache sharing.
///
/// `memory_keys`: keys of Memory entries in the conscious agent's
/// context, used to resolve {{seen_current}} in prompt templates.
/// `memory_keys`: Memory entry keys from conscious context (for {{seen_current}}).
/// `walked`: shared walked keys from previous runs (for {{walked}}).
pub async fn run_forked(
&mut self,
agent: &Agent,
memory_keys: &[String],
walked: &[String],
) -> Result<String, String> {
// Resolve prompt templates with current state
let resolved_steps: Vec<AutoStep> = self.steps.iter().map(|s| AutoStep {
prompt: resolve_prompt(&s.prompt, memory_keys, &self.walked),
prompt: resolve_prompt(&s.prompt, memory_keys, walked),
phase: s.phase.clone(),
}).collect();
let orig_steps = std::mem::replace(&mut self.steps, resolved_steps);
@ -205,7 +202,6 @@ impl AutoAgent {
let fork_point = forked.context.entries.len();
let mut backend = Backend::Forked(forked);
let result = self.run_with_backend(&mut backend, None).await;
// Capture entries added during this run
if let Backend::Forked(ref agent) = backend {
self.last_run_entries = agent.context.entries[fork_point..].to_vec();
}

View file

@ -91,6 +91,14 @@ impl SubconsciousAgent {
}
}
/// State shared between all subconscious agents. Lives on Mind,
/// passed to agents at run time. Enables splitting surface/observe
/// into separate agents that share walked keys.
#[derive(Clone, Default)]
pub struct SubconsciousSharedState {
pub walked: Vec<String>,
}
/// Lightweight snapshot of subconscious agent state for the TUI.
#[derive(Clone, Default)]
pub struct SubconsciousSnapshot {
@ -98,7 +106,6 @@ pub struct SubconsciousSnapshot {
pub running: bool,
pub current_phase: String,
pub turn: usize,
pub walked_count: usize,
pub last_run_secs_ago: Option<f64>,
/// Entries from the last forked run (after fork point).
pub last_run_entries: Vec<crate::agent::context::ConversationEntry>,
@ -111,7 +118,6 @@ impl SubconsciousAgent {
running: self.is_running(),
current_phase: self.auto.current_phase.clone(),
turn: self.auto.turn,
walked_count: self.auto.walked.len(),
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
last_run_entries: self.auto.last_run_entries.clone(),
}
@ -293,6 +299,7 @@ pub struct Mind {
pub shared: Arc<SharedMindState>,
pub config: SessionConfig,
subconscious: Arc<tokio::sync::Mutex<Vec<SubconsciousAgent>>>,
subconscious_state: Arc<tokio::sync::Mutex<SubconsciousSharedState>>,
turn_tx: mpsc::Sender<(Result<TurnResult>, StreamTarget)>,
turn_watch: tokio::sync::watch::Sender<bool>,
bg_tx: mpsc::UnboundedSender<BgEvent>,
@ -337,14 +344,19 @@ impl Mind {
sup.load_config();
sup.ensure_running();
Self { agent, shared, config, subconscious: Arc::new(tokio::sync::Mutex::new(subconscious)),
let subconscious_state = Arc::new(tokio::sync::Mutex::new(SubconsciousSharedState::default()));
Self { agent, shared, config,
subconscious: Arc::new(tokio::sync::Mutex::new(subconscious)),
subconscious_state,
turn_tx, turn_watch, bg_tx,
bg_rx: std::sync::Mutex::new(Some(bg_rx)), _supervisor: sup }
}
/// Initialize — restore log, start daemons and background agents.
pub async fn subconscious_snapshots(&self) -> Vec<SubconsciousSnapshot> {
self.subconscious.lock().await.iter().map(|s| s.snapshot()).collect()
pub async fn subconscious_snapshots(&self) -> (Vec<SubconsciousSnapshot>, SubconsciousSharedState) {
let snaps = self.subconscious.lock().await.iter().map(|s| s.snapshot()).collect();
let shared = self.subconscious_state.lock().await.clone();
(snaps, shared)
}
pub async fn init(&self) {
@ -468,15 +480,13 @@ impl Mind {
let name = subs[idx].auto.name.clone();
let outputs = std::mem::take(&mut subs[idx].auto.outputs);
// Walked keys — update all subconscious agents
// Walked keys — update shared state
if let Some(walked_str) = outputs.get("walked") {
let walked: Vec<String> = walked_str.lines()
.map(|l| l.trim().to_string())
.filter(|l| !l.is_empty())
.collect();
for sub in subs.iter_mut() {
sub.auto.walked = walked.clone();
}
self.subconscious_state.lock().await.walked = walked;
}
drop(subs);
@ -557,15 +567,17 @@ impl Mind {
// Fork from conscious agent and spawn tasks
let conscious = self.agent.lock().await;
let walked = self.subconscious_state.lock().await.walked.clone();
let mut spawns = Vec::new();
for (idx, mut auto) in to_run {
dbglog!("[mind] triggering {}", auto.name);
let forked = conscious.fork(auto.tools.clone());
let keys = memory_keys.clone();
let w = walked.clone();
let handle: tokio::task::JoinHandle<(AutoAgent, Result<String, String>)> =
tokio::spawn(async move {
let result = auto.run_forked(&forked, &keys).await;
let result = auto.run_forked(&forked, &keys, &w).await;
(auto, result)
});
spawns.push((idx, handle));

View file

@ -128,6 +128,7 @@ pub struct App {
pub(crate) context_info: Option<ContextInfo>,
pub(crate) shared_context: SharedContextState,
pub(crate) agent_state: Vec<crate::mind::SubconsciousSnapshot>,
pub(crate) subconscious_shared: crate::mind::SubconsciousSharedState,
pub(crate) channel_status: Vec<ChannelStatus>,
pub(crate) idle_info: Option<IdleInfo>,
}
@ -150,6 +151,7 @@ impl App {
should_quit: false, submitted: Vec::new(),
context_info: None, shared_context,
agent_state: Vec::new(),
subconscious_shared: Default::default(),
channel_status: Vec::new(), idle_info: None,
}
}
@ -406,7 +408,9 @@ pub async fn run(
// State sync on every wake
idle_state.decay_ewma();
app.update_idle(&idle_state);
app.agent_state = mind.subconscious_snapshots().await;
let (snaps, shared) = mind.subconscious_snapshots().await;
app.agent_state = snaps;
app.subconscious_shared = shared;
if !startup_done {
if let Ok(mut ag) = agent.try_lock() {
let model = ag.model().to_string();

View file

@ -76,7 +76,9 @@ impl SubconsciousScreen {
let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC);
lines.push(Line::raw(""));
lines.push(Line::styled("── Subconscious Agents ──", section));
let walked = app.subconscious_shared.walked.len();
lines.push(Line::styled(
format!("── Subconscious Agents ── walked: {}", walked), section));
lines.push(Line::styled(" (↑/↓ select, Enter view log)", hint));
lines.push(Line::raw(""));
@ -121,8 +123,8 @@ impl SubconsciousScreen {
),
Span::styled("", bg.fg(Color::DarkGray)),
Span::styled(
format!("idle last: {} entries: {} walked: {}",
ago, entries, snap.walked_count),
format!("idle last: {} entries: {}",
ago, entries),
bg.fg(Color::DarkGray),
),
]