Skip to main content

oxicode_sdk/
metrics.rs

1//! Agent execution metrics.
2//!
3//! Provides atomic counters for tracking agent runtime statistics:
4//! runs, tokens, tool calls, durations.
5
6use std::sync::atomic::{AtomicU64, Ordering};
7
8/// Atomic agent metrics, safe for concurrent updates.
9#[derive(Debug, Default)]
10pub struct AgentMetrics {
11    /// Total number of agent runs.
12    pub total_runs: AtomicU64,
13    /// Successful runs.
14    pub successful_runs: AtomicU64,
15    /// Failed runs.
16    pub failed_runs: AtomicU64,
17    /// Total input (prompt) tokens consumed.
18    pub total_input_tokens: AtomicU64,
19    /// Total output (completion) tokens consumed.
20    pub total_output_tokens: AtomicU64,
21    /// Total tokens consumed (input + output).
22    pub total_tokens: AtomicU64,
23    /// Total tool calls made.
24    pub tool_calls: AtomicU64,
25    /// Cumulative duration in milliseconds.
26    pub total_duration_ms: AtomicU64,
27}
28
29impl AgentMetrics {
30    /// Create a new zero-initialized metrics instance.
31    pub fn new() -> Self {
32        Self::default()
33    }
34
35    /// Record a successful run.
36    pub fn record_success(
37        &self,
38        duration_ms: u64,
39        input_tokens: u64,
40        output_tokens: u64,
41        tool_count: u64,
42    ) {
43        self.total_runs.fetch_add(1, Ordering::Relaxed);
44        self.successful_runs.fetch_add(1, Ordering::Relaxed);
45        self.total_input_tokens
46            .fetch_add(input_tokens, Ordering::Relaxed);
47        self.total_output_tokens
48            .fetch_add(output_tokens, Ordering::Relaxed);
49        self.total_tokens
50            .fetch_add(input_tokens + output_tokens, Ordering::Relaxed);
51        self.tool_calls.fetch_add(tool_count, Ordering::Relaxed);
52        self.total_duration_ms
53            .fetch_add(duration_ms, Ordering::Relaxed);
54    }
55
56    /// Record a failed run.
57    pub fn record_failure(&self, duration_ms: u64) {
58        self.total_runs.fetch_add(1, Ordering::Relaxed);
59        self.failed_runs.fetch_add(1, Ordering::Relaxed);
60        self.total_duration_ms
61            .fetch_add(duration_ms, Ordering::Relaxed);
62    }
63
64    /// Take a snapshot of all counters.
65    pub fn snapshot(&self) -> MetricsSnapshot {
66        MetricsSnapshot {
67            total_runs: self.total_runs.load(Ordering::Relaxed),
68            successful_runs: self.successful_runs.load(Ordering::Relaxed),
69            failed_runs: self.failed_runs.load(Ordering::Relaxed),
70            total_input_tokens: self.total_input_tokens.load(Ordering::Relaxed),
71            total_output_tokens: self.total_output_tokens.load(Ordering::Relaxed),
72            total_tokens: self.total_tokens.load(Ordering::Relaxed),
73            tool_calls: self.tool_calls.load(Ordering::Relaxed),
74            total_duration_ms: self.total_duration_ms.load(Ordering::Relaxed),
75        }
76    }
77
78    /// Reset all counters to zero.
79    pub fn reset(&self) {
80        self.total_runs.store(0, Ordering::Relaxed);
81        self.successful_runs.store(0, Ordering::Relaxed);
82        self.failed_runs.store(0, Ordering::Relaxed);
83        self.total_input_tokens.store(0, Ordering::Relaxed);
84        self.total_output_tokens.store(0, Ordering::Relaxed);
85        self.total_tokens.store(0, Ordering::Relaxed);
86        self.tool_calls.store(0, Ordering::Relaxed);
87        self.total_duration_ms.store(0, Ordering::Relaxed);
88    }
89}
90
91/// Point-in-time snapshot of agent metrics.
92#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
93pub struct MetricsSnapshot {
94    /// Total number of agent runs.
95    pub total_runs: u64,
96    /// Successful runs.
97    pub successful_runs: u64,
98    /// Failed runs.
99    pub failed_runs: u64,
100    /// Total input (prompt) tokens consumed.
101    #[serde(default)]
102    pub total_input_tokens: u64,
103    /// Total output (completion) tokens consumed.
104    #[serde(default)]
105    pub total_output_tokens: u64,
106    /// Total tokens consumed (input + output).
107    pub total_tokens: u64,
108    /// Total tool calls made.
109    pub tool_calls: u64,
110    /// Cumulative duration in milliseconds.
111    pub total_duration_ms: u64,
112}
113
114impl MetricsSnapshot {
115    /// Calculate the success rate (0.0 to 1.0).
116    pub fn success_rate(&self) -> f64 {
117        if self.total_runs == 0 {
118            return 0.0;
119        }
120        self.successful_runs as f64 / self.total_runs as f64
121    }
122
123    /// Calculate the average run duration in milliseconds.
124    pub fn avg_duration_ms(&self) -> f64 {
125        if self.total_runs == 0 {
126            return 0.0;
127        }
128        self.total_duration_ms as f64 / self.total_runs as f64
129    }
130
131    /// Calculate the average tokens per run.
132    pub fn avg_tokens(&self) -> f64 {
133        if self.total_runs == 0 {
134            return 0.0;
135        }
136        self.total_tokens as f64 / self.total_runs as f64
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_metrics_snapshot_empty() {
146        let metrics = AgentMetrics::new();
147        let snap = metrics.snapshot();
148        assert_eq!(snap.total_runs, 0);
149        assert_eq!(snap.success_rate(), 0.0);
150    }
151
152    #[test]
153    fn test_metrics_record_success() {
154        let metrics = AgentMetrics::new();
155        metrics.record_success(100, 300, 200, 3);
156        metrics.record_success(200, 500, 300, 5);
157
158        let snap = metrics.snapshot();
159        assert_eq!(snap.total_runs, 2);
160        assert_eq!(snap.successful_runs, 2);
161        assert_eq!(snap.failed_runs, 0);
162        assert_eq!(snap.total_input_tokens, 800);
163        assert_eq!(snap.total_output_tokens, 500);
164        assert_eq!(snap.total_tokens, 1300);
165        assert_eq!(snap.tool_calls, 8);
166        assert_eq!(snap.total_duration_ms, 300);
167        assert!((snap.success_rate() - 1.0).abs() < f64::EPSILON);
168        assert!((snap.avg_duration_ms() - 150.0).abs() < f64::EPSILON);
169        assert!((snap.avg_tokens() - 650.0).abs() < f64::EPSILON);
170    }
171
172    #[test]
173    fn test_metrics_record_failure() {
174        let metrics = AgentMetrics::new();
175        metrics.record_failure(50);
176
177        let snap = metrics.snapshot();
178        assert_eq!(snap.total_runs, 1);
179        assert_eq!(snap.failed_runs, 1);
180        assert!((snap.success_rate() - 0.0).abs() < f64::EPSILON);
181    }
182
183    #[test]
184    fn test_metrics_reset() {
185        let metrics = AgentMetrics::new();
186        metrics.record_success(100, 300, 200, 3);
187        metrics.reset();
188
189        let snap = metrics.snapshot();
190        assert_eq!(snap.total_runs, 0);
191        assert_eq!(snap.total_input_tokens, 0);
192        assert_eq!(snap.total_output_tokens, 0);
193    }
194
195    #[test]
196    fn test_snapshot_serialization() {
197        let metrics = AgentMetrics::new();
198        metrics.record_success(100, 300, 200, 3);
199        let snap = metrics.snapshot();
200
201        let json = serde_json::to_string(&snap).unwrap();
202        assert!(json.contains("\"total_runs\":1"));
203        assert!(json.contains("\"total_input_tokens\":300"));
204        assert!(json.contains("\"total_output_tokens\":200"));
205    }
206
207    #[test]
208    fn test_snapshot_deserialize_backward_compat() {
209        // Old JSON without new fields should deserialize with defaults
210        let old_json = r#"{"total_runs":5,"successful_runs":4,"failed_runs":1,"total_tokens":50000,"tool_calls":20,"total_duration_ms":30000}"#;
211        let snap: MetricsSnapshot = serde_json::from_str(old_json).unwrap();
212        assert_eq!(snap.total_runs, 5);
213        assert_eq!(snap.total_tokens, 50000);
214        assert_eq!(snap.total_input_tokens, 0);
215        assert_eq!(snap.total_output_tokens, 0);
216    }
217}