Skip to main content

ai_agents_recovery/
error.rs

1//! Recovery error types
2
3use 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}