Skip to main content

lc_core/observability/
agent_metrics.rs

1// lc-core/src/observability/agent_metrics.rs
2//! Aggregated metrics for a single agent execution.
3//!
4//! Moved from `lc-agents` (v0.20.2) so the unified observability payload
5//! (`ObsEvent`) can reference it from `lc-core`. `lc-agents` keeps a one-line
6//! re-export for path compatibility.
7
8use crate::language_models::TokenUsage;
9use serde::{Deserialize, Serialize};
10use std::time::Duration;
11
12/// Aggregated metrics for a single agent execution.
13#[derive(Debug, Clone, Default, Serialize, Deserialize)]
14pub struct AgentMetrics {
15    /// trace_id of this execution (`None` if absent).
16    pub trace_id: Option<String>,
17    /// Number of LLM planning calls (plan iterations).
18    pub llm_calls: usize,
19    /// Number of LLM result cache hits that skipped an LLM call.
20    pub cache_hits: usize,
21    /// Number of tool executions (including parallel).
22    pub tool_calls: usize,
23    /// Cumulative token usage (`None` if the agent does not report tokens).
24    pub total_tokens: Option<usize>,
25    /// Total duration of a single invoke.
26    pub duration: Duration,
27}
28
29impl AgentMetrics {
30    /// Accumulates token usage from one LLM call.
31    pub fn add_token_usage(&mut self, usage: &TokenUsage) {
32        self.total_tokens = Some(self.total_tokens.unwrap_or(0) + usage.total_tokens);
33    }
34
35    /// Average tokens per LLM call.
36    pub fn tokens_per_call(&self) -> Option<f64> {
37        if self.llm_calls == 0 {
38            return None;
39        }
40        self.total_tokens.map(|t| t as f64 / self.llm_calls as f64)
41    }
42
43    /// Emits the metrics audit-log line (target: `lc_agents::metrics`).
44    pub fn log_summary(&self) {
45        let trace = self.trace_id.as_deref().unwrap_or("-");
46        let duration_ms = self.duration.as_millis();
47        match self.total_tokens {
48            Some(tokens) => log::info!(
49                target: "lc_agents::metrics",
50                "agent_exec summary trace_id={} llm_calls={} tool_calls={} total_tokens={} duration_ms={}",
51                trace,
52                self.llm_calls,
53                self.tool_calls,
54                tokens,
55                duration_ms
56            ),
57            None => log::info!(
58                target: "lc_agents::metrics",
59                "agent_exec summary trace_id={} llm_calls={} tool_calls={} total_tokens=n/a duration_ms={}",
60                trace,
61                self.llm_calls,
62                self.tool_calls,
63                duration_ms
64            ),
65        }
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn test_metrics_default() {
75        let m = AgentMetrics::default();
76        assert_eq!(m.llm_calls, 0);
77        assert_eq!(m.tool_calls, 0);
78        assert_eq!(m.total_tokens, None);
79        assert_eq!(m.trace_id, None);
80        assert_eq!(m.duration, Duration::ZERO);
81    }
82
83    #[test]
84    fn test_add_token_usage() {
85        let mut m = AgentMetrics::default();
86        m.add_token_usage(&TokenUsage {
87            prompt_tokens: 10,
88            completion_tokens: 5,
89            total_tokens: 15,
90        });
91        m.add_token_usage(&TokenUsage {
92            prompt_tokens: 3,
93            completion_tokens: 2,
94            total_tokens: 5,
95        });
96        assert_eq!(m.total_tokens, Some(20));
97    }
98
99    #[test]
100    fn test_tokens_per_call() {
101        let mut m = AgentMetrics::default();
102        assert_eq!(m.tokens_per_call(), None);
103        m.llm_calls = 2;
104        m.total_tokens = Some(20);
105        assert_eq!(m.tokens_per_call(), Some(10.0));
106    }
107
108    #[test]
109    fn test_tokens_per_call_zero_calls() {
110        let m = AgentMetrics {
111            total_tokens: Some(10),
112            ..Default::default()
113        };
114        assert_eq!(m.tokens_per_call(), None);
115    }
116
117    #[test]
118    fn test_serde_roundtrip() {
119        let m = AgentMetrics {
120            trace_id: Some("trace-x".to_string()),
121            llm_calls: 3,
122            cache_hits: 1,
123            tool_calls: 2,
124            total_tokens: Some(42),
125            duration: Duration::from_millis(1500),
126        };
127        let json = serde_json::to_string(&m).expect("serialize");
128        let back: AgentMetrics = serde_json::from_str(&json).expect("deserialize");
129        assert_eq!(back.trace_id, m.trace_id);
130        assert_eq!(back.llm_calls, m.llm_calls);
131        assert_eq!(back.cache_hits, m.cache_hits);
132        assert_eq!(back.tool_calls, m.tool_calls);
133        assert_eq!(back.total_tokens, m.total_tokens);
134        assert_eq!(back.duration, m.duration);
135    }
136
137    #[test]
138    fn test_log_summary_no_panic() {
139        let m = AgentMetrics {
140            trace_id: Some("trace-x".to_string()),
141            ..Default::default()
142        };
143        m.log_summary();
144        let m = AgentMetrics {
145            total_tokens: Some(42),
146            ..Default::default()
147        };
148        m.log_summary();
149    }
150}