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 { 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    /// 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: ImprovementSeverity,
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 ImprovementSeverity {
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: ImprovementSeverity::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: ImprovementSeverity) -> 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 <= ImprovementSeverity::Error
107    }
108
109    /// Format for logging
110    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/// Observability event for tool improvements
134#[derive(Debug, Clone, Serialize, Deserialize)]
135pub struct ImprovementEvent {
136    /// Event type
137    pub event_type: EventType,
138
139    /// Component that generated event
140    pub component: String,
141
142    /// Detailed message
143    pub message: String,
144
145    /// Metric value (if applicable)
146    pub metric: Option<f32>,
147
148    /// Timestamp (unix seconds)
149    pub timestamp: u64,
150}
151
152/// Types of observable events
153#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
154#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
155pub enum EventType {
156    // Scoring events
157    ResultScored,
158    ScoreDegraded,
159
160    // Selection events
161    ToolSelected,
162    SelectionAlternative,
163
164    // Fallback events
165    FallbackAttempt,
166    FallbackSuccess,
167    ChainAborted,
168
169    // Cache events
170    CacheHit,
171    CacheMiss,
172    CacheEvicted,
173
174    // Context events
175    PatternDetected,
176    RedundancyDetected,
177
178    // Correlation events
179    IntentExtracted,
180    FulfillmentAssessed,
181
182    // Error events
183    ErrorOccurred,
184    ErrorRecovered,
185}
186
187/// Observability sink for receiving events
188pub trait ObservabilitySink: Send + Sync {
189    /// Record an event
190    fn record_event(&self, event: ImprovementEvent);
191
192    /// Record an error
193    fn record_error(&self, error: &ImprovementError);
194
195    /// Record a metric
196    fn record_metric(&self, component: &str, name: &str, value: f32);
197}
198
199/// No-op observability sink (for when observability is disabled)
200pub 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
208/// Logging-based observability sink
209pub 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
254/// Global observability context
255pub struct ObservabilityContext {
256    sink: Box<dyn ObservabilitySink>,
257}
258
259impl ObservabilityContext {
260    /// Create with no-op sink
261    pub fn noop() -> Self {
262        Self { sink: Box::new(NoOpSink) }
263    }
264
265    /// Create with logging sink
266    pub fn logging() -> Self {
267        Self { sink: Box::new(LoggingSink) }
268    }
269
270    /// Record event
271    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    /// Record error
292    pub fn error(&self, error: &ImprovementError) {
293        self.sink.record_error(error);
294    }
295
296    /// Record metric
297    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        // Should not panic
352        sink.record_event(event);
353    }
354}