1use std::time::Instant;
21
22#[derive(Debug, Clone)]
24pub struct StepMetrics {
25 pub unit_name: String,
26 pub step_name: String,
27 pub step_type: String,
28 pub duration_ms: u64,
29 pub input_tokens: u64,
30 pub output_tokens: u64,
31 pub anchor_breaches: u32,
32 pub chain_activations: u32,
33 pub was_retried: bool,
34}
35
36#[derive(Debug, Clone)]
38pub struct UnitMetrics {
39 pub unit_name: String,
40 pub persona_name: String,
41 pub duration_ms: u64,
42 pub total_steps: usize,
43 pub total_input_tokens: u64,
44 pub total_output_tokens: u64,
45 pub total_anchor_breaches: u32,
46 pub total_chain_activations: u32,
47}
48
49#[derive(Debug)]
51pub struct HookManager {
52 step_metrics: Vec<StepMetrics>,
53 unit_metrics: Vec<UnitMetrics>,
54 current_unit_start: Option<Instant>,
56 current_unit_name: String,
57 current_persona: String,
58 current_step_start: Option<Instant>,
59 current_step_name: String,
60 current_step_type: String,
61}
62
63impl HookManager {
64 pub fn new() -> Self {
66 HookManager {
67 step_metrics: Vec::new(),
68 unit_metrics: Vec::new(),
69 current_unit_start: None,
70 current_unit_name: String::new(),
71 current_persona: String::new(),
72 current_step_start: None,
73 current_step_name: String::new(),
74 current_step_type: String::new(),
75 }
76 }
77
78 pub fn on_unit_start(&mut self, unit_name: &str, persona_name: &str) {
80 self.current_unit_start = Some(Instant::now());
81 self.current_unit_name = unit_name.to_string();
82 self.current_persona = persona_name.to_string();
83 }
84
85 pub fn on_unit_end(&mut self) {
87 let duration_ms = self
88 .current_unit_start
89 .map(|s| s.elapsed().as_millis() as u64)
90 .unwrap_or(0);
91
92 let unit_steps: Vec<&StepMetrics> = self
94 .step_metrics
95 .iter()
96 .filter(|s| s.unit_name == self.current_unit_name)
97 .collect();
98
99 self.unit_metrics.push(UnitMetrics {
100 unit_name: self.current_unit_name.clone(),
101 persona_name: self.current_persona.clone(),
102 duration_ms,
103 total_steps: unit_steps.len(),
104 total_input_tokens: unit_steps.iter().map(|s| s.input_tokens).sum(),
105 total_output_tokens: unit_steps.iter().map(|s| s.output_tokens).sum(),
106 total_anchor_breaches: unit_steps.iter().map(|s| s.anchor_breaches).sum(),
107 total_chain_activations: unit_steps.iter().map(|s| s.chain_activations).sum(),
108 });
109
110 self.current_unit_start = None;
111 }
112
113 pub fn on_step_start(&mut self, step_name: &str, step_type: &str) {
115 self.current_step_start = Some(Instant::now());
116 self.current_step_name = step_name.to_string();
117 self.current_step_type = step_type.to_string();
118 }
119
120 pub fn on_step_end(
122 &mut self,
123 input_tokens: u64,
124 output_tokens: u64,
125 anchor_breaches: u32,
126 chain_activations: u32,
127 was_retried: bool,
128 ) {
129 let duration_ms = self
130 .current_step_start
131 .map(|s| s.elapsed().as_millis() as u64)
132 .unwrap_or(0);
133
134 self.step_metrics.push(StepMetrics {
135 unit_name: self.current_unit_name.clone(),
136 step_name: self.current_step_name.clone(),
137 step_type: self.current_step_type.clone(),
138 duration_ms,
139 input_tokens,
140 output_tokens,
141 anchor_breaches,
142 chain_activations,
143 was_retried,
144 });
145
146 self.current_step_start = None;
147 }
148
149 pub fn step_metrics(&self) -> &[StepMetrics] {
151 &self.step_metrics
152 }
153
154 pub fn unit_metrics(&self) -> &[UnitMetrics] {
156 &self.unit_metrics
157 }
158
159 pub fn total_duration_ms(&self) -> u64 {
161 self.unit_metrics.iter().map(|u| u.duration_ms).sum()
162 }
163
164 pub fn total_input_tokens(&self) -> u64 {
166 self.step_metrics.iter().map(|s| s.input_tokens).sum()
167 }
168
169 pub fn total_output_tokens(&self) -> u64 {
171 self.step_metrics.iter().map(|s| s.output_tokens).sum()
172 }
173
174 pub fn total_steps(&self) -> usize {
176 self.step_metrics.len()
177 }
178
179 pub fn retried_steps(&self) -> usize {
181 self.step_metrics.iter().filter(|s| s.was_retried).count()
182 }
183
184 pub fn slowest_step(&self) -> Option<&StepMetrics> {
186 self.step_metrics.iter().max_by_key(|s| s.duration_ms)
187 }
188
189 pub fn most_expensive_step(&self) -> Option<&StepMetrics> {
191 self.step_metrics
192 .iter()
193 .max_by_key(|s| s.input_tokens + s.output_tokens)
194 }
195
196 pub fn avg_step_duration_ms(&self) -> u64 {
198 if self.step_metrics.is_empty() {
199 return 0;
200 }
201 let total: u64 = self.step_metrics.iter().map(|s| s.duration_ms).sum();
202 total / self.step_metrics.len() as u64
203 }
204}
205
206#[cfg(test)]
209mod tests {
210 use super::*;
211 use std::thread;
212 use std::time::Duration;
213
214 #[test]
215 fn new_hook_manager_is_empty() {
216 let hm = HookManager::new();
217 assert_eq!(hm.total_steps(), 0);
218 assert_eq!(hm.total_duration_ms(), 0);
219 assert_eq!(hm.total_input_tokens(), 0);
220 assert_eq!(hm.total_output_tokens(), 0);
221 assert_eq!(hm.retried_steps(), 0);
222 assert!(hm.slowest_step().is_none());
223 assert!(hm.most_expensive_step().is_none());
224 assert_eq!(hm.avg_step_duration_ms(), 0);
225 }
226
227 #[test]
228 fn step_lifecycle() {
229 let mut hm = HookManager::new();
230 hm.on_unit_start("Flow1", "Expert");
231 hm.on_step_start("Analyze", "step");
232 thread::sleep(Duration::from_millis(5));
234 hm.on_step_end(100, 50, 0, 0, false);
235 hm.on_unit_end();
236
237 assert_eq!(hm.total_steps(), 1);
238 let s = &hm.step_metrics()[0];
239 assert_eq!(s.unit_name, "Flow1");
240 assert_eq!(s.step_name, "Analyze");
241 assert_eq!(s.step_type, "step");
242 assert_eq!(s.input_tokens, 100);
243 assert_eq!(s.output_tokens, 50);
244 assert!(s.duration_ms >= 4); assert!(!s.was_retried);
246 }
247
248 #[test]
249 fn unit_aggregates_steps() {
250 let mut hm = HookManager::new();
251 hm.on_unit_start("Flow1", "Expert");
252
253 hm.on_step_start("Step1", "step");
254 hm.on_step_end(100, 50, 1, 0, false);
255
256 hm.on_step_start("Step2", "step");
257 hm.on_step_end(200, 100, 0, 1, true);
258
259 hm.on_unit_end();
260
261 let u = &hm.unit_metrics()[0];
262 assert_eq!(u.unit_name, "Flow1");
263 assert_eq!(u.total_steps, 2);
264 assert_eq!(u.total_input_tokens, 300);
265 assert_eq!(u.total_output_tokens, 150);
266 assert_eq!(u.total_anchor_breaches, 1);
267 assert_eq!(u.total_chain_activations, 1);
268 }
269
270 #[test]
271 fn multiple_units() {
272 let mut hm = HookManager::new();
273
274 hm.on_unit_start("Flow1", "P1");
275 hm.on_step_start("S1", "step");
276 hm.on_step_end(10, 5, 0, 0, false);
277 hm.on_unit_end();
278
279 hm.on_unit_start("Flow2", "P2");
280 hm.on_step_start("S2", "step");
281 hm.on_step_end(20, 10, 0, 0, false);
282 hm.on_unit_end();
283
284 assert_eq!(hm.unit_metrics().len(), 2);
285 assert_eq!(hm.total_steps(), 2);
286 assert_eq!(hm.total_input_tokens(), 30);
287 assert_eq!(hm.total_output_tokens(), 15);
288 }
289
290 #[test]
291 fn retried_steps_count() {
292 let mut hm = HookManager::new();
293 hm.on_unit_start("F", "P");
294 hm.on_step_start("S1", "step");
295 hm.on_step_end(10, 5, 0, 0, false);
296 hm.on_step_start("S2", "step");
297 hm.on_step_end(20, 10, 2, 0, true);
298 hm.on_step_start("S3", "step");
299 hm.on_step_end(15, 8, 0, 0, false);
300 hm.on_unit_end();
301
302 assert_eq!(hm.retried_steps(), 1);
303 }
304
305 #[test]
306 fn slowest_step() {
307 let mut hm = HookManager::new();
308 hm.on_unit_start("F", "P");
309
310 hm.on_step_start("Fast", "step");
311 hm.on_step_end(10, 5, 0, 0, false);
312
313 hm.on_step_start("Slow", "step");
314 thread::sleep(Duration::from_millis(10));
315 hm.on_step_end(10, 5, 0, 0, false);
316
317 hm.on_unit_end();
318
319 let slowest = hm.slowest_step().unwrap();
320 assert_eq!(slowest.step_name, "Slow");
321 }
322
323 #[test]
324 fn most_expensive_step() {
325 let mut hm = HookManager::new();
326 hm.on_unit_start("F", "P");
327
328 hm.on_step_start("Cheap", "step");
329 hm.on_step_end(10, 5, 0, 0, false);
330
331 hm.on_step_start("Expensive", "step");
332 hm.on_step_end(1000, 500, 0, 0, false);
333
334 hm.on_unit_end();
335
336 let expensive = hm.most_expensive_step().unwrap();
337 assert_eq!(expensive.step_name, "Expensive");
338 assert_eq!(expensive.input_tokens + expensive.output_tokens, 1500);
339 }
340
341 #[test]
342 fn avg_step_duration() {
343 let mut hm = HookManager::new();
344 hm.on_unit_start("F", "P");
345
346 hm.step_metrics.push(StepMetrics {
348 unit_name: "F".into(),
349 step_name: "S1".into(),
350 step_type: "step".into(),
351 duration_ms: 100,
352 input_tokens: 0,
353 output_tokens: 0,
354 anchor_breaches: 0,
355 chain_activations: 0,
356 was_retried: false,
357 });
358 hm.step_metrics.push(StepMetrics {
359 unit_name: "F".into(),
360 step_name: "S2".into(),
361 step_type: "step".into(),
362 duration_ms: 200,
363 input_tokens: 0,
364 output_tokens: 0,
365 anchor_breaches: 0,
366 chain_activations: 0,
367 was_retried: false,
368 });
369
370 assert_eq!(hm.avg_step_duration_ms(), 150);
371 }
372
373 #[test]
374 fn step_with_anchor_breaches_and_chains() {
375 let mut hm = HookManager::new();
376 hm.on_unit_start("F", "P");
377 hm.on_step_start("S1", "step");
378 hm.on_step_end(100, 50, 3, 2, true);
379 hm.on_unit_end();
380
381 let s = &hm.step_metrics()[0];
382 assert_eq!(s.anchor_breaches, 3);
383 assert_eq!(s.chain_activations, 2);
384 assert!(s.was_retried);
385 }
386}