simple_agent_type/
error.rs1use std::time::Duration;
6use thiserror::Error;
7
8#[derive(Error, Debug)]
10pub enum SimpleAgentsError {
11 #[error("Provider error: {0}")]
13 Provider(#[from] ProviderError),
14
15 #[error("Healing error: {0}")]
17 Healing(#[from] HealingError),
18
19 #[error("Network error: {0}")]
21 Network(String),
22
23 #[error("Configuration error: {0}")]
25 Config(String),
26
27 #[error("Healing is disabled for this client")]
34 HealingDisabled,
35
36 #[error("Validation error: {0}")]
38 Validation(#[from] ValidationError),
39
40 #[error("Serialization error: {0}")]
42 Serialization(#[from] serde_json::Error),
43}
44
45pub type Result<T> = std::result::Result<T, SimpleAgentsError>;
47
48#[derive(Error, Debug, Clone)]
50pub enum ProviderError {
51 #[error("Rate limit exceeded (retry after {retry_after:?})")]
53 RateLimit {
54 retry_after: Option<Duration>,
56 },
57
58 #[error("Invalid API key")]
60 InvalidApiKey,
61
62 #[error("Model not found: {0}")]
64 ModelNotFound(String),
65
66 #[error("Timeout after {0:?}")]
68 Timeout(Duration),
69
70 #[error("Server error: {0}")]
72 ServerError(String),
73
74 #[error("Bad request: {0}")]
76 BadRequest(String),
77
78 #[error("Unsupported feature: {0}")]
80 UnsupportedFeature(String),
81
82 #[error("Invalid response format: {0}")]
84 InvalidResponse(String),
85}
86
87impl ProviderError {
88 pub fn is_retryable(&self) -> bool {
102 matches!(
103 self,
104 Self::RateLimit { .. } | Self::Timeout(_) | Self::ServerError(_)
105 )
106 }
107}
108
109#[derive(Error, Debug, Clone)]
111pub enum HealingError {
112 #[error("Failed to parse JSON: {error_message}")]
114 ParseFailed {
115 error_message: String,
117 input: String,
119 },
120
121 #[error("Type coercion failed: cannot convert {from} to {to}")]
123 CoercionFailed {
124 from: String,
126 to: String,
128 },
129
130 #[error("Missing required field: {field}")]
132 MissingField {
133 field: String,
135 },
136
137 #[error("Confidence {confidence} below threshold {threshold}")]
139 LowConfidence {
140 confidence: f32,
142 threshold: f32,
144 },
145
146 #[error("Invalid JSON structure: {0}")]
148 InvalidStructure(String),
149
150 #[error("Exceeded maximum healing attempts ({0})")]
152 MaxAttemptsExceeded(u32),
153
154 #[error("Coercion from {from} to {to} not allowed by configuration")]
156 CoercionNotAllowed {
157 from: String,
159 to: String,
161 },
162
163 #[error("Failed to parse '{input}' as {expected_type}")]
165 ParseError {
166 input: String,
168 expected_type: String,
170 },
171
172 #[error("Type mismatch: expected {expected}, found {found}")]
174 TypeMismatch {
175 expected: String,
177 found: String,
179 },
180
181 #[error("No matching variant in union for value: {value}")]
183 NoMatchingVariant {
184 value: serde_json::Value,
186 },
187
188 #[error("Truncated JSON is missing required field '{field_name}' — refusing to inject null in strict mode")]
190 TruncatedRequiredField {
191 field_name: String,
193 },
194}
195
196#[derive(Error, Debug, Clone)]
198pub enum ValidationError {
199 #[error("Field cannot be empty: {field}")]
201 Empty {
202 field: String,
204 },
205
206 #[error("Field too short: {field} (minimum: {min})")]
208 TooShort {
209 field: String,
211 min: usize,
213 },
214
215 #[error("Field too long: {field} (maximum: {max})")]
217 TooLong {
218 field: String,
220 max: usize,
222 },
223
224 #[error("Value out of range: {field} (must be {min}-{max})")]
226 OutOfRange {
227 field: String,
229 min: f32,
231 max: f32,
233 },
234
235 #[error("Invalid format: {field} ({reason})")]
237 InvalidFormat {
238 field: String,
240 reason: String,
242 },
243
244 #[error("{0}")]
246 Custom(String),
247}
248
249impl ValidationError {
250 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}