1use serde::{Deserialize, Serialize};
6
7pub type ImprovementResult<T> = Result<T, ImprovementError>;
9
10#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
12#[error("{operation}: {context} ({log_entry})", log_entry = format!("[{}] {} - {} ({}{})", match severity { ImprovementSeverity::Warning => "WARN", ImprovementSeverity::Error => "ERROR", ImprovementSeverity::Critical => "CRIT", }, operation, context, match kind { ErrorKind::ScoringFailed => "scoring_failed", ErrorKind::SelectionFailed => "selection_failed", ErrorKind::ChainExecutionFailed => "chain_failed", ErrorKind::CacheOperationFailed => "cache_failed", ErrorKind::ConfigurationInvalid => "config_invalid", _ => "unknown", }, source_message.as_ref().map(|s| format!(": {s}")).unwrap_or_default()))]
13pub struct ImprovementError {
14 pub kind: ErrorKind,
16
17 pub context: String,
19
20 #[serde(skip)]
22 pub source_message: Option<String>,
23
24 pub operation: String,
26
27 pub severity: ImprovementSeverity,
29}
30
31#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
34pub enum ErrorKind {
35 ScoringFailed,
37 InvalidMetadata,
38 UnsupportedToolType,
39
40 SelectionFailed,
42 NoViableCandidate,
43 ContextMissing,
44
45 ChainExecutionFailed,
47 AllFallbacksFailed,
48 TimeoutExceeded,
49
50 CacheOperationFailed,
52 CacheCorrupted,
53 SerializationFailed,
54
55 PatternDetectionFailed,
57 ContextTruncated,
58
59 IntentExtractionFailed,
61 CorrelationFailed,
62
63 ConfigurationInvalid,
65 ConfigurationMissing,
66}
67
68#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
70#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
71pub enum ImprovementSeverity {
72 Warning,
74 Error,
76 Critical,
78}
79
80impl ImprovementError {
81 pub fn new(kind: ErrorKind, context: impl Into<String>, operation: impl Into<String>) -> Self {
83 Self {
84 kind,
85 context: context.into(),
86 source_message: None,
87 operation: operation.into(),
88 severity: ImprovementSeverity::Error,
89 }
90 }
91
92 pub fn with_source(mut self, source: impl std::fmt::Display) -> Self {
94 self.source_message = Some(source.to_string());
95 self
96 }
97
98 pub fn with_severity(mut self, severity: ImprovementSeverity) -> Self {
100 self.severity = severity;
101 self
102 }
103
104 pub fn is_recoverable(&self) -> bool {
106 self.severity <= ImprovementSeverity::Error
107 }
108
109 pub fn to_log_entry(&self) -> String {
111 format!(
112 "[{}] {} - {} ({}{})",
113 match self.severity {
114 ImprovementSeverity::Warning => "WARN",
115 ImprovementSeverity::Error => "ERROR",
116 ImprovementSeverity::Critical => "CRIT",
117 },
118 self.operation,
119 self.context,
120 match self.kind {
121 ErrorKind::ScoringFailed => "scoring_failed",
122 ErrorKind::SelectionFailed => "selection_failed",
123 ErrorKind::ChainExecutionFailed => "chain_failed",
124 ErrorKind::CacheOperationFailed => "cache_failed",
125 ErrorKind::ConfigurationInvalid => "config_invalid",
126 _ => "unknown",
127 },
128 self.source_message.as_ref().map(|s| format!(": {s}")).unwrap_or_default()
129 )
130 }
131}
132
133#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ImprovementEvent {
136 pub event_type: EventType,
138
139 pub component: String,
141
142 pub message: String,
144
145 pub metric: Option<f32>,
147
148 pub timestamp: u64,
150}
151
152#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
154#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
155pub enum EventType {
156 ResultScored,
158 ScoreDegraded,
159
160 ToolSelected,
162 SelectionAlternative,
163
164 FallbackAttempt,
166 FallbackSuccess,
167 ChainAborted,
168
169 CacheHit,
171 CacheMiss,
172 CacheEvicted,
173
174 PatternDetected,
176 RedundancyDetected,
177
178 IntentExtracted,
180 FulfillmentAssessed,
181
182 ErrorOccurred,
184 ErrorRecovered,
185}
186
187pub trait ObservabilitySink: Send + Sync {
189 fn record_event(&self, event: ImprovementEvent);
191
192 fn record_error(&self, error: &ImprovementError);
194
195 fn record_metric(&self, component: &str, name: &str, value: f32);
197}
198
199pub struct NoOpSink;
201
202impl ObservabilitySink for NoOpSink {
203 fn record_event(&self, _event: ImprovementEvent) {}
204 fn record_error(&self, _error: &ImprovementError) {}
205 fn record_metric(&self, _component: &str, _name: &str, _value: f32) {}
206}
207
208pub struct LoggingSink;
210
211impl ObservabilitySink for LoggingSink {
212 fn record_event(&self, event: ImprovementEvent) {
213 macro_rules! log_event {
214 ($level:ident) => {
215 tracing::$level!(
216 component = %event.component,
217 event_type = ?event.event_type,
218 message = %event.message,
219 metric = event.metric,
220 timestamp = event.timestamp,
221 "improvement_event"
222 )
223 };
224 }
225 match event.event_type {
226 EventType::ErrorOccurred => log_event!(error),
227 EventType::PatternDetected => log_event!(debug),
228 EventType::CacheHit => log_event!(trace),
229 _ => log_event!(info),
230 }
231 }
232
233 fn record_error(&self, error: &ImprovementError) {
234 tracing::error!(
235 operation = %error.operation,
236 severity = ?error.severity,
237 context = %error.context,
238 source_message = ?error.source_message,
239 "improvement_error: {}",
240 error
241 );
242 }
243
244 fn record_metric(&self, component: &str, name: &str, value: f32) {
245 tracing::debug!(
246 component = %component,
247 metric = %name,
248 value = value,
249 "metric recorded"
250 );
251 }
252}
253
254pub struct ObservabilityContext {
256 sink: Box<dyn ObservabilitySink>,
257}
258
259impl ObservabilityContext {
260 pub fn noop() -> Self {
262 Self { sink: Box::new(NoOpSink) }
263 }
264
265 pub fn logging() -> Self {
267 Self { sink: Box::new(LoggingSink) }
268 }
269
270 pub fn event(
272 &self,
273 event_type: EventType,
274 component: impl Into<String>,
275 message: impl Into<String>,
276 metric: Option<f32>,
277 ) {
278 let event = ImprovementEvent {
279 event_type,
280 component: component.into(),
281 message: message.into(),
282 metric,
283 timestamp: std::time::SystemTime::now()
284 .duration_since(std::time::UNIX_EPOCH)
285 .unwrap_or_default()
286 .as_secs(),
287 };
288 self.sink.record_event(event);
289 }
290
291 pub fn error(&self, error: &ImprovementError) {
293 self.sink.record_error(error);
294 }
295
296 pub fn metric(&self, component: &str, name: &str, value: f32) {
298 self.sink.record_metric(component, name, value);
299 }
300}
301
302impl Default for ObservabilityContext {
303 fn default() -> Self {
304 Self::noop()
305 }
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
313 fn test_error_creation() {
314 let err = ImprovementError::new(ErrorKind::ScoringFailed, "result score too low", "score_result");
315
316 assert_eq!(err.kind, ErrorKind::ScoringFailed);
317 assert_eq!(err.severity, ImprovementSeverity::Error);
318 assert!(err.is_recoverable());
319 }
320
321 #[test]
322 fn test_error_severity() {
323 let err = ImprovementError::new(ErrorKind::CacheCorrupted, "cache state invalid", "cache_read")
324 .with_severity(ImprovementSeverity::Critical);
325
326 assert_eq!(err.severity, ImprovementSeverity::Critical);
327 assert!(!err.is_recoverable());
328 }
329
330 #[test]
331 fn test_error_logging() {
332 let err = ImprovementError::new(ErrorKind::SelectionFailed, "no candidates available", "select_tool")
333 .with_source("context is empty");
334
335 let log = err.to_log_entry();
336 assert!(log.contains("ERROR"));
337 assert!(log.contains("select_tool"));
338 }
339
340 #[test]
341 fn test_observability_sink() {
342 let sink = NoOpSink;
343 let event = ImprovementEvent {
344 event_type: EventType::ToolSelected,
345 component: "selector".to_string(),
346 message: "selected grep_file".to_string(),
347 metric: Some(0.95),
348 timestamp: 0,
349 };
350
351 sink.record_event(event);
353 }
354}