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