Skip to main content

flowgentra_ai/core/error/
mod.rs

1//! # Error Handling
2//!
3//! Comprehensive error types for all FlowgentraAI operations.
4//!
5//! This module provides a unified error type `FlowgentraError` that covers all possible
6//! failure modes in the framework, along with convenient conversion implementations
7//! for common error sources.
8//!
9//! ## Usage
10//!
11//! ```no_run
12//! use flowgentra_ai::core::error::{Result, FlowgentraError};
13//!
14//! // Most functions return Result<T>
15//! fn my_operation() -> Result<String> {
16//!     Err(FlowgentraError::ConfigError("Invalid configuration".into()))
17//! }
18//! ```
19
20use thiserror::Error;
21
22/// Convenient type alias for Results in FlowgentraAI operations
23pub type Result<T> = std::result::Result<T, FlowgentraError>;
24
25/// Comprehensive error enum covering all possible FlowgentraAI failures
26///
27/// Each variant is designed to provide context about what went wrong,
28/// making it easy to handle different error scenarios.
29#[non_exhaustive]
30#[derive(Error, Debug)]
31pub enum FlowgentraError {
32    // Configuration Errors
33    /// Invalid or malformed configuration
34    #[error("Configuration error: {0}")]
35    ConfigError(String),
36
37    // Graph Errors
38    /// Issues with graph structure or operations
39    #[error("Graph error: {0}")]
40    GraphError(String),
41
42    /// Node not found in the graph
43    #[error("Node not found: {0}")]
44    NodeNotFound(String),
45
46    /// Invalid edge definition
47    #[error("Invalid edge: {0}")]
48    InvalidEdge(String),
49
50    /// Routing condition failed
51    #[error("Routing error: {0}")]
52    RoutingError(String),
53
54    /// Cycle detected in acyclic graph
55    #[error("Cycle detected in graph")]
56    CycleDetected,
57
58    /// Recursion limit exceeded during graph execution
59    #[error("Recursion limit of {limit} steps exceeded. Ensure your graph has a termination condition that routes to END. You can raise the limit via `recursion_limit` in the graph config.")]
60    RecursionLimitExceeded { limit: usize },
61
62    /// Cyclic graph has nodes with no path to END (infinite loop risk)
63    #[error("Graph validation warning: node(s) [{nodes}] have cycles but no path to END. Add a conditional edge to END or the graph will loop forever.")]
64    NoTerminationPath { nodes: String },
65
66    // Node/Runtime Errors
67    /// Error during node execution
68    #[error("Node execution error: {0}")]
69    NodeExecutionError(String),
70
71    /// Runtime orchestration error
72    #[error("Runtime error: {0}")]
73    RuntimeError(String),
74
75    /// Invalid state transition
76    #[error("Invalid state transition: {0}")]
77    InvalidStateTransition(String),
78
79    /// Execution failed
80    #[error("Execution error: {0}")]
81    ExecutionError(String),
82
83    /// Execution aborted by middleware
84    #[error("Execution aborted: {0}")]
85    ExecutionAborted(String),
86
87    /// Operation timed out
88    #[error("Timeout error")]
89    TimeoutError,
90
91    /// Execution timeout
92    #[error("Execution timeout: {0}")]
93    ExecutionTimeout(String),
94
95    // Parallel Execution Errors
96    /// Error during parallel execution
97    #[error("Parallel execution error: {0}")]
98    ParallelExecutionError(String),
99
100    // State Errors
101    /// Error with state management
102    #[error("State error: {0}")]
103    StateError(String),
104
105    /// Error context wrapper preserving the original error trace
106    #[error("{0}")]
107    Context(String, #[source] Box<FlowgentraError>),
108
109    // Service Integration Errors
110    /// LLM operation failed
111    #[error("LLM error: {0}")]
112    LLMError(String),
113
114    /// MCP (Model Context Protocol) operation failed
115    #[error("MCP error: {0}")]
116    MCPError(String),
117
118    /// MCP transport-level error (connection refused, timeout, DNS failure).
119    /// Safe to retry because the request never reached the server.
120    #[error("MCP transport error: {0}")]
121    MCPTransportError(String),
122
123    /// MCP server-side error (HTTP 5xx, tool execution failure).
124    /// NOT safe to retry blindly — the tool may have executed.
125    #[error("MCP server error: {0}")]
126    MCPServerError(String),
127
128    /// Tool operation failed
129    #[error("Tool error: {0}")]
130    ToolError(String),
131
132    /// Validation error (e.g., schema validation)
133    #[error("Validation error: {0}")]
134    ValidationError(String),
135
136    // Serialization Errors
137    /// JSON serialization/deserialization failed
138    #[error("Serialization error: {0}")]
139    SerializationError(#[from] serde_json::Error),
140
141    /// YAML parsing failed
142    #[error("YAML parse error: {0}")]
143    YamlError(String),
144
145    // System Errors
146    /// File I/O operation failed
147    #[error("IO error: {0}")]
148    IoError(#[from] std::io::Error),
149}
150
151impl FlowgentraError {
152    /// Returns true if this is a transport-level error safe to retry.
153    pub fn is_retryable(&self) -> bool {
154        matches!(
155            self,
156            FlowgentraError::MCPTransportError(_) | FlowgentraError::TimeoutError
157        )
158    }
159}
160
161// NOTE: `From<String>` and `From<&str>` impls were intentionally removed.
162// They silently converted any string into `ExecutionError`, making it easy
163// to accidentally swallow structured errors via the `?` operator.
164// Use explicit error variants instead: `FlowgentraError::ExecutionError(msg)`.
165
166// =============================================================================
167// Error Context & Helper Methods
168// =============================================================================
169
170impl FlowgentraError {
171    /// Add context to an error message
172    ///
173    /// Useful for providing debugging information without re-wrapping the error.
174    ///
175    /// # Example
176    /// ```no_run
177    /// use flowgentra_ai::core::error::FlowgentraError;
178    ///
179    /// let err = FlowgentraError::ConfigError("invalid value".to_string());
180    /// let contextualized = err.context("while loading agent config from 'config.yaml'");
181    /// ```
182    pub fn context(self, msg: &str) -> Self {
183        match self {
184            FlowgentraError::Context(existing_msg, inner) => {
185                FlowgentraError::Context(format!("{}\nContext: {}", existing_msg, msg), inner)
186            }
187            _ => FlowgentraError::Context(msg.to_string(), Box::new(self)),
188        }
189    }
190
191    /// Check if error is a timeout
192    pub fn is_timeout(&self) -> bool {
193        match self {
194            FlowgentraError::TimeoutError | FlowgentraError::ExecutionTimeout(_) => true,
195            FlowgentraError::Context(_, inner) => inner.is_timeout(),
196            _ => false,
197        }
198    }
199
200    /// Check if error is a validation error
201    pub fn is_validation_error(&self) -> bool {
202        match self {
203            FlowgentraError::ValidationError(_) => true,
204            FlowgentraError::Context(_, inner) => inner.is_validation_error(),
205            _ => false,
206        }
207    }
208
209    /// Check if error is an LLM error
210    pub fn is_llm_error(&self) -> bool {
211        match self {
212            FlowgentraError::LLMError(_) => true,
213            FlowgentraError::Context(_, inner) => inner.is_llm_error(),
214            _ => false,
215        }
216    }
217
218    /// Check if error is a state-related error
219    pub fn is_state_error(&self) -> bool {
220        match self {
221            FlowgentraError::StateError(_) => true,
222            FlowgentraError::Context(_, inner) => inner.is_state_error(),
223            _ => false,
224        }
225    }
226
227    /// Get a hint for common error scenarios
228    pub fn hint(&self) -> Option<&'static str> {
229        match self {
230            FlowgentraError::NodeNotFound(_) => {
231                Some("Make sure the handler name in config.yaml matches a #[register_handler] function name")
232            }
233            FlowgentraError::StateError(msg) if msg.contains("not found") => {
234                Some("Check that previous nodes set this state field, or set it in main before agent.run()")
235            }
236            FlowgentraError::LLMError(msg) if msg.contains("401") || msg.contains("Unauthorized") => {
237                Some("Check your API key is valid and set correctly (e.g., $MISTRAL_API_KEY environment variable)")
238            }
239            FlowgentraError::TimeoutError | FlowgentraError::ExecutionTimeout(_) => {
240                Some("Increase the timeout value in config.yaml for this node, or optimize handler performance")
241            }
242            FlowgentraError::ConfigError(_) => {
243                Some("Ensure config.yaml is valid YAML and all required fields are present")
244            }
245            FlowgentraError::Context(_, inner) => inner.hint(),
246            _ => None,
247        }
248    }
249}