use std::time::Duration;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SimpleAgentsError {
#[error("Provider error: {0}")]
Provider(#[from] ProviderError),
#[error("Healing error: {0}")]
Healing(#[from] HealingError),
#[error("Network error: {0}")]
Network(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Healing is disabled for this client")]
HealingDisabled,
#[error("Validation error: {0}")]
Validation(#[from] ValidationError),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
}
pub type Result<T> = std::result::Result<T, SimpleAgentsError>;
#[derive(Error, Debug, Clone)]
pub enum ProviderError {
#[error("Rate limit exceeded (retry after {retry_after:?})")]
RateLimit {
retry_after: Option<Duration>,
},
#[error("Invalid API key")]
InvalidApiKey,
#[error("Model not found: {0}")]
ModelNotFound(String),
#[error("Timeout after {0:?}")]
Timeout(Duration),
#[error("Server error: {0}")]
ServerError(String),
#[error("Bad request: {0}")]
BadRequest(String),
#[error("Unsupported feature: {0}")]
UnsupportedFeature(String),
#[error("Invalid response format: {0}")]
InvalidResponse(String),
}
impl ProviderError {
pub fn is_retryable(&self) -> bool {
matches!(
self,
Self::RateLimit { .. } | Self::Timeout(_) | Self::ServerError(_)
)
}
}
#[derive(Error, Debug, Clone)]
pub enum HealingError {
#[error("Failed to parse JSON: {error_message}")]
ParseFailed {
error_message: String,
input: String,
},
#[error("Type coercion failed: cannot convert {from} to {to}")]
CoercionFailed {
from: String,
to: String,
},
#[error("Missing required field: {field}")]
MissingField {
field: String,
},
#[error("Confidence {confidence} below threshold {threshold}")]
LowConfidence {
confidence: f32,
threshold: f32,
},
#[error("Invalid JSON structure: {0}")]
InvalidStructure(String),
#[error("Exceeded maximum healing attempts ({0})")]
MaxAttemptsExceeded(u32),
#[error("Coercion from {from} to {to} not allowed by configuration")]
CoercionNotAllowed {
from: String,
to: String,
},
#[error("Failed to parse '{input}' as {expected_type}")]
ParseError {
input: String,
expected_type: String,
},
#[error("Type mismatch: expected {expected}, found {found}")]
TypeMismatch {
expected: String,
found: String,
},
#[error("No matching variant in union for value: {value}")]
NoMatchingVariant {
value: serde_json::Value,
},
#[error("Truncated JSON is missing required field '{field_name}' — refusing to inject null in strict mode")]
TruncatedRequiredField {
field_name: String,
},
}
#[derive(Error, Debug, Clone)]
pub enum ValidationError {
#[error("Field cannot be empty: {field}")]
Empty {
field: String,
},
#[error("Field too short: {field} (minimum: {min})")]
TooShort {
field: String,
min: usize,
},
#[error("Field too long: {field} (maximum: {max})")]
TooLong {
field: String,
max: usize,
},
#[error("Value out of range: {field} (must be {min}-{max})")]
OutOfRange {
field: String,
min: f32,
max: f32,
},
#[error("Invalid format: {field} ({reason})")]
InvalidFormat {
field: String,
reason: String,
},
#[error("{0}")]
Custom(String),
}
impl ValidationError {
pub fn new(msg: impl Into<String>) -> Self {
Self::Custom(msg.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provider_error_retryable() {
assert!(ProviderError::RateLimit { retry_after: None }.is_retryable());
assert!(ProviderError::Timeout(Duration::from_secs(30)).is_retryable());
assert!(ProviderError::ServerError("500".to_string()).is_retryable());
assert!(!ProviderError::InvalidApiKey.is_retryable());
assert!(!ProviderError::ModelNotFound("gpt-5".to_string()).is_retryable());
assert!(!ProviderError::BadRequest("invalid".to_string()).is_retryable());
}
#[test]
fn test_error_conversion() {
let validation_err = ValidationError::new("test");
let agents_err: SimpleAgentsError = validation_err.into();
assert!(matches!(agents_err, SimpleAgentsError::Validation(_)));
let provider_err = ProviderError::InvalidApiKey;
let agents_err: SimpleAgentsError = provider_err.into();
assert!(matches!(agents_err, SimpleAgentsError::Provider(_)));
}
#[test]
fn test_error_display() {
let err = ProviderError::RateLimit {
retry_after: Some(Duration::from_secs(60)),
};
let display = format!("{}", err);
assert!(display.contains("Rate limit"));
assert!(display.contains("60s"));
let err = ValidationError::Empty {
field: "model".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("model"));
assert!(display.contains("empty"));
}
#[test]
fn test_healing_error_types() {
let err = HealingError::ParseFailed {
error_message: "unexpected token".to_string(),
input: "{invalid}".to_string(),
};
assert!(format!("{}", err).contains("parse"));
let err = HealingError::CoercionFailed {
from: "string".to_string(),
to: "number".to_string(),
};
assert!(format!("{}", err).contains("coercion"));
}
}