1use 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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
46#[serde(tag = "type")]
47pub enum ToolPattern {
48 RedundantSearch {
50 tools: Vec<CompactStr>,
51 pattern: String,
52 },
53
54 SequentialRefinement {
56 tools: Vec<CompactStr>,
57 refinement_steps: usize,
58 },
59
60 ConvergentDiagnosis {
62 tools: Vec<CompactStr>,
63 common_finding: String,
64 },
65
66 LowQualityLoop { tool: CompactStr, attempts: usize },
68}
69
70#[derive(Debug, Clone)]
72pub struct ToolExecutionContext {
73 pub session_id: String,
75
76 pub current_task: String,
78
79 execution_history: VecDeque<ToolExecutionRecord>,
81
82 max_history_size: usize,
84
85 patterns: Vec<ToolPattern>,
87
88 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 pub fn add_record(&mut self, record: ToolExecutionRecord) {
106 self.detect_patterns(&record);
108
109 self.execution_history.push_back(record);
110
111 while self.execution_history.len() > self.max_history_size {
113 self.execution_history.pop_front();
114 }
115 }
116
117 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 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 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 pub fn suggest_fallback(&self, failed_tool: &str) -> Option<CompactStr> {
156 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 pub fn patterns(&self) -> &[ToolPattern] {
176 &self.patterns
177 }
178
179 pub fn set_effectiveness(&mut self, snapshot: HashMap<String, ToolEffectiveness>) {
181 self.effectiveness_snapshot = snapshot;
182 }
183
184 pub fn effectiveness(&self) -> &HashMap<String, ToolEffectiveness> {
186 &self.effectiveness_snapshot
187 }
188
189 pub fn history(&self) -> Vec<&ToolExecutionRecord> {
191 self.execution_history.iter().collect()
192 }
193
194 fn detect_patterns(&mut self, new_record: &ToolExecutionRecord) {
196 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 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
239pub fn are_args_equivalent(a: &Value, b: &Value) -> bool {
241 match (a, b) {
243 (Value::Object(a_map), Value::Object(b_map)) => {
244 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"); }
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}