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