From aade8a9cce5799c0ca3e0d59af065eaf539671a2 Mon Sep 17 00:00:00 2001 From: ProofOfConcept Date: Fri, 10 Apr 2026 13:44:41 -0400 Subject: [PATCH] Add per-agent run stats (messages, tool calls by type) compute_run_stats() walks the conversation AST after each agent completes, counting messages and tool calls by tool name. Stats are returned from save_agent_log(), stored on UnconsciousAgent, and displayed in the agent list UI. Co-Authored-By: Proof of Concept --- src/mind/unconscious.rs | 71 +++++++++++++++++++++++++++++++--------- src/user/subconscious.rs | 4 +++ 2 files changed, 59 insertions(+), 16 deletions(-) diff --git a/src/mind/unconscious.rs b/src/mind/unconscious.rs index a7e9e0a..a137fe0 100644 --- a/src/mind/unconscious.rs +++ b/src/mind/unconscious.rs @@ -34,11 +34,12 @@ struct UnconsciousAgent { name: String, enabled: bool, auto: AutoAgent, - handle: Option)>>, + handle: Option, RunStats)>>, /// Shared agent handle — UI locks to read context live. pub agent: Option>, last_run: Option, runs: usize, + last_stats: Option, } impl UnconsciousAgent { @@ -60,6 +61,7 @@ pub struct UnconsciousSnapshot { pub runs: usize, pub last_run_secs_ago: Option, pub agent: Option>, + pub last_stats: Option, } pub struct Unconscious { @@ -105,6 +107,7 @@ impl Unconscious { agent: None, last_run: None, runs: 0, + last_stats: None, }); } agents.sort_by(|a, b| a.name.cmp(&b.name)); @@ -144,6 +147,7 @@ impl Unconscious { runs: a.runs, last_run_secs_ago: a.last_run.map(|t| t.elapsed().as_secs_f64()), agent: a.agent.clone(), + last_stats: a.last_stats.clone(), }).collect() } @@ -173,8 +177,9 @@ impl Unconscious { agent.runs += 1; // Get the AutoAgent back from the finished task match handle.now_or_never() { - Some(Ok((auto_back, result))) => { + Some(Ok((auto_back, result, stats))) => { agent.auto = auto_back; + agent.last_stats = Some(stats); match result { Ok(_) => dbglog!("[unconscious] {} completed (run {})", agent.name, agent.runs), @@ -289,30 +294,64 @@ impl Unconscious { self.agents[idx].handle = Some(tokio::spawn(async move { let result = auto.run_shared(&agent).await; - save_agent_log(&auto.name, &agent).await; + let stats = save_agent_log(&auto.name, &agent).await; auto.steps = orig_steps; - (auto, result) + (auto, result, stats) })); } } -pub async fn save_agent_log(name: &str, agent: &std::sync::Arc) { +pub async fn save_agent_log(name: &str, agent: &std::sync::Arc) -> RunStats { let dir = dirs::home_dir().unwrap_or_default() .join(format!(".consciousness/logs/{}", name)); - if std::fs::create_dir_all(&dir).is_err() { return; } - let ts = chrono::Utc::now().format("%Y%m%d-%H%M%S"); - let path = dir.join(format!("{}.json", ts)); - let sections: serde_json::Value = { - let ctx = agent.context.lock().await; - serde_json::json!({ + let ctx = agent.context.lock().await; + let stats = compute_run_stats(ctx.conversation()); + if std::fs::create_dir_all(&dir).is_ok() { + let ts = chrono::Utc::now().format("%Y%m%d-%H%M%S"); + let path = dir.join(format!("{}.json", ts)); + let sections = serde_json::json!({ "system": ctx.system(), "identity": ctx.identity(), "journal": ctx.journal(), "conversation": ctx.conversation(), - }) - }; - if let Ok(json) = serde_json::to_string_pretty(§ions) { - let _ = std::fs::write(&path, json); - dbglog!("[unconscious] saved log to {}", path.display()); + "stats": stats, + }); + if let Ok(json) = serde_json::to_string_pretty(§ions) { + let _ = std::fs::write(&path, json); + } } + dbglog!("[unconscious] {} — {} msgs, {} tool calls", + name, stats.messages, stats.tool_calls); + stats +} + +#[derive(Clone, serde::Serialize)] +pub struct RunStats { + pub messages: usize, + pub tool_calls: usize, + pub tool_calls_by_type: HashMap, +} + +fn compute_run_stats(conversation: &[crate::agent::context::AstNode]) -> RunStats { + use crate::agent::context::{AstNode, NodeBody}; + + let mut messages = 0usize; + let mut tool_calls = 0usize; + let mut by_type: HashMap = HashMap::new(); + + for node in conversation { + if let AstNode::Branch { children, .. } = node { + messages += 1; + for child in children { + if let AstNode::Leaf(leaf) = child { + if let NodeBody::ToolCall { name, .. } = leaf.body() { + tool_calls += 1; + *by_type.entry(name.to_string()).or_default() += 1; + } + } + } + } + } + + RunStats { messages, tool_calls, tool_calls_by_type: by_type } } diff --git a/src/user/subconscious.rs b/src/user/subconscious.rs index d22742a..8c76b1d 100644 --- a/src/user/subconscious.rs +++ b/src/user/subconscious.rs @@ -250,6 +250,10 @@ impl SubconsciousScreen { format!("run {}", snap.runs + 1) } else if !snap.enabled { "off".to_string() + } else if let Some(ref stats) = snap.last_stats { + format!("×{} {} {}msg {}tc", + snap.runs, ago, + stats.messages, stats.tool_calls) } else { format!("×{} {}", snap.runs, ago) };