Skip to main content

simple_agent_type/
error.rs

1//! Error types for SimpleAgents.
2//!
3//! Comprehensive error hierarchy for all failure modes.
4
5use std::time::Duration;
6use thiserror::Error;
7
8/// Main error type for SimpleAgents operations.
9#[derive(Error, Debug)]
10pub enum SimpleAgentsError {
11    /// Provider-specific error
12    #[error("Provider error: {0}")]
13    Provider(#[from] ProviderError),
14
15    /// Healing/coercion error
16    #[error("Healing error: {0}")]
17    Healing(#[from] HealingError),
18
19    /// Network error
20    #[error("Network error: {0}")]
21    Network(String),
22
23    /// Configuration error
24    #[error("Configuration error: {0}")]
25    Config(String),
26
27    /// Validation error
28    #[error("Validation error: {0}")]
29    Validation(#[from] ValidationError),
30
31    /// Cache error
32    #[error("Cache error: {0}")]
33    Cache(String),
34
35    /// Routing error
36    #[error("Routing error: {0}")]
37    Routing(String),
38
39    /// Serialization error
40    #[error("Serialization error: {0}")]
41    Serialization(#[from] serde_json::Error),
42}
43
44/// Result type alias using SimpleAgentsError.
45pub type Result<T> = std::result::Result<T, SimpleAgentsError>;
46
47/// Provider-specific errors.
48#[derive(Error, Debug, Clone)]
49pub enum ProviderError {
50    /// Rate limit exceeded
51    #[error("Rate limit exceeded (retry after {retry_after:?})")]
52    RateLimit {
53        /// Optional duration to wait before retrying
54        retry_after: Option<Duration>,
55    },
56
57    /// Invalid API key
58    #[error("Invalid API key")]
59    InvalidApiKey,
60
61    /// Model not found
62    #[error("Model not found: {0}")]
63    ModelNotFound(String),
64
65    /// Request timeout
66    #[error("Timeout after {0:?}")]
67    Timeout(Duration),
68
69    /// Server error (5xx)
70    #[error("Server error: {0}")]
71    ServerError(String),
72
73    /// Bad request (4xx)
74    #[error("Bad request: {0}")]
75    BadRequest(String),
76
77    /// Unsupported feature
78    #[error("Unsupported feature: {0}")]
79    UnsupportedFeature(String),
80
81    /// Invalid response format
82    #[error("Invalid response format: {0}")]
83    InvalidResponse(String),
84}
85
86impl ProviderError {
87    /// Check if this error is retryable.
88    ///
89    /// # Example
90    /// ```
91    /// use simple_agent_type::error::ProviderError;
92    /// use std::time::Duration;
93    ///
94    /// let err = ProviderError::RateLimit { retry_after: None };
95    /// assert!(err.is_retryable());
96    ///
97    /// let err = ProviderError::InvalidApiKey;
98    /// assert!(!err.is_retryable());
99    /// ```
100    pub fn is_retryable(&self) -> bool {
101        matches!(
102            self,
103            Self::RateLimit { .. } | Self::Timeout(_) | Self::ServerError(_)
104        )
105    }
106}
107
108/// Healing and coercion errors.
109#[derive(Error, Debug, Clone)]
110pub enum HealingError {
111    /// JSON parsing failed
112    #[error("Failed to parse JSON: {error_message}")]
113    ParseFailed {
114        /// Error message
115        error_message: String,
116        /// Input that failed to parse
117        input: String,
118    },
119
120    /// Type coercion failed
121    #[error("Type coercion failed: cannot convert {from} to {to}")]
122    CoercionFailed {
123        /// Source type
124        from: String,
125        /// Target type
126        to: String,
127    },
128
129    /// Missing required field
130    #[error("Missing required field: {field}")]
131    MissingField {
132        /// Field name
133        field: String,
134    },
135
136    /// Confidence below threshold
137    #[error("Confidence {confidence} below threshold {threshold}")]
138    LowConfidence {
139        /// Actual confidence score
140        confidence: f32,
141        /// Required threshold
142        threshold: f32,
143    },
144
145    /// Invalid JSON structure
146    #[error("Invalid JSON structure: {0}")]
147    InvalidStructure(String),
148
149    /// Exceeded maximum healing attempts
150    #[error("Exceeded maximum healing attempts ({0})")]
151    MaxAttemptsExceeded(u32),
152
153    /// Coercion not allowed by configuration
154    #[error("Coercion from {from} to {to} not allowed by configuration")]
155    CoercionNotAllowed {
156        /// Source type
157        from: String,
158        /// Target type
159        to: String,
160    },
161
162    /// Parse error (specific type conversion)
163    #[error("Failed to parse '{input}' as {expected_type}")]
164    ParseError {
165        /// Input that failed to parse
166        input: String,
167        /// Expected type
168        expected_type: String,
169    },
170
171    /// Type mismatch
172    #[error("Type mismatch: expected {expected}, found {found}")]
173    TypeMismatch {
174        /// Expected type
175        expected: String,
176        /// Found type
177        found: String,
178    },
179
180    /// No matching variant in union
181    #[error("No matching variant in union for value: {value}")]
182    NoMatchingVariant {
183        /// Value that didn't match any variant
184        value: serde_json::Value,
185    },
186}
187
188/// Validation errors.
189#[derive(Error, Debug, Clone)]
190pub enum ValidationError {
191    /// Empty field
192    #[error("Field cannot be empty: {field}")]
193    Empty {
194        /// Field name
195        field: String,
196    },
197
198    /// Value too short
199    #[error("Field too short: {field} (minimum: {min})")]
200    TooShort {
201        /// Field name
202        field: String,
203        /// Minimum length
204        min: usize,
205    },
206
207    /// Value too long
208    #[error("Field too long: {field} (maximum: {max})")]
209    TooLong {
210        /// Field name
211        field: String,
212        /// Maximum length
213        max: usize,
214    },
215
216    /// Value out of range
217    #[error("Value out of range: {field} (must be {min}-{max})")]
218    OutOfRange {
219        /// Field name
220        field: String,
221        /// Minimum value
222        min: f32,
223        /// Maximum value
224        max: f32,
225    },
226
227    /// Invalid format
228    #[error("Invalid format: {field} ({reason})")]
229    InvalidFormat {
230        /// Field name
231        field: String,
232        /// Reason for rejection
233        reason: String,
234    },
235
236    /// Generic validation error
237    #[error("{0}")]
238    Custom(String),
239}
240
241impl ValidationError {
242    /// Create a custom validation error.
243    pub fn new(msg: impl Into<String>) -> Self {
244        Self::Custom(msg.into())
245    }
246}
247
248#[cfg(test)]
249mod tests {
250    use super::*;
251
252    #[test]
253    fn test_provider_error_retryable() {
254        assert!(ProviderError::RateLimit { retry_after: None }.is_retryable());
255        assert!(ProviderError::Timeout(Duration::from_secs(30)).is_retryable());
256        assert!(ProviderError::ServerError("500".to_string()).is_retryable());
257
258        assert!(!ProviderError::InvalidApiKey.is_retryable());
259        assert!(!ProviderError::ModelNotFound("gpt-5".to_string()).is_retryable());
260        assert!(!ProviderError::BadRequest("invalid".to_string()).is_retryable());
261    }
262
263    #[test]
264    fn test_error_conversion() {
265        let validation_err = ValidationError::new("test");
266        let agents_err: SimpleAgentsError = validation_err.into();
267        assert!(matches!(agents_err, SimpleAgentsError::Validation(_)));
268
269        let provider_err = ProviderError::InvalidApiKey;
270        let agents_err: SimpleAgentsError = provider_err.into();
271        assert!(matches!(agents_err, SimpleAgentsError::Provider(_)));
272    }
273
274    #[test]
275    fn test_error_display() {
276        let err = ProviderError::RateLimit {
277            retry_after: Some(Duration::from_secs(60)),
278        };
279        let display = format!("{}", err);
280        assert!(display.contains("Rate limit"));
281        assert!(display.contains("60s"));
282
283        let err = ValidationError::Empty {
284            field: "model".to_string(),
285        };
286        let display = format!("{}", err);
287        assert!(display.contains("model"));
288        assert!(display.contains("empty"));
289    }
290
291    #[test]
292    fn test_healing_error_types() {
293        let err = HealingError::ParseFailed {
294            error_message: "unexpected token".to_string(),
295            input: "{invalid}".to_string(),
296        };
297        assert!(format!("{}", err).contains("parse"));
298
299        let err = HealingError::CoercionFailed {
300            from: "string".to_string(),
301            to: "number".to_string(),
302        };
303        assert!(format!("{}", err).contains("coercion"));
304    }
305}