aof_core/
error_tracker.rs

1//! Error Tracking System with RAG (Retrieval-Augmented Generation)
2//!
3//! Maintains a local knowledge base of errors encountered during development,
4//! enabling agents to learn from patterns and avoid recurring mistakes.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use chrono::Utc;
9
10/// Error record for tracking and learning
11#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
12pub struct ErrorRecord {
13    /// Unique error identifier (hash of error type + message)
14    pub id: String,
15
16    /// Error type (MCP, Tool, Config, etc.)
17    pub error_type: String,
18
19    /// Error message
20    pub message: String,
21
22    /// Stack trace or context
23    pub context: String,
24
25    /// When the error occurred
26    pub timestamp: String,
27
28    /// How many times this error has occurred
29    pub occurrence_count: usize,
30
31    /// Proposed fix or workaround
32    pub solution: Option<String>,
33
34    /// Whether this error has been resolved
35    pub resolved: bool,
36
37    /// Tags for categorization (e.g., "mcp", "initialization", "kubernetes")
38    pub tags: Vec<String>,
39
40    /// Related files that triggered this error
41    pub related_files: Vec<String>,
42}
43
44impl ErrorRecord {
45    /// Create a new error record
46    pub fn new(
47        error_type: &str,
48        message: &str,
49        context: &str,
50    ) -> Self {
51        // Create a simple hash from message
52        let message_hash = message.chars().fold(0u32, |acc, c| {
53            acc.wrapping_mul(31).wrapping_add(c as u32)
54        });
55
56        let id = format!("{}-{}",
57            error_type,
58            message_hash
59        );
60
61        Self {
62            id,
63            error_type: error_type.to_string(),
64            message: message.to_string(),
65            context: context.to_string(),
66            timestamp: Utc::now().to_rfc3339(),
67            occurrence_count: 1,
68            solution: None,
69            resolved: false,
70            tags: vec![],
71            related_files: vec![],
72        }
73    }
74
75    /// Add a tag for categorization
76    pub fn with_tag(mut self, tag: &str) -> Self {
77        if !self.tags.contains(&tag.to_string()) {
78            self.tags.push(tag.to_string());
79        }
80        self
81    }
82
83    /// Add related file
84    pub fn with_file(mut self, file: &str) -> Self {
85        if !self.related_files.contains(&file.to_string()) {
86            self.related_files.push(file.to_string());
87        }
88        self
89    }
90
91    /// Set a solution
92    pub fn with_solution(mut self, solution: &str) -> Self {
93        self.solution = Some(solution.to_string());
94        self
95    }
96
97    /// Mark as resolved
98    pub fn resolve(mut self) -> Self {
99        self.resolved = true;
100        self
101    }
102}
103
104/// Local Error Knowledge Base (RAG system)
105pub struct ErrorKnowledgeBase {
106    /// Store of error records
107    errors: HashMap<String, ErrorRecord>,
108
109    /// Error frequency index for quick lookups
110    error_index: HashMap<String, Vec<String>>, // type -> error_ids
111
112    /// Tag index for categorization
113    tag_index: HashMap<String, Vec<String>>, // tag -> error_ids
114}
115
116impl ErrorKnowledgeBase {
117    /// Create a new knowledge base
118    pub fn new() -> Self {
119        Self {
120            errors: HashMap::new(),
121            error_index: HashMap::new(),
122            tag_index: HashMap::new(),
123        }
124    }
125
126    /// Record an error
127    pub fn record(&mut self, error: ErrorRecord) {
128        let error_id = error.id.clone();
129        let error_type = error.error_type.clone();
130
131        // Increment occurrence count if error already exists
132        if let Some(existing) = self.errors.get_mut(&error_id) {
133            existing.occurrence_count += 1;
134            return;
135        }
136
137        // Index by error type
138        self.error_index
139            .entry(error_type)
140            .or_default()
141            .push(error_id.clone());
142
143        // Index by tags
144        for tag in &error.tags {
145            self.tag_index
146                .entry(tag.clone())
147                .or_default()
148                .push(error_id.clone());
149        }
150
151        // Store the error
152        self.errors.insert(error_id, error);
153    }
154
155    /// Find similar errors (for learning from past mistakes)
156    pub fn find_similar(&self, error_type: &str, keywords: &[&str]) -> Vec<ErrorRecord> {
157        let mut matches = vec![];
158
159        // Get errors of the same type
160        if let Some(error_ids) = self.error_index.get(error_type) {
161            for error_id in error_ids {
162                if let Some(error) = self.errors.get(error_id) {
163                    // Check if any keywords match
164                    let matches_keywords = keywords.iter().any(|kw| {
165                        error.message.contains(kw)
166                            || error.context.contains(kw)
167                            || error.tags.iter().any(|t| t.contains(kw))
168                    });
169
170                    if matches_keywords || keywords.is_empty() {
171                        matches.push(error.clone());
172                    }
173                }
174            }
175        }
176
177        // Sort by occurrence count (most frequent first)
178        matches.sort_by(|a, b| b.occurrence_count.cmp(&a.occurrence_count));
179        matches
180    }
181
182    /// Get errors by tag
183    pub fn find_by_tag(&self, tag: &str) -> Vec<ErrorRecord> {
184        if let Some(error_ids) = self.tag_index.get(tag) {
185            error_ids
186                .iter()
187                .filter_map(|id| self.errors.get(id).cloned())
188                .collect()
189        } else {
190            vec![]
191        }
192    }
193
194    /// Get all unresolved errors
195    pub fn unresolved(&self) -> Vec<ErrorRecord> {
196        self.errors
197            .values()
198            .filter(|e| !e.resolved)
199            .cloned()
200            .collect()
201    }
202
203    /// Get most frequent errors
204    pub fn most_frequent(&self, limit: usize) -> Vec<ErrorRecord> {
205        let mut errors: Vec<_> = self.errors.values().cloned().collect();
206        errors.sort_by(|a, b| b.occurrence_count.cmp(&a.occurrence_count));
207        errors.into_iter().take(limit).collect()
208    }
209
210    /// Export to JSON for documentation/learning
211    pub fn export_json(&self) -> serde_json::Result<String> {
212        serde_json::to_string_pretty(&self.errors)
213    }
214
215    /// Get statistics about errors
216    pub fn stats(&self) -> ErrorStats {
217        let total_errors = self.errors.len();
218        let total_occurrences = self.errors.values().map(|e| e.occurrence_count).sum();
219        let unresolved_count = self.errors.values().filter(|e| !e.resolved).count();
220        let with_solutions = self.errors.values().filter(|e| e.solution.is_some()).count();
221
222        ErrorStats {
223            total_unique_errors: total_errors,
224            total_occurrences,
225            unresolved_count,
226            with_solutions,
227            avg_occurrences: if total_errors > 0 { total_occurrences / total_errors } else { 0 },
228        }
229    }
230}
231
232impl Default for ErrorKnowledgeBase {
233    fn default() -> Self {
234        Self::new()
235    }
236}
237
238/// Error statistics for monitoring and learning
239#[derive(Clone, Debug, Serialize, Deserialize)]
240pub struct ErrorStats {
241    pub total_unique_errors: usize,
242    pub total_occurrences: usize,
243    pub unresolved_count: usize,
244    pub with_solutions: usize,
245    pub avg_occurrences: usize,
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_error_record_creation() {
254        let error = ErrorRecord::new("MCP", "Client not initialized", "runtime.rs:332")
255            .with_tag("initialization")
256            .with_file("runtime.rs")
257            .with_solution("Call client.initialize() after creation");
258
259        assert_eq!(error.error_type, "MCP");
260        assert_eq!(error.message, "Client not initialized");
261        assert!(error.tags.contains(&"initialization".to_string()));
262        assert!(error.solution.is_some());
263    }
264
265    #[test]
266    fn test_knowledge_base_recording() {
267        let mut kb = ErrorKnowledgeBase::new();
268
269        let error1 = ErrorRecord::new("MCP", "Client not initialized", "context1")
270            .with_tag("initialization");
271        let error2 = ErrorRecord::new("Tool", "Tool not found", "context2")
272            .with_tag("execution");
273
274        kb.record(error1);
275        kb.record(error2);
276
277        assert_eq!(kb.errors.len(), 2);
278    }
279
280    #[test]
281    fn test_find_by_tag() {
282        let mut kb = ErrorKnowledgeBase::new();
283
284        kb.record(ErrorRecord::new("MCP", "Error 1", "ctx").with_tag("init"));
285        kb.record(ErrorRecord::new("MCP", "Error 2", "ctx").with_tag("execution"));
286        kb.record(ErrorRecord::new("MCP", "Error 3", "ctx").with_tag("init"));
287
288        let init_errors = kb.find_by_tag("init");
289        assert_eq!(init_errors.len(), 2);
290    }
291
292    #[test]
293    fn test_find_similar() {
294        let mut kb = ErrorKnowledgeBase::new();
295
296        kb.record(
297            ErrorRecord::new("MCP", "Client not initialized", "runtime").with_tag("init")
298        );
299        kb.record(
300            ErrorRecord::new("Tool", "Tool execution failed", "executor").with_tag("execution")
301        );
302
303        let similar = kb.find_similar("MCP", &["initialized"]);
304        assert_eq!(similar.len(), 1);
305        assert!(similar[0].message.contains("initialized"));
306    }
307}