Unconscious agents: persistent AutoAgent, shared Agent Arc for UI

- AutoAgent stored on UnconsciousAgent, swapped out for runs, restored
  on completion (same pattern as subconscious agents)
- Agent Arc created before spawn and stored on UnconsciousAgent so
  the TUI can lock it to read conversation context live
- run_shared() method on AutoAgent for running with a pre-created Agent
- Default tools: memory_tools (not memory_and_journal_tools)
- trigger/spawn_agent made async for Agent::new()

Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
ProofOfConcept 2026-04-09 01:00:48 -04:00
parent 5b75ad3553
commit dc07c92b28
4 changed files with 112 additions and 30 deletions

View file

@ -149,6 +149,16 @@ impl AutoAgent {
self.run_with_backend(&mut backend, bail_fn).await self.run_with_backend(&mut backend, bail_fn).await
} }
/// Run using a pre-created agent Arc. The caller retains the Arc
/// so the UI can lock it to read entries live.
pub async fn run_shared(
&mut self,
agent: &std::sync::Arc<Agent>,
) -> Result<String, String> {
let mut backend = Backend(agent.clone());
self.run_with_backend(&mut backend, None).await
}
/// Run forked using a shared agent Arc. The UI can lock the same /// Run forked using a shared agent Arc. The UI can lock the same
/// Arc to read entries live during the run. /// Arc to read entries live during the run.
pub async fn run_forked_shared( pub async fn run_forked_shared(

View file

@ -552,7 +552,7 @@ impl Mind {
if unc_handle.as_ref().map_or(true, |h| h.is_finished()) { if unc_handle.as_ref().map_or(true, |h| h.is_finished()) {
let unc = self.unconscious.clone(); let unc = self.unconscious.clone();
unc_handle = Some(tokio::spawn(async move { unc_handle = Some(tokio::spawn(async move {
unc.lock().await.trigger(); unc.lock().await.trigger().await;
})); }));
} }
} }

View file

@ -7,6 +7,7 @@
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
use std::collections::HashMap; use std::collections::HashMap;
use futures::FutureExt;
use crate::agent::oneshot::{AutoAgent, AutoStep}; use crate::agent::oneshot::{AutoAgent, AutoStep};
use crate::agent::tools; use crate::agent::tools;
@ -35,7 +36,10 @@ fn save_enabled_config(map: &HashMap<String, bool>) {
struct UnconsciousAgent { struct UnconsciousAgent {
name: String, name: String,
enabled: bool, enabled: bool,
auto: AutoAgent,
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<String, String>)>>, handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<String, String>)>>,
/// Shared agent handle — UI locks to read context live.
pub agent: Option<std::sync::Arc<crate::agent::Agent>>,
last_run: Option<Instant>, last_run: Option<Instant>,
runs: usize, runs: usize,
} }
@ -62,6 +66,7 @@ pub struct UnconsciousSnapshot {
pub enabled: bool, pub enabled: bool,
pub runs: usize, pub runs: usize,
pub last_run_secs_ago: Option<f64>, pub last_run_secs_ago: Option<f64>,
pub agent: Option<std::sync::Arc<crate::agent::Agent>>,
} }
pub struct Unconscious { pub struct Unconscious {
@ -77,15 +82,34 @@ impl Unconscious {
// Scan all .agent files, exclude subconscious-* and surface-observe // Scan all .agent files, exclude subconscious-* and surface-observe
let mut agents: Vec<UnconsciousAgent> = Vec::new(); let mut agents: Vec<UnconsciousAgent> = Vec::new();
let all_tools = tools::memory::memory_tools().to_vec();
for def in defs::load_defs() { for def in defs::load_defs() {
if def.agent.starts_with("subconscious-") { continue; } if def.agent.starts_with("subconscious-") { continue; }
if def.agent == "surface-observe" { continue; } if def.agent == "surface-observe" { continue; }
let enabled = enabled_map.get(&def.agent).copied() let enabled = enabled_map.get(&def.agent).copied()
.unwrap_or(false); // new agents default to off .unwrap_or(false);
let effective_tools: Vec<tools::Tool> = if def.tools.is_empty() {
all_tools.clone()
} else {
all_tools.iter()
.filter(|t| def.tools.iter().any(|w| w == t.name))
.cloned()
.collect()
};
let steps: Vec<AutoStep> = def.steps.iter().map(|s| AutoStep {
prompt: s.prompt.clone(),
phase: s.phase.clone(),
}).collect();
let auto = AutoAgent::new(
def.agent.clone(), effective_tools, steps,
def.temperature.unwrap_or(0.6), def.priority,
);
agents.push(UnconsciousAgent { agents.push(UnconsciousAgent {
name: def.agent.clone(), name: def.agent.clone(),
enabled, enabled,
auto,
handle: None, handle: None,
agent: None,
last_run: None, last_run: None,
runs: 0, runs: 0,
}); });
@ -103,13 +127,13 @@ impl Unconscious {
/// Toggle an agent on/off by name. Returns new enabled state. /// Toggle an agent on/off by name. Returns new enabled state.
/// If enabling, immediately spawns the agent if it's not running. /// If enabling, immediately spawns the agent if it's not running.
pub fn toggle(&mut self, name: &str) -> Option<bool> { pub async fn toggle(&mut self, name: &str) -> Option<bool> {
let idx = self.agents.iter().position(|a| a.name == name)?; let idx = self.agents.iter().position(|a| a.name == name)?;
self.agents[idx].enabled = !self.agents[idx].enabled; self.agents[idx].enabled = !self.agents[idx].enabled;
let new_state = self.agents[idx].enabled; let new_state = self.agents[idx].enabled;
self.save_enabled(); self.save_enabled();
if new_state && !self.agents[idx].is_running() { if new_state && !self.agents[idx].is_running() {
self.spawn_agent(idx); self.spawn_agent(idx).await;
} }
Some(new_state) Some(new_state)
} }
@ -128,6 +152,7 @@ impl Unconscious {
enabled: a.enabled, enabled: a.enabled,
runs: a.runs, runs: a.runs,
last_run_secs_ago: a.last_run.map(|t| t.elapsed().as_secs_f64()), last_run_secs_ago: a.last_run.map(|t| t.elapsed().as_secs_f64()),
agent: a.agent.clone(),
}).collect() }).collect()
} }
@ -141,7 +166,7 @@ impl Unconscious {
} }
/// Reap finished agents and spawn new ones. /// Reap finished agents and spawn new ones.
pub fn trigger(&mut self) { pub async fn trigger(&mut self) {
// Periodic graph health refresh // Periodic graph health refresh
if self.last_health_check if self.last_health_check
.map(|t| t.elapsed() > Duration::from_secs(600)) .map(|t| t.elapsed() > Duration::from_secs(600))
@ -152,11 +177,21 @@ impl Unconscious {
for agent in &mut self.agents { for agent in &mut self.agents {
if agent.handle.as_ref().is_some_and(|h| h.is_finished()) { if agent.handle.as_ref().is_some_and(|h| h.is_finished()) {
let handle = agent.handle.take().unwrap();
agent.last_run = Some(Instant::now()); agent.last_run = Some(Instant::now());
agent.runs += 1; agent.runs += 1;
dbglog!("[unconscious] {} completed (run {})", // Get the AutoAgent back from the finished task
agent.name, agent.runs); match handle.now_or_never() {
agent.handle = None; Some(Ok((auto_back, result))) => {
agent.auto = auto_back;
match result {
Ok(_) => dbglog!("[unconscious] {} completed (run {})",
agent.name, agent.runs),
Err(e) => dbglog!("[unconscious] {} failed: {}", agent.name, e),
}
}
_ => dbglog!("[unconscious] {} task lost", agent.name),
}
} }
} }
@ -171,11 +206,11 @@ impl Unconscious {
.collect(); .collect();
for idx in ready { for idx in ready {
self.spawn_agent(idx); self.spawn_agent(idx).await;
} }
} }
fn spawn_agent(&mut self, idx: usize) { async fn spawn_agent(&mut self, idx: usize) {
let name = self.agents[idx].name.clone(); let name = self.agents[idx].name.clone();
dbglog!("[unconscious] spawning {}", name); dbglog!("[unconscious] spawning {}", name);
@ -184,15 +219,7 @@ impl Unconscious {
None => return, None => return,
}; };
let all_tools = tools::memory_and_journal_tools(); // Run query and resolve placeholders
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() { let mut store = match crate::store::Store::load() {
Ok(s) => s, Ok(s) => s,
Err(e) => { Err(e) => {
@ -216,19 +243,64 @@ impl Unconscious {
store.record_agent_visits(&batch.node_keys, &name).ok(); store.record_agent_visits(&batch.node_keys, &name).ok();
} }
let steps: Vec<AutoStep> = batch.steps.iter().map(|s| AutoStep { // Swap auto out, replace steps with resolved prompts
let mut auto = std::mem::replace(&mut self.agents[idx].auto,
AutoAgent::new(String::new(), vec![], vec![], 0.0, 0));
let orig_steps = std::mem::replace(&mut auto.steps,
batch.steps.iter().map(|s| AutoStep {
prompt: s.prompt.clone(), prompt: s.prompt.clone(),
phase: s.phase.clone(), phase: s.phase.clone(),
}).collect(); }).collect());
let mut auto = AutoAgent::new( // Create standalone Agent — stored so UI can read context
name, effective_tools, steps, let config = crate::config::get();
def.temperature.unwrap_or(0.6), let base_url = config.api_base_url.as_deref().unwrap_or("");
def.priority, let api_key = config.api_key.as_deref().unwrap_or("");
); let model = config.api_model.as_deref().unwrap_or("");
if base_url.is_empty() || model.is_empty() {
dbglog!("[unconscious] API not configured");
auto.steps = orig_steps;
self.agents[idx].auto = auto;
return;
}
let cli = crate::user::CliArgs::default();
let (app, _) = match crate::config::load_app(&cli) {
Ok(r) => r,
Err(e) => {
dbglog!("[unconscious] config: {}", e);
auto.steps = orig_steps;
self.agents[idx].auto = auto;
return;
}
};
let (system_prompt, personality) = match crate::config::reload_for_model(&app, &app.prompts.other) {
Ok(r) => r,
Err(e) => {
dbglog!("[unconscious] config: {}", e);
auto.steps = orig_steps;
self.agents[idx].auto = auto;
return;
}
};
let client = crate::agent::api::ApiClient::new(base_url, api_key, model);
let agent = crate::agent::Agent::new(
client, system_prompt, personality,
app, String::new(), None,
crate::agent::tools::ActiveTools::new(),
).await;
{
let mut st = agent.state.lock().await;
st.provenance = format!("unconscious:{}", auto.name);
st.tools = auto.tools.clone();
}
self.agents[idx].agent = Some(agent.clone());
self.agents[idx].handle = Some(tokio::spawn(async move { self.agents[idx].handle = Some(tokio::spawn(async move {
let result = auto.run(None).await; let result = auto.run_shared(&agent).await;
auto.steps = orig_steps;
(auto, result) (auto, result)
})); }));
} }

View file

@ -384,7 +384,7 @@ async fn run(
let mut unc = mind.unconscious.lock().await; let mut unc = mind.unconscious.lock().await;
for name in &toggles { for name in &toggles {
if sub.toggle(name).is_none() { if sub.toggle(name).is_none() {
unc.toggle(name); unc.toggle(name).await;
} }
} }
} }