Skip to main content

vtcode_core/tools/
improvements_errors.rs

1//! Error types and observability for core improvements
2//!
3//! Provides structured error handling with context and observability hooks.
4
5use serde::{Deserialize, Serialize};
6
7/// Result type for tool improvements operations
8pub type ImprovementResult<T> = Result<T, ImprovementError>;
9
10/// Structured errors for tool improvements
11#[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    /// Error kind
15    pub kind: ErrorKind,
16
17    /// Context information
18    pub context: String,
19
20    /// Original source error message (if any)
21    #[serde(skip)]
22    pub source_message: Option<String>,
23
24    /// Operation that failed
25    pub operation: String,
26
27    /// Severity level
28    pub severity: ErrorSeverity,
29}
30
31/// Error classifications
32#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
33#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
34pub enum ErrorKind {
35    // Scoring errors
36    ScoringFailed,
37    InvalidMetadata,
38    UnsupportedToolType,
39
40    // Selection errors
41    SelectionFailed,
42    NoViableCandidate,
43    ContextMissing,
44
45    // Fallback errors
46    ChainExecutionFailed,
47    AllFallbacksFailed,
48    TimeoutExceeded,
49
50    // Cache errors
51    CacheOperationFailed,
52    CacheCorrupted,
53    SerializationFailed,
54
55    // Context errors
56    PatternDetectionFailed,
57    ContextTruncated,
58
59    // Correlation errors
60    IntentExtractionFailed,
61    CorrelationFailed,
62
63    // Configuration errors
64    ConfigurationInvalid,
65    ConfigurationMissing,
66}
67
68/// Error severity level
69#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
70#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
71pub enum ErrorSeverity {
72    /// Recoverable, operation should retry
73    Warning,
74    /// Operation failed but service continues
75    Error,
76    /// System integrity compromised
77    Critical,
78}
79
80impl ImprovementError {
81    /// Create a new error
82    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    /// Add source error context
93    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    /// Set severity level
99    pub fn with_severity(mut self, severity: ErrorSeverity) -> Self {
100        self.severity = severity;
101        self
102    }
103
104    /// Check if error is recoverable
105    pub fn is_recoverable(&self) -> bool {
106        self.severity <= ErrorSeverity::Error
107    }
108
109    /// Format for logging
110    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/// Observability event for tool improvements
137#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct ImprovementEvent {
139    /// Event type
140    pub event_type: EventType,
141
142    /// Component that generated event
143    pub component: String,
144
145    /// Detailed message
146    pub message: String,
147
148    /// Metric value (if applicable)
149    pub metric: Option<f32>,
150
151    /// Timestamp (unix seconds)
152    pub timestamp: u64,
153}
154
155/// Types of observable events
156#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
157#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
158pub enum EventType {
159    // Scoring events
160    ResultScored,
161    ScoreDegraded,
162
163    // Selection events
164    ToolSelected,
165    SelectionAlternative,
166
167    // Fallback events
168    FallbackAttempt,
169    FallbackSuccess,
170    ChainAborted,
171
172    // Cache events
173    CacheHit,
174    CacheMiss,
175    CacheEvicted,
176
177    // Context events
178    PatternDetected,
179    RedundancyDetected,
180
181    // Correlation events
182    IntentExtracted,
183    FulfillmentAssessed,
184
185    // Error events
186    ErrorOccurred,
187    ErrorRecovered,
188}
189
190/// Observability sink for receiving events
191pub trait ObservabilitySink: Send + Sync {
192    /// Record an event
193    fn record_event(&self, event: ImprovementEvent);
194
195    /// Record an error
196    fn record_error(&self, error: &ImprovementError);
197
198    /// Record a metric
199    fn record_metric(&self, component: &str, name: &str, value: f32);
200}
201
202/// No-op observability sink (for when observability is disabled)
203pub 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
211/// Logging-based observability sink
212pub 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
257/// Global observability context
258pub struct ObservabilityContext {
259    sink: Box<dyn ObservabilitySink>,
260}
261
262impl ObservabilityContext {
263    /// Create with no-op sink
264    pub fn noop() -> Self {
265        Self {
266            sink: Box::new(NoOpSink),
267        }
268    }
269
270    /// Create with logging sink
271    pub fn logging() -> Self {
272        Self {
273            sink: Box::new(LoggingSink),
274        }
275    }
276
277    /// Record event
278    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    /// Record error
299    pub fn error(&self, error: &ImprovementError) {
300        self.sink.record_error(error);
301    }
302
303    /// Record metric
304    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        // Should not panic
371        sink.record_event(event);
372    }
373}