agent_base/engine/
recovery.rs1use 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
29pub struct StopOnError;
34
35impl ToolErrorRecovery for StopOnError {
36 fn on_error(
37 &self,
38 _session_id: &SessionId,
39 _tool_names: &[String],
40 _error: &AgentError,
41 ) -> AgentResult<ToolErrorAction> {
42 Ok(ToolErrorAction::Stop)
43 }
44}
45
46pub struct RetryOnError;
50
51impl ToolErrorRecovery for RetryOnError {
52 fn on_error(
53 &self,
54 _session_id: &SessionId,
55 _tool_names: &[String],
56 _error: &AgentError,
57 ) -> AgentResult<ToolErrorAction> {
58 Ok(ToolErrorAction::Retry)
59 }
60}
61
62pub struct ConsecutiveFailureRecovery {
72 max_consecutive_failures: usize,
73 failure_counts: Mutex<HashMap<u64, HashMap<String, usize>>>,
75}
76
77impl ConsecutiveFailureRecovery {
78 pub fn new(max_consecutive_failures: usize) -> Self {
79 Self {
80 max_consecutive_failures,
81 failure_counts: Mutex::new(HashMap::new()),
82 }
83 }
84
85 pub fn reset_failures(&self, session_id: &SessionId, tool_name: &str) {
88 if let Ok(mut counts) = self.failure_counts.lock() {
89 if let Some(session_counts) = counts.get_mut(&session_id.id) {
90 session_counts.remove(tool_name);
91 }
92 }
93 }
94
95 pub fn reset_session(&self, session_id: &SessionId) {
98 if let Ok(mut counts) = self.failure_counts.lock() {
99 counts.remove(&session_id.id);
100 }
101 }
102}
103
104impl ToolErrorRecovery for ConsecutiveFailureRecovery {
105 fn on_error(
106 &self,
107 session_id: &SessionId,
108 tool_names: &[String],
109 _error: &AgentError,
110 ) -> AgentResult<ToolErrorAction> {
111 let mut counts = self
112 .failure_counts
113 .lock()
114 .map_err(|e| AgentError::internal(format!("Failed to lock failure counts: {}", e)))?;
115
116 let session_counts = counts.entry(session_id.id).or_insert_with(HashMap::new);
117
118 let mut max_failures = 0;
120 for name in tool_names {
121 let count = session_counts.entry(name.clone()).or_insert(0);
122 *count += 1;
123 if *count > max_failures {
124 max_failures = *count;
125 }
126 }
127
128 if max_failures >= self.max_consecutive_failures {
129 let failing_tools: Vec<String> = tool_names
130 .iter()
131 .filter(|name| {
132 session_counts
133 .get(*name)
134 .map_or(false, |&c| c >= self.max_consecutive_failures)
135 })
136 .cloned()
137 .collect();
138
139 tracing::warn!(
140 session_id = session_id.id,
141 failing_tools = ?failing_tools,
142 max_consecutive_failures = self.max_consecutive_failures,
143 "ConsecutiveFailureRecovery: stopping after repeated failures"
144 );
145
146 counts.remove(&session_id.id);
148
149 return Ok(ToolErrorAction::Stop);
150 }
151
152 Ok(ToolErrorAction::Retry)
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159
160 #[test]
161 fn stop_on_error_always_stops() {
162 let recovery = StopOnError;
163 let session_id = SessionId::new(1);
164 let names = vec!["tool_a".to_string()];
165 let error = AgentError::internal("test error");
166 assert_eq!(
167 recovery.on_error(&session_id, &names, &error).unwrap(),
168 ToolErrorAction::Stop
169 );
170 }
171
172 #[test]
173 fn retry_on_error_always_retries() {
174 let recovery = RetryOnError;
175 let session_id = SessionId::new(1);
176 let names = vec!["tool_a".to_string()];
177 let error = AgentError::internal("test error");
178 assert_eq!(
179 recovery.on_error(&session_id, &names, &error).unwrap(),
180 ToolErrorAction::Retry
181 );
182 }
183
184 #[test]
185 fn consecutive_failure_retries_then_stops() {
186 let recovery = ConsecutiveFailureRecovery::new(3);
187 let session_id = SessionId::new(1);
188 let names = vec!["tool_a".to_string()];
189 let error = AgentError::internal("test error");
190
191 assert_eq!(
193 recovery.on_error(&session_id, &names, &error).unwrap(),
194 ToolErrorAction::Retry
195 );
196 assert_eq!(
197 recovery.on_error(&session_id, &names, &error).unwrap(),
198 ToolErrorAction::Retry
199 );
200
201 assert_eq!(
203 recovery.on_error(&session_id, &names, &error).unwrap(),
204 ToolErrorAction::Stop
205 );
206 }
207
208 #[test]
209 fn consecutive_failure_resets_on_success() {
210 let recovery = ConsecutiveFailureRecovery::new(3);
211 let session_id = SessionId::new(1);
212 let names = vec!["tool_a".to_string()];
213 let error = AgentError::internal("test error");
214
215 recovery.on_error(&session_id, &names, &error).unwrap();
217 recovery.on_error(&session_id, &names, &error).unwrap();
218
219 recovery.reset_failures(&session_id, "tool_a");
221
222 assert_eq!(
224 recovery.on_error(&session_id, &names, &error).unwrap(),
225 ToolErrorAction::Retry
226 );
227 }
228
229 #[test]
230 fn consecutive_failure_per_session_isolation() {
231 let recovery = ConsecutiveFailureRecovery::new(2);
232 let session1 = SessionId::new(1);
233 let session2 = SessionId::new(2);
234 let names = vec!["tool_a".to_string()];
235 let error = AgentError::internal("test error");
236
237 recovery.on_error(&session1, &names, &error).unwrap();
239 assert_eq!(
240 recovery.on_error(&session1, &names, &error).unwrap(),
241 ToolErrorAction::Stop
242 );
243
244 assert_eq!(
246 recovery.on_error(&session2, &names, &error).unwrap(),
247 ToolErrorAction::Retry
248 );
249 }
250
251 #[test]
252 fn consecutive_failure_resets_session() {
253 let recovery = ConsecutiveFailureRecovery::new(2);
254 let session_id = SessionId::new(1);
255 let names = vec!["tool_a".to_string()];
256 let error = AgentError::internal("test error");
257
258 recovery.on_error(&session_id, &names, &error).unwrap();
260 assert_eq!(
261 recovery.on_error(&session_id, &names, &error).unwrap(),
262 ToolErrorAction::Stop
263 );
264
265 recovery.reset_session(&session_id);
267
268 assert_eq!(
270 recovery.on_error(&session_id, &names, &error).unwrap(),
271 ToolErrorAction::Retry
272 );
273 }
274}