use std::time::Duration; /// Events the observer can record #[derive(Debug, Clone)] pub enum ObserverEvent { AgentStart { provider: String, model: String, }, /// A request is about to be sent to an LLM provider. /// /// This is emitted immediately before a provider call so observers can print /// user-facing progress without leaking prompt contents. LlmRequest { provider: String, model: String, messages_count: usize, }, /// Result of a single LLM provider call. LlmResponse { provider: String, model: String, duration: Duration, success: bool, error_message: Option, }, AgentEnd { provider: String, model: String, duration: Duration, tokens_used: Option, cost_usd: Option, }, /// A tool call is about to be executed. ToolCallStart { tool: String, }, ToolCall { tool: String, duration: Duration, success: bool, }, /// The agent produced a final answer for the current user message. TurnComplete, ChannelMessage { channel: String, direction: String, }, HeartbeatTick, Error { component: String, message: String, }, } /// Numeric metrics #[derive(Debug, Clone)] pub enum ObserverMetric { RequestLatency(Duration), TokensUsed(u64), ActiveSessions(u64), QueueDepth(u64), } /// Core observability trait — implement for any backend pub trait Observer: Send + Sync + 'static { /// Record a discrete event fn record_event(&self, event: &ObserverEvent); /// Record a numeric metric fn record_metric(&self, metric: &ObserverMetric); /// Flush any buffered data (no-op for most backends) fn flush(&self) {} /// Human-readable name of this observer fn name(&self) -> &str; /// Downcast to `Any` for backend-specific operations fn as_any(&self) -> &dyn std::any::Any; } #[cfg(test)] mod tests { use super::*; use parking_lot::Mutex; use std::time::Duration; #[derive(Default)] struct DummyObserver { events: Mutex, metrics: Mutex, } impl Observer for DummyObserver { fn record_event(&self, _event: &ObserverEvent) { let mut guard = self.events.lock(); *guard += 1; } fn record_metric(&self, _metric: &ObserverMetric) { let mut guard = self.metrics.lock(); *guard += 1; } fn name(&self) -> &str { "dummy-observer" } fn as_any(&self) -> &dyn std::any::Any { self } } #[test] fn observer_records_events_and_metrics() { let observer = DummyObserver::default(); observer.record_event(&ObserverEvent::HeartbeatTick); observer.record_event(&ObserverEvent::Error { component: "test".into(), message: "boom".into(), }); observer.record_metric(&ObserverMetric::TokensUsed(42)); assert_eq!(*observer.events.lock(), 2); assert_eq!(*observer.metrics.lock(), 1); } #[test] fn observer_default_flush_and_as_any_work() { let observer = DummyObserver::default(); observer.flush(); assert_eq!(observer.name(), "dummy-observer"); assert!(observer.as_any().downcast_ref::().is_some()); } #[test] fn observer_event_and_metric_are_cloneable() { let event = ObserverEvent::ToolCall { tool: "shell".into(), duration: Duration::from_millis(10), success: true, }; let metric = ObserverMetric::RequestLatency(Duration::from_millis(8)); let cloned_event = event.clone(); let cloned_metric = metric.clone(); assert!(matches!(cloned_event, ObserverEvent::ToolCall { .. })); assert!(matches!(cloned_metric, ObserverMetric::RequestLatency(_))); } }