1use std::time::Duration;
4use thiserror::Error;
5
6fn llm_error_display(message: &str, status: &Option<u16>) -> String {
7 match status {
8 Some(code) => format!("LLM error ({code}): {message}"),
9 None => format!("LLM error: {message}"),
10 }
11}
12
13fn execution_failed_display(message: &str, exit_code: &Option<i32>) -> String {
14 match exit_code {
15 Some(code) => format!("Tool execution failed: {message} (exit code: {code})"),
16 None => format!("Tool execution failed: {message}"),
17 }
18}
19
20#[derive(Debug, Error)]
22pub enum ToolError {
23 #[error("Tool timeout after {}s", .0.as_secs())]
24 Timeout(Duration),
25
26 #[error("Invalid parameters: {0}")]
27 InvalidParams(String),
28
29 #[error("Tool not found: {0}")]
30 NotFound(String),
31
32 #[error("{}", execution_failed_display(.message, .exit_code))]
33 ExecutionFailed {
34 message: String,
35 exit_code: Option<i32>,
36 },
37
38 #[error("Tool execution cancelled")]
39 Cancelled,
40}
41
42impl ToolError {
43 pub fn is_retryable(&self) -> bool {
45 matches!(self, Self::Timeout(_))
46 }
47}
48
49#[derive(Debug, Error)]
51pub enum CoreError {
52 #[error("{}", llm_error_display(.message, .status))]
53 Llm {
54 message: String,
55 status: Option<u16>,
56 },
57
58 #[error("Tool error: {0}")]
59 Tool(#[from] ToolError),
60
61 #[error("Embedding error: {message}")]
62 Embedding { message: String },
63
64 #[error("Context store error: {message}")]
65 ContextStore { message: String },
66
67 #[error("Event bus error: {message}")]
68 EventBus { message: String },
69}
70
71impl CoreError {
72 pub fn is_retryable(&self) -> bool {
74 match self {
75 Self::Llm { status, .. } => {
76 matches!(status, None | Some(429 | 500..))
78 }
79 Self::Tool(err) => err.is_retryable(),
80 Self::Embedding { .. } | Self::ContextStore { .. } | Self::EventBus { .. } => false,
81 }
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use std::time::Duration;
89
90 #[test]
93 fn llm_error_displays_message_with_status() {
94 let err = CoreError::Llm {
95 message: "rate limited".into(),
96 status: Some(429),
97 };
98 assert_eq!(err.to_string(), "LLM error (429): rate limited");
99 }
100
101 #[test]
102 fn llm_error_displays_message_without_status() {
103 let err = CoreError::Llm {
104 message: "connection failed".into(),
105 status: None,
106 };
107 assert_eq!(err.to_string(), "LLM error: connection failed");
108 }
109
110 #[test]
113 fn tool_error_timeout_displays_duration() {
114 let err = ToolError::Timeout(Duration::from_secs(30));
115 assert_eq!(err.to_string(), "Tool timeout after 30s");
116 }
117
118 #[test]
119 fn tool_error_invalid_params_displays_message() {
120 let err = ToolError::InvalidParams("missing 'command' field".into());
121 assert_eq!(
122 err.to_string(),
123 "Invalid parameters: missing 'command' field"
124 );
125 }
126
127 #[test]
128 fn tool_error_not_found_displays_name() {
129 let err = ToolError::NotFound("calculator".into());
130 assert_eq!(err.to_string(), "Tool not found: calculator");
131 }
132
133 #[test]
134 fn tool_error_execution_failed_displays_details() {
135 let err = ToolError::ExecutionFailed {
136 message: "command failed".into(),
137 exit_code: Some(1),
138 };
139 assert!(err.to_string().contains("command failed"));
140 assert!(err.to_string().contains("exit code: 1"));
141 }
142
143 #[test]
146 fn tool_error_timeout_is_retryable() {
147 let err = ToolError::Timeout(Duration::from_secs(30));
148 assert!(err.is_retryable());
149 }
150
151 #[test]
152 fn tool_error_invalid_params_is_not_retryable() {
153 let err = ToolError::InvalidParams("bad json".into());
154 assert!(!err.is_retryable());
155 }
156
157 #[test]
158 fn tool_error_not_found_is_not_retryable() {
159 let err = ToolError::NotFound("unknown".into());
160 assert!(!err.is_retryable());
161 }
162
163 #[test]
164 fn tool_error_cancelled_is_not_retryable() {
165 let err = ToolError::Cancelled;
166 assert!(!err.is_retryable());
167 }
168
169 #[test]
170 fn core_error_llm_429_is_retryable() {
171 let err = CoreError::Llm {
172 message: "rate limited".into(),
173 status: Some(429),
174 };
175 assert!(err.is_retryable());
176 }
177
178 #[test]
179 fn core_error_llm_500_is_retryable() {
180 let err = CoreError::Llm {
181 message: "server error".into(),
182 status: Some(500),
183 };
184 assert!(err.is_retryable());
185 }
186
187 #[test]
188 fn core_error_llm_400_is_not_retryable() {
189 let err = CoreError::Llm {
190 message: "bad request".into(),
191 status: Some(400),
192 };
193 assert!(!err.is_retryable());
194 }
195
196 #[test]
197 fn core_error_llm_no_status_is_retryable() {
198 let err = CoreError::Llm {
200 message: "connection reset".into(),
201 status: None,
202 };
203 assert!(err.is_retryable());
204 }
205
206 #[test]
209 fn core_error_from_tool_error() {
210 let tool_err = ToolError::NotFound("test".into());
211 let core_err: CoreError = tool_err.into();
212 assert!(matches!(core_err, CoreError::Tool(_)));
213 }
214
215 #[test]
218 fn core_error_embedding_displays_message() {
219 let err = CoreError::Embedding {
220 message: "model load failed".into(),
221 };
222 assert_eq!(err.to_string(), "Embedding error: model load failed");
223 }
224
225 #[test]
226 fn core_error_embedding_is_not_retryable() {
227 let err = CoreError::Embedding {
228 message: "dimension mismatch".into(),
229 };
230 assert!(!err.is_retryable());
231 }
232
233 #[test]
236 fn core_error_event_bus_displays_message() {
237 let err = CoreError::EventBus {
238 message: "source failed".into(),
239 };
240 assert_eq!(err.to_string(), "Event bus error: source failed");
241 }
242
243 #[test]
244 fn core_error_event_bus_is_not_retryable() {
245 let err = CoreError::EventBus {
246 message: "channel closed".into(),
247 };
248 assert!(!err.is_retryable());
249 }
250
251 #[test]
254 fn core_error_context_store_displays_message() {
255 let err = CoreError::ContextStore {
256 message: "table not found".into(),
257 };
258 assert_eq!(err.to_string(), "Context store error: table not found");
259 }
260
261 #[test]
262 fn core_error_context_store_is_not_retryable() {
263 let err = CoreError::ContextStore {
264 message: "storage failure".into(),
265 };
266 assert!(!err.is_retryable());
267 }
268}