Skip to main content

vtcode_core/tools/
pattern_engine.rs

1//! Advanced pattern detection with sequence analysis
2//!
3//! Tracks execution sequences, detects behavioral patterns,
4//! predicts user intent based on action history.
5
6use crate::tools::improvement_algorithms::jaro_winkler_similarity;
7use hashbrown::HashMap;
8use parking_lot::RwLock;
9use smallvec::SmallVec;
10use std::collections::VecDeque;
11use std::sync::Arc;
12
13/// Single execution event.
14#[derive(Clone, Debug)]
15pub struct ExecutionEvent {
16    pub tool_name: String,
17    pub arguments: String,
18    pub success: bool,
19    pub quality_score: f32, // 0.0-1.0
20    pub duration_ms: u64,
21    pub timestamp: u64,
22}
23
24/// Detected pattern in execution sequence.
25#[derive(Clone, Debug, PartialEq, Eq)]
26pub enum DetectedPattern {
27    /// Tool called once.
28    Single,
29    /// Same tool, same args, 2x.
30    ExactRepeat,
31    /// Same tool, same args, 3+ times (likely stuck).
32    Loop,
33    /// Same tool, similar args (fuzzy match >0.85).
34    NearLoop,
35    /// Increasing quality across iterations.
36    Refinement,
37    /// Decreasing quality (user frustrated).
38    Degradation,
39    /// Different tools with consistent results.
40    Convergence,
41    /// Tool switching, exploring alternatives.
42    Exploration,
43}
44
45/// Pattern detection engine with state tracking.
46pub struct PatternEngine {
47    max_history: usize,
48    sequence_window: usize,
49    events: Arc<RwLock<VecDeque<ExecutionEvent>>>,
50}
51
52impl PatternEngine {
53    /// Create a new pattern engine.
54    pub fn new(max_history: usize, sequence_window: usize) -> Self {
55        Self {
56            max_history,
57            sequence_window,
58            events: Arc::new(RwLock::new(VecDeque::with_capacity(max_history))),
59        }
60    }
61
62    /// Record an execution event.
63    pub fn record(&self, event: ExecutionEvent) {
64        let mut events = self.events.write();
65
66        if events.len() >= self.max_history {
67            events.pop_front();
68        }
69
70        events.push_back(event);
71    }
72
73    /// Detect overall pattern in execution history.
74    pub fn detect_pattern(&self) -> DetectedPattern {
75        let events = self.events.read();
76
77        let len = events.len();
78        if len < 2 {
79            return DetectedPattern::Single;
80        }
81
82        // Use SmallVec to avoid heap allocation for the recent window.
83        // sequence_window is typically 20.
84        let mut recent = SmallVec::<[&ExecutionEvent; 32]>::new();
85        recent.extend(events.iter().rev().take(self.sequence_window));
86
87        detect_in_sequence(&recent)
88    }
89
90    /// Predict the user's next likely tool based on predecessor frequency.
91    pub fn predict_next_tool(&self) -> Option<String> {
92        let events = self.events.read();
93
94        let len = events.len();
95        if len < 2 {
96            return None;
97        }
98
99        // We only need the tool names for prediction.
100        let mut recent_tools = SmallVec::<[&str; 32]>::new();
101        recent_tools.extend(
102            events
103                .iter()
104                .rev()
105                .take(self.sequence_window)
106                .map(|e| e.tool_name.as_str()),
107        );
108
109        if recent_tools.is_empty() {
110            return None;
111        }
112
113        let last_tool = recent_tools[0];
114        let mut predecessors = HashMap::<&str, usize>::new();
115
116        for w in recent_tools.windows(2) {
117            if w[0] == last_tool {
118                *predecessors.entry(w[1]).or_default() += 1;
119            }
120        }
121
122        predecessors
123            .into_iter()
124            .max_by_key(|(_, count)| *count)
125            .map(|(tool, _)| tool.to_owned())
126    }
127
128    /// Compute an execution summary in a single pass over the event history.
129    ///
130    /// Multiple separate `.iter()` passes each pay the traversal cost again, which
131    /// prevents cache-friendly pipelining (turbopuffer "zero-cost" lesson). One
132    /// combined loop is clearer and faster.
133    pub fn summary(&self) -> ExecutionSummary {
134        let events = self.events.read();
135        if events.is_empty() {
136            return ExecutionSummary::default();
137        }
138
139        let total = events.len();
140        let mut successful = 0usize;
141        let mut quality_sum = 0.0f32;
142        let mut duration_sum = 0u64;
143        // `HashSet` capacity = total is an upper bound; most runs use ≤ a handful of tools.
144        let mut unique_tools: hashbrown::HashSet<&str> =
145            hashbrown::HashSet::with_capacity(total.min(16));
146
147        for e in events.iter() {
148            if e.success {
149                successful += 1;
150            }
151            quality_sum += e.quality_score;
152            duration_sum += e.duration_ms;
153            unique_tools.insert(e.tool_name.as_str());
154        }
155
156        ExecutionSummary {
157            total_executions: total,
158            successful_executions: successful,
159            success_rate: successful as f32 / total as f32,
160            average_quality: quality_sum / total as f32,
161            average_duration_ms: duration_sum / total as u64,
162            unique_tools: unique_tools.len(),
163            current_pattern: self.detect_pattern_with_guard(&events),
164        }
165    }
166
167    /// Internal helper to detect patterns given a read guard.
168    fn detect_pattern_with_guard(&self, events: &VecDeque<ExecutionEvent>) -> DetectedPattern {
169        let len = events.len();
170        if len < 2 {
171            return DetectedPattern::Single;
172        }
173
174        let mut recent = SmallVec::<[&ExecutionEvent; 32]>::new();
175        recent.extend(events.iter().rev().take(self.sequence_window));
176
177        detect_in_sequence(&recent)
178    }
179}
180
181/// Core pattern classification over a reversed-chronological event slice.
182///
183/// Extracted from `PatternEngine` so it can be tested without the `Arc<RwLock<…>>`
184/// wrapper and called without `&self` (it uses no engine state).
185///
186/// `events[0]` is the **newest** event.
187fn detect_in_sequence(events: &[&ExecutionEvent]) -> DetectedPattern {
188    let len = events.len();
189    if len == 0 {
190        return DetectedPattern::Single;
191    }
192
193    let first = events[0];
194
195    // --- Single combined scan: exact repeats, qualities, tool variety ---
196    //
197    // We combine all metrics into one traversal to maximize cache locality and
198    // enable LLM/SIMD optimization (fused iteration pattern).
199    let mut qualities = SmallVec::<[f32; 32]>::with_capacity(len);
200    let mut same_tool = true;
201    let mut same_args = true;
202
203    for e in events {
204        qualities.push(e.quality_score);
205        if e.tool_name != first.tool_name {
206            same_tool = false;
207        }
208        if e.arguments != first.arguments {
209            same_args = false;
210        }
211    }
212
213    // 1. Check for exact repeats/loops first (strictest pattern)
214    if same_tool && same_args {
215        return match len {
216            1 | 2 => DetectedPattern::ExactRepeat,
217            _ => DetectedPattern::Loop,
218        };
219    }
220
221    // 2. Trend analysis (needs ≥3 points)
222    if len >= 3 {
223        // events[0] is newest → qualities[0] is the latest score.
224        // Fusing the window check to optimize trend detection.
225        let is_refinement = qualities.windows(2).all(|w| w[0] > w[1] + 0.05);
226        if is_refinement {
227            return DetectedPattern::Refinement;
228        }
229
230        let is_degradation = qualities.windows(2).all(|w| w[0] < w[1] - 0.05);
231        if is_degradation {
232            return DetectedPattern::Degradation;
233        }
234    }
235
236    // 3. Near-loop check (same tool, similar args)
237    if same_tool
238        && len >= 3
239        && events
240            .windows(2)
241            .all(|w| jaro_winkler_similarity(&w[0].arguments, &w[1].arguments) > 0.85)
242    {
243        return DetectedPattern::NearLoop;
244    }
245
246    // 4. Multi-tool analysis (Exploration/Convergence)
247    if !same_tool && len >= 3 {
248        // Single-pass mean + mean-absolute-deviation over qualities.
249        let n = len as f32;
250        let avg = qualities.iter().sum::<f32>() / n;
251        let mad = qualities.iter().map(|q| (q - avg).abs()).sum::<f32>() / n;
252
253        if mad < 0.15 {
254            return DetectedPattern::Convergence;
255        }
256        return DetectedPattern::Exploration;
257    }
258
259    if !same_tool {
260        return DetectedPattern::Exploration;
261    }
262
263    DetectedPattern::Single
264}
265
266/// Execution summary statistics.
267#[derive(Clone, Debug)]
268pub struct ExecutionSummary {
269    pub total_executions: usize,
270    pub successful_executions: usize,
271    pub success_rate: f32,
272    pub average_quality: f32,
273    pub average_duration_ms: u64,
274    pub unique_tools: usize,
275    pub current_pattern: DetectedPattern,
276}
277
278impl Default for ExecutionSummary {
279    fn default() -> Self {
280        Self {
281            total_executions: 0,
282            successful_executions: 0,
283            success_rate: 0.0,
284            average_quality: 0.0,
285            average_duration_ms: 0,
286            unique_tools: 0,
287            current_pattern: DetectedPattern::Single,
288        }
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295
296    fn make_event(tool: &str, args: &str, quality: f32) -> ExecutionEvent {
297        ExecutionEvent {
298            tool_name: tool.to_owned(),
299            arguments: args.to_owned(),
300            success: true,
301            quality_score: quality,
302            duration_ms: 100,
303            timestamp: 0,
304        }
305    }
306
307    #[test]
308    fn test_pattern_single() {
309        let engine = PatternEngine::new(100, 10);
310        engine.record(make_event("grep", "pattern:test", 0.8));
311        assert_eq!(engine.detect_pattern(), DetectedPattern::Single);
312    }
313
314    #[test]
315    fn test_pattern_loop() {
316        let engine = PatternEngine::new(100, 10);
317        for _ in 0..3 {
318            engine.record(make_event("grep", "pattern:test", 0.5));
319        }
320        assert_eq!(engine.detect_pattern(), DetectedPattern::Loop);
321    }
322
323    #[test]
324    fn test_pattern_refinement() {
325        let engine = PatternEngine::new(100, 10);
326        for (i, &quality) in [0.3f32, 0.5, 0.8].iter().enumerate() {
327            engine.record(ExecutionEvent {
328                tool_name: "grep".to_owned(),
329                arguments: format!("pattern:{i}"),
330                success: true,
331                quality_score: quality,
332                duration_ms: 100,
333                timestamp: i as u64,
334            });
335        }
336        assert_eq!(engine.detect_pattern(), DetectedPattern::Refinement);
337    }
338
339    #[test]
340    fn test_pattern_degradation() {
341        let engine = PatternEngine::new(100, 10);
342        for (i, &quality) in [0.9f32, 0.6, 0.2].iter().enumerate() {
343            engine.record(ExecutionEvent {
344                tool_name: "grep".to_owned(),
345                arguments: format!("pattern:{i}"),
346                success: true,
347                quality_score: quality,
348                duration_ms: 100,
349                timestamp: i as u64,
350            });
351        }
352        assert_eq!(engine.detect_pattern(), DetectedPattern::Degradation);
353    }
354
355    #[test]
356    fn test_predict_next_tool() {
357        let engine = PatternEngine::new(100, 10);
358        for (i, &tool) in ["grep", "read", "grep", "read"].iter().enumerate() {
359            engine.record(ExecutionEvent {
360                tool_name: tool.to_owned(),
361                arguments: "arg".to_owned(),
362                success: true,
363                quality_score: 0.8,
364                duration_ms: 100,
365                timestamp: i as u64,
366            });
367        }
368        assert_eq!(engine.predict_next_tool(), Some("grep".to_owned()));
369    }
370
371    #[test]
372    fn test_execution_summary() {
373        let engine = PatternEngine::new(100, 10);
374        for i in 0..5u64 {
375            engine.record(ExecutionEvent {
376                tool_name: "grep".to_owned(),
377                arguments: "arg".to_owned(),
378                success: i != 2,
379                quality_score: 0.8,
380                duration_ms: 100,
381                timestamp: i,
382            });
383        }
384
385        let summary = engine.summary();
386        assert_eq!(summary.total_executions, 5);
387        assert_eq!(summary.successful_executions, 4);
388        assert_eq!(summary.success_rate, 0.8);
389        assert_eq!(summary.unique_tools, 1);
390    }
391}