use crate::language_models::TokenUsage;
use serde::{Deserialize, Serialize};
use std::time::Duration;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct AgentMetrics {
pub trace_id: Option<String>,
pub llm_calls: usize,
pub cache_hits: usize,
pub tool_calls: usize,
#[serde(default)]
pub compactions: usize,
pub total_tokens: Option<usize>,
pub duration: Duration,
}
impl AgentMetrics {
pub fn add_token_usage(&mut self, usage: &TokenUsage) {
self.total_tokens = Some(self.total_tokens.unwrap_or(0) + usage.total_tokens);
}
pub fn tokens_per_call(&self) -> Option<f64> {
if self.llm_calls == 0 {
return None;
}
self.total_tokens.map(|t| t as f64 / self.llm_calls as f64)
}
pub fn log_summary(&self) {
let trace = self.trace_id.as_deref().unwrap_or("-");
let duration_ms = self.duration.as_millis();
match self.total_tokens {
Some(tokens) => log::info!(
target: "lc_agents::metrics",
"agent_exec summary trace_id={} llm_calls={} tool_calls={} total_tokens={} duration_ms={}",
trace,
self.llm_calls,
self.tool_calls,
tokens,
duration_ms
),
None => log::info!(
target: "lc_agents::metrics",
"agent_exec summary trace_id={} llm_calls={} tool_calls={} total_tokens=n/a duration_ms={}",
trace,
self.llm_calls,
self.tool_calls,
duration_ms
),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_metrics_default() {
let m = AgentMetrics::default();
assert_eq!(m.llm_calls, 0);
assert_eq!(m.tool_calls, 0);
assert_eq!(m.total_tokens, None);
assert_eq!(m.trace_id, None);
assert_eq!(m.duration, Duration::ZERO);
}
#[test]
fn test_add_token_usage() {
let mut m = AgentMetrics::default();
m.add_token_usage(&TokenUsage {
prompt_tokens: 10,
completion_tokens: 5,
total_tokens: 15,
});
m.add_token_usage(&TokenUsage {
prompt_tokens: 3,
completion_tokens: 2,
total_tokens: 5,
});
assert_eq!(m.total_tokens, Some(20));
}
#[test]
fn test_tokens_per_call() {
let mut m = AgentMetrics::default();
assert_eq!(m.tokens_per_call(), None);
m.llm_calls = 2;
m.total_tokens = Some(20);
assert_eq!(m.tokens_per_call(), Some(10.0));
}
#[test]
fn test_tokens_per_call_zero_calls() {
let m = AgentMetrics {
total_tokens: Some(10),
..Default::default()
};
assert_eq!(m.tokens_per_call(), None);
}
#[test]
fn test_serde_roundtrip() {
let m = AgentMetrics {
trace_id: Some("trace-x".to_string()),
llm_calls: 3,
cache_hits: 1,
tool_calls: 2,
compactions: 1,
total_tokens: Some(42),
duration: Duration::from_millis(1500),
};
let json = serde_json::to_string(&m).expect("serialize");
let back: AgentMetrics = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back.trace_id, m.trace_id);
assert_eq!(back.llm_calls, m.llm_calls);
assert_eq!(back.cache_hits, m.cache_hits);
assert_eq!(back.tool_calls, m.tool_calls);
assert_eq!(back.compactions, m.compactions);
assert_eq!(back.total_tokens, m.total_tokens);
assert_eq!(back.duration, m.duration);
}
#[test]
fn test_serde_roundtrip_without_compactions_field() {
let legacy = r#"{
"trace_id": "trace-x",
"llm_calls": 3,
"cache_hits": 1,
"tool_calls": 2,
"total_tokens": 42,
"duration": {"secs": 1, "nanos": 500000000}
}"#;
let back: AgentMetrics = serde_json::from_str(legacy).expect("legacy payload deserializes");
assert_eq!(back.compactions, 0, "missing field defaults to 0");
}
#[test]
fn test_log_summary_no_panic() {
let m = AgentMetrics {
trace_id: Some("trace-x".to_string()),
..Default::default()
};
m.log_summary();
let m = AgentMetrics {
total_tokens: Some(42),
..Default::default()
};
m.log_summary();
}
}