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 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 Self::Llm(_) | Self::LlmApi { .. } | Self::LlmStream(_) => ErrorKind::ModelOverloaded,
138 _ => ErrorKind::Internal,
140 }
141 }
142}
143
144impl 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#[derive(Debug, Clone, PartialEq, Eq)]
164pub enum ErrorKind {
165 ToolCallFailed { tool_name: String },
167 ToolNotFound,
169 ToolArgsInvalid,
171 ToolTimeout,
173 ModelOverloaded,
175 RateLimited,
177 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 #[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 #[test]
314 fn from_impls_work_with_try_operator() -> AgentResult<()> {
315 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 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}