xz_agent_core/error.rs
1//! Error types for the xz-agent engine.
2//!
3//! Defines the single error type used throughout the engine,
4//! with retry classification for error recovery.
5
6use thiserror::Error;
7
8/// Errors that can occur during agent engine operation.
9#[derive(Error, Debug)]
10pub enum EngineError {
11 /// The Thinker failed to produce a response.
12 ///
13 /// Defaults to retryable (transient provider failures are common).
14 /// Callers may inspect the message for permanent failures.
15 #[error("thinker failed: {0}")]
16 ThinkFailed(String),
17
18 /// The output processor failed during processing.
19 ///
20 /// Not retryable by default — usually indicates invalid input or
21 /// a deterministic execution failure.
22 #[error("output processor failed: {0}")]
23 ProcessFailed(String),
24
25 /// The context builder failed during processing.
26 ///
27 /// Not retryable by default — usually indicates broken state or
28 /// configuration.
29 #[error("context builder failed: {0}")]
30 ContextFailed(String),
31
32 /// Engine construction or configuration failed (e.g. missing thinker).
33 ///
34 /// Not retryable — fix the configuration before rebuilding.
35 #[error("engine configuration error: {0}")]
36 Config(String),
37
38 /// The engine was cancelled by the user or system.
39 #[error("engine cancelled")]
40 Cancelled,
41}
42
43impl EngineError {
44 /// Returns `true` if the operation can be safely retried.
45 ///
46 /// Classification is conservative for structural failures and optimistic
47 /// for thinker/cancellation paths:
48 ///
49 /// | Variant | Retryable |
50 /// |---------|-----------|
51 /// | [`Cancelled`](Self::Cancelled) | yes |
52 /// | [`ThinkFailed`](Self::ThinkFailed) | yes (default; may be permanent) |
53 /// | [`ContextFailed`](Self::ContextFailed) | no |
54 /// | [`ProcessFailed`](Self::ProcessFailed) | no |
55 /// | [`Config`](Self::Config) | no |
56 pub fn is_retryable(&self) -> bool {
57 match self {
58 EngineError::Cancelled => true,
59 EngineError::ThinkFailed(_) => true,
60 EngineError::ContextFailed(_) => false,
61 EngineError::ProcessFailed(_) => false,
62 EngineError::Config(_) => false,
63 }
64 }
65}
66
67#[cfg(test)]
68mod tests {
69 use super::*;
70
71 #[test]
72 fn test_cancelled_is_retryable() {
73 assert!(EngineError::Cancelled.is_retryable());
74 }
75
76 #[test]
77 fn test_think_failed_is_retryable() {
78 assert!(EngineError::ThinkFailed("rate limited".into()).is_retryable());
79 }
80
81 #[test]
82 fn test_process_failed_not_retryable() {
83 assert!(!EngineError::ProcessFailed("invalid input".into()).is_retryable());
84 }
85
86 #[test]
87 fn test_context_failed_not_retryable() {
88 assert!(!EngineError::ContextFailed("broken".into()).is_retryable());
89 }
90
91 #[test]
92 fn test_config_not_retryable() {
93 assert!(!EngineError::Config("thinker is required".into()).is_retryable());
94 }
95
96 #[test]
97 fn test_debug_display() {
98 let e = EngineError::ThinkFailed("timeout".into());
99 assert_eq!(format!("{}", e), "thinker failed: timeout");
100 }
101}