Skip to main content

agent_base/types/
error.rs

1use thiserror::Error;
2
3#[derive(Debug, Error)]
4pub enum AgentError {
5    #[error("LLM call failed: {0}")]
6    Llm(String),
7
8    #[error("LLM API error: {message}")]
9    LlmApi { message: String },
10
11    #[error("LLM rate limit exceeded")]
12    RateLimitExceeded,
13
14    #[error("LLM service unavailable: {0}")]
15    ServiceUnavailable(String),
16
17    #[error("SSE stream error: {0}")]
18    LlmStream(String),
19
20    #[error("JSON parse error: {0}")]
21    Json(String),
22
23    #[error("Tool '{name}' not registered")]
24    ToolNotFound { name: String },
25
26    #[error("Tool '{name}' argument parsing failed: {raw}")]
27    ToolArgsInvalid { name: String, raw: String },
28
29    #[error("Tool '{name}' execution failed: {source}")]
30    ToolExecution {
31        name: String,
32        #[source]
33        source: Box<AgentError>,
34    },
35
36    #[error("Tool timeout exceeded")]
37    ToolTimeout,
38
39    #[error("Tool call rejected by approval: {tool_name}")]
40    ApprovalDenied { tool_name: String },
41
42    #[error("Session {0} not found")]
43    SessionNotFound(u64),
44
45    #[error("Max turns ({limit}) reached, stopping forcibly")]
46    MaxTurnsExceeded { limit: u32 },
47
48    #[error("Operation cancelled")]
49    Cancelled,
50
51    #[error("Resource unavailable: {0}")]
52    ResourceUnavailable(String),
53
54    #[error("Configuration error: {0}")]
55    ConfigError(String),
56
57    #[error("Internal error: {0}")]
58    Internal(String),
59}
60
61impl AgentError {
62    pub fn llm(message: impl Into<String>) -> Self {
63        Self::Llm(message.into())
64    }
65
66    pub fn json(message: impl Into<String>) -> Self {
67        Self::Json(message.into())
68    }
69
70    pub fn internal(message: impl Into<String>) -> Self {
71        Self::Internal(message.into())
72    }
73
74    pub fn tool_not_found(name: impl Into<String>) -> Self {
75        Self::ToolNotFound { name: name.into() }
76    }
77
78    pub fn session_not_found(id: u64) -> Self {
79        Self::SessionNotFound(id)
80    }
81
82    pub fn tool_timeout() -> Self {
83        Self::ToolTimeout
84    }
85
86    pub fn rate_limit_exceeded() -> Self {
87        Self::RateLimitExceeded
88    }
89
90    pub fn service_unavailable(message: impl Into<String>) -> Self {
91        Self::ServiceUnavailable(message.into())
92    }
93
94    pub fn resource_unavailable(message: impl Into<String>) -> Self {
95        Self::ResourceUnavailable(message.into())
96    }
97
98    pub fn config_error(message: impl Into<String>) -> Self {
99        Self::ConfigError(message.into())
100    }
101
102    pub fn is_cancelled(&self) -> bool {
103        matches!(self, Self::Cancelled)
104    }
105
106    pub fn is_retryable(&self) -> bool {
107        matches!(
108            self,
109            Self::Llm(_)
110                | Self::LlmApi { .. }
111                | Self::LlmStream(_)
112                | Self::ServiceUnavailable(_)
113                | Self::RateLimitExceeded
114        )
115    }
116
117    pub fn is_rate_limited(&self) -> bool {
118        matches!(self, Self::RateLimitExceeded)
119    }
120
121    pub fn is_resource_unavailable(&self) -> bool {
122        matches!(self, Self::ResourceUnavailable(_))
123    }
124
125    /// Classify this error into an `ErrorKind` for recovery decisions.
126    pub fn kind(&self) -> ErrorKind {
127        match self {
128            Self::ToolExecution { name, .. } => ErrorKind::ToolCallFailed {
129                tool_name: name.clone(),
130            },
131            Self::ToolNotFound { .. } => ErrorKind::ToolNotFound,
132            Self::ToolArgsInvalid { .. } => ErrorKind::ToolArgsInvalid,
133            Self::ToolTimeout => ErrorKind::ToolTimeout,
134            Self::ServiceUnavailable(_) => ErrorKind::ModelOverloaded,
135            Self::RateLimitExceeded => ErrorKind::RateLimited,
136            // Llm, LlmApi, LlmStream → overloaded (transient LLM failures)
137            Self::Llm(_) | Self::LlmApi { .. } | Self::LlmStream(_) => ErrorKind::ModelOverloaded,
138            // Everything else → internal
139            _ => ErrorKind::Internal,
140        }
141    }
142}
143
144// ── Convenience From impls for common error types ──
145
146impl From<std::io::Error> for AgentError {
147    fn from(e: std::io::Error) -> Self {
148        AgentError::internal(e.to_string())
149    }
150}
151
152impl From<serde_json::Error> for AgentError {
153    fn from(e: serde_json::Error) -> Self {
154        AgentError::json(e.to_string())
155    }
156}
157
158/// Classifies an `AgentError` into a broad category for recovery decisions.
159///
160/// Unlike `AgentError` which carries full context (messages, nested errors, etc.),
161/// `ErrorKind` is a lightweight discriminant that lets `RecoveryPolicy` and other
162/// decision-makers branch without pattern-matching on every `AgentError` variant.
163#[derive(Debug, Clone, PartialEq, Eq)]
164pub enum ErrorKind {
165    /// A tool call failed during execution.
166    ToolCallFailed { tool_name: String },
167    /// The requested tool was not found in the registry.
168    ToolNotFound,
169    /// Tool arguments were invalid.
170    ToolArgsInvalid,
171    /// Tool execution timed out.
172    ToolTimeout,
173    /// The model/LLM service is overloaded (e.g. 529, 503).
174    ModelOverloaded,
175    /// Rate limit was exceeded.
176    RateLimited,
177    /// Catch-all for errors that don't fit a specific category.
178    Internal,
179}
180
181impl std::fmt::Display for ErrorKind {
182    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
183        match self {
184            Self::ToolCallFailed { tool_name } => write!(f, "tool call failed: {tool_name}"),
185            Self::ToolNotFound => write!(f, "tool not found"),
186            Self::ToolArgsInvalid => write!(f, "tool args invalid"),
187            Self::ToolTimeout => write!(f, "tool timeout"),
188            Self::ModelOverloaded => write!(f, "model overloaded"),
189            Self::RateLimited => write!(f, "rate limited"),
190            Self::Internal => write!(f, "internal error"),
191        }
192    }
193}
194
195pub type AgentResult<T> = Result<T, AgentError>;
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200
201    #[test]
202    fn kind_tool_execution() {
203        let err = AgentError::ToolExecution {
204            name: "my_tool".to_string(),
205            source: Box::new(AgentError::internal("boom")),
206        };
207        assert_eq!(
208            err.kind(),
209            ErrorKind::ToolCallFailed {
210                tool_name: "my_tool".to_string()
211            }
212        );
213    }
214
215    #[test]
216    fn kind_tool_not_found() {
217        let err = AgentError::tool_not_found("missing");
218        assert_eq!(err.kind(), ErrorKind::ToolNotFound);
219    }
220
221    #[test]
222    fn kind_tool_args_invalid() {
223        let err = AgentError::ToolArgsInvalid {
224            name: "t".to_string(),
225            raw: "bad".to_string(),
226        };
227        assert_eq!(err.kind(), ErrorKind::ToolArgsInvalid);
228    }
229
230    #[test]
231    fn kind_tool_timeout() {
232        let err = AgentError::tool_timeout();
233        assert_eq!(err.kind(), ErrorKind::ToolTimeout);
234    }
235
236    #[test]
237    fn kind_service_unavailable() {
238        let err = AgentError::service_unavailable("overloaded");
239        assert_eq!(err.kind(), ErrorKind::ModelOverloaded);
240    }
241
242    #[test]
243    fn kind_rate_limit() {
244        let err = AgentError::rate_limit_exceeded();
245        assert_eq!(err.kind(), ErrorKind::RateLimited);
246    }
247
248    #[test]
249    fn kind_llm_maps_to_overloaded() {
250        let err = AgentError::llm("connection refused");
251        assert_eq!(err.kind(), ErrorKind::ModelOverloaded);
252    }
253
254    #[test]
255    fn kind_llm_api_maps_to_overloaded() {
256        let err = AgentError::LlmApi {
257            message: "529".to_string(),
258        };
259        assert_eq!(err.kind(), ErrorKind::ModelOverloaded);
260    }
261
262    #[test]
263    fn kind_llm_stream_maps_to_overloaded() {
264        let err = AgentError::LlmStream("stream broken".to_string());
265        assert_eq!(err.kind(), ErrorKind::ModelOverloaded);
266    }
267
268    #[test]
269    fn kind_internal_fallback() {
270        let err = AgentError::internal("something");
271        assert_eq!(err.kind(), ErrorKind::Internal);
272
273        let err = AgentError::Cancelled;
274        assert_eq!(err.kind(), ErrorKind::Internal);
275
276        let err = AgentError::config_error("bad config");
277        assert_eq!(err.kind(), ErrorKind::Internal);
278    }
279
280    #[test]
281    fn error_kind_display() {
282        assert_eq!(
283            ErrorKind::ToolCallFailed {
284                tool_name: "t".to_string()
285            }
286            .to_string(),
287            "tool call failed: t"
288        );
289        assert_eq!(ErrorKind::ToolNotFound.to_string(), "tool not found");
290        assert_eq!(ErrorKind::ModelOverloaded.to_string(), "model overloaded");
291        assert_eq!(ErrorKind::RateLimited.to_string(), "rate limited");
292    }
293
294    // ── From impls ──
295
296    #[test]
297    fn from_io_error_maps_to_internal() {
298        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
299        let agent_err: AgentError = io_err.into();
300        assert!(matches!(agent_err, AgentError::Internal(_)));
301        assert!(agent_err.to_string().contains("file missing"));
302    }
303
304    #[test]
305    fn from_serde_json_error_maps_to_json() {
306        let json_err = serde_json::from_str::<serde_json::Value>("not json").unwrap_err();
307        let agent_err: AgentError = json_err.into();
308        assert!(matches!(agent_err, AgentError::Json(_)));
309    }
310
311    /// Verify `?` works: a function returning AgentResult<T> can use `?`
312    /// on io::Error and serde_json::Error through the From impls.
313    #[test]
314    fn from_impls_work_with_try_operator() -> AgentResult<()> {
315        // io::Error via ?
316        fn read_file() -> AgentResult<String> {
317            let _ = std::fs::read_to_string("/nonexistent/path")?;
318            unreachable!()
319        }
320        assert!(read_file().is_err());
321
322        // serde_json::Error via ?
323        fn parse_json() -> AgentResult<serde_json::Value> {
324            let v: serde_json::Value = serde_json::from_str("bad json")?;
325            Ok(v)
326        }
327        assert!(parse_json().is_err());
328
329        Ok(())
330    }
331}