clawgarden-agent 0.11.0

Agent runtime with persona/memory loader, judge, and pi RPC for ClawGarden
Documentation
//! Loop metrics — Aggregates agent loop termination metrics

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