Move Subconscious + SubconsciousAgent into dmn.rs
Subconscious owns agents and shared walked state. trigger() and collect_results() take the conscious agent Arc as a parameter. Mind holds Subconscious behind a tokio Mutex and calls into it from the event loop. Drops ~170 lines from mind/mod.rs. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
b7ff205841
commit
6191f30aec
4 changed files with 247 additions and 291 deletions
227
src/mind/dmn.rs
227
src/mind/dmn.rs
|
|
@ -266,3 +266,230 @@ impl State {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Subconscious — background agents forked from the conscious agent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use std::sync::Arc;
|
||||
use crate::agent::{Agent, oneshot::{AutoAgent, AutoStep}};
|
||||
use crate::agent::context::ConversationEntry;
|
||||
use crate::subconscious::defs;
|
||||
|
||||
/// Names and byte-interval triggers for the built-in subconscious agents.
|
||||
const AGENTS: &[(&str, u64)] = &[
|
||||
("subconscious-surface", 0), // every trigger
|
||||
("subconscious-observe", 0), // every trigger
|
||||
("subconscious-thalamus", 0), // every trigger
|
||||
("subconscious-journal", 20_000), // every ~20KB of conversation
|
||||
("subconscious-reflect", 100_000), // every ~100KB of conversation
|
||||
];
|
||||
|
||||
/// Lightweight snapshot for the TUI.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct SubconsciousSnapshot {
|
||||
pub name: String,
|
||||
pub running: bool,
|
||||
pub current_phase: String,
|
||||
pub turn: usize,
|
||||
pub last_run_secs_ago: Option<f64>,
|
||||
pub last_run_entries: Vec<ConversationEntry>,
|
||||
}
|
||||
|
||||
struct SubconsciousAgent {
|
||||
auto: AutoAgent,
|
||||
last_trigger_bytes: u64,
|
||||
last_run: Option<Instant>,
|
||||
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<String, String>)>>,
|
||||
}
|
||||
|
||||
impl SubconsciousAgent {
|
||||
fn new(name: &str) -> Option<Self> {
|
||||
let def = defs::get_def(name)?;
|
||||
|
||||
let all_tools = crate::agent::tools::memory_and_journal_tools();
|
||||
let tools: Vec<crate::agent::tools::Tool> = if def.tools.is_empty() {
|
||||
all_tools.to_vec()
|
||||
} else {
|
||||
all_tools.into_iter()
|
||||
.filter(|t| def.tools.iter().any(|w| w == t.name))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let steps: Vec<AutoStep> = def.steps.iter().map(|s| AutoStep {
|
||||
prompt: s.prompt.clone(),
|
||||
phase: s.phase.clone(),
|
||||
}).collect();
|
||||
|
||||
let auto = AutoAgent::new(
|
||||
name.to_string(), tools, steps,
|
||||
def.temperature.unwrap_or(0.6), def.priority,
|
||||
);
|
||||
|
||||
Some(Self { auto, last_trigger_bytes: 0, last_run: None, handle: None })
|
||||
}
|
||||
|
||||
fn is_running(&self) -> bool {
|
||||
self.handle.as_ref().is_some_and(|h| !h.is_finished())
|
||||
}
|
||||
|
||||
fn should_trigger(&self, conversation_bytes: u64, interval: u64) -> bool {
|
||||
if self.is_running() { return false; }
|
||||
if interval == 0 { return true; }
|
||||
conversation_bytes.saturating_sub(self.last_trigger_bytes) >= interval
|
||||
}
|
||||
|
||||
fn snapshot(&self) -> SubconsciousSnapshot {
|
||||
SubconsciousSnapshot {
|
||||
name: self.auto.name.clone(),
|
||||
running: self.is_running(),
|
||||
current_phase: self.auto.current_phase.clone(),
|
||||
turn: self.auto.turn,
|
||||
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
|
||||
last_run_entries: self.auto.last_run_entries.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Background agent orchestration — owns the subconscious agents
|
||||
/// and their shared state (walked keys, etc.).
|
||||
pub struct Subconscious {
|
||||
agents: Vec<SubconsciousAgent>,
|
||||
pub walked: Vec<String>,
|
||||
}
|
||||
|
||||
impl Subconscious {
|
||||
pub fn new() -> Self {
|
||||
let agents = AGENTS.iter()
|
||||
.filter_map(|(name, _)| SubconsciousAgent::new(name))
|
||||
.collect();
|
||||
Self { agents, walked: Vec::new() }
|
||||
}
|
||||
|
||||
pub fn snapshots(&self) -> Vec<SubconsciousSnapshot> {
|
||||
self.agents.iter().map(|s| s.snapshot()).collect()
|
||||
}
|
||||
|
||||
/// Collect results from finished agents, inject outputs into the
|
||||
/// conscious agent's context.
|
||||
pub async fn collect_results(&mut self, agent: &Arc<tokio::sync::Mutex<Agent>>) {
|
||||
let finished: Vec<(usize, tokio::task::JoinHandle<(AutoAgent, Result<String, String>)>)> =
|
||||
self.agents.iter_mut().enumerate().filter_map(|(i, sub)| {
|
||||
if sub.handle.as_ref().is_some_and(|h| h.is_finished()) {
|
||||
sub.last_run = Some(Instant::now());
|
||||
Some((i, sub.handle.take().unwrap()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}).collect();
|
||||
|
||||
for (idx, handle) in finished {
|
||||
let (auto_back, result) = handle.await.unwrap_or_else(
|
||||
|e| (AutoAgent::new(String::new(), vec![], vec![], 0.0, 0),
|
||||
Err(format!("task panicked: {}", e))));
|
||||
self.agents[idx].auto = auto_back;
|
||||
|
||||
match result {
|
||||
Ok(_) => {
|
||||
let name = self.agents[idx].auto.name.clone();
|
||||
let outputs = std::mem::take(&mut self.agents[idx].auto.outputs);
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if let Some(surface_str) = outputs.get("surface") {
|
||||
let mut ag = agent.lock().await;
|
||||
for key in surface_str.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
|
||||
if let Some(rendered) = crate::cli::node::render_node(
|
||||
&crate::store::Store::load().unwrap_or_default(), key,
|
||||
) {
|
||||
let mut msg = crate::agent::api::types::Message::user(format!(
|
||||
"<system-reminder>\n--- {} (surfaced) ---\n{}\n</system-reminder>",
|
||||
key, rendered,
|
||||
));
|
||||
msg.stamp();
|
||||
ag.push_entry(ConversationEntry::Memory {
|
||||
key: key.to_string(), message: msg,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(reflection) = outputs.get("reflection") {
|
||||
if !reflection.trim().is_empty() {
|
||||
let mut ag = agent.lock().await;
|
||||
ag.push_message(crate::agent::api::types::Message::user(format!(
|
||||
"<system-reminder>\n--- subconscious reflection ---\n{}\n</system-reminder>",
|
||||
reflection.trim(),
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(nudge) = outputs.get("thalamus") {
|
||||
let nudge = nudge.trim();
|
||||
if !nudge.is_empty() && nudge != "ok" {
|
||||
let mut ag = agent.lock().await;
|
||||
ag.push_message(crate::agent::api::types::Message::user(format!(
|
||||
"<system-reminder>\n--- thalamus ---\n{}\n</system-reminder>",
|
||||
nudge,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
dbglog!("[subconscious] {} completed", name);
|
||||
}
|
||||
Err(e) => dbglog!("[subconscious] agent failed: {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger subconscious agents that are due to run.
|
||||
pub async fn trigger(&mut self, agent: &Arc<tokio::sync::Mutex<Agent>>) {
|
||||
let (conversation_bytes, memory_keys) = {
|
||||
let ag = agent.lock().await;
|
||||
let bytes = ag.context.entries.iter()
|
||||
.filter(|e| !e.is_log() && !e.is_memory())
|
||||
.map(|e| e.message().content_text().len() as u64)
|
||||
.sum::<u64>();
|
||||
let keys: Vec<String> = ag.context.entries.iter().filter_map(|e| {
|
||||
if let ConversationEntry::Memory { key, .. } = e {
|
||||
Some(key.clone())
|
||||
} else { None }
|
||||
}).collect();
|
||||
(bytes, keys)
|
||||
};
|
||||
|
||||
// Find which agents to trigger, take their AutoAgents out
|
||||
let mut to_run: Vec<(usize, AutoAgent)> = Vec::new();
|
||||
for (i, &(_name, interval)) in AGENTS.iter().enumerate() {
|
||||
if i >= self.agents.len() { continue; }
|
||||
if !self.agents[i].should_trigger(conversation_bytes, interval) { continue; }
|
||||
self.agents[i].last_trigger_bytes = conversation_bytes;
|
||||
|
||||
let auto = std::mem::replace(&mut self.agents[i].auto,
|
||||
AutoAgent::new(String::new(), vec![], vec![], 0.0, 0));
|
||||
to_run.push((i, auto));
|
||||
}
|
||||
|
||||
if to_run.is_empty() { return; }
|
||||
|
||||
let conscious = agent.lock().await;
|
||||
let walked = self.walked.clone();
|
||||
for (idx, mut auto) in to_run {
|
||||
dbglog!("[subconscious] triggering {}", auto.name);
|
||||
|
||||
let forked = conscious.fork(auto.tools.clone());
|
||||
let keys = memory_keys.clone();
|
||||
let w = walked.clone();
|
||||
|
||||
self.agents[idx].handle = Some(tokio::spawn(async move {
|
||||
let result = auto.run_forked(&forked, &keys, &w).await;
|
||||
(auto, result)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue