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

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));
}