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