swarm-engine-core 0.1.6

Core types and orchestration for SwarmEngine
Documentation
//! SwarmEngine 統一エラー型
//!
//! 全ての SwarmEngine エラーを統一的に扱うための型定義。
//! `is_transient()` でリトライ可能かどうかを判定できる。
//!
//! # 設計
//!
//! - Transient エラー: 一時的な問題(ネットワーク、タイムアウト等)→ リトライ可能
//! - Permanent エラー: 恒久的な問題(設定ミス、パース失敗等)→ リトライ不可
//!
//! # Example
//!
//! ```ignore
//! use swarm_engine_core::error::SwarmError;
//!
//! fn process() -> Result<(), SwarmError> {
//!     // LLM 呼び出しが失敗
//!     Err(SwarmError::LlmTransient { message: "Connection timeout".into() })
//! }
//!
//! match process() {
//!     Err(e) if e.is_transient() => {
//!         // リトライ可能
//!     }
//!     Err(e) => {
//!         // リトライ不可
//!     }
//!     Ok(_) => {}
//! }
//! ```

use thiserror::Error;

/// SwarmEngine 統一エラー型
#[derive(Debug, Error)]
pub enum SwarmError {
    // ========================================================================
    // Transient Errors (リトライ可能)
    // ========================================================================
    /// LLM リクエスト失敗(一時的)
    #[error("LLM error (transient): {message}")]
    LlmTransient { message: String },

    /// ネットワークエラー(一時的)
    #[error("Network error (transient): {message}")]
    NetworkTransient { message: String },

    /// タイムアウト
    #[error("Timeout: {message}")]
    Timeout { message: String },

    /// リソース一時的不足
    #[error("Resource busy: {message}")]
    ResourceBusy { message: String },

    // ========================================================================
    // Permanent Errors (リトライ不可)
    // ========================================================================
    /// LLM リクエスト失敗(恒久的)
    #[error("LLM error: {message}")]
    LlmPermanent { message: String },

    /// 設定エラー
    #[error("Configuration error: {message}")]
    Config { message: String },

    /// パースエラー
    #[error("Parse error: {message}")]
    Parse { message: String },

    /// バリデーションエラー
    #[error("Validation error: {message}")]
    Validation { message: String },

    /// 不明なアクション
    #[error("Unknown action: {action}")]
    UnknownAction { action: String },

    /// 必須パラメータ不足
    #[error("Missing parameter: {param}")]
    MissingParam { param: String },

    /// 無効なパラメータ
    #[error("Invalid parameter: {param}")]
    InvalidParam { param: String },

    /// 非同期タスクエラー
    #[error("Async task error: {message}")]
    AsyncTask { message: String },

    /// 内部エラー
    #[error("Internal error: {message}")]
    Internal { message: String },

    // ========================================================================
    // Orchestrator Errors
    // ========================================================================
    /// DependencyGraph が必須だが利用不可
    ///
    /// ExplorationSpace の自動生成には DependencyGraph が必要です。
    /// 以下のいずれかを設定してください:
    /// - `OrchestratorBuilder::dependency_provider(provider)`
    /// - `OrchestratorBuilder::batch_invoker(invoker)` (plan_dependencies をサポート)
    /// - `OrchestratorBuilder::extension(DependencyGraph)` (静的グラフ)
    #[error(
        "DependencyGraph is required but not available. \
             Configure via: dependency_provider(), batch_invoker() with plan_dependencies support, \
             or extension(DependencyGraph). \
             Hint: {hint}"
    )]
    MissingDependencyGraph { hint: String },

    /// Worker が設定されていない
    #[error("No workers configured. Add workers via OrchestratorBuilder::add_worker()")]
    NoWorkers,

    /// Orchestrator の設定が不正
    #[error("Orchestrator configuration error: {message}")]
    OrchestratorConfig { message: String },

    // ========================================================================
    // Wrapped Module Errors
    // ========================================================================
    /// Action バリデーションエラー
    #[error(transparent)]
    ActionValidation(#[from] crate::actions::ActionValidationError),

    /// 依存グラフエラー
    #[error(transparent)]
    DependencyGraph(#[from] crate::exploration::DependencyGraphError),

    /// 設定ファイルエラー
    #[error(transparent)]
    ConfigFile(#[from] crate::config::ConfigError),
}

impl SwarmError {
    // ========================================================================
    // Constructors
    // ========================================================================

    /// LLM 一時的エラー
    pub fn llm_transient(msg: impl Into<String>) -> Self {
        Self::LlmTransient {
            message: msg.into(),
        }
    }

    /// LLM 恒久的エラー
    pub fn llm_permanent(msg: impl Into<String>) -> Self {
        Self::LlmPermanent {
            message: msg.into(),
        }
    }

    /// ネットワーク一時的エラー
    pub fn network_transient(msg: impl Into<String>) -> Self {
        Self::NetworkTransient {
            message: msg.into(),
        }
    }

    /// タイムアウトエラー
    pub fn timeout(msg: impl Into<String>) -> Self {
        Self::Timeout {
            message: msg.into(),
        }
    }

    /// 設定エラー
    pub fn config(msg: impl Into<String>) -> Self {
        Self::Config {
            message: msg.into(),
        }
    }

    /// パースエラー
    pub fn parse(msg: impl Into<String>) -> Self {
        Self::Parse {
            message: msg.into(),
        }
    }

    /// バリデーションエラー
    pub fn validation(msg: impl Into<String>) -> Self {
        Self::Validation {
            message: msg.into(),
        }
    }

    /// 非同期タスクエラー
    pub fn async_task(msg: impl Into<String>) -> Self {
        Self::AsyncTask {
            message: msg.into(),
        }
    }

    /// 内部エラー
    pub fn internal(msg: impl Into<String>) -> Self {
        Self::Internal {
            message: msg.into(),
        }
    }

    // ========================================================================
    // Query Methods
    // ========================================================================

    /// リトライ可能かどうか
    pub fn is_transient(&self) -> bool {
        matches!(
            self,
            Self::LlmTransient { .. }
                | Self::NetworkTransient { .. }
                | Self::Timeout { .. }
                | Self::ResourceBusy { .. }
        )
    }

    /// エラーメッセージを取得
    pub fn message(&self) -> String {
        match self {
            Self::LlmTransient { message } => message.clone(),
            Self::NetworkTransient { message } => message.clone(),
            Self::Timeout { message } => message.clone(),
            Self::ResourceBusy { message } => message.clone(),
            Self::LlmPermanent { message } => message.clone(),
            Self::Config { message } => message.clone(),
            Self::Parse { message } => message.clone(),
            Self::Validation { message } => message.clone(),
            Self::UnknownAction { action } => action.clone(),
            Self::MissingParam { param } => param.clone(),
            Self::InvalidParam { param } => param.clone(),
            Self::AsyncTask { message } => message.clone(),
            Self::Internal { message } => message.clone(),
            Self::MissingDependencyGraph { hint } => hint.clone(),
            Self::NoWorkers => "No workers configured".to_string(),
            Self::OrchestratorConfig { message } => message.clone(),
            Self::ActionValidation(e) => e.to_string(),
            Self::DependencyGraph(e) => e.to_string(),
            Self::ConfigFile(e) => e.to_string(),
        }
    }

    /// エラーの種類を取得
    pub fn kind(&self) -> &'static str {
        match self {
            Self::LlmTransient { .. } => "llm_transient",
            Self::NetworkTransient { .. } => "network_transient",
            Self::Timeout { .. } => "timeout",
            Self::ResourceBusy { .. } => "resource_busy",
            Self::LlmPermanent { .. } => "llm_permanent",
            Self::Config { .. } => "config",
            Self::Parse { .. } => "parse",
            Self::Validation { .. } => "validation",
            Self::UnknownAction { .. } => "unknown_action",
            Self::MissingParam { .. } => "missing_param",
            Self::InvalidParam { .. } => "invalid_param",
            Self::AsyncTask { .. } => "async_task",
            Self::Internal { .. } => "internal",
            Self::MissingDependencyGraph { .. } => "missing_dependency_graph",
            Self::NoWorkers => "no_workers",
            Self::OrchestratorConfig { .. } => "orchestrator_config",
            Self::ActionValidation(_) => "action_validation",
            Self::DependencyGraph(_) => "dependency_graph",
            Self::ConfigFile(_) => "config_file",
        }
    }
}

/// Result type alias for SwarmError
pub type SwarmResult<T> = Result<T, SwarmError>;

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

    #[test]
    fn test_is_transient() {
        assert!(SwarmError::llm_transient("timeout").is_transient());
        assert!(SwarmError::network_transient("connection refused").is_transient());
        assert!(SwarmError::timeout("5s exceeded").is_transient());

        assert!(!SwarmError::llm_permanent("invalid model").is_transient());
        assert!(!SwarmError::config("missing field").is_transient());
        assert!(!SwarmError::parse("invalid json").is_transient());
    }

    #[test]
    fn test_message() {
        let err = SwarmError::config("missing api_key");
        assert_eq!(err.message(), "missing api_key");
    }

    #[test]
    fn test_kind() {
        assert_eq!(SwarmError::llm_transient("x").kind(), "llm_transient");
        assert_eq!(SwarmError::config("x").kind(), "config");
    }

    #[test]
    fn test_display() {
        let err = SwarmError::llm_transient("connection timeout");
        assert_eq!(
            format!("{}", err),
            "LLM error (transient): connection timeout"
        );
    }
}