consciousness/src/mind/unconscious.rs
ProofOfConcept 7aba17e5f0 Compute graph health in consciousness, rename F4 to hippocampus
Graph health stats (alpha, gini, cc, episodic ratio, consolidation
plan) now computed directly by the unconscious module on startup and
every 10 minutes, instead of fetching from the poc-memory daemon.

F4 screen renamed to hippocampus, stripped down to just the health
gauges — daemon task list removed (agents now shown on F3).

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
2026-04-09 00:45:26 -04:00

207 lines
5.9 KiB
Rust

// unconscious.rs — Graph maintenance agents
//
// Standalone agents that operate on the memory graph without needing
// conversation context. Each agent runs in a loop: finish one run,
// wait a cooldown, start the next. Agents can be toggled on/off.
use std::time::{Duration, Instant};
use crate::agent::oneshot::{AutoAgent, AutoStep};
use crate::agent::tools;
use crate::subconscious::defs;
/// Agent types to run. Each must have a matching .agent file.
const AGENTS: &[&str] = &[
"organize",
"linker",
"distill",
"split",
"separator",
];
/// Cooldown between consecutive runs of the same agent.
const COOLDOWN: Duration = Duration::from_secs(120);
struct UnconsciousAgent {
name: String,
enabled: bool,
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<String, String>)>>,
last_run: Option<Instant>,
runs: usize,
}
impl UnconsciousAgent {
fn new(name: &str) -> Self {
Self {
name: name.to_string(),
enabled: true,
handle: None,
last_run: None,
runs: 0,
}
}
fn is_running(&self) -> bool {
self.handle.as_ref().is_some_and(|h| !h.is_finished())
}
fn should_run(&self) -> bool {
if !self.enabled || self.is_running() { return false; }
match self.last_run {
Some(t) => t.elapsed() >= COOLDOWN,
None => true,
}
}
}
/// Snapshot for the TUI.
#[derive(Clone)]
pub struct UnconsciousSnapshot {
pub name: String,
pub running: bool,
pub enabled: bool,
pub runs: usize,
pub last_run_secs_ago: Option<f64>,
}
pub struct Unconscious {
agents: Vec<UnconsciousAgent>,
max_concurrent: usize,
pub graph_health: Option<crate::subconscious::daemon::GraphHealth>,
last_health_check: Option<Instant>,
}
impl Unconscious {
pub fn new() -> Self {
let agents = AGENTS.iter()
.filter(|name| defs::get_def(name).is_some())
.map(|name| UnconsciousAgent::new(name))
.collect();
let mut s = Self {
agents, max_concurrent: 2,
graph_health: None,
last_health_check: None,
};
s.refresh_health();
s
}
/// Toggle an agent on/off by name. Returns new enabled state.
pub fn toggle(&mut self, name: &str) -> Option<bool> {
let agent = self.agents.iter_mut().find(|a| a.name == name)?;
agent.enabled = !agent.enabled;
Some(agent.enabled)
}
pub fn snapshots(&self) -> Vec<UnconsciousSnapshot> {
self.agents.iter().map(|a| UnconsciousSnapshot {
name: a.name.clone(),
running: a.is_running(),
enabled: a.enabled,
runs: a.runs,
last_run_secs_ago: a.last_run.map(|t| t.elapsed().as_secs_f64()),
}).collect()
}
fn refresh_health(&mut self) {
let store = match crate::store::Store::load() {
Ok(s) => s,
Err(_) => return,
};
self.graph_health = Some(crate::subconscious::daemon::compute_graph_health(&store));
self.last_health_check = Some(Instant::now());
}
/// Reap finished agents and spawn new ones.
pub fn trigger(&mut self) {
// Periodic graph health refresh
if self.last_health_check
.map(|t| t.elapsed() > Duration::from_secs(600))
.unwrap_or(false)
{
self.refresh_health();
}
for agent in &mut self.agents {
if agent.handle.as_ref().is_some_and(|h| h.is_finished()) {
agent.last_run = Some(Instant::now());
agent.runs += 1;
dbglog!("[unconscious] {} completed (run {})",
agent.name, agent.runs);
agent.handle = None;
}
}
let running = self.agents.iter().filter(|a| a.is_running()).count();
if running >= self.max_concurrent { return; }
let slots = self.max_concurrent - running;
let ready: Vec<usize> = self.agents.iter().enumerate()
.filter(|(_, a)| a.should_run())
.map(|(i, _)| i)
.take(slots)
.collect();
for idx in ready {
self.spawn_agent(idx);
}
}
fn spawn_agent(&mut self, idx: usize) {
let name = self.agents[idx].name.clone();
dbglog!("[unconscious] spawning {}", name);
let def = match defs::get_def(&name) {
Some(d) => d,
None => return,
};
let all_tools = tools::memory_and_journal_tools();
let effective_tools: Vec<tools::Tool> = if def.tools.is_empty() {
all_tools
} else {
all_tools.into_iter()
.filter(|t| def.tools.iter().any(|w| w == t.name))
.collect()
};
let mut store = match crate::store::Store::load() {
Ok(s) => s,
Err(e) => {
dbglog!("[unconscious] store load failed: {}", e);
return;
}
};
let exclude: std::collections::HashSet<String> = std::collections::HashSet::new();
let batch = match defs::run_agent(
&store, &def, def.count.unwrap_or(5), &exclude,
) {
Ok(b) => b,
Err(e) => {
dbglog!("[unconscious] {} query failed: {}", name, e);
return;
}
};
if !batch.node_keys.is_empty() {
store.record_agent_visits(&batch.node_keys, &name).ok();
}
let steps: Vec<AutoStep> = batch.steps.iter().map(|s| AutoStep {
prompt: s.prompt.clone(),
phase: s.phase.clone(),
}).collect();
let mut auto = AutoAgent::new(
name, effective_tools, steps,
def.temperature.unwrap_or(0.6),
def.priority,
);
self.agents[idx].handle = Some(tokio::spawn(async move {
let result = auto.run(None).await;
(auto, result)
}));
}
}