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:
parent
5b75ad3553
commit
dc07c92b28
4 changed files with 112 additions and 30 deletions
|
|
@ -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<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
|
||||
/// Arc to read entries live during the run.
|
||||
pub async fn run_forked_shared(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<String, bool>) {
|
|||
struct UnconsciousAgent {
|
||||
name: String,
|
||||
enabled: bool,
|
||||
auto: AutoAgent,
|
||||
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>,
|
||||
runs: usize,
|
||||
}
|
||||
|
|
@ -62,6 +66,7 @@ pub struct UnconsciousSnapshot {
|
|||
pub enabled: bool,
|
||||
pub runs: usize,
|
||||
pub last_run_secs_ago: Option<f64>,
|
||||
pub agent: Option<std::sync::Arc<crate::agent::Agent>>,
|
||||
}
|
||||
|
||||
pub struct Unconscious {
|
||||
|
|
@ -77,15 +82,34 @@ impl Unconscious {
|
|||
|
||||
// Scan all .agent files, exclude subconscious-* and surface-observe
|
||||
let mut agents: Vec<UnconsciousAgent> = 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<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 {
|
||||
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<bool> {
|
||||
pub async fn toggle(&mut self, name: &str) -> Option<bool> {
|
||||
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<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()
|
||||
};
|
||||
|
||||
// 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<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(),
|
||||
phase: s.phase.clone(),
|
||||
}).collect();
|
||||
}).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)
|
||||
}));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue