1use std::fmt;
4use thiserror::Error;
5
6#[derive(Debug, Error)]
20pub enum LellmError {
21 #[error("LLM error: {0}")]
22 Llm(#[from] LlmError),
23
24 #[error("Tool error: {0}")]
25 Tool(#[from] ToolError),
26
27 #[error("Memory error: {0}")]
28 Memory(#[from] MemoryError),
29
30 #[error("Parse error: {0}")]
31 Parse(#[from] ParseError),
32}
33
34#[derive(Debug, Error, Clone)]
46pub enum LlmError {
47 #[error("invalid request: {message}")]
48 InvalidRequest { message: String },
49
50 #[error("unsupported feature: {feature}")]
51 UnsupportedFeature { feature: String },
52
53 #[error("duplicate system prompt: both config and conversation contain system message")]
54 DuplicateSystemPrompt,
55
56 #[error("network error: {detail}")]
57 Network { detail: String },
58
59 #[error("request timeout: {detail}")]
60 Timeout { detail: String },
61
62 #[error("provider error [{provider}]: {message}")]
63 Provider {
64 provider: String,
65 status: Option<u16>,
66 code: Option<String>,
67 message: String,
68 },
69
70 #[error("response parse error: {detail}")]
71 Parse { detail: String },
72
73 #[error("unexpected EOF: stream ended without ResponseComplete")]
74 UnexpectedEof,
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum ToolErrorKind {
82 NotFound,
84 ToolUnavailable,
86 Timeout,
88 Network,
90 PermissionDenied,
92 InvalidInput,
94 RateLimited,
96 LoopDetected,
98 Internal,
100 External { source: &'static str },
104}
105
106impl ToolErrorKind {
107 pub fn is_retryable(self) -> bool {
121 matches!(
122 self,
123 Self::Timeout | Self::Network | Self::RateLimited | Self::ToolUnavailable
124 )
125 }
126}
127
128impl fmt::Display for ToolErrorKind {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 match self {
131 Self::NotFound => write!(f, "NotFound"),
132 Self::ToolUnavailable => write!(f, "ToolUnavailable"),
133 Self::Timeout => write!(f, "Timeout"),
134 Self::Network => write!(f, "Network"),
135 Self::PermissionDenied => write!(f, "PermissionDenied"),
136 Self::InvalidInput => write!(f, "InvalidInput"),
137 Self::RateLimited => write!(f, "RateLimited"),
138 Self::LoopDetected => write!(f, "LoopDetected"),
139 Self::Internal => write!(f, "Internal"),
140 Self::External { source } => write!(f, "External({})", source),
141 }
142 }
143}
144
145#[derive(Clone)]
147pub struct ToolError {
148 pub kind: ToolErrorKind,
149 pub message: String,
150}
151
152impl ToolError {
153 pub fn invalid_input(msg: impl Into<String>) -> Self {
155 Self {
156 kind: ToolErrorKind::InvalidInput,
157 message: msg.into(),
158 }
159 }
160
161 pub fn not_found(msg: impl Into<String>) -> Self {
163 Self {
164 kind: ToolErrorKind::NotFound,
165 message: msg.into(),
166 }
167 }
168
169 pub fn external<E: std::fmt::Display>(source: E) -> Self {
171 Self {
172 kind: ToolErrorKind::External {
173 source: std::any::type_name::<E>(),
174 },
175 message: source.to_string(),
176 }
177 }
178}
179
180impl fmt::Display for ToolError {
181 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
182 write!(f, "[{}] {}", self.kind, self.message)
183 }
184}
185
186impl fmt::Debug for ToolError {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 write!(f, "ToolError({}: {})", self.kind, self.message)
189 }
190}
191
192impl std::error::Error for ToolError {}
193
194pub type ToolResult = Result<serde_json::Value, ToolError>;
199
200pub trait IntoToolError {
213 fn into_tool_error(self) -> ToolError;
214}
215
216impl IntoToolError for ToolError {
218 fn into_tool_error(self) -> ToolError {
219 self
220 }
221}
222
223impl IntoToolError for std::io::Error {
225 fn into_tool_error(self) -> ToolError {
226 ToolError::external(self)
227 }
228}
229
230impl IntoToolError for serde_json::Error {
232 fn into_tool_error(self) -> ToolError {
233 ToolError {
234 kind: ToolErrorKind::Internal,
235 message: self.to_string(),
236 }
237 }
238}
239
240#[cfg(feature = "anyhow")]
242impl IntoToolError for anyhow::Error {
243 fn into_tool_error(self) -> ToolError {
244 ToolError::external(self)
245 }
246}
247
248pub trait IntoToolResult: Sized {
262 fn into_tool(self) -> ToolResult;
263}
264
265impl IntoToolResult for String {
267 fn into_tool(self) -> ToolResult {
268 Ok(serde_json::Value::String(self))
269 }
270}
271
272impl IntoToolResult for serde_json::Value {
274 fn into_tool(self) -> ToolResult {
275 Ok(self)
276 }
277}
278
279impl<T> IntoToolResult for Option<T>
281where
282 T: serde::Serialize,
283{
284 fn into_tool(self) -> ToolResult {
285 match self {
286 Some(v) => serde_json::to_value(v).map_err(|e| ToolError {
287 kind: ToolErrorKind::Internal,
288 message: format!("failed to serialize tool result: {}", e),
289 }),
290 None => Ok(serde_json::Value::Null),
291 }
292 }
293}
294
295impl<T, E> IntoToolResult for Result<T, E>
300where
301 T: serde::Serialize,
302 E: IntoToolError,
303{
304 fn into_tool(self) -> ToolResult {
305 match self {
306 Ok(v) => serde_json::to_value(v).map_err(|e| ToolError {
307 kind: ToolErrorKind::Internal,
308 message: format!("failed to serialize tool result: {}", e),
309 }),
310 Err(e) => Err(e.into_tool_error()),
311 }
312 }
313}
314
315#[derive(Debug, Error)]
319pub enum MemoryError {
320 #[error("memory IO error: {0}")]
321 IoError(String),
322
323 #[error("memory database error: {0}")]
324 DatabaseError(String),
325}
326
327#[derive(Debug, Error)]
329#[error("parse error: {detail}")]
330pub struct ParseError {
331 pub detail: String,
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_llm_error_display() {
340 let err = LlmError::Timeout {
341 detail: "timed out after 60s".into(),
342 };
343 assert!(format!("{}", err).contains("timeout"));
344 assert!(format!("{}", err).contains("60s"));
345 }
346
347 #[test]
348 fn test_llm_error_provider_display() {
349 let err = LlmError::Provider {
350 provider: "openai".into(),
351 status: Some(429),
352 code: Some("rate_limit".into()),
353 message: "Too many requests".into(),
354 };
355 assert!(format!("{}", err).contains("openai"));
356 assert!(format!("{}", err).contains("Too many requests"));
357 }
358
359 #[test]
360 fn test_llm_error_invalid_request_display() {
361 let err = LlmError::InvalidRequest {
362 message: "Anthropic requires max_tokens".into(),
363 };
364 assert!(format!("{}", err).contains("invalid request"));
365 assert!(format!("{}", err).contains("max_tokens"));
366 }
367
368 #[test]
369 fn test_tool_error_display() {
370 let err = ToolError {
371 kind: ToolErrorKind::NotFound,
372 message: "read_file".into(),
373 };
374 assert!(format!("{}", err).contains("read_file"));
375 }
376
377 #[test]
378 fn test_lellm_error_from_tool_error() {
379 let tool_err = ToolError {
380 kind: ToolErrorKind::Timeout,
381 message: "timeout".into(),
382 };
383 let top_err: LellmError = tool_err.into();
384 assert!(format!("{}", top_err).contains("Tool error"));
385 }
386
387 #[test]
388 fn test_tool_error_is_retryable() {
389 assert!(ToolErrorKind::Timeout.is_retryable());
391 assert!(ToolErrorKind::Network.is_retryable());
392 assert!(ToolErrorKind::RateLimited.is_retryable());
393 assert!(ToolErrorKind::ToolUnavailable.is_retryable());
394
395 assert!(!ToolErrorKind::NotFound.is_retryable());
397 assert!(!ToolErrorKind::InvalidInput.is_retryable());
398 assert!(!ToolErrorKind::PermissionDenied.is_retryable());
399 assert!(!ToolErrorKind::Internal.is_retryable());
400 assert!(!ToolErrorKind::LoopDetected.is_retryable());
401 assert!(!ToolErrorKind::External { source: "test" }.is_retryable());
402 }
403
404 #[test]
405 fn test_into_tool_result_string() {
406 let result: ToolResult = "hello".to_string().into_tool();
407 assert_eq!(result.unwrap(), serde_json::json!("hello"));
408 }
409
410 #[test]
411 fn test_into_tool_result_option() {
412 let some: Option<String> = Some("hello".to_string());
413 assert_eq!(some.into_tool().unwrap(), serde_json::json!("hello"));
414
415 let none: Option<String> = None;
416 assert_eq!(none.into_tool().unwrap(), serde_json::json!(null));
417 }
418
419 #[test]
420 fn test_into_tool_result_external_error() {
421 #[derive(Debug)]
422 struct MyError;
423 impl fmt::Display for MyError {
424 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
425 write!(f, "my error")
426 }
427 }
428 impl IntoToolError for MyError {
430 fn into_tool_error(self) -> ToolError {
431 ToolError::external(self)
432 }
433 }
434
435 let result: ToolResult = Err::<(), MyError>(MyError).into_tool();
436 let err = result.unwrap_err();
437 assert_eq!(
438 err.kind,
439 ToolErrorKind::External {
440 source: std::any::type_name::<MyError>()
441 }
442 );
443 assert_eq!(err.message, "my error");
444 }
445
446 #[test]
447 fn test_into_tool_result_tool_error_passthrough() {
448 let err = ToolError::invalid_input("bad param");
450 let result: ToolResult = Err::<serde_json::Value, ToolError>(err).into_tool();
451 let out_err = result.unwrap_err();
452 assert_eq!(out_err.kind, ToolErrorKind::InvalidInput);
453 assert_eq!(out_err.message, "bad param");
454 }
455
456 #[test]
457 fn test_tool_error_factories() {
458 let err = ToolError::invalid_input("bad input");
459 assert_eq!(err.kind, ToolErrorKind::InvalidInput);
460 assert_eq!(err.message, "bad input");
461
462 let err = ToolError::not_found("search");
463 assert_eq!(err.kind, ToolErrorKind::NotFound);
464 assert_eq!(err.message, "search");
465 }
466}