Skip to main content

vtcode_core/tools/
execution_context.rs

1//! Tool execution context tracking
2//!
3//! Tracks the context of tool executions within a session to detect patterns,
4//! prevent redundancy, and suggest better alternatives.
5
6use crate::types::CompactStr;
7use crate::utils::current_timestamp;
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use std::collections::VecDeque;
11
12use crate::tools::result_metadata::EnhancedToolResult;
13use crate::tools::tool_effectiveness::ToolEffectiveness;
14use hashbrown::HashMap;
15
16/// A record of a single tool execution
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct ToolExecutionRecord {
19    pub tool_name: CompactStr,
20    pub args: Value,
21    pub result: EnhancedToolResult,
22    pub timestamp: u64,
23    pub execution_time_ms: u64,
24}
25
26impl ToolExecutionRecord {
27    #[inline]
28    pub fn new(
29        tool_name: impl Into<CompactStr>,
30        args: Value,
31        result: EnhancedToolResult,
32        execution_time_ms: u64,
33    ) -> Self {
34        Self {
35            tool_name: tool_name.into(),
36            args,
37            result,
38            timestamp: current_timestamp(),
39            execution_time_ms,
40        }
41    }
42}
43
44/// Detected pattern across tool executions
45#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(tag = "type")]
47pub enum ToolPattern {
48    /// Same tool/pattern searched with multiple tools
49    RedundantSearch {
50        tools: Vec<CompactStr>,
51        pattern: String,
52    },
53
54    /// Results build on each other sequentially
55    SequentialRefinement {
56        tools: Vec<CompactStr>,
57        refinement_steps: usize,
58    },
59
60    /// Multiple tools converged on same finding
61    ConvergentDiagnosis {
62        tools: Vec<CompactStr>,
63        common_finding: String,
64    },
65
66    /// Tool produced low quality despite multiple attempts
67    LowQualityLoop { tool: CompactStr, attempts: usize },
68}
69
70/// Context for cross-tool awareness
71#[derive(Debug, Clone)]
72pub struct ToolExecutionContext {
73    /// Session identifier
74    pub session_id: String,
75
76    /// Description of current task
77    pub current_task: String,
78
79    /// Recent execution history (limited to prevent memory bloat)
80    execution_history: VecDeque<ToolExecutionRecord>,
81
82    /// Maximum history size
83    max_history_size: usize,
84
85    /// Detected patterns
86    patterns: Vec<ToolPattern>,
87
88    /// Tool effectiveness metrics
89    effectiveness_snapshot: HashMap<String, ToolEffectiveness>,
90}
91
92impl ToolExecutionContext {
93    pub fn new(session_id: String, task: String) -> Self {
94        Self {
95            session_id,
96            current_task: task,
97            execution_history: VecDeque::with_capacity(100),
98            max_history_size: 100,
99            patterns: vec![],
100            effectiveness_snapshot: HashMap::new(),
101        }
102    }
103
104    /// Add an execution record
105    pub fn add_record(&mut self, record: ToolExecutionRecord) {
106        // Detect patterns before adding
107        self.detect_patterns(&record);
108
109        self.execution_history.push_back(record);
110
111        // Keep history size bounded
112        while self.execution_history.len() > self.max_history_size {
113            self.execution_history.pop_front();
114        }
115    }
116
117    /// Check if current call is redundant with recent history
118    pub fn is_redundant(&self, tool: &str, args: &Value) -> bool {
119        let recent_limit = 5;
120
121        self.execution_history
122            .iter()
123            .rev()
124            .take(recent_limit)
125            .any(|record| record.tool_name == tool && are_args_equivalent(&record.args, args))
126    }
127
128    /// Get recent tool names (up to N)
129    pub fn recent_tools(&self, n: usize) -> Vec<CompactStr> {
130        self.execution_history
131            .iter()
132            .rev()
133            .take(n)
134            .map(|r| r.tool_name.clone())
135            .collect()
136    }
137
138    /// Get tools that produced good results recently
139    pub fn high_performing_tools(&self, n: usize) -> Vec<CompactStr> {
140        let mut tools: Vec<_> = self
141            .execution_history
142            .iter()
143            .rev()
144            .filter(|r| r.result.metadata.quality_score() > 0.7)
145            .take(n)
146            .map(|r| &r.tool_name)
147            .collect();
148
149        tools.sort();
150        tools.dedup();
151        tools.into_iter().cloned().collect()
152    }
153
154    /// Suggest a fallback tool based on prior effectiveness
155    pub fn suggest_fallback(&self, failed_tool: &str) -> Option<CompactStr> {
156        // Find most effective tool that hasn't been tried recently
157        let recent = self.recent_tools(3);
158
159        self.effectiveness_snapshot
160            .values()
161            .filter(|eff| {
162                eff.success_rate > 0.7
163                    && !recent.contains(&eff.tool_name)
164                    && eff.tool_name != failed_tool
165            })
166            .max_by(|a, b| {
167                a.effectiveness_score()
168                    .partial_cmp(&b.effectiveness_score())
169                    .unwrap_or(std::cmp::Ordering::Equal)
170            })
171            .map(|eff| eff.tool_name.clone())
172    }
173
174    /// Get all detected patterns
175    pub fn patterns(&self) -> &[ToolPattern] {
176        &self.patterns
177    }
178
179    /// Update effectiveness snapshot
180    pub fn set_effectiveness(&mut self, snapshot: HashMap<String, ToolEffectiveness>) {
181        self.effectiveness_snapshot = snapshot;
182    }
183
184    /// Get effectiveness snapshot
185    pub fn effectiveness(&self) -> &HashMap<String, ToolEffectiveness> {
186        &self.effectiveness_snapshot
187    }
188
189    /// Get full execution history
190    pub fn history(&self) -> Vec<&ToolExecutionRecord> {
191        self.execution_history.iter().collect()
192    }
193
194    /// Detect patterns in execution
195    fn detect_patterns(&mut self, new_record: &ToolExecutionRecord) {
196        // Pattern 1: Redundant searches
197        let recent_records: Vec<_> = self.execution_history.iter().rev().take(10).collect();
198
199        let mut same_pattern_tools = vec![&new_record.tool_name];
200        for record in &recent_records {
201            if are_args_equivalent(&record.args, &new_record.args) {
202                same_pattern_tools.push(&record.tool_name);
203            }
204        }
205
206        if same_pattern_tools.len() > 2 {
207            let mut tools: Vec<CompactStr> = same_pattern_tools.into_iter().cloned().collect();
208            tools.sort();
209            tools.dedup();
210            self.patterns.push(ToolPattern::RedundantSearch {
211                tools,
212                pattern: format!("{:?}", new_record.args),
213            });
214        }
215
216        // Pattern 2: Low quality loop
217        let recent_same_tool: Vec<_> = recent_records
218            .iter()
219            .filter(|r| r.tool_name == new_record.tool_name)
220            .collect();
221
222        if recent_same_tool.len() > 3 {
223            let avg_quality = recent_same_tool
224                .iter()
225                .map(|r| r.result.metadata.quality_score())
226                .sum::<f32>()
227                / recent_same_tool.len() as f32;
228
229            if avg_quality < 0.4 {
230                self.patterns.push(ToolPattern::LowQualityLoop {
231                    tool: new_record.tool_name.clone(),
232                    attempts: recent_same_tool.len() + 1,
233                });
234            }
235        }
236    }
237}
238
239/// Check if two sets of arguments are equivalent (for deduplication)
240pub fn are_args_equivalent(a: &Value, b: &Value) -> bool {
241    // Normalize and compare
242    match (a, b) {
243        (Value::Object(a_map), Value::Object(b_map)) => {
244            // Exact match of all keys and values
245            a_map.len() == b_map.len()
246                && a_map
247                    .iter()
248                    .all(|(k, v)| b_map.get(k).is_some_and(|bv| bv == v))
249        }
250        (Value::Array(a_arr), Value::Array(b_arr)) => {
251            a_arr.len() == b_arr.len() && a_arr.iter().zip(b_arr.iter()).all(|(av, bv)| av == bv)
252        }
253        (Value::String(a_str), Value::String(b_str)) => a_str == b_str,
254        (Value::Number(a_num), Value::Number(b_num)) => a_num == b_num,
255        (Value::Bool(a_bool), Value::Bool(b_bool)) => a_bool == b_bool,
256        (Value::Null, Value::Null) => true,
257        _ => false,
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::tools::result_metadata::ResultMetadata;
265
266    fn make_record(tool: &str, arg_val: i32) -> ToolExecutionRecord {
267        ToolExecutionRecord::new(
268            tool.to_owned(),
269            Value::Number(arg_val.into()),
270            EnhancedToolResult::new(
271                Value::Null,
272                ResultMetadata::success(0.8, 0.8),
273                tool.to_owned(),
274            ),
275            100,
276        )
277    }
278
279    #[test]
280    fn test_execution_context_creation() {
281        let ctx = ToolExecutionContext::new("session-1".to_owned(), "find errors".to_owned());
282
283        assert_eq!(ctx.session_id, "session-1");
284        assert_eq!(ctx.current_task, "find errors");
285    }
286
287    #[test]
288    fn test_add_record() {
289        let mut ctx = ToolExecutionContext::new("session-1".to_owned(), "test".to_owned());
290
291        let record = make_record("grep", 1);
292        ctx.add_record(record);
293
294        assert_eq!(ctx.history().len(), 1);
295    }
296
297    #[test]
298    fn test_is_redundant() {
299        let mut ctx = ToolExecutionContext::new("session-1".to_owned(), "test".to_owned());
300
301        let args = Value::String("pattern".to_owned());
302
303        ctx.add_record(ToolExecutionRecord::new(
304            "grep".to_string(),
305            args.clone(),
306            EnhancedToolResult::new(Value::Null, ResultMetadata::default(), "grep".to_owned()),
307            100,
308        ));
309
310        assert!(ctx.is_redundant("grep", &args));
311    }
312
313    #[test]
314    fn test_recent_tools() {
315        let mut ctx = ToolExecutionContext::new("session-1".to_owned(), "test".to_owned());
316
317        ctx.add_record(make_record("grep", 1));
318        ctx.add_record(make_record("find", 2));
319        ctx.add_record(make_record("grep", 3));
320
321        let recent = ctx.recent_tools(2);
322        assert_eq!(recent.len(), 2);
323        assert_eq!(recent[0], "grep"); // Most recent first
324    }
325
326    #[test]
327    fn test_args_equivalent() {
328        let a = Value::String("pattern".to_owned());
329        let b = Value::String("pattern".to_owned());
330        assert!(are_args_equivalent(&a, &b));
331
332        let c = Value::String("different".to_string());
333        assert!(!are_args_equivalent(&a, &c));
334    }
335}