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
105pub trait IntoClassifiedError {
106 fn classify(self) -> ClassifiedError;
107}
108
109impl IntoClassifiedError for ClassifiedError {
110 fn classify(self) -> ClassifiedError {
111 self
112 }
113}
114
115impl IntoClassifiedError for ai_agents_llm::LLMError {
116 fn classify(self) -> ClassifiedError {
117 match &self {
118 ai_agents_llm::LLMError::RateLimit { .. } => {
119 ClassifiedError::rate_limit(self.to_string())
120 }
121 ai_agents_llm::LLMError::Network(_) => ClassifiedError::connection(self.to_string()),
122 ai_agents_llm::LLMError::API { status, .. } => {
123 if let Some(code) = status {
124 if *code >= 500 {
125 return ClassifiedError::server(self.to_string());
126 }
127 if *code == 401 || *code == 403 {
128 return ClassifiedError::invalid_api_key(self.to_string());
129 }
130 }
131 ClassifiedError::new(ErrorType::InvalidRequest, self.to_string())
132 }
133 ai_agents_llm::LLMError::Config(_) => {
134 ClassifiedError::new(ErrorType::InvalidRequest, self.to_string())
135 }
136 _ => ClassifiedError::new(ErrorType::InvalidResponse, self.to_string()),
137 }
138 }
139}
140
141impl IntoClassifiedError for ai_agents_core::AgentError {
142 fn classify(self) -> ClassifiedError {
143 match &self {
144 ai_agents_core::AgentError::Tool(msg) => ClassifiedError::tool_error(msg),
145 ai_agents_core::AgentError::LLM(msg) | ai_agents_core::AgentError::LLMError(msg) => {
146 if msg.to_lowercase().contains("timeout") {
147 ClassifiedError::timeout(msg)
148 } else if msg.to_lowercase().contains("rate") {
149 ClassifiedError::rate_limit(msg)
150 } else {
151 ClassifiedError::new(ErrorType::InvalidResponse, msg)
152 }
153 }
154 _ => ClassifiedError::new(ErrorType::InvalidRequest, self.to_string()),
155 }
156 }
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn test_classified_error() {
165 let err = ClassifiedError::timeout("request timed out");
166 assert!(err.retryable);
167 assert_eq!(err.error_type, ErrorType::Timeout);
168 }
169
170 #[test]
171 fn test_non_retryable() {
172 let err = ClassifiedError::invalid_api_key("bad key");
173 assert!(!err.retryable);
174 }
175}