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 { ErrorSeverity::Warning => "WARN", ErrorSeverity::Error => "ERROR", ErrorSeverity::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: ErrorSeverity,
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 ErrorSeverity {
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: ErrorSeverity::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: ErrorSeverity) -> Self {
100 self.severity = severity;
101 self
102 }
103
104 pub fn is_recoverable(&self) -> bool {
106 self.severity <= ErrorSeverity::Error
107 }
108
109 pub fn to_log_entry(&self) -> String {
111 format!(
112 "[{}] {} - {} ({}{})",
113 match self.severity {
114 ErrorSeverity::Warning => "WARN",
115 ErrorSeverity::Error => "ERROR",
116 ErrorSeverity::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
129 .as_ref()
130 .map(|s| format!(": {}", s))
131 .unwrap_or_default()
132 )
133 }
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct ImprovementEvent {
139 pub event_type: EventType,
141
142 pub component: String,
144
145 pub message: String,
147
148 pub metric: Option<f32>,
150
151 pub timestamp: u64,
153}
154
155#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
157#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
158pub enum EventType {
159 ResultScored,
161 ScoreDegraded,
162
163 ToolSelected,
165 SelectionAlternative,
166
167 FallbackAttempt,
169 FallbackSuccess,
170 ChainAborted,
171
172 CacheHit,
174 CacheMiss,
175 CacheEvicted,
176
177 PatternDetected,
179 RedundancyDetected,
180
181 IntentExtracted,
183 FulfillmentAssessed,
184
185 ErrorOccurred,
187 ErrorRecovered,
188}
189
190pub trait ObservabilitySink: Send + Sync {
192 fn record_event(&self, event: ImprovementEvent);
194
195 fn record_error(&self, error: &ImprovementError);
197
198 fn record_metric(&self, component: &str, name: &str, value: f32);
200}
201
202pub struct NoOpSink;
204
205impl ObservabilitySink for NoOpSink {
206 fn record_event(&self, _event: ImprovementEvent) {}
207 fn record_error(&self, _error: &ImprovementError) {}
208 fn record_metric(&self, _component: &str, _name: &str, _value: f32) {}
209}
210
211pub struct LoggingSink;
213
214impl ObservabilitySink for LoggingSink {
215 fn record_event(&self, event: ImprovementEvent) {
216 macro_rules! log_event {
217 ($level:ident) => {
218 tracing::$level!(
219 component = %event.component,
220 event_type = ?event.event_type,
221 message = %event.message,
222 metric = event.metric,
223 timestamp = event.timestamp,
224 "improvement_event"
225 )
226 };
227 }
228 match event.event_type {
229 EventType::ErrorOccurred => log_event!(error),
230 EventType::PatternDetected => log_event!(debug),
231 EventType::CacheHit => log_event!(trace),
232 _ => log_event!(info),
233 }
234 }
235
236 fn record_error(&self, error: &ImprovementError) {
237 tracing::error!(
238 operation = %error.operation,
239 severity = ?error.severity,
240 context = %error.context,
241 source_message = ?error.source_message,
242 "improvement_error: {}",
243 error
244 );
245 }
246
247 fn record_metric(&self, component: &str, name: &str, value: f32) {
248 tracing::debug!(
249 component = %component,
250 metric = %name,
251 value = value,
252 "metric recorded"
253 );
254 }
255}
256
257pub struct ObservabilityContext {
259 sink: Box<dyn ObservabilitySink>,
260}
261
262impl ObservabilityContext {
263 pub fn noop() -> Self {
265 Self {
266 sink: Box::new(NoOpSink),
267 }
268 }
269
270 pub fn logging() -> Self {
272 Self {
273 sink: Box::new(LoggingSink),
274 }
275 }
276
277 pub fn event(
279 &self,
280 event_type: EventType,
281 component: impl Into<String>,
282 message: impl Into<String>,
283 metric: Option<f32>,
284 ) {
285 let event = ImprovementEvent {
286 event_type,
287 component: component.into(),
288 message: message.into(),
289 metric,
290 timestamp: std::time::SystemTime::now()
291 .duration_since(std::time::UNIX_EPOCH)
292 .unwrap_or_default()
293 .as_secs(),
294 };
295 self.sink.record_event(event);
296 }
297
298 pub fn error(&self, error: &ImprovementError) {
300 self.sink.record_error(error);
301 }
302
303 pub fn metric(&self, component: &str, name: &str, value: f32) {
305 self.sink.record_metric(component, name, value);
306 }
307}
308
309impl Default for ObservabilityContext {
310 fn default() -> Self {
311 Self::noop()
312 }
313}
314
315#[cfg(test)]
316mod tests {
317 use super::*;
318
319 #[test]
320 fn test_error_creation() {
321 let err = ImprovementError::new(
322 ErrorKind::ScoringFailed,
323 "result score too low",
324 "score_result",
325 );
326
327 assert_eq!(err.kind, ErrorKind::ScoringFailed);
328 assert_eq!(err.severity, ErrorSeverity::Error);
329 assert!(err.is_recoverable());
330 }
331
332 #[test]
333 fn test_error_severity() {
334 let err = ImprovementError::new(
335 ErrorKind::CacheCorrupted,
336 "cache state invalid",
337 "cache_read",
338 )
339 .with_severity(ErrorSeverity::Critical);
340
341 assert_eq!(err.severity, ErrorSeverity::Critical);
342 assert!(!err.is_recoverable());
343 }
344
345 #[test]
346 fn test_error_logging() {
347 let err = ImprovementError::new(
348 ErrorKind::SelectionFailed,
349 "no candidates available",
350 "select_tool",
351 )
352 .with_source("context is empty");
353
354 let log = err.to_log_entry();
355 assert!(log.contains("ERROR"));
356 assert!(log.contains("select_tool"));
357 }
358
359 #[test]
360 fn test_observability_sink() {
361 let sink = NoOpSink;
362 let event = ImprovementEvent {
363 event_type: EventType::ToolSelected,
364 component: "selector".to_string(),
365 message: "selected grep_file".to_string(),
366 metric: Some(0.95),
367 timestamp: 0,
368 };
369
370 sink.record_event(event);
372 }
373}