unconscious/subconscious: use Option<AutoAgent> instead of placeholder
Previously, spawning an agent used std::mem::replace with an empty-name AutoAgent as placeholder. This caused ghost stats entries under "" when those placeholders accidentally got their stats logged. Now uses Option<AutoAgent> with .take() - the type honestly represents that the agent is unavailable while running. Panic recovery in subconscious now properly recreates the agent from its definition. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
33156d9ab3
commit
b94e056372
3 changed files with 37 additions and 23 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
|
@ -612,6 +612,7 @@ dependencies = [
|
||||||
"dirs",
|
"dirs",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures",
|
"futures",
|
||||||
|
"json5",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -632,8 +633,8 @@ dependencies = [
|
||||||
"json5",
|
"json5",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
"scopeguard",
|
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -311,7 +311,7 @@ pub struct SubconsciousSnapshot {
|
||||||
|
|
||||||
struct SubconsciousAgent {
|
struct SubconsciousAgent {
|
||||||
name: String,
|
name: String,
|
||||||
auto: AutoAgent,
|
auto: Option<AutoAgent>,
|
||||||
last_trigger_bytes: u64,
|
last_trigger_bytes: u64,
|
||||||
last_run: Option<Instant>,
|
last_run: Option<Instant>,
|
||||||
/// The forked agent for the current/last run. Shared with the
|
/// The forked agent for the current/last run. Shared with the
|
||||||
|
|
@ -347,7 +347,7 @@ impl SubconsciousAgent {
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
name: name.to_string(),
|
name: name.to_string(),
|
||||||
auto, last_trigger_bytes: 0, last_run: None,
|
auto: Some(auto), last_trigger_bytes: 0, last_run: None,
|
||||||
forked_agent: None, fork_point: 0, handle: None,
|
forked_agent: None, fork_point: 0, handle: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -357,7 +357,8 @@ impl SubconsciousAgent {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn should_trigger(&self, conversation_bytes: u64, interval: u64) -> bool {
|
fn should_trigger(&self, conversation_bytes: u64, interval: u64) -> bool {
|
||||||
if !self.auto.enabled || self.is_running() { return false; }
|
let enabled = self.auto.as_ref().map_or(false, |a| a.enabled);
|
||||||
|
if !enabled || self.is_running() { return false; }
|
||||||
if interval == 0 {
|
if interval == 0 {
|
||||||
return conversation_bytes > self.last_trigger_bytes;
|
return conversation_bytes > self.last_trigger_bytes;
|
||||||
}
|
}
|
||||||
|
|
@ -367,12 +368,15 @@ impl SubconsciousAgent {
|
||||||
fn snapshot(&self, state: &std::collections::BTreeMap<String, String>, history: Vec<(String, i64)>) -> SubconsciousSnapshot {
|
fn snapshot(&self, state: &std::collections::BTreeMap<String, String>, history: Vec<(String, i64)>) -> SubconsciousSnapshot {
|
||||||
let stats = crate::agent::oneshot::get_stats(&self.name);
|
let stats = crate::agent::oneshot::get_stats(&self.name);
|
||||||
let tool_calls_ewma: f64 = stats.by_tool.values().map(|t| t.ewma).sum();
|
let tool_calls_ewma: f64 = stats.by_tool.values().map(|t| t.ewma).sum();
|
||||||
|
let (enabled, current_phase, turn) = self.auto.as_ref()
|
||||||
|
.map(|a| (a.enabled, a.current_phase.clone(), a.turn))
|
||||||
|
.unwrap_or((false, String::new(), 0));
|
||||||
SubconsciousSnapshot {
|
SubconsciousSnapshot {
|
||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
running: self.is_running(),
|
running: self.is_running(),
|
||||||
enabled: self.auto.enabled,
|
enabled,
|
||||||
current_phase: self.auto.current_phase.clone(),
|
current_phase,
|
||||||
turn: self.auto.turn,
|
turn,
|
||||||
runs: stats.runs,
|
runs: stats.runs,
|
||||||
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
|
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
|
||||||
forked_agent: self.forked_agent.clone(),
|
forked_agent: self.forked_agent.clone(),
|
||||||
|
|
@ -408,8 +412,9 @@ impl Subconscious {
|
||||||
/// closure can capture a reference back.
|
/// closure can capture a reference back.
|
||||||
pub fn init_output_tool(&mut self, self_arc: std::sync::Arc<tokio::sync::Mutex<Self>>) {
|
pub fn init_output_tool(&mut self, self_arc: std::sync::Arc<tokio::sync::Mutex<Self>>) {
|
||||||
for agent in &mut self.agents {
|
for agent in &mut self.agents {
|
||||||
|
let Some(ref mut auto) = agent.auto else { continue };
|
||||||
let sub = self_arc.clone();
|
let sub = self_arc.clone();
|
||||||
agent.auto.tools.push(crate::agent::tools::Tool {
|
auto.tools.push(crate::agent::tools::Tool {
|
||||||
name: "output",
|
name: "output",
|
||||||
description: "Produce a named output value for passing between steps.",
|
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"]}"#,
|
parameters_json: r#"{"type":"object","properties":{"key":{"type":"string","description":"Output name"},"value":{"type":"string","description":"Output value"}},"required":["key","value"]}"#,
|
||||||
|
|
@ -454,8 +459,9 @@ impl Subconscious {
|
||||||
/// Toggle an agent on/off by name. Returns new enabled state.
|
/// Toggle an agent on/off by name. Returns new enabled state.
|
||||||
pub fn toggle(&mut self, name: &str) -> Option<bool> {
|
pub fn toggle(&mut self, name: &str) -> Option<bool> {
|
||||||
let agent = self.agents.iter_mut().find(|a| a.name == name)?;
|
let agent = self.agents.iter_mut().find(|a| a.name == name)?;
|
||||||
agent.auto.enabled = !agent.auto.enabled;
|
let auto = agent.auto.as_mut()?;
|
||||||
Some(agent.auto.enabled)
|
auto.enabled = !auto.enabled;
|
||||||
|
Some(auto.enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn walked(&self) -> Vec<String> {
|
pub fn walked(&self) -> Vec<String> {
|
||||||
|
|
@ -486,9 +492,15 @@ impl Subconscious {
|
||||||
self.agents[i].last_run = Some(Instant::now());
|
self.agents[i].last_run = Some(Instant::now());
|
||||||
any_finished = true;
|
any_finished = true;
|
||||||
|
|
||||||
let (auto_back, result) = handle.await.unwrap_or_else(
|
let (auto_back, result) = match handle.await {
|
||||||
|e| (AutoAgent::new(String::new(), vec![], vec![], 0.6, 0),
|
Ok(r) => (Some(r.0), r.1),
|
||||||
Err(format!("task panicked: {}", e))));
|
Err(e) => {
|
||||||
|
// Task panicked — auto is lost, need to recreate from def
|
||||||
|
let recovered = SubconsciousAgent::new(&self.agents[i].name)
|
||||||
|
.map(|a| a.auto).flatten();
|
||||||
|
(recovered, Err(format!("task panicked: {}", e)))
|
||||||
|
}
|
||||||
|
};
|
||||||
self.agents[i].auto = auto_back;
|
self.agents[i].auto = auto_back;
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
|
|
@ -585,8 +597,7 @@ impl Subconscious {
|
||||||
if !self.agents[i].should_trigger(conversation_bytes, interval) { continue; }
|
if !self.agents[i].should_trigger(conversation_bytes, interval) { continue; }
|
||||||
self.agents[i].last_trigger_bytes = conversation_bytes;
|
self.agents[i].last_trigger_bytes = conversation_bytes;
|
||||||
|
|
||||||
let auto = std::mem::replace(&mut self.agents[i].auto,
|
let Some(auto) = self.agents[i].auto.take() else { continue };
|
||||||
AutoAgent::new(String::new(), vec![], vec![], 0.6, 0));
|
|
||||||
to_run.push((i, auto));
|
to_run.push((i, auto));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,7 @@ fn save_enabled_config(map: &HashMap<String, bool>) {
|
||||||
struct UnconsciousAgent {
|
struct UnconsciousAgent {
|
||||||
name: String,
|
name: String,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
auto: AutoAgent,
|
auto: Option<AutoAgent>,
|
||||||
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<(), String>)>>,
|
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<(), String>)>>,
|
||||||
/// Shared agent handle — UI locks to read context live.
|
/// Shared agent handle — UI locks to read context live.
|
||||||
pub agent: Option<std::sync::Arc<crate::agent::Agent>>,
|
pub agent: Option<std::sync::Arc<crate::agent::Agent>>,
|
||||||
|
|
@ -103,7 +103,7 @@ impl Unconscious {
|
||||||
agents.push(UnconsciousAgent {
|
agents.push(UnconsciousAgent {
|
||||||
name: def.agent.clone(),
|
name: def.agent.clone(),
|
||||||
enabled,
|
enabled,
|
||||||
auto,
|
auto: Some(auto),
|
||||||
handle: None,
|
handle: None,
|
||||||
agent: None,
|
agent: None,
|
||||||
last_run: None,
|
last_run: None,
|
||||||
|
|
@ -187,7 +187,7 @@ impl Unconscious {
|
||||||
// Get the AutoAgent back from the finished task (stats already updated)
|
// Get the AutoAgent back from the finished task (stats already updated)
|
||||||
match handle.now_or_never() {
|
match handle.now_or_never() {
|
||||||
Some(Ok((auto_back, result))) => {
|
Some(Ok((auto_back, result))) => {
|
||||||
agent.auto = auto_back;
|
agent.auto = Some(auto_back);
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => dbglog!("[unconscious] {} completed (run {})",
|
Ok(_) => dbglog!("[unconscious] {} completed (run {})",
|
||||||
agent.name, crate::agent::oneshot::get_stats(&agent.name).runs),
|
agent.name, crate::agent::oneshot::get_stats(&agent.name).runs),
|
||||||
|
|
@ -244,9 +244,11 @@ impl Unconscious {
|
||||||
store.record_agent_visits(&batch.node_keys, &name).ok();
|
store.record_agent_visits(&batch.node_keys, &name).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Swap auto out, replace steps with resolved prompts
|
// Take auto out for the spawned task
|
||||||
let mut auto = std::mem::replace(&mut self.agents[idx].auto,
|
let Some(mut auto) = self.agents[idx].auto.take() else {
|
||||||
AutoAgent::new(String::new(), vec![], vec![], 0.6, 0));
|
dbglog!("[unconscious] {} already running", name);
|
||||||
|
return;
|
||||||
|
};
|
||||||
let orig_steps = std::mem::replace(&mut auto.steps,
|
let orig_steps = std::mem::replace(&mut auto.steps,
|
||||||
batch.steps.iter().map(|s| AutoStep {
|
batch.steps.iter().map(|s| AutoStep {
|
||||||
prompt: s.prompt.clone(),
|
prompt: s.prompt.clone(),
|
||||||
|
|
@ -261,7 +263,7 @@ impl Unconscious {
|
||||||
if base_url.is_empty() || model.is_empty() {
|
if base_url.is_empty() || model.is_empty() {
|
||||||
dbglog!("[unconscious] API not configured");
|
dbglog!("[unconscious] API not configured");
|
||||||
auto.steps = orig_steps;
|
auto.steps = orig_steps;
|
||||||
self.agents[idx].auto = auto;
|
self.agents[idx].auto = Some(auto);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,7 +273,7 @@ impl Unconscious {
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
dbglog!("[unconscious] config: {}", e);
|
dbglog!("[unconscious] config: {}", e);
|
||||||
auto.steps = orig_steps;
|
auto.steps = orig_steps;
|
||||||
self.agents[idx].auto = auto;
|
self.agents[idx].auto = Some(auto);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue