construct/observability/
verbose.rs1use super::traits::{Observer, ObserverEvent, ObserverMetric};
2use std::any::Any;
3
4pub struct VerboseObserver;
9
10impl VerboseObserver {
11 pub fn new() -> Self {
12 Self
13 }
14}
15
16impl Observer for VerboseObserver {
17 fn record_event(&self, event: &ObserverEvent) {
18 match event {
19 ObserverEvent::LlmRequest {
20 provider,
21 model,
22 messages_count,
23 } => {
24 eprintln!("> Thinking");
25 eprintln!(
26 "> Send (provider={}, model={}, messages={})",
27 provider, model, messages_count
28 );
29 }
30 ObserverEvent::LlmResponse {
31 duration, success, ..
32 } => {
33 let ms = u64::try_from(duration.as_millis()).unwrap_or(u64::MAX);
34 eprintln!("< Receive (success={success}, duration_ms={ms})");
35 }
36 ObserverEvent::ToolCallStart { tool, .. } => {
37 eprintln!("> Tool {tool}");
38 }
39 ObserverEvent::ToolCall {
40 tool,
41 duration,
42 success,
43 } => {
44 let ms = u64::try_from(duration.as_millis()).unwrap_or(u64::MAX);
45 eprintln!("< Tool {tool} (success={success}, duration_ms={ms})");
46 }
47 ObserverEvent::TurnComplete => {
48 eprintln!("< Complete");
49 }
50 _ => {}
51 }
52 }
53
54 #[inline(always)]
55 fn record_metric(&self, _metric: &ObserverMetric) {}
56
57 fn name(&self) -> &str {
58 "verbose"
59 }
60
61 fn as_any(&self) -> &dyn Any {
62 self
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use std::time::Duration;
70
71 #[test]
72 fn verbose_name() {
73 assert_eq!(VerboseObserver::new().name(), "verbose");
74 }
75
76 #[test]
77 fn verbose_events_do_not_panic() {
78 let obs = VerboseObserver::new();
79 obs.record_event(&ObserverEvent::LlmRequest {
80 provider: "openrouter".into(),
81 model: "claude".into(),
82 messages_count: 3,
83 });
84 obs.record_event(&ObserverEvent::LlmResponse {
85 provider: "openrouter".into(),
86 model: "claude".into(),
87 duration: Duration::from_millis(12),
88 success: true,
89 error_message: None,
90 input_tokens: Some(50),
91 output_tokens: Some(25),
92 });
93 obs.record_event(&ObserverEvent::ToolCallStart {
94 tool: "shell".into(),
95 arguments: None,
96 });
97 obs.record_event(&ObserverEvent::ToolCall {
98 tool: "shell".into(),
99 duration: Duration::from_millis(2),
100 success: true,
101 });
102 obs.record_event(&ObserverEvent::TurnComplete);
103 }
104
105 #[test]
106 fn verbose_hand_events_do_not_panic() {
107 let obs = VerboseObserver::new();
108 obs.record_event(&ObserverEvent::HandStarted {
109 hand_name: "review".into(),
110 });
111 obs.record_event(&ObserverEvent::HandCompleted {
112 hand_name: "review".into(),
113 duration_ms: 1500,
114 findings_count: 3,
115 });
116 obs.record_event(&ObserverEvent::HandFailed {
117 hand_name: "review".into(),
118 error: "timeout".into(),
119 duration_ms: 5000,
120 });
121 }
122}