Skip to main content

swarm_engine_core/
error.rs

1//! SwarmEngine 統一エラー型
2//!
3//! 全ての SwarmEngine エラーを統一的に扱うための型定義。
4//! `is_transient()` でリトライ可能かどうかを判定できる。
5//!
6//! # 設計
7//!
8//! - Transient エラー: 一時的な問題(ネットワーク、タイムアウト等)→ リトライ可能
9//! - Permanent エラー: 恒久的な問題(設定ミス、パース失敗等)→ リトライ不可
10//!
11//! # Example
12//!
13//! ```ignore
14//! use swarm_engine_core::error::SwarmError;
15//!
16//! fn process() -> Result<(), SwarmError> {
17//!     // LLM 呼び出しが失敗
18//!     Err(SwarmError::LlmTransient { message: "Connection timeout".into() })
19//! }
20//!
21//! match process() {
22//!     Err(e) if e.is_transient() => {
23//!         // リトライ可能
24//!     }
25//!     Err(e) => {
26//!         // リトライ不可
27//!     }
28//!     Ok(_) => {}
29//! }
30//! ```
31
32use thiserror::Error;
33
34/// SwarmEngine 統一エラー型
35#[derive(Debug, Error)]
36pub enum SwarmError {
37    // ========================================================================
38    // Transient Errors (リトライ可能)
39    // ========================================================================
40    /// LLM リクエスト失敗(一時的)
41    #[error("LLM error (transient): {message}")]
42    LlmTransient { message: String },
43
44    /// ネットワークエラー(一時的)
45    #[error("Network error (transient): {message}")]
46    NetworkTransient { message: String },
47
48    /// タイムアウト
49    #[error("Timeout: {message}")]
50    Timeout { message: String },
51
52    /// リソース一時的不足
53    #[error("Resource busy: {message}")]
54    ResourceBusy { message: String },
55
56    // ========================================================================
57    // Permanent Errors (リトライ不可)
58    // ========================================================================
59    /// LLM リクエスト失敗(恒久的)
60    #[error("LLM error: {message}")]
61    LlmPermanent { message: String },
62
63    /// 設定エラー
64    #[error("Configuration error: {message}")]
65    Config { message: String },
66
67    /// パースエラー
68    #[error("Parse error: {message}")]
69    Parse { message: String },
70
71    /// バリデーションエラー
72    #[error("Validation error: {message}")]
73    Validation { message: String },
74
75    /// 不明なアクション
76    #[error("Unknown action: {action}")]
77    UnknownAction { action: String },
78
79    /// 必須パラメータ不足
80    #[error("Missing parameter: {param}")]
81    MissingParam { param: String },
82
83    /// 無効なパラメータ
84    #[error("Invalid parameter: {param}")]
85    InvalidParam { param: String },
86
87    /// 非同期タスクエラー
88    #[error("Async task error: {message}")]
89    AsyncTask { message: String },
90
91    /// 内部エラー
92    #[error("Internal error: {message}")]
93    Internal { message: String },
94
95    // ========================================================================
96    // Orchestrator Errors
97    // ========================================================================
98    /// DependencyGraph が必須だが利用不可
99    ///
100    /// ExplorationSpace の自動生成には DependencyGraph が必要です。
101    /// 以下のいずれかを設定してください:
102    /// - `OrchestratorBuilder::dependency_provider(provider)`
103    /// - `OrchestratorBuilder::batch_invoker(invoker)` (plan_dependencies をサポート)
104    /// - `OrchestratorBuilder::extension(DependencyGraph)` (静的グラフ)
105    #[error(
106        "DependencyGraph is required but not available. \
107             Configure via: dependency_provider(), batch_invoker() with plan_dependencies support, \
108             or extension(DependencyGraph). \
109             Hint: {hint}"
110    )]
111    MissingDependencyGraph { hint: String },
112
113    /// Worker が設定されていない
114    #[error("No workers configured. Add workers via OrchestratorBuilder::add_worker()")]
115    NoWorkers,
116
117    /// Orchestrator の設定が不正
118    #[error("Orchestrator configuration error: {message}")]
119    OrchestratorConfig { message: String },
120
121    // ========================================================================
122    // Wrapped Module Errors
123    // ========================================================================
124    /// Action バリデーションエラー
125    #[error(transparent)]
126    ActionValidation(#[from] crate::actions::ActionValidationError),
127
128    /// 依存グラフエラー
129    #[error(transparent)]
130    DependencyGraph(#[from] crate::exploration::DependencyGraphError),
131
132    /// 設定ファイルエラー
133    #[error(transparent)]
134    ConfigFile(#[from] crate::config::ConfigError),
135}
136
137impl SwarmError {
138    // ========================================================================
139    // Constructors
140    // ========================================================================
141
142    /// LLM 一時的エラー
143    pub fn llm_transient(msg: impl Into<String>) -> Self {
144        Self::LlmTransient {
145            message: msg.into(),
146        }
147    }
148
149    /// LLM 恒久的エラー
150    pub fn llm_permanent(msg: impl Into<String>) -> Self {
151        Self::LlmPermanent {
152            message: msg.into(),
153        }
154    }
155
156    /// ネットワーク一時的エラー
157    pub fn network_transient(msg: impl Into<String>) -> Self {
158        Self::NetworkTransient {
159            message: msg.into(),
160        }
161    }
162
163    /// タイムアウトエラー
164    pub fn timeout(msg: impl Into<String>) -> Self {
165        Self::Timeout {
166            message: msg.into(),
167        }
168    }
169
170    /// 設定エラー
171    pub fn config(msg: impl Into<String>) -> Self {
172        Self::Config {
173            message: msg.into(),
174        }
175    }
176
177    /// パースエラー
178    pub fn parse(msg: impl Into<String>) -> Self {
179        Self::Parse {
180            message: msg.into(),
181        }
182    }
183
184    /// バリデーションエラー
185    pub fn validation(msg: impl Into<String>) -> Self {
186        Self::Validation {
187            message: msg.into(),
188        }
189    }
190
191    /// 非同期タスクエラー
192    pub fn async_task(msg: impl Into<String>) -> Self {
193        Self::AsyncTask {
194            message: msg.into(),
195        }
196    }
197
198    /// 内部エラー
199    pub fn internal(msg: impl Into<String>) -> Self {
200        Self::Internal {
201            message: msg.into(),
202        }
203    }
204
205    // ========================================================================
206    // Query Methods
207    // ========================================================================
208
209    /// リトライ可能かどうか
210    pub fn is_transient(&self) -> bool {
211        matches!(
212            self,
213            Self::LlmTransient { .. }
214                | Self::NetworkTransient { .. }
215                | Self::Timeout { .. }
216                | Self::ResourceBusy { .. }
217        )
218    }
219
220    /// エラーメッセージを取得
221    pub fn message(&self) -> String {
222        match self {
223            Self::LlmTransient { message } => message.clone(),
224            Self::NetworkTransient { message } => message.clone(),
225            Self::Timeout { message } => message.clone(),
226            Self::ResourceBusy { message } => message.clone(),
227            Self::LlmPermanent { message } => message.clone(),
228            Self::Config { message } => message.clone(),
229            Self::Parse { message } => message.clone(),
230            Self::Validation { message } => message.clone(),
231            Self::UnknownAction { action } => action.clone(),
232            Self::MissingParam { param } => param.clone(),
233            Self::InvalidParam { param } => param.clone(),
234            Self::AsyncTask { message } => message.clone(),
235            Self::Internal { message } => message.clone(),
236            Self::MissingDependencyGraph { hint } => hint.clone(),
237            Self::NoWorkers => "No workers configured".to_string(),
238            Self::OrchestratorConfig { message } => message.clone(),
239            Self::ActionValidation(e) => e.to_string(),
240            Self::DependencyGraph(e) => e.to_string(),
241            Self::ConfigFile(e) => e.to_string(),
242        }
243    }
244
245    /// エラーの種類を取得
246    pub fn kind(&self) -> &'static str {
247        match self {
248            Self::LlmTransient { .. } => "llm_transient",
249            Self::NetworkTransient { .. } => "network_transient",
250            Self::Timeout { .. } => "timeout",
251            Self::ResourceBusy { .. } => "resource_busy",
252            Self::LlmPermanent { .. } => "llm_permanent",
253            Self::Config { .. } => "config",
254            Self::Parse { .. } => "parse",
255            Self::Validation { .. } => "validation",
256            Self::UnknownAction { .. } => "unknown_action",
257            Self::MissingParam { .. } => "missing_param",
258            Self::InvalidParam { .. } => "invalid_param",
259            Self::AsyncTask { .. } => "async_task",
260            Self::Internal { .. } => "internal",
261            Self::MissingDependencyGraph { .. } => "missing_dependency_graph",
262            Self::NoWorkers => "no_workers",
263            Self::OrchestratorConfig { .. } => "orchestrator_config",
264            Self::ActionValidation(_) => "action_validation",
265            Self::DependencyGraph(_) => "dependency_graph",
266            Self::ConfigFile(_) => "config_file",
267        }
268    }
269}
270
271/// Result type alias for SwarmError
272pub type SwarmResult<T> = Result<T, SwarmError>;
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_is_transient() {
280        assert!(SwarmError::llm_transient("timeout").is_transient());
281        assert!(SwarmError::network_transient("connection refused").is_transient());
282        assert!(SwarmError::timeout("5s exceeded").is_transient());
283
284        assert!(!SwarmError::llm_permanent("invalid model").is_transient());
285        assert!(!SwarmError::config("missing field").is_transient());
286        assert!(!SwarmError::parse("invalid json").is_transient());
287    }
288
289    #[test]
290    fn test_message() {
291        let err = SwarmError::config("missing api_key");
292        assert_eq!(err.message(), "missing api_key");
293    }
294
295    #[test]
296    fn test_kind() {
297        assert_eq!(SwarmError::llm_transient("x").kind(), "llm_transient");
298        assert_eq!(SwarmError::config("x").kind(), "config");
299    }
300
301    #[test]
302    fn test_display() {
303        let err = SwarmError::llm_transient("connection timeout");
304        assert_eq!(
305            format!("{}", err),
306            "LLM error (transient): connection timeout"
307        );
308    }
309}