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 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 Self::Llm(_) | Self::LlmApi { .. } | Self::LlmStream(_) => ErrorKind::ModelOverloaded,
148 _ => ErrorKind::Internal,
150 }
151 }
152}
153
154impl 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#[derive(Debug, Clone, PartialEq, Eq)]
195pub enum ErrorKind {
196 ToolCallFailed { tool_name: String },
198 ToolNotFound,
200 ToolArgsInvalid,
202 ToolTimeout,
204 ModelOverloaded,
206 RateLimited,
208 Cancelled,
210 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 #[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 #[test]
350 fn from_impls_work_with_try_operator() -> AgentResult<()> {
351 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 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}