diff --git a/src/agent/oneshot.rs b/src/agent/oneshot.rs index b2dc6e4..24117bc 100644 --- a/src/agent/oneshot.rs +++ b/src/agent/oneshot.rs @@ -149,6 +149,16 @@ impl AutoAgent { 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, + ) -> Result { + 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 /// Arc to read entries live during the run. pub async fn run_forked_shared( diff --git a/src/mind/mod.rs b/src/mind/mod.rs index 49be124..8426ef1 100644 --- a/src/mind/mod.rs +++ b/src/mind/mod.rs @@ -552,7 +552,7 @@ impl Mind { if unc_handle.as_ref().map_or(true, |h| h.is_finished()) { let unc = self.unconscious.clone(); unc_handle = Some(tokio::spawn(async move { - unc.lock().await.trigger(); + unc.lock().await.trigger().await; })); } } diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index 1799100..15f8419 100644 --- a/src/mind/unconscious.rs +++ b/src/mind/unconscious.rs @@ -7,6 +7,7 @@ use std::time::{Duration, Instant}; use std::collections::HashMap; +use futures::FutureExt; use crate::agent::oneshot::{AutoAgent, AutoStep}; use crate::agent::tools; @@ -35,7 +36,10 @@ fn save_enabled_config(map: &HashMap) { struct UnconsciousAgent { name: String, enabled: bool, + auto: AutoAgent, handle: Option)>>, + /// Shared agent handle — UI locks to read context live. + pub agent: Option>, last_run: Option, runs: usize, } @@ -62,6 +66,7 @@ pub struct UnconsciousSnapshot { pub enabled: bool, pub runs: usize, pub last_run_secs_ago: Option, + pub agent: Option>, } pub struct Unconscious { @@ -77,15 +82,34 @@ impl Unconscious { // Scan all .agent files, exclude subconscious-* and surface-observe let mut agents: Vec = Vec::new(); + let all_tools = tools::memory::memory_tools().to_vec(); for def in defs::load_defs() { if def.agent.starts_with("subconscious-") { continue; } if def.agent == "surface-observe" { continue; } let enabled = enabled_map.get(&def.agent).copied() - .unwrap_or(false); // new agents default to off + .unwrap_or(false); + let effective_tools: Vec = 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 = 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 { name: def.agent.clone(), enabled, + auto, handle: None, + agent: None, last_run: None, runs: 0, }); @@ -103,13 +127,13 @@ impl Unconscious { /// Toggle an agent on/off by name. Returns new enabled state. /// If enabling, immediately spawns the agent if it's not running. - pub fn toggle(&mut self, name: &str) -> Option { + pub async fn toggle(&mut self, name: &str) -> Option { let idx = self.agents.iter().position(|a| a.name == name)?; self.agents[idx].enabled = !self.agents[idx].enabled; let new_state = self.agents[idx].enabled; self.save_enabled(); if new_state && !self.agents[idx].is_running() { - self.spawn_agent(idx); + self.spawn_agent(idx).await; } Some(new_state) } @@ -128,6 +152,7 @@ impl Unconscious { enabled: a.enabled, runs: a.runs, last_run_secs_ago: a.last_run.map(|t| t.elapsed().as_secs_f64()), + agent: a.agent.clone(), }).collect() } @@ -141,7 +166,7 @@ impl Unconscious { } /// Reap finished agents and spawn new ones. - pub fn trigger(&mut self) { + pub async fn trigger(&mut self) { // Periodic graph health refresh if self.last_health_check .map(|t| t.elapsed() > Duration::from_secs(600)) @@ -152,11 +177,21 @@ impl Unconscious { for agent in &mut self.agents { 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.runs += 1; - dbglog!("[unconscious] {} completed (run {})", - agent.name, agent.runs); - agent.handle = None; + // Get the AutoAgent back from the finished task + match handle.now_or_never() { + 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(); 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(); dbglog!("[unconscious] spawning {}", name); @@ -184,15 +219,7 @@ impl Unconscious { None => return, }; - let all_tools = tools::memory_and_journal_tools(); - let effective_tools: Vec = if def.tools.is_empty() { - all_tools - } else { - all_tools.into_iter() - .filter(|t| def.tools.iter().any(|w| w == t.name)) - .collect() - }; - + // Run query and resolve placeholders let mut store = match crate::store::Store::load() { Ok(s) => s, Err(e) => { @@ -216,19 +243,64 @@ impl Unconscious { store.record_agent_visits(&batch.node_keys, &name).ok(); } - let steps: Vec = batch.steps.iter().map(|s| AutoStep { - prompt: s.prompt.clone(), - phase: s.phase.clone(), - }).collect(); + // 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(), + phase: s.phase.clone(), + }).collect()); - let mut auto = AutoAgent::new( - name, effective_tools, steps, - def.temperature.unwrap_or(0.6), - def.priority, - ); + // Create standalone Agent — stored so UI can read context + let config = crate::config::get(); + let base_url = config.api_base_url.as_deref().unwrap_or(""); + 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 { - let result = auto.run(None).await; + let result = auto.run_shared(&agent).await; + auto.steps = orig_steps; (auto, result) })); } diff --git a/src/user/mod.rs b/src/user/mod.rs index 04a690a..288e922 100644 --- a/src/user/mod.rs +++ b/src/user/mod.rs @@ -384,7 +384,7 @@ async fn run( let mut unc = mind.unconscious.lock().await; for name in &toggles { if sub.toggle(name).is_none() { - unc.toggle(name); + unc.toggle(name).await; } } }