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.failure_counts.lock().map_err(|e| {
112 AgentError::internal(format!("Failed to lock failure counts: {}", e))
113 })?;
114
115 let session_counts = counts.entry(session_id.id).or_insert_with(HashMap::new);
116
117 let mut max_failures = 0;
119 for name in tool_names {
120 let count = session_counts.entry(name.clone()).or_insert(0);
121 *count += 1;
122 if *count > max_failures {
123 max_failures = *count;
124 }
125 }
126
127 if max_failures >= self.max_consecutive_failures {
128 let failing_tools: Vec<String> = tool_names
129 .iter()
130 .filter(|name| {
131 session_counts
132 .get(*name)
133 .map_or(false, |&c| c >= self.max_consecutive_failures)
134 })
135 .cloned()
136 .collect();
137
138 tracing::warn!(
139 session_id = session_id.id,
140 failing_tools = ?failing_tools,
141 max_consecutive_failures = self.max_consecutive_failures,
142 "ConsecutiveFailureRecovery: stopping after repeated failures"
143 );
144
145 counts.remove(&session_id.id);
147
148 return Ok(ToolErrorAction::Stop);
149 }
150
151 Ok(ToolErrorAction::Retry)
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn stop_on_error_always_stops() {
161 let recovery = StopOnError;
162 let session_id = SessionId::new(1);
163 let names = vec!["tool_a".to_string()];
164 let error = AgentError::internal("test error");
165 assert_eq!(
166 recovery.on_error(&session_id, &names, &error).unwrap(),
167 ToolErrorAction::Stop
168 );
169 }
170
171 #[test]
172 fn retry_on_error_always_retries() {
173 let recovery = RetryOnError;
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::Retry
180 );
181 }
182
183 #[test]
184 fn consecutive_failure_retries_then_stops() {
185 let recovery = ConsecutiveFailureRecovery::new(3);
186 let session_id = SessionId::new(1);
187 let names = vec!["tool_a".to_string()];
188 let error = AgentError::internal("test error");
189
190 assert_eq!(
192 recovery.on_error(&session_id, &names, &error).unwrap(),
193 ToolErrorAction::Retry
194 );
195 assert_eq!(
196 recovery.on_error(&session_id, &names, &error).unwrap(),
197 ToolErrorAction::Retry
198 );
199
200 assert_eq!(
202 recovery.on_error(&session_id, &names, &error).unwrap(),
203 ToolErrorAction::Stop
204 );
205 }
206
207 #[test]
208 fn consecutive_failure_resets_on_success() {
209 let recovery = ConsecutiveFailureRecovery::new(3);
210 let session_id = SessionId::new(1);
211 let names = vec!["tool_a".to_string()];
212 let error = AgentError::internal("test error");
213
214 recovery.on_error(&session_id, &names, &error).unwrap();
216 recovery.on_error(&session_id, &names, &error).unwrap();
217
218 recovery.reset_failures(&session_id, "tool_a");
220
221 assert_eq!(
223 recovery.on_error(&session_id, &names, &error).unwrap(),
224 ToolErrorAction::Retry
225 );
226 }
227
228 #[test]
229 fn consecutive_failure_per_session_isolation() {
230 let recovery = ConsecutiveFailureRecovery::new(2);
231 let session1 = SessionId::new(1);
232 let session2 = SessionId::new(2);
233 let names = vec!["tool_a".to_string()];
234 let error = AgentError::internal("test error");
235
236 recovery.on_error(&session1, &names, &error).unwrap();
238 assert_eq!(
239 recovery.on_error(&session1, &names, &error).unwrap(),
240 ToolErrorAction::Stop
241 );
242
243 assert_eq!(
245 recovery.on_error(&session2, &names, &error).unwrap(),
246 ToolErrorAction::Retry
247 );
248 }
249
250 #[test]
251 fn consecutive_failure_resets_session() {
252 let recovery = ConsecutiveFailureRecovery::new(2);
253 let session_id = SessionId::new(1);
254 let names = vec!["tool_a".to_string()];
255 let error = AgentError::internal("test error");
256
257 recovery.on_error(&session_id, &names, &error).unwrap();
259 assert_eq!(
260 recovery.on_error(&session_id, &names, &error).unwrap(),
261 ToolErrorAction::Stop
262 );
263
264 recovery.reset_session(&session_id);
266
267 assert_eq!(
269 recovery.on_error(&session_id, &names, &error).unwrap(),
270 ToolErrorAction::Retry
271 );
272 }
273}