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