1use serde::Serialize;
8use thiserror::Error;
9
10#[derive(Debug, Error, Clone, Serialize)]
15pub enum ProviderError {
16 #[error("HTTP {status}: {message}")]
18 Http { status: u16, message: String },
19 #[error("network error: {0}")]
21 Network(String),
22 #[error("API error: {0}")]
24 Api(String),
25 #[error("timeout")]
27 Timeout,
28 #[error("stream ended unexpectedly")]
30 StreamEnded,
31 #[error("invalid response: {0}")]
33 InvalidResponse(String),
34 #[error("{0}")]
36 Other(String),
37}
38
39#[derive(Debug, Error, Clone, Serialize)]
41pub enum AgentError {
42 #[error("provider error: {0}")]
44 Provider(ProviderError),
45 #[error("configuration error: {0}")]
47 Config(String),
48 #[error("validation error: {0}")]
50 Validation(String),
51 #[error("permission denied: {0}")]
53 PermissionDenied(String),
54 #[error("tool not found: {0}")]
56 ToolNotFound(String),
57 #[error("tool `{tool}` failed: {message}")]
59 ToolExecution { tool: String, message: String },
60 #[error("maximum tool iterations reached: {0}")]
62 MaxIterations(usize),
63 #[error("provider retries exhausted after {attempts} attempts: {last_error}")]
65 ProviderRetriesExhausted { attempts: usize, last_error: String },
66 #[error("session is already running a turn")]
68 SessionBusy,
69 #[error("cancelled")]
71 Cancelled,
72}
73
74impl AgentError {
75 pub fn is_retryable(&self) -> bool {
80 match self {
81 AgentError::Provider(err) => match err {
82 ProviderError::Http { status, .. } => {
83 *status == 429 || (500..=599).contains(status)
84 }
85 ProviderError::Network(_) | ProviderError::Timeout => true,
86 _ => false,
87 },
88 _ => false,
89 }
90 }
91
92 pub fn is_context_too_long(&self) -> bool {
99 match self {
100 AgentError::Provider(ProviderError::Http { status, message }) if *status == 400 => {
101 let lower = message.to_lowercase();
102 lower.contains("context")
103 || lower.contains("token")
104 || lower.contains("too long")
105 || lower.contains("too large")
106 || lower.contains("exceeds")
107 || lower.contains("maximum")
108 || lower.contains("length")
109 }
110 AgentError::Provider(ProviderError::Api(message)) => {
111 let lower = message.to_lowercase();
112 lower.contains("context") || lower.contains("token") || lower.contains("too long")
113 }
114 _ => false,
115 }
116 }
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122
123 #[test]
124 fn http_429_is_retryable() {
125 let err = AgentError::Provider(ProviderError::Http {
126 status: 429,
127 message: "rate limited".into(),
128 });
129 assert!(err.is_retryable());
130 }
131
132 #[test]
133 fn http_5xx_is_retryable() {
134 for status in [500, 502, 503, 504] {
135 let err = AgentError::Provider(ProviderError::Http {
136 status,
137 message: "server error".into(),
138 });
139 assert!(err.is_retryable(), "status {status} should be retryable");
140 }
141 }
142
143 #[test]
144 fn http_4xx_other_than_429_is_not_retryable() {
145 for status in [400, 401, 403, 404, 422] {
146 let err = AgentError::Provider(ProviderError::Http {
147 status,
148 message: "client error".into(),
149 });
150 assert!(!err.is_retryable(), "status {status} should not be retryable");
151 }
152 }
153
154 #[test]
155 fn network_and_timeout_errors_are_retryable() {
156 assert!(AgentError::Provider(ProviderError::Network("reset".into())).is_retryable());
157 assert!(AgentError::Provider(ProviderError::Timeout).is_retryable());
158 }
159
160 #[test]
161 fn api_invalid_response_stream_ended_are_not_retryable() {
162 assert!(!AgentError::Provider(ProviderError::Api("invalid key".into())).is_retryable());
163 assert!(
164 !AgentError::Provider(ProviderError::InvalidResponse("bad json".into())).is_retryable()
165 );
166 assert!(!AgentError::Provider(ProviderError::StreamEnded).is_retryable());
167 }
168
169 #[test]
170 fn non_provider_errors_are_not_retryable() {
171 assert!(!AgentError::Config("bad".into()).is_retryable());
172 assert!(!AgentError::Validation("bad".into()).is_retryable());
173 assert!(!AgentError::PermissionDenied("no".into()).is_retryable());
174 assert!(!AgentError::ToolNotFound("x".into()).is_retryable());
175 }
176}