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)]
79pub enum ToolErrorKind {
80 NotFound,
82 Timeout,
84 Network,
86 PermissionDenied,
88 InvalidInput,
90 RateLimited,
92 LoopDetected,
94 Internal,
96}
97
98impl ToolErrorKind {
99 pub fn is_retriable(self) -> bool {
101 matches!(self, Self::Timeout | Self::Network | Self::RateLimited)
102 }
103}
104
105impl fmt::Display for ToolErrorKind {
106 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
107 match self {
108 Self::NotFound => write!(f, "NotFound"),
109 Self::Timeout => write!(f, "Timeout"),
110 Self::Network => write!(f, "Network"),
111 Self::PermissionDenied => write!(f, "PermissionDenied"),
112 Self::InvalidInput => write!(f, "InvalidInput"),
113 Self::RateLimited => write!(f, "RateLimited"),
114 Self::LoopDetected => write!(f, "LoopDetected"),
115 Self::Internal => write!(f, "Internal"),
116 }
117 }
118}
119
120#[derive(Clone)]
122pub struct ToolError {
123 pub kind: ToolErrorKind,
124 pub message: String,
125}
126
127impl fmt::Display for ToolError {
128 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
129 write!(f, "[{}] {}", self.kind, self.message)
130 }
131}
132
133impl fmt::Debug for ToolError {
134 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135 write!(f, "ToolError({}: {})", self.kind, self.message)
136 }
137}
138
139impl std::error::Error for ToolError {}
140
141pub type ToolResult = Result<String, ToolError>;
143
144#[derive(Debug, Error)]
146pub enum MemoryError {
147 #[error("memory IO error: {0}")]
148 IoError(String),
149
150 #[error("memory database error: {0}")]
151 DatabaseError(String),
152}
153
154#[derive(Debug, Error)]
156#[error("parse error: {detail}")]
157pub struct ParseError {
158 pub detail: String,
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn test_llm_error_display() {
167 let err = LlmError::Timeout {
168 detail: "timed out after 60s".into(),
169 };
170 assert!(format!("{}", err).contains("timeout"));
171 assert!(format!("{}", err).contains("60s"));
172 }
173
174 #[test]
175 fn test_llm_error_provider_display() {
176 let err = LlmError::Provider {
177 provider: "openai".into(),
178 status: Some(429),
179 code: Some("rate_limit".into()),
180 message: "Too many requests".into(),
181 };
182 assert!(format!("{}", err).contains("openai"));
183 assert!(format!("{}", err).contains("Too many requests"));
184 }
185
186 #[test]
187 fn test_llm_error_invalid_request_display() {
188 let err = LlmError::InvalidRequest {
189 message: "Anthropic requires max_tokens".into(),
190 };
191 assert!(format!("{}", err).contains("invalid request"));
192 assert!(format!("{}", err).contains("max_tokens"));
193 }
194
195 #[test]
196 fn test_tool_error_display() {
197 let err = ToolError {
198 kind: ToolErrorKind::NotFound,
199 message: "read_file".into(),
200 };
201 assert!(format!("{}", err).contains("read_file"));
202 }
203
204 #[test]
205 fn test_lellm_error_from_tool_error() {
206 let tool_err = ToolError {
207 kind: ToolErrorKind::Timeout,
208 message: "timeout".into(),
209 };
210 let top_err: LellmError = tool_err.into();
211 assert!(format!("{}", top_err).contains("Tool error"));
212 }
213
214 #[test]
215 fn test_tool_error_is_retriable() {
216 assert!(ToolErrorKind::Timeout.is_retriable());
217 assert!(ToolErrorKind::Network.is_retriable());
218 assert!(ToolErrorKind::RateLimited.is_retriable());
219 assert!(!ToolErrorKind::NotFound.is_retriable());
220 assert!(!ToolErrorKind::InvalidInput.is_retriable());
221 }
222}