Skip to main content

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