1use std::collections::HashMap;
6
7#[derive(Debug, Clone)]
9pub struct ReflectionResult {
10 pub category: String,
11 pub content: String,
12 pub confidence: f64,
13 pub reasoning: String,
14}
15
16#[derive(Debug, Clone)]
18pub struct ToolCallInfo {
19 pub name: String,
20 pub parameters: HashMap<String, String>,
21}
22
23pub struct ExecutionReflector {
28 pub min_tool_calls: usize,
29 pub min_confidence: f64,
30}
31
32impl ExecutionReflector {
33 pub fn new(min_tool_calls: usize, min_confidence: f64) -> Self {
35 Self {
36 min_tool_calls,
37 min_confidence,
38 }
39 }
40
41 pub fn reflect(
43 &self,
44 _query: &str,
45 tool_calls: &[ToolCallInfo],
46 outcome: &str,
47 ) -> Option<ReflectionResult> {
48 if !self.is_worth_learning(tool_calls, outcome) {
49 return None;
50 }
51
52 let result = self
53 .extract_file_operation_pattern(tool_calls)
54 .or_else(|| self.extract_code_navigation_pattern(tool_calls))
55 .or_else(|| self.extract_testing_pattern(tool_calls))
56 .or_else(|| self.extract_shell_command_pattern(tool_calls))
57 .or_else(|| self.extract_error_recovery_pattern(tool_calls, outcome));
58
59 result.filter(|r| r.confidence >= self.min_confidence)
60 }
61
62 fn is_worth_learning(&self, tool_calls: &[ToolCallInfo], outcome: &str) -> bool {
63 if outcome == "error" && !tool_calls.is_empty() {
65 return true;
66 }
67
68 if tool_calls.len() == 1 {
70 let name = &tool_calls[0].name;
71 if name == "read_file" || name == "list_files" {
72 return false;
73 }
74 }
75
76 if tool_calls.len() >= self.min_tool_calls {
78 return true;
79 }
80
81 false
82 }
83
84 fn extract_file_operation_pattern(
85 &self,
86 tool_calls: &[ToolCallInfo],
87 ) -> Option<ReflectionResult> {
88 let names: Vec<&str> = tool_calls.iter().map(|tc| tc.name.as_str()).collect();
89
90 if let (Some(list_idx), Some(read_idx)) = (
92 names.iter().position(|&n| n == "list_files"),
93 names.iter().position(|&n| n == "read_file"),
94 ) && list_idx < read_idx
95 {
96 return Some(ReflectionResult {
97 category: "file_operations".to_string(),
98 content: "List directory contents before reading files to understand \
99 structure and locate files"
100 .to_string(),
101 confidence: 0.75,
102 reasoning: "Sequential list_files -> read_file pattern shows exploratory \
103 file access"
104 .to_string(),
105 });
106 }
107
108 if let (Some(read_idx), Some(write_idx)) = (
110 names.iter().position(|&n| n == "read_file"),
111 names.iter().position(|&n| n == "write_file"),
112 ) && read_idx < write_idx
113 {
114 return Some(ReflectionResult {
115 category: "file_operations".to_string(),
116 content: "Read file contents before writing to understand current state \
117 and preserve important data"
118 .to_string(),
119 confidence: 0.8,
120 reasoning: "Sequential read_file -> write_file shows safe modification \
121 workflow"
122 .to_string(),
123 });
124 }
125
126 let read_count = names.iter().filter(|&&n| n == "read_file").count();
128 if read_count >= 3 {
129 return Some(ReflectionResult {
130 category: "code_navigation".to_string(),
131 content: "When understanding complex code, read multiple related files to \
132 build complete picture"
133 .to_string(),
134 confidence: 0.7,
135 reasoning: format!(
136 "Multiple file reads ({read_count}) indicates thorough code exploration"
137 ),
138 });
139 }
140
141 None
142 }
143
144 fn extract_code_navigation_pattern(
145 &self,
146 tool_calls: &[ToolCallInfo],
147 ) -> Option<ReflectionResult> {
148 let names: Vec<&str> = tool_calls.iter().map(|tc| tc.name.as_str()).collect();
149
150 if let (Some(search_idx), Some(read_idx)) = (
152 names.iter().position(|&n| n == "search"),
153 names.iter().position(|&n| n == "read_file"),
154 ) && search_idx < read_idx
155 {
156 return Some(ReflectionResult {
157 category: "code_navigation".to_string(),
158 content: "Search for keywords or patterns before reading files to locate \
159 relevant code efficiently"
160 .to_string(),
161 confidence: 0.8,
162 reasoning: "Search followed by read shows targeted file access".to_string(),
163 });
164 }
165
166 let search_count = names.iter().filter(|&&n| n == "search").count();
168 if search_count >= 2 {
169 return Some(ReflectionResult {
170 category: "code_navigation".to_string(),
171 content: "Use multiple searches with different keywords to thoroughly explore \
172 codebase and find all relevant locations"
173 .to_string(),
174 confidence: 0.7,
175 reasoning: format!(
176 "Multiple searches ({search_count}) shows iterative code exploration"
177 ),
178 });
179 }
180
181 None
182 }
183
184 fn extract_testing_pattern(&self, tool_calls: &[ToolCallInfo]) -> Option<ReflectionResult> {
185 let names: Vec<&str> = tool_calls.iter().map(|tc| tc.name.as_str()).collect();
186
187 let test_keywords = ["test", "pytest", "jest", "npm test"];
188 let has_test_command = tool_calls.iter().any(|tc| {
189 tc.name == "run_command"
190 && tc.parameters.get("command").is_some_and(|cmd| {
191 let lower = cmd.to_lowercase();
192 test_keywords.iter().any(|kw| lower.contains(kw))
193 })
194 });
195
196 if !has_test_command {
197 return None;
198 }
199
200 if names.contains(&"write_file") || names.contains(&"edit_file") {
201 return Some(ReflectionResult {
202 category: "testing".to_string(),
203 content: "Run tests after making code changes to verify correctness and \
204 catch regressions early"
205 .to_string(),
206 confidence: 0.85,
207 reasoning: "Code modification followed by test execution shows good \
208 development practice"
209 .to_string(),
210 });
211 }
212
213 None
214 }
215
216 fn extract_shell_command_pattern(
217 &self,
218 tool_calls: &[ToolCallInfo],
219 ) -> Option<ReflectionResult> {
220 let commands: Vec<&str> = tool_calls
221 .iter()
222 .filter(|tc| tc.name == "run_command")
223 .filter_map(|tc| tc.parameters.get("command").map(String::as_str))
224 .collect();
225
226 if commands.len() < 2 {
227 return None;
228 }
229
230 let install_keywords = [
231 "npm install",
232 "pip install",
233 "yarn install",
234 "poetry install",
235 ];
236 let run_keywords = ["npm start", "python", "node", "pytest"];
237
238 let has_install = commands.iter().any(|cmd| {
239 install_keywords
240 .iter()
241 .any(|kw| cmd.to_lowercase().contains(kw))
242 });
243 let has_run = commands.iter().any(|cmd| {
244 run_keywords
245 .iter()
246 .any(|kw| cmd.to_lowercase().contains(kw))
247 });
248
249 if has_install && has_run {
250 return Some(ReflectionResult {
251 category: "shell_commands".to_string(),
252 content: "Install dependencies before running or testing applications to \
253 ensure all requirements are met"
254 .to_string(),
255 confidence: 0.8,
256 reasoning: "Install followed by run/test shows proper setup workflow".to_string(),
257 });
258 }
259
260 if commands.iter().any(|cmd| cmd.contains("git status")) {
261 return Some(ReflectionResult {
262 category: "git_operations".to_string(),
263 content: "Check git status before performing git operations to understand \
264 current state and avoid mistakes"
265 .to_string(),
266 confidence: 0.75,
267 reasoning: "Git status check before operations shows careful version control \
268 practice"
269 .to_string(),
270 });
271 }
272
273 None
274 }
275
276 fn extract_error_recovery_pattern(
277 &self,
278 tool_calls: &[ToolCallInfo],
279 outcome: &str,
280 ) -> Option<ReflectionResult> {
281 if outcome != "error" {
282 return None;
283 }
284
285 let names: Vec<&str> = tool_calls.iter().map(|tc| tc.name.as_str()).collect();
286
287 if names.contains(&"read_file") {
288 return Some(ReflectionResult {
289 category: "error_handling".to_string(),
290 content: "When file access fails, list directory first to verify file exists \
291 and check path correctness"
292 .to_string(),
293 confidence: 0.7,
294 reasoning: "File access error suggests need for directory verification".to_string(),
295 });
296 }
297
298 if names.contains(&"run_command") {
299 return Some(ReflectionResult {
300 category: "error_handling".to_string(),
301 content: "When commands fail, verify environment setup, dependencies, and \
302 working directory before retrying"
303 .to_string(),
304 confidence: 0.65,
305 reasoning: "Command execution error suggests environment or dependency issue"
306 .to_string(),
307 });
308 }
309
310 None
311 }
312}
313
314impl Default for ExecutionReflector {
315 fn default() -> Self {
316 Self::new(2, 0.6)
317 }
318}
319
320const RECENCY_DECAY: f64 = 0.95;
322
323pub fn score_reflection(_reflection: &str, evidence_count: usize, age_days: u64) -> f64 {
337 if evidence_count == 0 {
338 return 0.0;
339 }
340 let decay = RECENCY_DECAY.powi(age_days as i32);
341 evidence_count as f64 * decay
342}
343
344#[cfg(test)]
345#[path = "reflector_tests.rs"]
346mod tests;