Skip to main content

construct/observability/
verbose.rs

1use super::traits::{Observer, ObserverEvent, ObserverMetric};
2use std::any::Any;
3
4/// Human-readable progress observer for interactive CLI sessions.
5///
6/// This observer prints compact `>` / `<` progress lines without exposing
7/// prompt contents. It is intended to be opt-in (e.g. `--verbose`).
8pub 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}