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