1use std::collections::HashMap;
2use std::sync::Mutex;
3
4use crate::types::{AgentError, AgentResult, SessionId};
5
6#[derive(Clone, Debug, PartialEq, Eq)]
8pub enum ToolErrorAction {
9 Stop,
11 Retry,
13 RetryWithHistory {
20 errors: Vec<String>,
22 },
23}
24
25pub trait ToolErrorRecovery: Send + Sync {
31 fn on_error(
32 &self,
33 _session_id: &SessionId,
34 _tool_names: &[String],
35 _error: &AgentError,
36 ) -> AgentResult<ToolErrorAction>;
37
38 fn on_success(&self, _session_id: &SessionId, _tool_name: &str) {}
44}
45
46pub struct StopOnError;
51
52impl ToolErrorRecovery for StopOnError {
53 fn on_error(
54 &self,
55 _session_id: &SessionId,
56 _tool_names: &[String],
57 _error: &AgentError,
58 ) -> AgentResult<ToolErrorAction> {
59 Ok(ToolErrorAction::Stop)
60 }
61}
62
63pub struct RetryOnError;
67
68impl ToolErrorRecovery for RetryOnError {
69 fn on_error(
70 &self,
71 _session_id: &SessionId,
72 _tool_names: &[String],
73 _error: &AgentError,
74 ) -> AgentResult<ToolErrorAction> {
75 Ok(ToolErrorAction::Retry)
76 }
77}
78
79pub struct ConsecutiveFailureRecovery {
89 max_consecutive_failures: usize,
90 failure_counts: Mutex<HashMap<u64, HashMap<String, usize>>>,
92 error_messages: Mutex<HashMap<u64, HashMap<String, Vec<String>>>>,
94 grace_used: Mutex<HashMap<u64, HashMap<String, bool>>>,
96}
97
98impl ConsecutiveFailureRecovery {
99 pub fn new(max_consecutive_failures: usize) -> Self {
100 Self {
101 max_consecutive_failures,
102 failure_counts: Mutex::new(HashMap::new()),
103 error_messages: Mutex::new(HashMap::new()),
104 grace_used: Mutex::new(HashMap::new()),
105 }
106 }
107
108 pub fn reset_failures(&self, session_id: &SessionId, tool_name: &str) {
111 if let Ok(mut counts) = self.failure_counts.lock()
112 && let Some(session_counts) = counts.get_mut(&session_id.id)
113 {
114 session_counts.remove(tool_name);
115 }
116 if let Ok(mut msgs) = self.error_messages.lock()
117 && let Some(session_msgs) = msgs.get_mut(&session_id.id)
118 {
119 session_msgs.remove(tool_name);
120 }
121 if let Ok(mut grace) = self.grace_used.lock()
122 && let Some(session_grace) = grace.get_mut(&session_id.id)
123 {
124 session_grace.remove(tool_name);
125 }
126 }
127
128 pub fn reset_session(&self, session_id: &SessionId) {
131 if let Ok(mut counts) = self.failure_counts.lock() {
132 counts.remove(&session_id.id);
133 }
134 if let Ok(mut msgs) = self.error_messages.lock() {
135 msgs.remove(&session_id.id);
136 }
137 if let Ok(mut grace) = self.grace_used.lock() {
138 grace.remove(&session_id.id);
139 }
140 }
141}
142
143impl ToolErrorRecovery for ConsecutiveFailureRecovery {
144 fn on_error(
145 &self,
146 session_id: &SessionId,
147 tool_names: &[String],
148 error: &AgentError,
149 ) -> AgentResult<ToolErrorAction> {
150 let mut counts = self
151 .failure_counts
152 .lock()
153 .map_err(|e| AgentError::internal(format!("Failed to lock failure counts: {}", e)))?;
154 let mut msgs = self
155 .error_messages
156 .lock()
157 .map_err(|e| AgentError::internal(format!("Failed to lock error messages: {}", e)))?;
158 let mut grace = self
159 .grace_used
160 .lock()
161 .map_err(|e| AgentError::internal(format!("Failed to lock grace_used: {}", e)))?;
162
163 let session_counts = counts.entry(session_id.id).or_insert_with(HashMap::new);
164 let session_msgs = msgs.entry(session_id.id).or_insert_with(HashMap::new);
165 let session_grace = grace.entry(session_id.id).or_insert_with(HashMap::new);
166
167 let error_text = error.to_string();
169 let mut max_failures = 0;
170 let mut threshold_tool: Option<String> = None;
171 for name in tool_names {
172 let count = session_counts.entry(name.clone()).or_insert(0);
173 *count += 1;
174 session_msgs
175 .entry(name.clone())
176 .or_insert_with(Vec::new)
177 .push(error_text.clone());
178 if *count > max_failures {
179 max_failures = *count;
180 }
181 if *count >= self.max_consecutive_failures {
182 threshold_tool = Some(name.clone());
183 }
184 }
185
186 if max_failures >= self.max_consecutive_failures {
187 let tool_name = threshold_tool.unwrap_or_else(|| tool_names[0].clone());
188
189 tracing::warn!(
190 session_id = session_id.id,
191 tool = %tool_name,
192 failures = max_failures,
193 max_consecutive_failures = self.max_consecutive_failures,
194 "ConsecutiveFailureRecovery: threshold reached"
195 );
196
197 let already_used = session_grace.get(&tool_name).copied().unwrap_or(false);
199
200 if already_used {
201 counts.remove(&session_id.id);
203 msgs.remove(&session_id.id);
204 grace.remove(&session_id.id);
205 return Ok(ToolErrorAction::Stop);
206 }
207
208 session_grace.insert(tool_name.clone(), true);
210
211 let errors = session_msgs.get(&tool_name).cloned().unwrap_or_default();
212
213 msgs.remove(&session_id.id);
216
217 return Ok(ToolErrorAction::RetryWithHistory { errors });
218 }
219
220 Ok(ToolErrorAction::Retry)
221 }
222
223 fn on_success(&self, session_id: &SessionId, tool_name: &str) {
224 self.reset_failures(session_id, tool_name);
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn stop_on_error_always_stops() {
234 let recovery = StopOnError;
235 let session_id = SessionId::new(1);
236 let names = vec!["tool_a".to_string()];
237 let error = AgentError::internal("test error");
238 assert_eq!(
239 recovery.on_error(&session_id, &names, &error).unwrap(),
240 ToolErrorAction::Stop
241 );
242 }
243
244 #[test]
245 fn retry_on_error_always_retries() {
246 let recovery = RetryOnError;
247 let session_id = SessionId::new(1);
248 let names = vec!["tool_a".to_string()];
249 let error = AgentError::internal("test error");
250 assert_eq!(
251 recovery.on_error(&session_id, &names, &error).unwrap(),
252 ToolErrorAction::Retry
253 );
254 }
255
256 #[test]
259 fn consecutive_failure_retries_then_retry_with_history() {
260 let recovery = ConsecutiveFailureRecovery::new(3);
261 let session_id = SessionId::new(1);
262 let names = vec!["tool_a".to_string()];
263 let error = AgentError::internal("test error");
264
265 assert_eq!(
267 recovery.on_error(&session_id, &names, &error).unwrap(),
268 ToolErrorAction::Retry
269 );
270 assert_eq!(
271 recovery.on_error(&session_id, &names, &error).unwrap(),
272 ToolErrorAction::Retry
273 );
274
275 let action = recovery.on_error(&session_id, &names, &error).unwrap();
277 assert!(matches!(action, ToolErrorAction::RetryWithHistory { .. }));
278 }
279
280 #[test]
281 fn consecutive_failure_stop_after_grace() {
282 let recovery = ConsecutiveFailureRecovery::new(3);
283 let session_id = SessionId::new(1);
284 let names = vec!["tool_a".to_string()];
285 let error = AgentError::internal("test error");
286
287 for _ in 0..2 {
289 recovery.on_error(&session_id, &names, &error).unwrap();
290 }
291 let action = recovery.on_error(&session_id, &names, &error).unwrap();
292 assert!(matches!(action, ToolErrorAction::RetryWithHistory { .. }));
293
294 let action = recovery.on_error(&session_id, &names, &error).unwrap();
297 assert!(
298 matches!(action, ToolErrorAction::Stop),
299 "should Stop after grace exhausted, got {:?}",
300 action
301 );
302 }
303
304 #[test]
305 fn consecutive_failure_retry_with_history_collects_errors() {
306 let recovery = ConsecutiveFailureRecovery::new(2);
307 let session_id = SessionId::new(1);
308 let names = vec!["tool_a".to_string()];
309
310 let error1 = AgentError::internal("first error");
312 let error2 = AgentError::internal("second error");
313
314 recovery.on_error(&session_id, &names, &error1).unwrap();
315 let action = recovery.on_error(&session_id, &names, &error2).unwrap();
316
317 match action {
318 ToolErrorAction::RetryWithHistory { errors } => {
319 assert_eq!(errors.len(), 2);
320 assert!(errors[0].contains("first error"));
321 assert!(errors[1].contains("second error"));
322 }
323 _ => panic!("expected RetryWithHistory, got {:?}", action),
324 }
325 }
326
327 #[test]
330 fn consecutive_failure_resets_on_success() {
331 let recovery = ConsecutiveFailureRecovery::new(3);
332 let session_id = SessionId::new(1);
333 let names = vec!["tool_a".to_string()];
334 let error = AgentError::internal("test error");
335
336 recovery.on_error(&session_id, &names, &error).unwrap();
338 recovery.on_error(&session_id, &names, &error).unwrap();
339
340 recovery.reset_failures(&session_id, "tool_a");
342
343 assert_eq!(
345 recovery.on_error(&session_id, &names, &error).unwrap(),
346 ToolErrorAction::Retry
347 );
348 }
349
350 #[test]
351 fn consecutive_failure_on_success_resets_via_trait() {
352 let recovery = ConsecutiveFailureRecovery::new(2);
353 let session_id = SessionId::new(1);
354 let names = vec!["tool_a".to_string()];
355 let error = AgentError::internal("test error");
356
357 assert_eq!(
359 recovery.on_error(&session_id, &names, &error).unwrap(),
360 ToolErrorAction::Retry
361 );
362
363 recovery.on_success(&session_id, "tool_a");
365
366 assert_eq!(
368 recovery.on_error(&session_id, &names, &error).unwrap(),
369 ToolErrorAction::Retry
370 );
371 }
372
373 #[test]
374 fn consecutive_failure_on_success_resets_grace() {
375 let recovery = ConsecutiveFailureRecovery::new(2);
376 let session_id = SessionId::new(1);
377 let names = vec!["tool_a".to_string()];
378 let error = AgentError::internal("test error");
379
380 recovery.on_error(&session_id, &names, &error).unwrap();
382 let action = recovery.on_error(&session_id, &names, &error).unwrap();
383 assert!(matches!(action, ToolErrorAction::RetryWithHistory { .. }));
384
385 recovery.on_success(&session_id, "tool_a");
387
388 recovery.on_error(&session_id, &names, &error).unwrap();
390 let action = recovery.on_error(&session_id, &names, &error).unwrap();
391 assert!(
392 matches!(action, ToolErrorAction::RetryWithHistory { .. }),
393 "grace should be reusable after success, got {:?}",
394 action
395 );
396 }
397
398 #[test]
399 fn consecutive_failure_resets_clears_error_messages() {
400 let recovery = ConsecutiveFailureRecovery::new(3);
401 let session_id = SessionId::new(1);
402 let names = vec!["tool_a".to_string()];
403 let error = AgentError::internal("test error");
404
405 recovery.on_error(&session_id, &names, &error).unwrap();
407 recovery.on_error(&session_id, &names, &error).unwrap();
408
409 recovery.reset_failures(&session_id, "tool_a");
411
412 let error_new = AgentError::internal("new error");
415 recovery.on_error(&session_id, &names, &error_new).unwrap();
416 recovery.on_error(&session_id, &names, &error_new).unwrap();
417 let action = recovery.on_error(&session_id, &names, &error_new).unwrap();
418 match action {
419 ToolErrorAction::RetryWithHistory { errors } => {
420 assert_eq!(errors.len(), 3);
421 for e in &errors {
422 assert!(e.contains("new error"), "old errors should be cleared");
423 }
424 }
425 _ => panic!("expected RetryWithHistory, got {:?}", action),
426 }
427 }
428
429 #[test]
430 fn consecutive_failure_resets_session_clears_everything() {
431 let recovery = ConsecutiveFailureRecovery::new(2);
432 let session_id = SessionId::new(1);
433 let names = vec!["tool_a".to_string()];
434 let error = AgentError::internal("test error");
435
436 recovery.on_error(&session_id, &names, &error).unwrap();
438 let action = recovery.on_error(&session_id, &names, &error).unwrap();
439 assert!(matches!(action, ToolErrorAction::RetryWithHistory { .. }));
440
441 recovery.reset_session(&session_id);
443
444 assert_eq!(
446 recovery.on_error(&session_id, &names, &error).unwrap(),
447 ToolErrorAction::Retry
448 );
449 }
450
451 #[test]
454 fn consecutive_failure_per_session_isolation() {
455 let recovery = ConsecutiveFailureRecovery::new(2);
456 let session1 = SessionId::new(1);
457 let session2 = SessionId::new(2);
458 let names = vec!["tool_a".to_string()];
459 let error = AgentError::internal("test error");
460
461 recovery.on_error(&session1, &names, &error).unwrap();
463 let action = recovery.on_error(&session1, &names, &error).unwrap();
464 assert!(matches!(action, ToolErrorAction::RetryWithHistory { .. }));
465
466 let action = recovery.on_error(&session1, &names, &error).unwrap();
468 assert!(
469 matches!(action, ToolErrorAction::Stop),
470 "session 1 should Stop after grace exhausted, got {:?}",
471 action
472 );
473
474 assert_eq!(
476 recovery.on_error(&session2, &names, &error).unwrap(),
477 ToolErrorAction::Retry
478 );
479 }
480
481 #[test]
484 fn consecutive_failure_different_tools_independent() {
485 let recovery = ConsecutiveFailureRecovery::new(2);
486 let session_id = SessionId::new(1);
487 let error = AgentError::internal("test error");
488
489 recovery
491 .on_error(&session_id, &["tool_a".to_string()], &error)
492 .unwrap();
493
494 assert_eq!(
496 recovery
497 .on_error(&session_id, &["tool_b".to_string()], &error,)
498 .unwrap(),
499 ToolErrorAction::Retry
500 );
501
502 let action = recovery
504 .on_error(&session_id, &["tool_a".to_string()], &error)
505 .unwrap();
506 assert!(
507 matches!(action, ToolErrorAction::RetryWithHistory { .. }),
508 "tool_a should trigger RetryWithHistory independently of tool_b"
509 );
510 }
511
512 #[test]
515 fn consecutive_failure_records_serde_error_details() {
516 let recovery = ConsecutiveFailureRecovery::new(2);
517 let session_id = SessionId::new(1);
518 let names = vec!["write_file".to_string()];
519
520 let error = AgentError::ToolArgsInvalid {
522 name: "write_file".to_string(),
523 raw: "missing field `path` at line 1 column 2 (args: {})".to_string(),
524 };
525
526 recovery.on_error(&session_id, &names, &error).unwrap();
527 let action = recovery.on_error(&session_id, &names, &error).unwrap();
528
529 match action {
530 ToolErrorAction::RetryWithHistory { errors } => {
531 assert_eq!(errors.len(), 2);
532 assert!(
533 errors[0].contains("missing field"),
534 "should contain serde error details"
535 );
536 }
537 _ => panic!("expected RetryWithHistory, got {:?}", action),
538 }
539 }
540}