use std::collections::HashMap;
use std::sync::Mutex;
use serde::Serialize;
use crate::agent_loop::LoopState;
#[derive(Debug, Clone, Serialize)]
pub struct LoopMetricsSnapshot {
pub total_runs: u64,
pub avg_steps: f64,
pub avg_tool_calls: f64,
pub terminations: HashMap<String, u64>,
}
#[derive(Debug, Default)]
struct LoopMetrics {
total_runs: u64,
total_steps: u64,
total_tool_calls: u64,
terminations: HashMap<String, u64>,
}
static METRICS: once_cell::sync::Lazy<Mutex<LoopMetrics>> =
once_cell::sync::Lazy::new(|| Mutex::new(LoopMetrics::default()));
pub fn record(termination: &LoopState, steps: usize, tool_calls: usize) -> LoopMetricsSnapshot {
let mut m = METRICS.lock().unwrap_or_else(|e| e.into_inner());
m.total_runs += 1;
m.total_steps += steps as u64;
m.total_tool_calls += tool_calls as u64;
let key = format!("{:?}", termination);
*m.terminations.entry(key).or_insert(0) += 1;
to_snapshot(&m)
}
pub fn snapshot() -> LoopMetricsSnapshot {
let m = METRICS.lock().unwrap_or_else(|e| e.into_inner());
to_snapshot(&m)
}
pub fn export_json() -> anyhow::Result<String> {
let snap = snapshot();
Ok(serde_json::to_string_pretty(&snap)?)
}
pub fn export_prometheus() -> String {
let snap = snapshot();
let mut out = String::new();
out.push_str("# HELP clawgarden_agent_loop_total_runs Total completed agent loops\n");
out.push_str("# TYPE clawgarden_agent_loop_total_runs counter\n");
out.push_str(&format!("clawgarden_agent_loop_total_runs {}\n", snap.total_runs));
out.push_str("# HELP clawgarden_agent_loop_avg_steps Average loop steps per run\n");
out.push_str("# TYPE clawgarden_agent_loop_avg_steps gauge\n");
out.push_str(&format!("clawgarden_agent_loop_avg_steps {:.6}\n", snap.avg_steps));
out.push_str("# HELP clawgarden_agent_loop_avg_tool_calls Average tool calls per run\n");
out.push_str("# TYPE clawgarden_agent_loop_avg_tool_calls gauge\n");
out.push_str(&format!("clawgarden_agent_loop_avg_tool_calls {:.6}\n", snap.avg_tool_calls));
out.push_str("# HELP clawgarden_agent_loop_termination_total Loop termination counts by reason\n");
out.push_str("# TYPE clawgarden_agent_loop_termination_total counter\n");
for (k, v) in &snap.terminations {
out.push_str(&format!(
"clawgarden_agent_loop_termination_total{{reason=\"{}\"}} {}\n",
k, v
));
}
out
}
fn to_snapshot(m: &LoopMetrics) -> LoopMetricsSnapshot {
LoopMetricsSnapshot {
total_runs: m.total_runs,
avg_steps: if m.total_runs == 0 {
0.0
} else {
m.total_steps as f64 / m.total_runs as f64
},
avg_tool_calls: if m.total_runs == 0 {
0.0
} else {
m.total_tool_calls as f64 / m.total_runs as f64
},
terminations: m.terminations.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_export_formats() {
let _ = record(&LoopState::FinalRespond, 3, 2);
let _ = record(&LoopState::AbortStall, 5, 5);
let json = export_json().unwrap();
assert!(json.contains("total_runs"));
assert!(json.contains("terminations"));
let prom = export_prometheus();
assert!(prom.contains("clawgarden_agent_loop_total_runs"));
assert!(prom.contains("clawgarden_agent_loop_termination_total"));
}
}