xz-agent-core 0.10.0

Agent engine abstraction layer — traits, types, and a minimalist CoreEngine loop
Documentation
//! Error types for the xz-agent engine.
//!
//! Defines the single error type used throughout the engine,
//! with retry classification for error recovery.

use thiserror::Error;

/// Errors that can occur during agent engine operation.
#[derive(Error, Debug)]
pub enum EngineError {
    /// The Thinker failed to produce a response.
    ///
    /// Defaults to retryable (transient provider failures are common).
    /// Callers may inspect the message for permanent failures.
    #[error("thinker failed: {0}")]
    ThinkFailed(String),

    /// The output processor failed during processing.
    ///
    /// Not retryable by default — usually indicates invalid input or
    /// a deterministic execution failure.
    #[error("output processor failed: {0}")]
    ProcessFailed(String),

    /// The context builder failed during processing.
    ///
    /// Not retryable by default — usually indicates broken state or
    /// configuration.
    #[error("context builder failed: {0}")]
    ContextFailed(String),

    /// Engine construction or configuration failed (e.g. missing thinker).
    ///
    /// Not retryable — fix the configuration before rebuilding.
    #[error("engine configuration error: {0}")]
    Config(String),

    /// The engine was cancelled by the user or system.
    #[error("engine cancelled")]
    Cancelled,
}

impl EngineError {
    /// Returns `true` if the operation can be safely retried.
    ///
    /// Classification is conservative for structural failures and optimistic
    /// for thinker/cancellation paths:
    ///
    /// | Variant | Retryable |
    /// |---------|-----------|
    /// | [`Cancelled`](Self::Cancelled) | yes |
    /// | [`ThinkFailed`](Self::ThinkFailed) | yes (default; may be permanent) |
    /// | [`ContextFailed`](Self::ContextFailed) | no |
    /// | [`ProcessFailed`](Self::ProcessFailed) | no |
    /// | [`Config`](Self::Config) | no |
    pub fn is_retryable(&self) -> bool {
        match self {
            EngineError::Cancelled => true,
            EngineError::ThinkFailed(_) => true,
            EngineError::ContextFailed(_) => false,
            EngineError::ProcessFailed(_) => false,
            EngineError::Config(_) => false,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_cancelled_is_retryable() {
        assert!(EngineError::Cancelled.is_retryable());
    }

    #[test]
    fn test_think_failed_is_retryable() {
        assert!(EngineError::ThinkFailed("rate limited".into()).is_retryable());
    }

    #[test]
    fn test_process_failed_not_retryable() {
        assert!(!EngineError::ProcessFailed("invalid input".into()).is_retryable());
    }

    #[test]
    fn test_context_failed_not_retryable() {
        assert!(!EngineError::ContextFailed("broken".into()).is_retryable());
    }

    #[test]
    fn test_config_not_retryable() {
        assert!(!EngineError::Config("thinker is required".into()).is_retryable());
    }

    #[test]
    fn test_debug_display() {
        let e = EngineError::ThinkFailed("timeout".into());
        assert_eq!(format!("{}", e), "thinker failed: timeout");
    }
}