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 <poc@bcachefs.org>
This commit is contained in:
parent
b4dfd3c092
commit
aade8a9cce
2 changed files with 59 additions and 16 deletions
|
|
@ -34,11 +34,12 @@ struct UnconsciousAgent {
|
||||||
name: String,
|
name: String,
|
||||||
enabled: bool,
|
enabled: bool,
|
||||||
auto: AutoAgent,
|
auto: AutoAgent,
|
||||||
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<String, String>)>>,
|
handle: Option<tokio::task::JoinHandle<(AutoAgent, Result<String, String>, RunStats)>>,
|
||||||
/// 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>>,
|
||||||
last_run: Option<Instant>,
|
last_run: Option<Instant>,
|
||||||
runs: usize,
|
runs: usize,
|
||||||
|
last_stats: Option<RunStats>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UnconsciousAgent {
|
impl UnconsciousAgent {
|
||||||
|
|
@ -60,6 +61,7 @@ pub struct UnconsciousSnapshot {
|
||||||
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 agent: Option<std::sync::Arc<crate::agent::Agent>>,
|
||||||
|
pub last_stats: Option<RunStats>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Unconscious {
|
pub struct Unconscious {
|
||||||
|
|
@ -105,6 +107,7 @@ impl Unconscious {
|
||||||
agent: None,
|
agent: None,
|
||||||
last_run: None,
|
last_run: None,
|
||||||
runs: 0,
|
runs: 0,
|
||||||
|
last_stats: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
agents.sort_by(|a, b| a.name.cmp(&b.name));
|
agents.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
|
@ -144,6 +147,7 @@ impl Unconscious {
|
||||||
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(),
|
agent: a.agent.clone(),
|
||||||
|
last_stats: a.last_stats.clone(),
|
||||||
}).collect()
|
}).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -173,8 +177,9 @@ impl Unconscious {
|
||||||
agent.runs += 1;
|
agent.runs += 1;
|
||||||
// Get the AutoAgent back from the finished task
|
// Get the AutoAgent back from the finished task
|
||||||
match handle.now_or_never() {
|
match handle.now_or_never() {
|
||||||
Some(Ok((auto_back, result))) => {
|
Some(Ok((auto_back, result, stats))) => {
|
||||||
agent.auto = auto_back;
|
agent.auto = auto_back;
|
||||||
|
agent.last_stats = Some(stats);
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => dbglog!("[unconscious] {} completed (run {})",
|
Ok(_) => dbglog!("[unconscious] {} completed (run {})",
|
||||||
agent.name, agent.runs),
|
agent.name, agent.runs),
|
||||||
|
|
@ -289,30 +294,64 @@ impl Unconscious {
|
||||||
|
|
||||||
self.agents[idx].handle = Some(tokio::spawn(async move {
|
self.agents[idx].handle = Some(tokio::spawn(async move {
|
||||||
let result = auto.run_shared(&agent).await;
|
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.steps = orig_steps;
|
||||||
(auto, result)
|
(auto, result, stats)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn save_agent_log(name: &str, agent: &std::sync::Arc<crate::agent::Agent>) {
|
pub async fn save_agent_log(name: &str, agent: &std::sync::Arc<crate::agent::Agent>) -> RunStats {
|
||||||
let dir = dirs::home_dir().unwrap_or_default()
|
let dir = dirs::home_dir().unwrap_or_default()
|
||||||
.join(format!(".consciousness/logs/{}", name));
|
.join(format!(".consciousness/logs/{}", name));
|
||||||
if std::fs::create_dir_all(&dir).is_err() { return; }
|
let ctx = agent.context.lock().await;
|
||||||
let ts = chrono::Utc::now().format("%Y%m%d-%H%M%S");
|
let stats = compute_run_stats(ctx.conversation());
|
||||||
let path = dir.join(format!("{}.json", ts));
|
if std::fs::create_dir_all(&dir).is_ok() {
|
||||||
let sections: serde_json::Value = {
|
let ts = chrono::Utc::now().format("%Y%m%d-%H%M%S");
|
||||||
let ctx = agent.context.lock().await;
|
let path = dir.join(format!("{}.json", ts));
|
||||||
serde_json::json!({
|
let sections = serde_json::json!({
|
||||||
"system": ctx.system(),
|
"system": ctx.system(),
|
||||||
"identity": ctx.identity(),
|
"identity": ctx.identity(),
|
||||||
"journal": ctx.journal(),
|
"journal": ctx.journal(),
|
||||||
"conversation": ctx.conversation(),
|
"conversation": ctx.conversation(),
|
||||||
})
|
"stats": stats,
|
||||||
};
|
});
|
||||||
if let Ok(json) = serde_json::to_string_pretty(§ions) {
|
if let Ok(json) = serde_json::to_string_pretty(§ions) {
|
||||||
let _ = std::fs::write(&path, json);
|
let _ = std::fs::write(&path, json);
|
||||||
dbglog!("[unconscious] saved log to {}", path.display());
|
}
|
||||||
}
|
}
|
||||||
|
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<String, usize>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String, usize> = 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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -250,6 +250,10 @@ impl SubconsciousScreen {
|
||||||
format!("run {}", snap.runs + 1)
|
format!("run {}", snap.runs + 1)
|
||||||
} else if !snap.enabled {
|
} else if !snap.enabled {
|
||||||
"off".to_string()
|
"off".to_string()
|
||||||
|
} else if let Some(ref stats) = snap.last_stats {
|
||||||
|
format!("×{} {} {}msg {}tc",
|
||||||
|
snap.runs, ago,
|
||||||
|
stats.messages, stats.tool_calls)
|
||||||
} else {
|
} else {
|
||||||
format!("×{} {}", snap.runs, ago)
|
format!("×{} {}", snap.runs, ago)
|
||||||
};
|
};
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue