ai_agents_recovery/
error.rs1use super::ErrorType;
4use std::fmt;
5use thiserror::Error;
6
7#[derive(Debug, Clone)]
8pub struct ClassifiedError {
9 pub error_type: ErrorType,
10 pub message: String,
11 pub retryable: bool,
12}
13
14impl ClassifiedError {
15 pub fn new(error_type: ErrorType, message: impl Into<String>) -> Self {
16 let retryable = matches!(
17 error_type,
18 ErrorType::Timeout
19 | ErrorType::RateLimit
20 | ErrorType::ConnectionError
21 | ErrorType::ServerError
22 );
23 Self {
24 error_type,
25 message: message.into(),
26 retryable,
27 }
28 }
29
30 pub fn timeout(message: impl Into<String>) -> Self {
31 Self::new(ErrorType::Timeout, message)
32 }
33
34 pub fn rate_limit(message: impl Into<String>) -> Self {
35 Self::new(ErrorType::RateLimit, message)
36 }
37
38 pub fn connection(message: impl Into<String>) -> Self {
39 Self::new(ErrorType::ConnectionError, message)
40 }
41
42 pub fn server(message: impl Into<String>) -> Self {
43 Self::new(ErrorType::ServerError, message)
44 }
45
46 pub fn invalid_api_key(message: impl Into<String>) -> Self {
47 Self::new(ErrorType::InvalidApiKey, message)
48 }
49
50 pub fn context_too_long(message: impl Into<String>) -> Self {
51 Self::new(ErrorType::ContextTooLong, message)
52 }
53
54 pub fn tool_error(message: impl Into<String>) -> Self {
55 Self::new(ErrorType::ToolError, message)
56 }
57}
58
59impl fmt::Display for ClassifiedError {
60 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61 write!(f, "[{:?}] {}", self.error_type, self.message)
62 }
63}
64
65impl std::error::Error for ClassifiedError {}
66
67#[derive(Debug, Error)]
68pub enum RecoveryError {
69 #[error("Retry limit exceeded after {attempts} attempts: {last_error}")]
70 MaxRetriesExceeded {
71 attempts: u32,
72 last_error: ClassifiedError,
73 },
74
75 #[error("Non-retryable error: {0}")]
76 NonRetryable(ClassifiedError),
77
78 #[error("Circuit breaker open for: {resource}")]
79 CircuitOpen { resource: String },
80
81 #[error("Timeout after {duration_ms}ms")]
82 Timeout { duration_ms: u64 },
83
84 #[error("No fallback available: {0}")]
85 NoFallback(String),
86
87 #[error("{0}")]
88 Other(String),
89}
90
91impl RecoveryError {
92 pub fn is_retryable(&self) -> bool {
93 matches!(self, RecoveryError::Timeout { .. })
94 }
95
96 pub fn last_error(&self) -> Option<&ClassifiedError> {
97 match self {
98 RecoveryError::MaxRetriesExceeded { last_error, .. } => Some(last_error),
99 RecoveryError::NonRetryable(e) => Some(e),
100 _ => None,
101 }
102 }
103}
104
105#[derive(Debug)]
110pub enum RetryFailure<E> {
111 Terminal { attempts: u32, error: E },
113 NonRetryable {
115 attempts: u32,
116 error: E,
117 classified: ClassifiedError,
118 },
119 Exhausted {
121 attempts: u32,
122 error: E,
123 classified: ClassifiedError,
124 },
125}
126
127impl<E> RetryFailure<E> {
128 pub fn attempts(&self) -> u32 {
130 match self {
131 Self::Terminal { attempts, .. }
132 | Self::NonRetryable { attempts, .. }
133 | Self::Exhausted { attempts, .. } => *attempts,
134 }
135 }
136
137 pub fn is_terminal(&self) -> bool {
139 matches!(self, Self::Terminal { .. })
140 }
141
142 pub fn error(&self) -> &E {
144 match self {
145 Self::Terminal { error, .. }
146 | Self::NonRetryable { error, .. }
147 | Self::Exhausted { error, .. } => error,
148 }
149 }
150
151 pub fn classified(&self) -> Option<&ClassifiedError> {
153 match self {
154 Self::Terminal { .. } => None,
155 Self::NonRetryable { classified, .. } | Self::Exhausted { classified, .. } => {
156 Some(classified)
157 }
158 }
159 }
160
161 pub fn into_error(self) -> E {
163 match self {
164 Self::Terminal { error, .. }
165 | Self::NonRetryable { error, .. }
166 | Self::Exhausted { error, .. } => error,
167 }
168 }
169}
170
171impl<E: fmt::Display> fmt::Display for RetryFailure<E> {
172 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
173 match self {
174 Self::Terminal { attempts, error } => {
175 write!(
176 formatter,
177 "Terminal error after {attempts} attempt(s): {error}"
178 )
179 }
180 Self::NonRetryable {
181 attempts, error, ..
182 } => write!(
183 formatter,
184 "Non-retryable error after {attempts} attempt(s): {error}"
185 ),
186 Self::Exhausted {
187 attempts, error, ..
188 } => write!(
189 formatter,
190 "Retry limit exceeded after {attempts} attempt(s): {error}"
191 ),
192 }
193 }
194}
195
196impl<E: std::error::Error + 'static> std::error::Error for RetryFailure<E> {}
197
198pub trait IntoClassifiedError {
199 fn classify(self) -> ClassifiedError;
200}
201
202impl IntoClassifiedError for ClassifiedError {
203 fn classify(self) -> ClassifiedError {
204 self
205 }
206}
207
208impl IntoClassifiedError for ai_agents_llm::LLMError {
209 fn classify(self) -> ClassifiedError {
210 classify_llm_error(&self)
211 }
212}
213
214pub fn classify_llm_error(error: &ai_agents_llm::LLMError) -> ClassifiedError {
216 match error {
217 ai_agents_llm::LLMError::RateLimit { .. } => ClassifiedError::rate_limit(error.to_string()),
218 ai_agents_llm::LLMError::Network(_) => ClassifiedError::connection(error.to_string()),
219 ai_agents_llm::LLMError::API { status, .. } => {
220 if let Some(code) = status {
221 if *code >= 500 {
222 return ClassifiedError::server(error.to_string());
223 }
224 if *code == 401 || *code == 403 {
225 return ClassifiedError::invalid_api_key(error.to_string());
226 }
227 }
228 ClassifiedError::new(ErrorType::InvalidRequest, error.to_string())
229 }
230 ai_agents_llm::LLMError::Config(_) => {
231 ClassifiedError::new(ErrorType::InvalidRequest, error.to_string())
232 }
233 _ => ClassifiedError::new(ErrorType::InvalidResponse, error.to_string()),
234 }
235}
236
237impl IntoClassifiedError for ai_agents_core::AgentError {
238 fn classify(self) -> ClassifiedError {
239 match &self {
240 ai_agents_core::AgentError::Tool(msg) => ClassifiedError::tool_error(msg),
241 ai_agents_core::AgentError::LLM(msg) | ai_agents_core::AgentError::LLMError(msg) => {
242 if msg.to_lowercase().contains("timeout") {
243 ClassifiedError::timeout(msg)
244 } else if msg.to_lowercase().contains("rate") {
245 ClassifiedError::rate_limit(msg)
246 } else {
247 ClassifiedError::new(ErrorType::InvalidResponse, msg)
248 }
249 }
250 _ => ClassifiedError::new(ErrorType::InvalidRequest, self.to_string()),
251 }
252 }
253}
254
255#[cfg(test)]
256mod tests {
257 use super::*;
258
259 #[test]
260 fn test_classified_error() {
261 let err = ClassifiedError::timeout("request timed out");
262 assert!(err.retryable);
263 assert_eq!(err.error_type, ErrorType::Timeout);
264 }
265
266 #[test]
267 fn test_non_retryable() {
268 let err = ClassifiedError::invalid_api_key("bad key");
269 assert!(!err.retryable);
270 }
271}