Skip to main content

vtcode_core/core/
error_recovery.rs

1use crate::core::timeout_detector::{OperationType, TIMEOUT_DETECTOR};
2use crate::utils::current_timestamp;
3use anyhow::Result;
4use indexmap::IndexMap;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8/// Represents an error that occurred during execution
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ExecutionError {
11    pub id: String,
12    pub timestamp: u64,
13    pub error_type: ErrorType,
14    pub message: String,
15    pub context: ErrorContext,
16    pub recovery_attempts: Vec<RecoveryAttempt>,
17    pub resolved: bool,
18}
19
20/// Operation domain in which an error occurred.
21///
22/// Used by the global error recovery manager and agent-specific error state
23/// tracking to group failures by *where* they happened (tool execution, API
24/// call, file system, ...). This is a different axis from
25/// [`vtcode_commons::ErrorCategory`], which classifies *why* an operation
26/// failed (rate limit, timeout, permission denied, ...); do not use this enum
27/// for failure classification or retry decisions.
28#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, Hash, PartialEq)]
29pub enum ErrorType {
30    ToolExecution,
31    ApiCall,
32    FileSystem,
33    Network,
34    Validation,
35    CircuitBreaker,
36    Timeout,
37    PermissionDenied,
38    InvalidArguments,
39    ResourceNotFound,
40    Other,
41}
42
43/// Context information about where and why the error occurred
44#[derive(Debug, Clone, Serialize, Deserialize)]
45pub struct ErrorContext {
46    pub conversation_turn: usize,
47    pub user_input: Option<String>,
48    pub tool_name: Option<String>,
49    pub tool_args: Option<Value>,
50    pub api_request_size: Option<usize>,
51    pub context_size: Option<usize>,
52    pub stack_trace: Option<String>,
53}
54
55/// A recovery attempt that was made
56#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct RecoveryAttempt {
58    pub timestamp: u64,
59    pub strategy: RecoveryStrategy,
60    pub success: bool,
61    pub result: String,
62    pub new_context_size: Option<usize>,
63}
64
65/// Recovery strategy used to handle the error
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub enum RecoveryStrategy {
68    RetryWithBackoff {
69        delay_ms: u64,
70        attempt_number: usize,
71    },
72
73    SimplifyRequest {
74        removed_parameters: Vec<String>,
75    },
76    AlternativeTool {
77        original_tool: String,
78        alternative_tool: String,
79    },
80    ContextReset {
81        preserved_data: IndexMap<String, Value>,
82    },
83    ManualIntervention,
84}
85
86/// Error recovery manager
87pub struct ErrorRecoveryManager {
88    errors: Vec<ExecutionError>,
89    recovery_strategies: IndexMap<ErrorType, Vec<RecoveryStrategy>>,
90    operation_type_mapping: IndexMap<ErrorType, OperationType>,
91}
92
93impl Default for ErrorRecoveryManager {
94    fn default() -> Self {
95        Self::new()
96    }
97}
98
99impl ErrorRecoveryManager {
100    pub fn new() -> Self {
101        // Pre-allocate with known capacity
102        let mut recovery_strategies = IndexMap::with_capacity(2);
103        let mut operation_type_mapping = IndexMap::with_capacity(11);
104
105        // Define recovery strategies for different error types
106        recovery_strategies.insert(
107            ErrorType::ToolExecution,
108            vec![
109                RecoveryStrategy::RetryWithBackoff { delay_ms: 1000, attempt_number: 1 },
110                RecoveryStrategy::AlternativeTool {
111                    original_tool: String::new(),
112                    alternative_tool: String::new(),
113                },
114            ],
115        );
116
117        recovery_strategies.insert(
118            ErrorType::ApiCall,
119            vec![
120                RecoveryStrategy::RetryWithBackoff { delay_ms: 2000, attempt_number: 1 },
121                RecoveryStrategy::ContextReset { preserved_data: IndexMap::new() },
122            ],
123        );
124
125        // Map error types to operation types for timeout detector integration
126        operation_type_mapping.insert(ErrorType::ToolExecution, OperationType::ToolExecution);
127        operation_type_mapping.insert(ErrorType::ApiCall, OperationType::ApiCall);
128        operation_type_mapping.insert(ErrorType::Network, OperationType::NetworkRequest);
129        operation_type_mapping.insert(ErrorType::FileSystem, OperationType::FileOperation);
130        operation_type_mapping.insert(ErrorType::Validation, OperationType::Processing);
131        operation_type_mapping.insert(ErrorType::CircuitBreaker, OperationType::ToolExecution);
132        operation_type_mapping.insert(ErrorType::Timeout, OperationType::Processing);
133        operation_type_mapping.insert(ErrorType::PermissionDenied, OperationType::Processing);
134        operation_type_mapping.insert(ErrorType::InvalidArguments, OperationType::Processing);
135        operation_type_mapping.insert(ErrorType::ResourceNotFound, OperationType::FileOperation);
136        operation_type_mapping.insert(ErrorType::Other, OperationType::Processing);
137
138        Self {
139            errors: Vec::with_capacity(16), // Pre-allocate for typical session
140            recovery_strategies,
141            operation_type_mapping,
142        }
143    }
144
145    /// Record a new error
146    pub fn record_error(&mut self, error_type: ErrorType, message: String, context: ErrorContext) -> String {
147        // Use a more efficient ID generation with minimal formatting
148        let error_count = self.errors.len();
149        let timestamp_short = current_timestamp() % 10000;
150        let error_id = format!("e{error_count}_{timestamp_short}");
151
152        let error = ExecutionError {
153            id: error_id.clone(),
154            timestamp: current_timestamp(),
155            error_type, // ErrorType is Copy now, no need to clone
156            message,
157            context,
158            recovery_attempts: Vec::with_capacity(2), // Most errors have 1-2 recovery attempts
159            resolved: false,
160        };
161
162        self.errors.push(error);
163        error_id
164    }
165
166    /// Record a recovery attempt
167    #[cold]
168    pub fn record_recovery_attempt(
169        &mut self,
170        error_id: &str,
171        strategy: RecoveryStrategy,
172        success: bool,
173        result: String,
174        new_context_size: Option<usize>,
175    ) {
176        let attempt = RecoveryAttempt {
177            timestamp: current_timestamp(),
178            strategy,
179            success,
180            result,
181            new_context_size,
182        };
183
184        if let Some(error) = self.errors.iter_mut().find(|e| e.id == error_id) {
185            error.recovery_attempts.push(attempt);
186            if success {
187                error.resolved = true;
188            }
189        }
190    }
191
192    /// Get recovery strategies for a specific error type
193    #[cold]
194    pub fn get_recovery_strategies(&self, error_type: &ErrorType) -> &[RecoveryStrategy] {
195        self.recovery_strategies
196            .get(error_type)
197            .map(|strategies| strategies.as_slice())
198            .unwrap_or(&[])
199    }
200
201    /// Generate a context preservation plan
202    pub fn generate_context_preservation_plan(
203        &self,
204        context_size: usize,
205        error_count: usize,
206    ) -> ContextPreservationPlan {
207        let critical_errors = error_count > 5;
208
209        let strategies = if critical_errors {
210            vec![
211                PreservationStrategy::SelectiveRetention { preserve_decisions: true, preserve_errors: true },
212                PreservationStrategy::ContextReset { preserve_session_data: true },
213            ]
214        } else {
215            vec![PreservationStrategy::NoAction]
216        };
217
218        ContextPreservationPlan {
219            current_context_size: context_size,
220            error_count,
221            recommended_strategies: strategies,
222            urgency: if critical_errors {
223                Urgency::Critical
224            } else {
225                Urgency::Low
226            },
227        }
228    }
229
230    /// Get error statistics
231    pub fn get_error_statistics(&self) -> ErrorStatistics {
232        let total_errors = self.errors.len();
233        if total_errors == 0 {
234            return ErrorStatistics {
235                total_errors: 0,
236                resolved_errors: 0,
237                unresolved_errors: 0,
238                errors_by_type: IndexMap::new(),
239                avg_recovery_attempts: 0.0,
240                recent_errors: Vec::new(),
241            };
242        }
243
244        let resolved_errors = self.errors.iter().filter(|e| e.resolved).count();
245        let unresolved_errors = total_errors - resolved_errors;
246
247        // Use a more efficient approach for counting by type
248        let mut errors_by_type = IndexMap::new();
249        let mut total_attempts = 0usize;
250
251        for error in &self.errors {
252            *errors_by_type.entry(error.error_type).or_insert(0) += 1;
253            total_attempts += error.recovery_attempts.len();
254        }
255
256        let avg_recovery_attempts = total_attempts as f64 / total_errors as f64;
257
258        // Get recent errors more efficiently
259        let recent_count = total_errors.min(5);
260        let recent_errors: Vec<_> = self.errors.iter().rev().take(recent_count).cloned().collect();
261
262        ErrorStatistics {
263            total_errors,
264            resolved_errors,
265            unresolved_errors,
266            errors_by_type,
267            avg_recovery_attempts,
268            recent_errors,
269        }
270    }
271
272    /// Check if a specific error pattern is recurring
273    pub fn detect_error_pattern(&self, error_type: &ErrorType, time_window_seconds: u64) -> bool {
274        let now = current_timestamp();
275
276        let recent_errors = self
277            .errors
278            .iter()
279            .filter(|e| e.error_type == *error_type && (now - e.timestamp) < time_window_seconds)
280            .count();
281
282        recent_errors >= 3 // Consider it a pattern if 3+ similar errors in time window
283    }
284
285    /// Get the corresponding operation type for an error type
286    pub fn get_operation_type(&self, error_type: &ErrorType) -> OperationType {
287        self.operation_type_mapping
288            .get(error_type)
289            .cloned()
290            .unwrap_or(OperationType::Processing)
291    }
292
293    /// Execute an operation with intelligent timeout detection and recovery
294    pub async fn execute_with_recovery<F, Fut, T>(
295        &mut self,
296        operation_id: String,
297        error_type: ErrorType,
298        _context: ErrorContext,
299        operation: F,
300    ) -> Result<T, anyhow::Error>
301    where
302        F: Fn() -> Fut,
303        Fut: Future<Output = Result<T, anyhow::Error>>,
304    {
305        let operation_type = self.get_operation_type(&error_type);
306
307        TIMEOUT_DETECTOR
308            .execute_with_timeout_retry(operation_id, operation_type, operation)
309            .await
310    }
311
312    /// Check if an operation should be retried based on error analysis
313    /// Cold path: only called after an error has occurred.
314    #[cold]
315    pub async fn should_retry_operation(&self, error_type: &ErrorType, error: &anyhow::Error, attempt: u32) -> bool {
316        let operation_type = self.get_operation_type(error_type);
317        TIMEOUT_DETECTOR.should_retry(&operation_type, error, attempt).await
318    }
319
320    /// Get timeout statistics for monitoring and optimization
321    pub async fn get_timeout_stats(&self) -> crate::core::timeout_detector::TimeoutStats {
322        TIMEOUT_DETECTOR.get_stats().await
323    }
324
325    /// Configure timeout settings for a specific error type
326    /// Cold path: only called during proactive error recovery.
327    #[cold]
328    pub async fn configure_timeout_for_error_type(
329        &self,
330        error_type: ErrorType,
331        config: crate::core::timeout_detector::TimeoutConfig,
332    ) {
333        let operation_type = self.get_operation_type(&error_type);
334        TIMEOUT_DETECTOR.set_config(operation_type, config).await;
335    }
336
337    /// Generate enhanced recovery plan based on timeout detector insights
338    /// Cold path: only called during actual error recovery.
339    #[cold]
340    pub async fn generate_enhanced_recovery_plan(
341        &self,
342        context_size: usize,
343        error_count: usize,
344    ) -> EnhancedContextPreservationPlan {
345        let timeout_stats = self.get_timeout_stats().await;
346        let base_plan = self.generate_context_preservation_plan(context_size, error_count);
347
348        // Enhance the plan based on timeout detector insights
349        let timeout_rate = if timeout_stats.total_operations > 0 {
350            timeout_stats.timed_out_operations as f64 / timeout_stats.total_operations as f64
351        } else {
352            0.0
353        };
354
355        let retry_success_rate = if timeout_stats.total_retry_attempts > 0 {
356            timeout_stats.successful_retries as f64 / timeout_stats.total_retry_attempts as f64
357        } else {
358            1.0
359        };
360
361        // Adjust urgency based on timeout patterns
362        let _adjusted_urgency = if timeout_rate > 0.3 {
363            // High timeout rate indicates systemic issues
364            Urgency::Critical
365        } else if retry_success_rate < 0.5 {
366            // Low retry success rate indicates recovery issues
367            Urgency::High
368        } else {
369            base_plan.urgency
370        };
371
372        EnhancedContextPreservationPlan {
373            base_plan,
374            timeout_rate,
375            retry_success_rate,
376            timeout_stats,
377        }
378    }
379
380    /// Get the number of errors (for context preservation plan)
381    pub fn error_count(&self) -> usize {
382        self.errors.len()
383    }
384}
385
386/// Plan for preserving context during error recovery
387#[derive(Debug, Clone, Serialize, Deserialize)]
388pub struct ContextPreservationPlan {
389    pub current_context_size: usize,
390    pub error_count: usize,
391    pub recommended_strategies: Vec<PreservationStrategy>,
392    pub urgency: Urgency,
393}
394
395/// Strategy for preserving context
396#[derive(Debug, Clone, Serialize, Deserialize)]
397pub enum PreservationStrategy {
398    SelectiveRetention {
399        preserve_decisions: bool,
400        preserve_errors: bool,
401    },
402    ContextReset {
403        preserve_session_data: bool,
404    },
405    NoAction,
406}
407
408/// Urgency level for context preservation
409#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
410pub enum Urgency {
411    Low,
412    High,
413    Critical,
414}
415
416/// Statistics about errors in the session
417#[derive(Debug, Clone, Serialize, Deserialize)]
418pub struct ErrorStatistics {
419    pub total_errors: usize,
420    pub resolved_errors: usize,
421    pub unresolved_errors: usize,
422    pub errors_by_type: IndexMap<ErrorType, usize>,
423    pub avg_recovery_attempts: f64,
424    pub recent_errors: Vec<ExecutionError>,
425}
426
427/// Enhanced context preservation plan with timeout detector insights
428#[derive(Debug, Clone)]
429pub struct EnhancedContextPreservationPlan {
430    pub base_plan: ContextPreservationPlan,
431    pub timeout_rate: f64,
432    pub retry_success_rate: f64,
433    pub timeout_stats: crate::core::timeout_detector::TimeoutStats,
434}