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 { tools: Vec<CompactStr>, pattern: String },
50
51 SequentialRefinement {
53 tools: Vec<CompactStr>,
54 refinement_steps: usize,
55 },
56
57 ConvergentDiagnosis {
59 tools: Vec<CompactStr>,
60 common_finding: String,
61 },
62
63 LowQualityLoop { tool: CompactStr, attempts: usize },
65}
66
67#[derive(Debug, Clone)]
69pub struct ToolExecutionContext {
70 pub session_id: String,
72
73 pub current_task: String,
75
76 execution_history: VecDeque<ToolExecutionRecord>,
78
79 max_history_size: usize,
81
82 patterns: Vec<ToolPattern>,
84
85 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 pub fn add_record(&mut self, record: ToolExecutionRecord) {
103 self.detect_patterns(&record);
105
106 self.execution_history.push_back(record);
107
108 while self.execution_history.len() > self.max_history_size {
110 self.execution_history.pop_front();
111 }
112 }
113
114 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 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 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 pub fn suggest_fallback(&self, failed_tool: &str) -> Option<CompactStr> {
153 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 pub fn patterns(&self) -> &[ToolPattern] {
169 &self.patterns
170 }
171
172 pub fn set_effectiveness(&mut self, snapshot: HashMap<String, ToolEffectiveness>) {
174 self.effectiveness_snapshot = snapshot;
175 }
176
177 pub fn effectiveness(&self) -> &HashMap<String, ToolEffectiveness> {
179 &self.effectiveness_snapshot
180 }
181
182 pub fn history(&self) -> Vec<&ToolExecutionRecord> {
184 self.execution_history.iter().collect()
185 }
186
187 fn detect_patterns(&mut self, new_record: &ToolExecutionRecord) {
189 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 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
224pub fn are_args_equivalent(a: &Value, b: &Value) -> bool {
226 match (a, b) {
228 (Value::Object(a_map), Value::Object(b_map)) => {
229 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"); }
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}