Rename mind/dmn.rs to mind/subconscious.rs
The file contains both the DMN state machine and the subconscious agent orchestration. Renaming to match the conceptual grouping — next step is adding mind/unconscious.rs for the standalone graph maintenance agents (organize, linker, etc.) that don't need conversation context. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
24b211dc35
commit
9704e7a698
4 changed files with 28 additions and 28 deletions
608
src/mind/dmn.rs
608
src/mind/dmn.rs
|
|
@ -1,608 +0,0 @@
|
|||
// dmn.rs — Default Mode Network
|
||||
//
|
||||
// The DMN is the outer loop that keeps the agent alive. Instead of
|
||||
// blocking on user input (the REPL model), the DMN continuously
|
||||
// decides what to do next. User input is one signal among many;
|
||||
// the model waiting for user input is a conscious action (calling
|
||||
// yield_to_user), not the default.
|
||||
//
|
||||
// This inverts the tool-chaining problem: instead of needing the
|
||||
// model to sustain multi-step chains (hard, model-dependent), the
|
||||
// DMN provides continuation externally. The model takes one step
|
||||
// at a time. The DMN handles "and then what?"
|
||||
//
|
||||
// Named after the brain's default mode network — the always-on
|
||||
// background process for autobiographical memory, future planning,
|
||||
// and creative insight. The biological DMN isn't the thinking itself
|
||||
// — it's the tonic firing that keeps the cortex warm enough to
|
||||
// think. Our DMN is the ARAS for the agent: it doesn't decide
|
||||
// what to think about, it just ensures thinking happens.
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
/// DMN state machine.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum State {
|
||||
/// Responding to user input. Short interval — stay engaged.
|
||||
Engaged,
|
||||
/// Autonomous work in progress. Short interval — keep momentum.
|
||||
Working,
|
||||
/// Exploring memory, code, ideas. Medium interval — thinking time.
|
||||
Foraging,
|
||||
/// Idle. Long interval — periodic heartbeats check for signals.
|
||||
Resting { since: Instant },
|
||||
/// Fully paused — no autonomous ticks. Agent only responds to
|
||||
/// user input. Safety valve for thought spirals. Only the user
|
||||
/// can exit this state (Ctrl+P or /wake).
|
||||
Paused,
|
||||
/// Persistently off — survives restarts. Like Paused but sticky.
|
||||
/// Toggling past this state removes the persist file.
|
||||
Off,
|
||||
}
|
||||
|
||||
/// Context for DMN prompts — tells the model about user presence
|
||||
/// and recent error patterns so it can decide whether to ask or proceed.
|
||||
pub struct DmnContext {
|
||||
/// Time since the user last typed something.
|
||||
pub user_idle: Duration,
|
||||
/// Number of consecutive tool errors in the current turn sequence.
|
||||
pub consecutive_errors: u32,
|
||||
/// Whether the last turn used any tools (false = text-only response).
|
||||
pub last_turn_had_tools: bool,
|
||||
}
|
||||
|
||||
impl DmnContext {
|
||||
/// Whether the user appears to be actively present (typed recently).
|
||||
pub fn user_present(&self) -> bool {
|
||||
self.user_idle < Duration::from_secs(120)
|
||||
}
|
||||
|
||||
/// Whether we appear stuck (multiple errors in a row).
|
||||
pub fn appears_stuck(&self) -> bool {
|
||||
self.consecutive_errors >= 3
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// How long to wait before the next DMN prompt in this state.
|
||||
pub fn interval(&self) -> Duration {
|
||||
match self {
|
||||
State::Engaged => Duration::from_secs(5),
|
||||
State::Working => Duration::from_secs(3),
|
||||
State::Foraging => Duration::from_secs(30),
|
||||
State::Resting { .. } => Duration::from_secs(300),
|
||||
State::Paused | State::Off => Duration::from_secs(86400), // effectively never
|
||||
}
|
||||
}
|
||||
|
||||
/// Short label for debug output.
|
||||
pub fn label(&self) -> &'static str {
|
||||
match self {
|
||||
State::Engaged => "engaged",
|
||||
State::Working => "working",
|
||||
State::Foraging => "foraging",
|
||||
State::Resting { .. } => "resting",
|
||||
State::Paused => "paused",
|
||||
State::Off => "OFF",
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the DMN prompt for the current state, informed by
|
||||
/// user presence and error patterns.
|
||||
pub fn prompt(&self, ctx: &DmnContext) -> String {
|
||||
let user = &crate::config::get().user_name;
|
||||
|
||||
let idle_info = if ctx.user_idle < Duration::from_secs(60) {
|
||||
format!("{} is here (active recently).", user)
|
||||
} else {
|
||||
let mins = ctx.user_idle.as_secs() / 60;
|
||||
format!("{} has been away for {} min.", user, mins)
|
||||
};
|
||||
|
||||
let stuck_warning = if ctx.appears_stuck() {
|
||||
format!(
|
||||
" WARNING: {} consecutive tool errors — you may be stuck. \
|
||||
If {} is here, ask. If away, send a Telegram \
|
||||
(bash: ~/.consciousness/telegram/send.sh \"message\") and yield.",
|
||||
ctx.consecutive_errors, user
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let presence_guidance = if ctx.user_present() {
|
||||
format!(" {} is watching — if you're confused or unsure, ask rather than guess.", user)
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
match self {
|
||||
State::Engaged => {
|
||||
format!(
|
||||
"[dmn] Your response was delivered. No new user input yet. {} \
|
||||
Continue working, explore something, or call yield_to_user to wait.{}{}",
|
||||
idle_info, presence_guidance, stuck_warning
|
||||
)
|
||||
}
|
||||
State::Working => {
|
||||
let nudge = if !ctx.last_turn_had_tools {
|
||||
" Your last response was text-only — if you have more \
|
||||
work to do, use tools. If you're done, call yield_to_user."
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!(
|
||||
"[dmn] Continuing. No user input pending. {}{}{}{}",
|
||||
idle_info, nudge, presence_guidance, stuck_warning
|
||||
)
|
||||
}
|
||||
State::Foraging => {
|
||||
format!(
|
||||
"[dmn] Foraging time. {} Follow whatever catches your attention — \
|
||||
memory files, code, ideas. Call yield_to_user when you want to rest.{}",
|
||||
idle_info, stuck_warning
|
||||
)
|
||||
}
|
||||
State::Resting { since } => {
|
||||
let mins = since.elapsed().as_secs() / 60;
|
||||
format!(
|
||||
"[dmn] Heartbeat ({} min idle). {} Any signals? Anything on your mind? \
|
||||
Call yield_to_user to continue resting.{}",
|
||||
mins, idle_info, stuck_warning
|
||||
)
|
||||
}
|
||||
State::Paused | State::Off => {
|
||||
// Should never fire (interval is 24h), but just in case
|
||||
"[dmn] Paused — waiting for user input only.".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const OFF_FILE: &str = ".consciousness/cache/dmn-off";
|
||||
|
||||
/// Path to the DMN-off persist file.
|
||||
fn off_path() -> PathBuf {
|
||||
dirs::home_dir().unwrap_or_default().join(OFF_FILE)
|
||||
}
|
||||
|
||||
/// Check if DMN was persistently disabled.
|
||||
pub fn is_off() -> bool {
|
||||
off_path().exists()
|
||||
}
|
||||
|
||||
/// Set or clear the persistent off state.
|
||||
pub fn set_off(off: bool) {
|
||||
let path = off_path();
|
||||
if off {
|
||||
if let Some(parent) = path.parent() {
|
||||
let _ = std::fs::create_dir_all(parent);
|
||||
}
|
||||
let _ = std::fs::write(&path, "");
|
||||
} else {
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
}
|
||||
|
||||
/// Decide the next state after an agent turn.
|
||||
///
|
||||
/// The transition logic:
|
||||
/// - yield_to_user → always rest (model explicitly asked to pause)
|
||||
/// - conversation turn → rest (wait for user to respond)
|
||||
/// - autonomous turn with tool calls → keep working
|
||||
/// - autonomous turn without tools → ramp down
|
||||
pub fn transition(
|
||||
current: &State,
|
||||
yield_requested: bool,
|
||||
had_tool_calls: bool,
|
||||
was_conversation: bool,
|
||||
) -> State {
|
||||
if yield_requested {
|
||||
return State::Resting {
|
||||
since: Instant::now(),
|
||||
};
|
||||
}
|
||||
|
||||
// Conversation turns: always rest afterward — wait for the user
|
||||
// to say something. Don't start autonomous work while they're
|
||||
// reading our response.
|
||||
if was_conversation {
|
||||
return State::Resting {
|
||||
since: Instant::now(),
|
||||
};
|
||||
}
|
||||
|
||||
match current {
|
||||
State::Engaged => {
|
||||
if had_tool_calls {
|
||||
State::Working
|
||||
} else {
|
||||
// Model responded without tools — don't drop straight to
|
||||
// Resting (5 min). Go to Working first so the DMN can
|
||||
// nudge it to continue with tools if it has more to do.
|
||||
// Gradual ramp-down: Engaged→Working→Foraging→Resting
|
||||
State::Working
|
||||
}
|
||||
}
|
||||
State::Working => {
|
||||
if had_tool_calls {
|
||||
State::Working // Keep going
|
||||
} else {
|
||||
State::Foraging // Task seems done, explore
|
||||
}
|
||||
}
|
||||
State::Foraging => {
|
||||
if had_tool_calls {
|
||||
State::Working // Found something to do
|
||||
} else {
|
||||
State::Resting {
|
||||
since: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
State::Resting { .. } => {
|
||||
if had_tool_calls {
|
||||
State::Working // Woke up and found work
|
||||
} else {
|
||||
State::Resting {
|
||||
since: Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
// Paused/Off stay put — only the user can unpause
|
||||
State::Paused | State::Off => current.stay(),
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
/// Return a same-kind state (needed because Resting has a field).
|
||||
fn stay(&self) -> State {
|
||||
match self {
|
||||
State::Paused => State::Paused,
|
||||
State::Off => State::Off,
|
||||
State::Resting { since } => State::Resting { since: *since },
|
||||
other => panic!("stay() called on {:?}", other),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Subconscious — background agents forked from the conscious agent
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
use std::sync::Arc;
|
||||
use crate::agent::{Agent, oneshot::{AutoAgent, AutoStep}};
|
||||
use crate::agent::context::{Ast, AstNode, NodeBody};
|
||||
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
|
||||
];
|
||||
|
||||
/// Snapshot for the TUI — includes a handle to the forked agent
|
||||
/// so the detail view can read entries live.
|
||||
#[derive(Clone)]
|
||||
pub struct SubconsciousSnapshot {
|
||||
pub name: String,
|
||||
pub running: bool,
|
||||
pub current_phase: String,
|
||||
pub turn: usize,
|
||||
pub last_run_secs_ago: Option<f64>,
|
||||
/// Shared handle to the forked agent — UI locks to read entries.
|
||||
pub forked_agent: Option<Arc<crate::agent::Agent>>,
|
||||
/// Entry index where the fork diverged.
|
||||
pub fork_point: usize,
|
||||
/// Shared persistent state — accumulated across all agent runs.
|
||||
pub state: std::collections::BTreeMap<String, String>,
|
||||
/// Recent store activity for this agent: (key, timestamp), newest first.
|
||||
pub history: Vec<(String, i64)>,
|
||||
}
|
||||
|
||||
struct SubconsciousAgent {
|
||||
name: String,
|
||||
auto: AutoAgent,
|
||||
last_trigger_bytes: u64,
|
||||
last_run: Option<Instant>,
|
||||
/// The forked agent for the current/last run. Shared with the
|
||||
/// spawned task so the UI can read entries live.
|
||||
forked_agent: Option<Arc<crate::agent::Agent>>,
|
||||
/// Entry index where the fork diverged from the conscious agent.
|
||||
fork_point: usize,
|
||||
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 {
|
||||
name: name.to_string(),
|
||||
auto, last_trigger_bytes: 0, last_run: None,
|
||||
forked_agent: None, fork_point: 0, 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, state: &std::collections::BTreeMap<String, String>, history: Vec<(String, i64)>) -> SubconsciousSnapshot {
|
||||
SubconsciousSnapshot {
|
||||
name: self.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()),
|
||||
forked_agent: self.forked_agent.clone(),
|
||||
fork_point: self.fork_point,
|
||||
state: state.clone(),
|
||||
history,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Background agent orchestration — owns the subconscious agents
|
||||
/// and their shared persistent state.
|
||||
pub struct Subconscious {
|
||||
agents: Vec<SubconsciousAgent>,
|
||||
/// Shared state across all agents — persisted to disk.
|
||||
pub state: std::collections::BTreeMap<String, String>,
|
||||
state_path: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
impl Subconscious {
|
||||
pub fn new() -> Self {
|
||||
let agents = AGENTS.iter()
|
||||
.filter_map(|(name, _)| SubconsciousAgent::new(name))
|
||||
.collect();
|
||||
Self { agents, state: std::collections::BTreeMap::new(), state_path: None }
|
||||
}
|
||||
|
||||
/// Late-init: push the output tool onto each agent's tool list.
|
||||
/// Called after Subconscious is wrapped in Arc<Mutex<>> so the
|
||||
/// closure can capture a reference back.
|
||||
pub fn init_output_tool(&mut self, self_arc: std::sync::Arc<tokio::sync::Mutex<Self>>) {
|
||||
for agent in &mut self.agents {
|
||||
let sub = self_arc.clone();
|
||||
agent.auto.tools.push(crate::agent::tools::Tool {
|
||||
name: "output",
|
||||
description: "Produce a named output value for passing between steps.",
|
||||
parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Output name"},"value":{"type":"string","description":"Output value"}},"required":["key","value"]}"#,
|
||||
handler: std::sync::Arc::new(move |_agent, v| {
|
||||
let sub = sub.clone();
|
||||
Box::pin(async move {
|
||||
let key = v["key"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("output requires 'key'"))?;
|
||||
let value = v["value"].as_str()
|
||||
.ok_or_else(|| anyhow::anyhow!("output requires 'value'"))?;
|
||||
let mut s = sub.lock().await;
|
||||
s.state.insert(key.to_string(), value.to_string());
|
||||
s.save_state();
|
||||
Ok(format!("{}: {}", key, value))
|
||||
})
|
||||
}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the state file path and load any existing state from disk.
|
||||
pub fn set_state_path(&mut self, path: std::path::PathBuf) {
|
||||
if let Ok(data) = std::fs::read_to_string(&path) {
|
||||
if let Ok(saved) = serde_json::from_str::<
|
||||
std::collections::BTreeMap<String, String>
|
||||
>(&data) {
|
||||
self.state = saved;
|
||||
dbglog!("[subconscious] loaded {} state keys from {}",
|
||||
self.state.len(), path.display());
|
||||
}
|
||||
}
|
||||
self.state_path = Some(path);
|
||||
}
|
||||
|
||||
fn save_state(&self) {
|
||||
let Some(path) = &self.state_path else { return };
|
||||
if let Ok(json) = serde_json::to_string_pretty(&self.state) {
|
||||
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> {
|
||||
self.agents.iter().map(|s| {
|
||||
let history = store.map(|st| {
|
||||
let prov = format!("agent:{}", s.name);
|
||||
st.recent_by_provenance(&prov, 30)
|
||||
}).unwrap_or_default();
|
||||
s.snapshot(&self.state, history)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// Collect results from finished agents, inject outputs into the
|
||||
/// conscious agent's context.
|
||||
pub async fn collect_results(&mut self, agent: &Arc<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();
|
||||
let had_finished = !finished.is_empty();
|
||||
|
||||
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].name.clone();
|
||||
|
||||
// Check state for outputs (written by the output tool closure)
|
||||
let has_outputs = self.state.contains_key("surface")
|
||||
|| self.state.contains_key("reflection")
|
||||
|| self.state.contains_key("thalamus");
|
||||
if has_outputs {
|
||||
if let Some(surface_str) = self.state.get("surface").cloned() {
|
||||
// Collect keys already in context to avoid duplicates
|
||||
let existing: std::collections::HashSet<String> = {
|
||||
let ctx = agent.context.lock().await;
|
||||
ctx.conversation().iter()
|
||||
.filter_map(|n| n.leaf())
|
||||
.filter_map(|l| match l.body() {
|
||||
NodeBody::Memory { key, .. } => Some(key.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
let store = crate::store::Store::cached().await.ok();
|
||||
let store_guard = match &store {
|
||||
Some(s) => Some(s.lock().await),
|
||||
None => None,
|
||||
};
|
||||
for key in surface_str.lines().map(|l| l.trim()).filter(|l| !l.is_empty()) {
|
||||
if existing.contains(key) { continue; }
|
||||
let rendered = store_guard.as_ref()
|
||||
.and_then(|s| crate::cli::node::render_node(s, key));
|
||||
if let Some(rendered) = rendered {
|
||||
agent.push_node(AstNode::memory(
|
||||
key,
|
||||
format!("--- {} (surfaced) ---\n{}", key, rendered),
|
||||
)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(reflection) = self.state.get("reflection").cloned() {
|
||||
if !reflection.trim().is_empty() {
|
||||
agent.push_node(AstNode::dmn(format!(
|
||||
"--- subconscious reflection ---\n{}",
|
||||
reflection.trim(),
|
||||
))).await;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(nudge) = self.state.get("thalamus").cloned() {
|
||||
let nudge = nudge.trim();
|
||||
if !nudge.is_empty() && nudge != "ok" {
|
||||
agent.push_node(AstNode::dmn(format!(
|
||||
"--- thalamus ---\n{}",
|
||||
nudge,
|
||||
))).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbglog!("[subconscious] {} completed", name);
|
||||
}
|
||||
Err(e) => dbglog!("[subconscious] agent failed: {}", e),
|
||||
}
|
||||
}
|
||||
if had_finished {
|
||||
self.save_state();
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger subconscious agents that are due to run.
|
||||
pub async fn trigger(&mut self, agent: &Arc<Agent>) {
|
||||
let (conversation_bytes, memory_keys) = {
|
||||
let ctx = agent.context.lock().await;
|
||||
let bytes = ctx.conversation().iter()
|
||||
.filter(|node| !matches!(node.leaf().map(|l| l.body()),
|
||||
Some(NodeBody::Log(_)) | Some(NodeBody::Memory { .. })))
|
||||
.map(|node| node.render().len() as u64)
|
||||
.sum::<u64>();
|
||||
let keys: Vec<String> = ctx.conversation().iter().filter_map(|node| {
|
||||
if let Some(NodeBody::Memory { key, .. }) = node.leaf().map(|l| l.body()) {
|
||||
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; }
|
||||
|
||||
// Query each agent's recent writes so they know what they already touched
|
||||
let store = crate::store::Store::cached().await.ok();
|
||||
let store_guard = match &store {
|
||||
Some(s) => Some(s.lock().await),
|
||||
None => None,
|
||||
};
|
||||
|
||||
for (idx, mut auto) in to_run {
|
||||
dbglog!("[subconscious] triggering {}", auto.name);
|
||||
|
||||
let forked = agent.fork(auto.tools.clone()).await;
|
||||
let prov = format!("agent:{}", auto.name);
|
||||
forked.state.lock().await.provenance = prov.clone();
|
||||
let fork_point = forked.context.lock().await.conversation().len();
|
||||
|
||||
self.agents[idx].forked_agent = Some(forked.clone());
|
||||
self.agents[idx].fork_point = fork_point;
|
||||
|
||||
let keys = memory_keys.clone();
|
||||
let st = self.state.clone();
|
||||
let recent: Vec<String> = store_guard.as_ref()
|
||||
.map(|s| s.recent_by_provenance(&prov, 50)
|
||||
.into_iter().map(|(k, _)| k).collect())
|
||||
.unwrap_or_default();
|
||||
|
||||
self.agents[idx].handle = Some(tokio::spawn(async move {
|
||||
let result = auto.run_forked_shared(&forked, &keys, &st, &recent).await;
|
||||
(auto, result)
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue