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}
14
15pub trait ToolErrorRecovery: Send + Sync {
21 fn on_error(
22 &self,
23 _session_id: &SessionId,
24 _tool_names: &[String],
25 _error: &AgentError,
26 ) -> AgentResult<ToolErrorAction>;
27
28 fn on_success(&self, _session_id: &SessionId, _tool_name: &str) {}
34}
35
36pub struct StopOnError;
41
42impl ToolErrorRecovery for StopOnError {
43 fn on_error(
44 &self,
45 _session_id: &SessionId,
46 _tool_names: &[String],
47 _error: &AgentError,
48 ) -> AgentResult<ToolErrorAction> {
49 Ok(ToolErrorAction::Stop)
50 }
51}
52
53pub struct RetryOnError;
57
58impl ToolErrorRecovery for RetryOnError {
59 fn on_error(
60 &self,
61 _session_id: &SessionId,
62 _tool_names: &[String],
63 _error: &AgentError,
64 ) -> AgentResult<ToolErrorAction> {
65 Ok(ToolErrorAction::Retry)
66 }
67}
68
69pub struct ConsecutiveFailureRecovery {
79 max_consecutive_failures: usize,
80 failure_counts: Mutex<HashMap<u64, HashMap<String, usize>>>,
82}
83
84impl ConsecutiveFailureRecovery {
85 pub fn new(max_consecutive_failures: usize) -> Self {
86 Self {
87 max_consecutive_failures,
88 failure_counts: Mutex::new(HashMap::new()),
89 }
90 }
91
92 pub fn reset_failures(&self, session_id: &SessionId, tool_name: &str) {
95 if let Ok(mut counts) = self.failure_counts.lock()
96 && let Some(session_counts) = counts.get_mut(&session_id.id)
97 {
98 session_counts.remove(tool_name);
99 }
100 }
101
102 pub fn reset_session(&self, session_id: &SessionId) {
105 if let Ok(mut counts) = self.failure_counts.lock() {
106 counts.remove(&session_id.id);
107 }
108 }
109}
110
111impl ToolErrorRecovery for ConsecutiveFailureRecovery {
112 fn on_error(
113 &self,
114 session_id: &SessionId,
115 tool_names: &[String],
116 _error: &AgentError,
117 ) -> AgentResult<ToolErrorAction> {
118 let mut counts = self
119 .failure_counts
120 .lock()
121 .map_err(|e| AgentError::internal(format!("Failed to lock failure counts: {}", e)))?;
122
123 let session_counts = counts.entry(session_id.id).or_insert_with(HashMap::new);
124
125 let mut max_failures = 0;
127 for name in tool_names {
128 let count = session_counts.entry(name.clone()).or_insert(0);
129 *count += 1;
130 if *count > max_failures {
131 max_failures = *count;
132 }
133 }
134
135 if max_failures >= self.max_consecutive_failures {
136 let failing_tools: Vec<String> = tool_names
137 .iter()
138 .filter(|name| {
139 session_counts
140 .get(*name)
141 .is_some_and(|&c| c >= self.max_consecutive_failures)
142 })
143 .cloned()
144 .collect();
145
146 tracing::warn!(
147 session_id = session_id.id,
148 failing_tools = ?failing_tools,
149 max_consecutive_failures = self.max_consecutive_failures,
150 "ConsecutiveFailureRecovery: stopping after repeated failures"
151 );
152
153 counts.remove(&session_id.id);
155
156 return Ok(ToolErrorAction::Stop);
157 }
158
159 Ok(ToolErrorAction::Retry)
160 }
161
162 fn on_success(&self, session_id: &SessionId, tool_name: &str) {
163 self.reset_failures(session_id, tool_name);
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn stop_on_error_always_stops() {
173 let recovery = StopOnError;
174 let session_id = SessionId::new(1);
175 let names = vec!["tool_a".to_string()];
176 let error = AgentError::internal("test error");
177 assert_eq!(
178 recovery.on_error(&session_id, &names, &error).unwrap(),
179 ToolErrorAction::Stop
180 );
181 }
182
183 #[test]
184 fn retry_on_error_always_retries() {
185 let recovery = RetryOnError;
186 let session_id = SessionId::new(1);
187 let names = vec!["tool_a".to_string()];
188 let error = AgentError::internal("test error");
189 assert_eq!(
190 recovery.on_error(&session_id, &names, &error).unwrap(),
191 ToolErrorAction::Retry
192 );
193 }
194
195 #[test]
196 fn consecutive_failure_retries_then_stops() {
197 let recovery = ConsecutiveFailureRecovery::new(3);
198 let session_id = SessionId::new(1);
199 let names = vec!["tool_a".to_string()];
200 let error = AgentError::internal("test error");
201
202 assert_eq!(
204 recovery.on_error(&session_id, &names, &error).unwrap(),
205 ToolErrorAction::Retry
206 );
207 assert_eq!(
208 recovery.on_error(&session_id, &names, &error).unwrap(),
209 ToolErrorAction::Retry
210 );
211
212 assert_eq!(
214 recovery.on_error(&session_id, &names, &error).unwrap(),
215 ToolErrorAction::Stop
216 );
217 }
218
219 #[test]
220 fn consecutive_failure_resets_on_success() {
221 let recovery = ConsecutiveFailureRecovery::new(3);
222 let session_id = SessionId::new(1);
223 let names = vec!["tool_a".to_string()];
224 let error = AgentError::internal("test error");
225
226 recovery.on_error(&session_id, &names, &error).unwrap();
228 recovery.on_error(&session_id, &names, &error).unwrap();
229
230 recovery.reset_failures(&session_id, "tool_a");
232
233 assert_eq!(
235 recovery.on_error(&session_id, &names, &error).unwrap(),
236 ToolErrorAction::Retry
237 );
238 }
239
240 #[test]
241 fn consecutive_failure_on_success_resets_via_trait() {
242 let recovery = ConsecutiveFailureRecovery::new(2);
243 let session_id = SessionId::new(1);
244 let names = vec!["tool_a".to_string()];
245 let error = AgentError::internal("test error");
246
247 assert_eq!(
249 recovery.on_error(&session_id, &names, &error).unwrap(),
250 ToolErrorAction::Retry
251 );
252
253 recovery.on_success(&session_id, "tool_a");
255
256 assert_eq!(
258 recovery.on_error(&session_id, &names, &error).unwrap(),
259 ToolErrorAction::Retry
260 );
261 }
262
263 #[test]
264 fn consecutive_failure_per_session_isolation() {
265 let recovery = ConsecutiveFailureRecovery::new(2);
266 let session1 = SessionId::new(1);
267 let session2 = SessionId::new(2);
268 let names = vec!["tool_a".to_string()];
269 let error = AgentError::internal("test error");
270
271 recovery.on_error(&session1, &names, &error).unwrap();
273 assert_eq!(
274 recovery.on_error(&session1, &names, &error).unwrap(),
275 ToolErrorAction::Stop
276 );
277
278 assert_eq!(
280 recovery.on_error(&session2, &names, &error).unwrap(),
281 ToolErrorAction::Retry
282 );
283 }
284
285 #[test]
286 fn consecutive_failure_resets_session() {
287 let recovery = ConsecutiveFailureRecovery::new(2);
288 let session_id = SessionId::new(1);
289 let names = vec!["tool_a".to_string()];
290 let error = AgentError::internal("test error");
291
292 recovery.on_error(&session_id, &names, &error).unwrap();
294 assert_eq!(
295 recovery.on_error(&session_id, &names, &error).unwrap(),
296 ToolErrorAction::Stop
297 );
298
299 recovery.reset_session(&session_id);
301
302 assert_eq!(
304 recovery.on_error(&session_id, &names, &error).unwrap(),
305 ToolErrorAction::Retry
306 );
307 }
308}