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/// Classifies an `AgentError` into a broad category for recovery decisions.
145///
146/// Unlike `AgentError` which carries full context (messages, nested errors, etc.),
147/// `ErrorKind` is a lightweight discriminant that lets `RecoveryPolicy` and other
148/// decision-makers branch without pattern-matching on every `AgentError` variant.
149#[derive(Debug, Clone, PartialEq, Eq)]
150pub enum ErrorKind {
151    /// A tool call failed during execution.
152    ToolCallFailed { tool_name: String },
153    /// The requested tool was not found in the registry.
154    ToolNotFound,
155    /// Tool arguments were invalid.
156    ToolArgsInvalid,
157    /// Tool execution timed out.
158    ToolTimeout,
159    /// The model/LLM service is overloaded (e.g. 529, 503).
160    ModelOverloaded,
161    /// Rate limit was exceeded.
162    RateLimited,
163    /// Catch-all for errors that don't fit a specific category.
164    Internal,
165}
166
167impl std::fmt::Display for ErrorKind {
168    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
169        match self {
170            Self::ToolCallFailed { tool_name } => write!(f, "tool call failed: {tool_name}"),
171            Self::ToolNotFound => write!(f, "tool not found"),
172            Self::ToolArgsInvalid => write!(f, "tool args invalid"),
173            Self::ToolTimeout => write!(f, "tool timeout"),
174            Self::ModelOverloaded => write!(f, "model overloaded"),
175            Self::RateLimited => write!(f, "rate limited"),
176            Self::Internal => write!(f, "internal error"),
177        }
178    }
179}
180
181pub type AgentResult<T> = Result<T, AgentError>;
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186
187    #[test]
188    fn kind_tool_execution() {
189        let err = AgentError::ToolExecution {
190            name: "my_tool".to_string(),
191            source: Box::new(AgentError::internal("boom")),
192        };
193        assert_eq!(
194            err.kind(),
195            ErrorKind::ToolCallFailed {
196                tool_name: "my_tool".to_string()
197            }
198        );
199    }
200
201    #[test]
202    fn kind_tool_not_found() {
203        let err = AgentError::tool_not_found("missing");
204        assert_eq!(err.kind(), ErrorKind::ToolNotFound);
205    }
206
207    #[test]
208    fn kind_tool_args_invalid() {
209        let err = AgentError::ToolArgsInvalid {
210            name: "t".to_string(),
211            raw: "bad".to_string(),
212        };
213        assert_eq!(err.kind(), ErrorKind::ToolArgsInvalid);
214    }
215
216    #[test]
217    fn kind_tool_timeout() {
218        let err = AgentError::tool_timeout();
219        assert_eq!(err.kind(), ErrorKind::ToolTimeout);
220    }
221
222    #[test]
223    fn kind_service_unavailable() {
224        let err = AgentError::service_unavailable("overloaded");
225        assert_eq!(err.kind(), ErrorKind::ModelOverloaded);
226    }
227
228    #[test]
229    fn kind_rate_limit() {
230        let err = AgentError::rate_limit_exceeded();
231        assert_eq!(err.kind(), ErrorKind::RateLimited);
232    }
233
234    #[test]
235    fn kind_llm_maps_to_overloaded() {
236        let err = AgentError::llm("connection refused");
237        assert_eq!(err.kind(), ErrorKind::ModelOverloaded);
238    }
239
240    #[test]
241    fn kind_llm_api_maps_to_overloaded() {
242        let err = AgentError::LlmApi {
243            message: "529".to_string(),
244        };
245        assert_eq!(err.kind(), ErrorKind::ModelOverloaded);
246    }
247
248    #[test]
249    fn kind_llm_stream_maps_to_overloaded() {
250        let err = AgentError::LlmStream("stream broken".to_string());
251        assert_eq!(err.kind(), ErrorKind::ModelOverloaded);
252    }
253
254    #[test]
255    fn kind_internal_fallback() {
256        let err = AgentError::internal("something");
257        assert_eq!(err.kind(), ErrorKind::Internal);
258
259        let err = AgentError::Cancelled;
260        assert_eq!(err.kind(), ErrorKind::Internal);
261
262        let err = AgentError::config_error("bad config");
263        assert_eq!(err.kind(), ErrorKind::Internal);
264    }
265
266    #[test]
267    fn error_kind_display() {
268        assert_eq!(
269            ErrorKind::ToolCallFailed {
270                tool_name: "t".to_string()
271            }
272            .to_string(),
273            "tool call failed: t"
274        );
275        assert_eq!(ErrorKind::ToolNotFound.to_string(), "tool not found");
276        assert_eq!(ErrorKind::ModelOverloaded.to_string(), "model overloaded");
277        assert_eq!(ErrorKind::RateLimited.to_string(), "rate limited");
278    }
279}