llm_agent_runtime/
error.rs1#[non_exhaustive]
18#[derive(Debug, thiserror::Error)]
19pub enum AgentRuntimeError {
20 #[error("Memory operation failed: {0}")]
22 Memory(String),
23
24 #[error("Graph operation failed: {0}")]
26 Graph(String),
27
28 #[error("Orchestration failed: {0}")]
30 Orchestration(String),
31
32 #[error("Agent loop error: {0}")]
34 AgentLoop(String),
35
36 #[error("Runtime not configured: missing '{0}'")]
38 NotConfigured(&'static str),
39
40 #[error("Circuit breaker open for '{service}'")]
42 CircuitOpen {
43 service: String,
45 },
46
47 #[error("Backpressure threshold exceeded: queue depth {depth}/{capacity}")]
49 BackpressureShed {
50 depth: usize,
52 capacity: usize,
54 },
55
56 #[error("Deduplication key collision: {key}")]
58 DeduplicationConflict {
59 key: String,
61 },
62
63 #[error("Provider error: {0}")]
65 Provider(String),
66
67 #[error("Persistence error: {0}")]
69 Persistence(String),
70
71 #[error("Validation failed for field '{field}': [{code}] {message}")]
73 Validation {
74 field: String,
76 code: String,
78 message: String,
80 },
81}
82
83impl AgentRuntimeError {
84 pub fn is_circuit_open(&self) -> bool {
86 matches!(self, Self::CircuitOpen { .. })
87 }
88
89 pub fn is_backpressure(&self) -> bool {
91 matches!(self, Self::BackpressureShed { .. })
92 }
93
94 pub fn is_provider(&self) -> bool {
96 matches!(self, Self::Provider(_))
97 }
98
99 pub fn is_validation(&self) -> bool {
101 matches!(self, Self::Validation { .. })
102 }
103
104 pub fn is_memory(&self) -> bool {
106 matches!(self, Self::Memory(_))
107 }
108
109 pub fn is_graph(&self) -> bool {
111 matches!(self, Self::Graph(_))
112 }
113
114 pub fn is_agent_loop(&self) -> bool {
116 matches!(self, Self::AgentLoop(_))
117 }
118
119 pub fn is_orchestration(&self) -> bool {
121 matches!(self, Self::Orchestration(_))
122 }
123
124 pub fn is_persistence(&self) -> bool {
126 matches!(self, Self::Persistence(_))
127 }
128
129 pub fn is_not_configured(&self) -> bool {
131 matches!(self, Self::NotConfigured(_))
132 }
133
134 pub fn is_deduplication_conflict(&self) -> bool {
136 matches!(self, Self::DeduplicationConflict { .. })
137 }
138
139 pub fn is_retryable(&self) -> bool {
146 matches!(self, Self::Provider(_) | Self::Persistence(_))
147 }
148
149 pub fn message(&self) -> String {
156 match self {
157 Self::Memory(s)
158 | Self::Graph(s)
159 | Self::Orchestration(s)
160 | Self::AgentLoop(s)
161 | Self::Provider(s)
162 | Self::Persistence(s) => s.clone(),
163 Self::NotConfigured(s) => s.to_string(),
164 Self::CircuitOpen { service } => format!("circuit open for '{service}'"),
165 Self::BackpressureShed { depth, capacity } => {
166 format!("backpressure: queue depth {depth}/{capacity}")
167 }
168 Self::DeduplicationConflict { key } => format!("dedup conflict: {key}"),
169 Self::Validation { field, code, message } => {
170 format!("[{code}] {field}: {message}")
171 }
172 }
173 }
174}
175
176impl From<serde_json::Error> for AgentRuntimeError {
177 fn from(e: serde_json::Error) -> Self {
178 AgentRuntimeError::AgentLoop(format!("JSON error: {e}"))
179 }
180}
181
182impl From<std::io::Error> for AgentRuntimeError {
183 fn from(e: std::io::Error) -> Self {
184 AgentRuntimeError::Persistence(format!("I/O error: {e}"))
185 }
186}
187
188impl From<Box<dyn std::error::Error + Send + Sync>> for AgentRuntimeError {
189 fn from(e: Box<dyn std::error::Error + Send + Sync>) -> Self {
195 AgentRuntimeError::AgentLoop(e.to_string())
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn test_memory_error_display() {
205 let e = AgentRuntimeError::Memory("store full".into());
206 assert_eq!(e.to_string(), "Memory operation failed: store full");
207 }
208
209 #[test]
210 fn test_graph_error_display() {
211 let e = AgentRuntimeError::Graph("entity not found".into());
212 assert_eq!(e.to_string(), "Graph operation failed: entity not found");
213 }
214
215 #[test]
216 fn test_orchestration_error_display() {
217 let e = AgentRuntimeError::Orchestration("pipeline stalled".into());
218 assert_eq!(e.to_string(), "Orchestration failed: pipeline stalled");
219 }
220
221 #[test]
222 fn test_agent_loop_error_display() {
223 let e = AgentRuntimeError::AgentLoop("max iterations".into());
224 assert_eq!(e.to_string(), "Agent loop error: max iterations");
225 }
226
227 #[test]
228 fn test_not_configured_error_display() {
229 let e = AgentRuntimeError::NotConfigured("memory");
230 assert_eq!(e.to_string(), "Runtime not configured: missing 'memory'");
231 }
232
233 #[test]
234 fn test_circuit_open_error_display() {
235 let e = AgentRuntimeError::CircuitOpen {
236 service: "llm-api".into(),
237 };
238 assert_eq!(e.to_string(), "Circuit breaker open for 'llm-api'");
239 }
240
241 #[test]
242 fn test_backpressure_shed_error_display() {
243 let e = AgentRuntimeError::BackpressureShed {
244 depth: 100,
245 capacity: 100,
246 };
247 assert_eq!(
248 e.to_string(),
249 "Backpressure threshold exceeded: queue depth 100/100"
250 );
251 }
252
253 #[test]
254 fn test_deduplication_conflict_display() {
255 let e = AgentRuntimeError::DeduplicationConflict {
256 key: "abc123".into(),
257 };
258 assert_eq!(e.to_string(), "Deduplication key collision: abc123");
259 }
260
261 #[test]
262 fn test_error_is_send_sync() {
263 fn assert_send_sync<T: Send + Sync>() {}
264 assert_send_sync::<AgentRuntimeError>();
265 }
266
267 #[test]
268 fn test_error_debug_format() {
269 let e = AgentRuntimeError::Memory("test".into());
270 let debug = format!("{:?}", e);
271 assert!(debug.contains("Memory"));
272 }
273
274 #[test]
275 fn test_validation_error_display() {
276 let e = AgentRuntimeError::Validation {
277 field: "n".into(),
278 code: "out_of_range".into(),
279 message: "n must be between 1 and 100".into(),
280 };
281 assert_eq!(
282 e.to_string(),
283 "Validation failed for field 'n': [out_of_range] n must be between 1 and 100"
284 );
285 }
286
287 #[test]
288 fn test_is_circuit_open_true() {
289 let e = AgentRuntimeError::CircuitOpen { service: "svc".into() };
290 assert!(e.is_circuit_open());
291 assert!(!e.is_backpressure());
292 assert!(!e.is_provider());
293 assert!(!e.is_validation());
294 assert!(!e.is_memory());
295 assert!(!e.is_graph());
296 }
297
298 #[test]
299 fn test_is_backpressure_true() {
300 let e = AgentRuntimeError::BackpressureShed { depth: 5, capacity: 5 };
301 assert!(e.is_backpressure());
302 assert!(!e.is_circuit_open());
303 }
304
305 #[test]
306 fn test_is_provider_true() {
307 let e = AgentRuntimeError::Provider("timeout".into());
308 assert!(e.is_provider());
309 assert!(!e.is_memory());
310 }
311
312 #[test]
313 fn test_is_validation_true() {
314 let e = AgentRuntimeError::Validation {
315 field: "x".into(),
316 code: "bad".into(),
317 message: "msg".into(),
318 };
319 assert!(e.is_validation());
320 assert!(!e.is_graph());
321 }
322
323 #[test]
324 fn test_is_memory_true() {
325 let e = AgentRuntimeError::Memory("oom".into());
326 assert!(e.is_memory());
327 assert!(!e.is_validation());
328 }
329
330 #[test]
331 fn test_is_graph_true() {
332 let e = AgentRuntimeError::Graph("no such entity".into());
333 assert!(e.is_graph());
334 assert!(!e.is_memory());
335 }
336
337 #[test]
340 fn test_is_persistence_true() {
341 let e = AgentRuntimeError::Persistence("disk full".into());
342 assert!(e.is_persistence());
343 assert!(!e.is_memory());
344 }
345
346 #[test]
347 fn test_is_not_configured_true() {
348 let e = AgentRuntimeError::NotConfigured("graph");
349 assert!(e.is_not_configured());
350 assert!(!e.is_persistence());
351 }
352
353 #[test]
354 fn test_is_deduplication_conflict_true() {
355 let e = AgentRuntimeError::DeduplicationConflict { key: "req-1".into() };
356 assert!(e.is_deduplication_conflict());
357 assert!(!e.is_circuit_open());
358 }
359
360 #[test]
361 fn test_is_retryable_true_for_provider() {
362 let e = AgentRuntimeError::Provider("503".into());
363 assert!(e.is_retryable());
364 }
365
366 #[test]
367 fn test_is_retryable_true_for_persistence() {
368 let e = AgentRuntimeError::Persistence("io error".into());
369 assert!(e.is_retryable());
370 }
371
372 #[test]
373 fn test_is_retryable_false_for_logic_errors() {
374 assert!(!AgentRuntimeError::Memory("x".into()).is_retryable());
375 assert!(!AgentRuntimeError::Graph("x".into()).is_retryable());
376 assert!(!AgentRuntimeError::Orchestration("x".into()).is_retryable());
377 assert!(!AgentRuntimeError::CircuitOpen { service: "s".into() }.is_retryable());
378 }
379
380 #[test]
381 fn test_from_serde_json_error() {
382 let json_err = serde_json::from_str::<serde_json::Value>("{invalid}").unwrap_err();
383 let e = AgentRuntimeError::from(json_err);
384 assert!(matches!(e, AgentRuntimeError::AgentLoop(_)));
385 }
386
387 #[test]
388 fn test_provider_error_display() {
389 let e = AgentRuntimeError::Provider("rate limited".into());
390 assert!(e.to_string().contains("rate limited"));
391 }
392
393 #[test]
394 fn test_persistence_error_display() {
395 let e = AgentRuntimeError::Persistence("file not found".into());
396 assert!(e.to_string().contains("file not found"));
397 }
398
399 #[test]
402 fn test_is_agent_loop_true_for_agent_loop_variant() {
403 let e = AgentRuntimeError::AgentLoop("step failed".into());
404 assert!(e.is_agent_loop());
405 }
406
407 #[test]
408 fn test_is_agent_loop_false_for_other_variants() {
409 let e = AgentRuntimeError::Memory("oom".into());
410 assert!(!e.is_agent_loop());
411 }
412
413 #[test]
414 fn test_is_orchestration_true_for_orchestration_variant() {
415 let e = AgentRuntimeError::Orchestration("pipeline stalled".into());
416 assert!(e.is_orchestration());
417 }
418
419 #[test]
420 fn test_is_orchestration_false_for_other_variants() {
421 let e = AgentRuntimeError::Graph("cycle".into());
422 assert!(!e.is_orchestration());
423 }
424
425 #[test]
428 fn test_from_boxed_error_produces_agent_loop_variant() {
429 let boxed: Box<dyn std::error::Error + Send + Sync> =
430 Box::new(std::io::Error::new(std::io::ErrorKind::Other, "generic failure"));
431 let e = AgentRuntimeError::from(boxed);
432 assert!(matches!(e, AgentRuntimeError::AgentLoop(_)));
433 assert!(e.to_string().contains("generic failure"));
434 }
435
436 #[test]
437 fn test_from_boxed_error_preserves_message() {
438 let boxed: Box<dyn std::error::Error + Send + Sync> =
439 "custom error message".parse::<i32>().unwrap_err().into();
440 let e = AgentRuntimeError::from(boxed);
441 assert!(e.is_agent_loop());
442 }
443
444 #[test]
447 fn test_message_returns_inner_string_for_memory_variant() {
448 let e = AgentRuntimeError::Memory("store full".into());
449 assert_eq!(e.message(), "store full");
450 }
451
452 #[test]
453 fn test_message_returns_inner_string_for_provider_variant() {
454 let e = AgentRuntimeError::Provider("timeout".into());
455 assert_eq!(e.message(), "timeout");
456 }
457
458 #[test]
459 fn test_message_returns_structured_text_for_circuit_open() {
460 let e = AgentRuntimeError::CircuitOpen { service: "llm".into() };
461 assert!(e.message().contains("llm"));
462 }
463
464 #[test]
465 fn test_message_returns_structured_text_for_validation() {
466 let e = AgentRuntimeError::Validation {
467 field: "n".into(),
468 code: "range".into(),
469 message: "must be positive".into(),
470 };
471 let msg = e.message();
472 assert!(msg.contains("n") && msg.contains("must be positive"));
473 }
474
475 #[test]
476 fn test_message_returns_structured_text_for_backpressure() {
477 let e = AgentRuntimeError::BackpressureShed { depth: 10, capacity: 10 };
478 let msg = e.message();
479 assert!(msg.contains("10"));
480 }
481}