2026-04-08 23:39:48 -04:00
|
|
|
// unconscious.rs — Graph maintenance agents
|
|
|
|
|
//
|
|
|
|
|
// Standalone agents that operate on the memory graph without needing
|
2026-04-09 00:41:18 -04:00
|
|
|
// conversation context. Each agent runs in a loop: finish one run,
|
|
|
|
|
// wait a cooldown, start the next. Agents can be toggled on/off.
|
2026-04-08 23:39:48 -04:00
|
|
|
|
|
|
|
|
use std::time::{Duration, Instant};
|
|
|
|
|
|
|
|
|
|
use crate::agent::oneshot::{AutoAgent, AutoStep};
|
|
|
|
|
use crate::agent::tools;
|
|
|
|
|
use crate::subconscious::defs;
|
|
|
|
|
|
2026-04-09 00:41:18 -04:00
|
|
|
/// 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);
|
|
|
|
|
|
2026-04-08 23:39:48 -04:00
|
|
|
struct UnconsciousAgent {
|
|
|
|
|
name: String,
|
2026-04-09 00:41:18 -04:00
|
|
|
enabled: bool,
|
2026-04-08 23:39:48 -04:00
|
|
|
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<String, String>)>>,
|
|
|
|
|
last_run: Option<Instant>,
|
2026-04-09 00:41:18 -04:00
|
|
|
runs: usize,
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl UnconsciousAgent {
|
2026-04-09 00:41:18 -04:00
|
|
|
fn new(name: &str) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
enabled: true,
|
|
|
|
|
handle: None,
|
|
|
|
|
last_run: None,
|
|
|
|
|
runs: 0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-08 23:39:48 -04:00
|
|
|
fn is_running(&self) -> bool {
|
|
|
|
|
self.handle.as_ref().is_some_and(|h| !h.is_finished())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn should_run(&self) -> bool {
|
2026-04-09 00:41:18 -04:00
|
|
|
if !self.enabled || self.is_running() { return false; }
|
|
|
|
|
match self.last_run {
|
|
|
|
|
Some(t) => t.elapsed() >= COOLDOWN,
|
|
|
|
|
None => true,
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Snapshot for the TUI.
|
|
|
|
|
#[derive(Clone)]
|
|
|
|
|
pub struct UnconsciousSnapshot {
|
|
|
|
|
pub name: String,
|
|
|
|
|
pub running: bool,
|
2026-04-09 00:41:18 -04:00
|
|
|
pub enabled: bool,
|
|
|
|
|
pub runs: usize,
|
2026-04-08 23:39:48 -04:00
|
|
|
pub last_run_secs_ago: Option<f64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct Unconscious {
|
|
|
|
|
agents: Vec<UnconsciousAgent>,
|
|
|
|
|
max_concurrent: usize,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Unconscious {
|
|
|
|
|
pub fn new() -> Self {
|
2026-04-09 00:41:18 -04:00
|
|
|
let agents = AGENTS.iter()
|
|
|
|
|
.filter(|name| defs::get_def(name).is_some())
|
|
|
|
|
.map(|name| UnconsciousAgent::new(name))
|
|
|
|
|
.collect();
|
|
|
|
|
Self { agents, max_concurrent: 2 }
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
|
2026-04-09 00:41:18 -04:00
|
|
|
/// 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)
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn snapshots(&self) -> Vec<UnconsciousSnapshot> {
|
|
|
|
|
self.agents.iter().map(|a| UnconsciousSnapshot {
|
|
|
|
|
name: a.name.clone(),
|
|
|
|
|
running: a.is_running(),
|
2026-04-09 00:41:18 -04:00
|
|
|
enabled: a.enabled,
|
|
|
|
|
runs: a.runs,
|
2026-04-08 23:39:48 -04:00
|
|
|
last_run_secs_ago: a.last_run.map(|t| t.elapsed().as_secs_f64()),
|
|
|
|
|
}).collect()
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 00:41:18 -04:00
|
|
|
/// Reap finished agents and spawn new ones.
|
2026-04-09 00:21:46 -04:00
|
|
|
pub fn trigger(&mut self) {
|
2026-04-08 23:39:48 -04:00
|
|
|
for agent in &mut self.agents {
|
|
|
|
|
if agent.handle.as_ref().is_some_and(|h| h.is_finished()) {
|
|
|
|
|
agent.last_run = Some(Instant::now());
|
2026-04-09 00:41:18 -04:00
|
|
|
agent.runs += 1;
|
|
|
|
|
dbglog!("[unconscious] {} completed (run {})",
|
|
|
|
|
agent.name, agent.runs);
|
2026-04-09 00:21:46 -04:00
|
|
|
agent.handle = None;
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let running = self.agents.iter().filter(|a| a.is_running()).count();
|
|
|
|
|
if running >= self.max_concurrent { return; }
|
|
|
|
|
let slots = self.max_concurrent - running;
|
|
|
|
|
|
2026-04-09 00:41:18 -04:00
|
|
|
let ready: Vec<usize> = self.agents.iter().enumerate()
|
2026-04-08 23:39:48 -04:00
|
|
|
.filter(|(_, a)| a.should_run())
|
|
|
|
|
.map(|(i, _)| i)
|
2026-04-09 00:41:18 -04:00
|
|
|
.take(slots)
|
2026-04-08 23:39:48 -04:00
|
|
|
.collect();
|
|
|
|
|
|
2026-04-09 00:41:18 -04:00
|
|
|
for idx in ready {
|
2026-04-08 23:39:48 -04:00
|
|
|
self.spawn_agent(idx);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn spawn_agent(&mut self, idx: usize) {
|
|
|
|
|
let name = self.agents[idx].name.clone();
|
2026-04-09 00:41:18 -04:00
|
|
|
dbglog!("[unconscious] spawning {}", name);
|
2026-04-08 23:39:48 -04:00
|
|
|
|
|
|
|
|
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()
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-09 00:21:46 -04:00
|
|
|
let mut store = match crate::store::Store::load() {
|
2026-04-08 23:39:48 -04:00
|
|
|
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() {
|
2026-04-09 00:21:46 -04:00
|
|
|
store.record_agent_visits(&batch.node_keys, &name).ok();
|
2026-04-08 23:39:48 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let steps: Vec<AutoStep> = batch.steps.iter().map(|s| AutoStep {
|
|
|
|
|
prompt: s.prompt.clone(),
|
|
|
|
|
phase: s.phase.clone(),
|
|
|
|
|
}).collect();
|
|
|
|
|
|
|
|
|
|
let mut auto = AutoAgent::new(
|
2026-04-09 00:41:18 -04:00
|
|
|
name, effective_tools, steps,
|
2026-04-08 23:39:48 -04:00
|
|
|
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)
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
}
|