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