Skip to main content

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}