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 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 Self::Llm(_) | Self::LlmApi { .. } | Self::LlmStream(_) => ErrorKind::ModelOverloaded,
145 _ => ErrorKind::Internal,
147 }
148 }
149}
150
151impl 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#[derive(Debug, Clone, PartialEq, Eq)]
171pub enum ErrorKind {
172 ToolCallFailed { tool_name: String },
174 ToolNotFound,
176 ToolArgsInvalid,
178 ToolTimeout,
180 ModelOverloaded,
182 RateLimited,
184 Cancelled,
186 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 #[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 #[test]
326 fn from_impls_work_with_try_operator() -> AgentResult<()> {
327 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 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}