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:
Kent Overstreet 2026-04-12 20:11:40 -04:00
parent 33156d9ab3
commit b94e056372
3 changed files with 37 additions and 23 deletions

3
Cargo.lock generated
View file

@ -612,6 +612,7 @@ dependencies = [
"dirs",
"env_logger",
"futures",
"json5",
"log",
"serde",
"serde_json",
@ -632,8 +633,8 @@ dependencies = [
"json5",
"libc",
"log",
"scopeguard",
"serde",
"serde_json",
"tokio",
"tokio-util",
]

View file

@ -311,7 +311,7 @@ pub struct SubconsciousSnapshot {
struct SubconsciousAgent {
name: String,
auto: AutoAgent,
auto: Option<AutoAgent>,
last_trigger_bytes: u64,
last_run: Option<Instant>,
/// The forked agent for the current/last run. Shared with the
@ -347,7 +347,7 @@ impl SubconsciousAgent {
Some(Self {
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,
})
}
@ -357,7 +357,8 @@ impl SubconsciousAgent {
}
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 {
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 {
let stats = crate::agent::oneshot::get_stats(&self.name);
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 {
name: self.name.clone(),
running: self.is_running(),
enabled: self.auto.enabled,
current_phase: self.auto.current_phase.clone(),
turn: self.auto.turn,
enabled,
current_phase,
turn,
runs: stats.runs,
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
forked_agent: self.forked_agent.clone(),
@ -408,8 +412,9 @@ impl Subconscious {
/// closure can capture a reference back.
pub fn init_output_tool(&mut self, self_arc: std::sync::Arc<tokio::sync::Mutex<Self>>) {
for agent in &mut self.agents {
let Some(ref mut auto) = agent.auto else { continue };
let sub = self_arc.clone();
agent.auto.tools.push(crate::agent::tools::Tool {
auto.tools.push(crate::agent::tools::Tool {
name: "output",
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"]}"#,
@ -454,8 +459,9 @@ impl Subconscious {
/// Toggle an agent on/off by name. Returns new enabled state.
pub fn toggle(&mut self, name: &str) -> Option<bool> {
let agent = self.agents.iter_mut().find(|a| a.name == name)?;
agent.auto.enabled = !agent.auto.enabled;
Some(agent.auto.enabled)
let auto = agent.auto.as_mut()?;
auto.enabled = !auto.enabled;
Some(auto.enabled)
}
pub fn walked(&self) -> Vec<String> {
@ -486,9 +492,15 @@ impl Subconscious {
self.agents[i].last_run = Some(Instant::now());
any_finished = true;
let (auto_back, result) = handle.await.unwrap_or_else(
|e| (AutoAgent::new(String::new(), vec![], vec![], 0.6, 0),
Err(format!("task panicked: {}", e))));
let (auto_back, result) = match handle.await {
Ok(r) => (Some(r.0), r.1),
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;
match result {
@ -585,8 +597,7 @@ impl Subconscious {
if !self.agents[i].should_trigger(conversation_bytes, interval) { continue; }
self.agents[i].last_trigger_bytes = conversation_bytes;
let auto = std::mem::replace(&mut self.agents[i].auto,
AutoAgent::new(String::new(), vec![], vec![], 0.6, 0));
let Some(auto) = self.agents[i].auto.take() else { continue };
to_run.push((i, auto));
}

View file

@ -33,7 +33,7 @@ fn save_enabled_config(map: &HashMap<String, bool>) {
struct UnconsciousAgent {
name: String,
enabled: bool,
auto: AutoAgent,
auto: Option<AutoAgent>,
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<(), String>)>>,
/// Shared agent handle — UI locks to read context live.
pub agent: Option<std::sync::Arc<crate::agent::Agent>>,
@ -103,7 +103,7 @@ impl Unconscious {
agents.push(UnconsciousAgent {
name: def.agent.clone(),
enabled,
auto,
auto: Some(auto),
handle: None,
agent: None,
last_run: None,
@ -187,7 +187,7 @@ impl Unconscious {
// Get the AutoAgent back from the finished task (stats already updated)
match handle.now_or_never() {
Some(Ok((auto_back, result))) => {
agent.auto = auto_back;
agent.auto = Some(auto_back);
match result {
Ok(_) => dbglog!("[unconscious] {} completed (run {})",
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();
}
// 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.6, 0));
// Take auto out for the spawned task
let Some(mut auto) = self.agents[idx].auto.take() else {
dbglog!("[unconscious] {} already running", name);
return;
};
let orig_steps = std::mem::replace(&mut auto.steps,
batch.steps.iter().map(|s| AutoStep {
prompt: s.prompt.clone(),
@ -261,7 +263,7 @@ impl Unconscious {
if base_url.is_empty() || model.is_empty() {
dbglog!("[unconscious] API not configured");
auto.steps = orig_steps;
self.agents[idx].auto = auto;
self.agents[idx].auto = Some(auto);
return;
}
@ -271,7 +273,7 @@ impl Unconscious {
Err(e) => {
dbglog!("[unconscious] config: {}", e);
auto.steps = orig_steps;
self.agents[idx].auto = auto;
self.agents[idx].auto = Some(auto);
return;
}
};