1use std::future::Future;
8use std::pin::Pin;
9use std::time::Duration;
10
11use serde::{Deserialize, Serialize};
12
13use crate::types::{Cost, Usage};
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ToolExecMetrics {
20 pub tool_name: String,
22 pub duration: Duration,
24 pub success: bool,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TurnMetrics {
33 pub turn_index: usize,
35 pub llm_call_duration: Duration,
37 pub tool_executions: Vec<ToolExecMetrics>,
39 pub usage: Usage,
41 pub cost: Cost,
43 pub turn_duration: Duration,
45}
46
47pub type MetricsFuture<'a> = Pin<Box<dyn Future<Output = ()> + Send + 'a>>;
50pub trait MetricsCollector: Send + Sync {
75 fn on_metrics<'a>(&'a self, metrics: &'a TurnMetrics) -> MetricsFuture<'a>;
77}
78
79const _: () = {
82 const fn assert_send_sync<T: Send + Sync>() {}
83 assert_send_sync::<ToolExecMetrics>();
84 assert_send_sync::<TurnMetrics>();
85};
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use std::sync::Arc;
91 use std::sync::atomic::{AtomicUsize, Ordering};
92
93 struct CountingCollector {
94 count: AtomicUsize,
95 }
96
97 impl MetricsCollector for CountingCollector {
98 fn on_metrics<'a>(&'a self, _metrics: &'a TurnMetrics) -> MetricsFuture<'a> {
99 Box::pin(async move {
100 self.count.fetch_add(1, Ordering::SeqCst);
101 })
102 }
103 }
104
105 #[tokio::test]
106 async fn collector_receives_metrics() {
107 let collector = CountingCollector {
108 count: AtomicUsize::new(0),
109 };
110 let metrics = TurnMetrics {
111 turn_index: 0,
112 llm_call_duration: Duration::from_millis(150),
113 tool_executions: vec![
114 ToolExecMetrics {
115 tool_name: "bash".into(),
116 duration: Duration::from_millis(50),
117 success: true,
118 },
119 ToolExecMetrics {
120 tool_name: "read_file".into(),
121 duration: Duration::from_millis(10),
122 success: false,
123 },
124 ],
125 usage: Usage {
126 input: 100,
127 output: 50,
128 total: 150,
129 ..Default::default()
130 },
131 cost: Cost {
132 input: 0.001,
133 output: 0.002,
134 total: 0.003,
135 ..Default::default()
136 },
137 turn_duration: Duration::from_millis(210),
138 };
139 collector.on_metrics(&metrics).await;
140 assert_eq!(collector.count.load(Ordering::SeqCst), 1);
141 }
142
143 #[tokio::test]
144 async fn metrics_captures_tool_details() {
145 let metrics = TurnMetrics {
146 turn_index: 2,
147 llm_call_duration: Duration::from_secs(1),
148 tool_executions: vec![ToolExecMetrics {
149 tool_name: "bash".into(),
150 duration: Duration::from_millis(500),
151 success: true,
152 }],
153 usage: Usage::default(),
154 cost: Cost::default(),
155 turn_duration: Duration::from_millis(1500),
156 };
157 assert_eq!(metrics.tool_executions.len(), 1);
158 assert_eq!(metrics.tool_executions[0].tool_name, "bash");
159 assert!(metrics.tool_executions[0].success);
160 assert_eq!(metrics.turn_index, 2);
161 }
162
163 #[tokio::test]
164 async fn arc_collector_is_send_sync() {
165 let collector: Arc<dyn MetricsCollector> = Arc::new(CountingCollector {
166 count: AtomicUsize::new(0),
167 });
168 let metrics = TurnMetrics {
169 turn_index: 0,
170 llm_call_duration: Duration::ZERO,
171 tool_executions: vec![],
172 usage: Usage::default(),
173 cost: Cost::default(),
174 turn_duration: Duration::ZERO,
175 };
176 collector.on_metrics(&metrics).await;
177 }
178
179 #[test]
180 fn turn_metrics_serde_roundtrip() {
181 let metrics = TurnMetrics {
182 turn_index: 1,
183 llm_call_duration: Duration::from_millis(200),
184 tool_executions: vec![ToolExecMetrics {
185 tool_name: "write_file".into(),
186 duration: Duration::from_millis(30),
187 success: true,
188 }],
189 usage: Usage {
190 input: 50,
191 output: 25,
192 total: 75,
193 ..Default::default()
194 },
195 cost: Cost {
196 total: 0.005,
197 ..Default::default()
198 },
199 turn_duration: Duration::from_millis(230),
200 };
201 let json = serde_json::to_string(&metrics).unwrap();
202 let parsed: TurnMetrics = serde_json::from_str(&json).unwrap();
203 assert_eq!(parsed.turn_index, 1);
204 assert_eq!(parsed.tool_executions.len(), 1);
205 assert_eq!(parsed.usage.input, 50);
206 }
207}