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
|
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(
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue