Skip to main content

echo_core/
error.rs

1//! Unified error types
2//!
3//! All public APIs return [`Result<T>`]; underlying errors are automatically converted
4//! to [`ReactError`] through `From`.
5
6use thiserror::Error;
7
8/// Top-level framework error, aggregating all subsystem errors
9#[derive(Debug, Error)]
10pub enum ReactError {
11    /// LLM-related error
12    #[error("LLM Error: {0}")]
13    Llm(Box<LlmError>),
14    /// Tool execution error
15    #[error("Tool Error: {0}")]
16    Tool(#[from] ToolError),
17    /// Parse error
18    #[error("Parse Error: {0}")]
19    Parse(#[from] ParseError),
20    /// Agent execution error
21    #[error("Agent Error: {0}")]
22    Agent(#[from] AgentError),
23    /// Configuration error
24    #[error("Config Error: {0}")]
25    Config(Box<ConfigError>),
26    /// MCP-related error
27    #[cfg(feature = "mcp")]
28    #[error("MCP Error: {0}")]
29    Mcp(#[from] McpError),
30    /// Memory system error
31    #[error("Memory Error: {0}")]
32    Memory(Box<MemoryError>),
33    /// Sandbox error
34    #[error("Sandbox Error: {0}")]
35    Sandbox(#[from] SandboxError),
36    /// Channel / IM integration error
37    #[cfg(feature = "channels")]
38    #[error("Channel Error: {0}")]
39    Channel(#[from] ChannelError),
40    /// IO error
41    #[error("IO Error: {0}")]
42    Io(#[from] std::io::Error),
43    /// Other error
44    #[error("{0}")]
45    Other(String),
46}
47
48/// Memory system error
49#[derive(Debug, Error)]
50pub enum MemoryError {
51    /// I/O error
52    #[error("IO error: {0}")]
53    IoError(String),
54    /// Serialization error
55    #[error("Serialization error: {0}")]
56    SerializationError(String),
57    /// Memory not found
58    #[error("Memory '{0}' not found")]
59    NotFound(String),
60    /// Unsupported operation
61    #[error("Unsupported operation: {0}")]
62    Unsupported(String),
63}
64
65impl From<std::io::Error> for MemoryError {
66    fn from(err: std::io::Error) -> Self {
67        MemoryError::IoError(err.to_string())
68    }
69}
70
71/// LLM-related error
72#[derive(Debug, Error)]
73pub enum LlmError {
74    /// Network error
75    #[error("Network error: {0}")]
76    NetworkError(String),
77    /// API error (status code and message)
78    #[error("API error (status {status}): {message}")]
79    ApiError {
80        /// HTTP status code
81        status: u16,
82        /// Error message
83        message: String,
84    },
85    /// Invalid response
86    #[error("Invalid response: {0}")]
87    InvalidResponse(String),
88    /// Empty response
89    #[error("Empty response from LLM")]
90    EmptyResponse,
91    /// Serialization error
92    #[error("Serialization error: {0}")]
93    SerializationError(String),
94}
95
96/// Tool execution error
97#[derive(Debug, Error)]
98pub enum ToolError {
99    /// Tool not found
100    #[error("Tool '{0}' not found")]
101    NotFound(String),
102    /// Missing parameter
103    #[error("Missing parameter: {0}")]
104    MissingParameter(String),
105    /// Invalid parameter
106    #[error("Invalid parameter '{name}': {message}")]
107    InvalidParameter {
108        /// Parameter name
109        name: String,
110        /// Error message
111        message: String,
112    },
113    /// Tool execution failed
114    #[error("Tool '{tool}' execution failed: {message}")]
115    ExecutionFailed {
116        /// Tool name
117        tool: String,
118        /// Error message
119        message: String,
120    },
121    /// Execution timed out
122    #[error("Tool '{0}' execution timed out")]
123    Timeout(String),
124    /// Invalid path (path traversal attack detected)
125    #[error("Invalid path: {path} ({reason})")]
126    InvalidPath {
127        /// Rejected path
128        path: String,
129        /// Rejection reason
130        reason: String,
131    },
132    /// Access denied (outside allowed directory scope)
133    #[error("Access denied: {path} ({reason})")]
134    AccessDenied {
135        /// Rejected path
136        path: String,
137        /// Rejection reason
138        reason: String,
139    },
140    /// File too large
141    #[error("File too large: {size} bytes (max: {max} bytes)")]
142    FileTooLarge {
143        /// File size (bytes)
144        size: u64,
145        /// Maximum allowed file size (bytes)
146        max: u64,
147    },
148}
149
150/// Parse error
151#[derive(Debug, Error)]
152pub enum ParseError {
153    /// Invalid Thought format
154    #[error("Invalid Thought: {0}")]
155    InvalidThought(String),
156    /// Invalid Action format
157    #[error("Invalid Action: {0}")]
158    InvalidAction(String),
159    /// Invalid Action Input
160    #[error("Invalid Action Input: {0}")]
161    InvalidActionInput(String),
162    /// JSON parse error
163    #[error("JSON parse error: {0}")]
164    JsonError(#[from] serde_json::Error),
165    /// Unexpected format
166    #[error("Unexpected format: {0}")]
167    UnexpectedFormat(String),
168}
169
170/// Agent execution error
171#[derive(Debug, Error)]
172pub enum AgentError {
173    /// Max iterations exceeded
174    #[error("Max iterations exceeded: {0}")]
175    MaxIterationsExceeded(usize),
176    /// No tools available
177    #[error("No tools available")]
178    NoToolsAvailable,
179    /// Initialization failed
180    #[error("Initialization failed: {0}")]
181    InitializationFailed(String),
182    /// Execution interrupted
183    #[error("Execution interrupted")]
184    Interrupted,
185    /// No response from LLM
186    #[error("No response from LLM (model: {model}, agent: {agent})")]
187    NoResponse {
188        /// Model name used
189        model: String,
190        /// Agent name
191        agent: String,
192    },
193    /// Token limit exceeded
194    #[error("Token limit exceeded")]
195    TokenLimitExceeded,
196    /// Permission denied
197    #[error("Permission denied: {0}")]
198    PermissionDenied(String),
199    /// Hook execution error
200    #[error("Hook error: {0}")]
201    HookError(String),
202    /// Subagent execution error
203    #[error("Subagent error: {0}")]
204    SubagentError(String),
205    /// Execution timeout
206    #[error("Timeout: {0}")]
207    Timeout(String),
208    /// Context limit exceeded (e.g. delegation depth, memory limit, etc.)
209    #[error("Context limit exceeded: {0}")]
210    ContextLimitExceeded(String),
211}
212
213/// MCP-related error
214#[cfg(feature = "mcp")]
215#[derive(Debug, Error)]
216pub enum McpError {
217    /// Connection failed
218    #[error("Connection failed: {0}")]
219    ConnectionFailed(String),
220    /// Initialization failed
221    #[error("Initialization failed: {0}")]
222    InitializationFailed(String),
223    /// Protocol error
224    #[error("Protocol error: {0}")]
225    ProtocolError(String),
226    /// Tool call failed
227    #[error("Tool call failed: {0}")]
228    ToolCallFailed(String),
229    /// Transport channel closed
230    #[error("MCP transport closed unexpectedly")]
231    TransportClosed,
232}
233
234/// Sandbox error
235#[derive(Debug, Error)]
236pub enum SandboxError {
237    /// Sandbox unavailable (Docker not installed, no K8s cluster, etc.)
238    #[error("Sandbox unavailable: {0}")]
239    Unavailable(String),
240    /// Sandbox start failed
241    #[error("Sandbox start failed: {0}")]
242    StartFailed(String),
243    /// Execution timeout
244    #[error("Sandbox timeout: {0}")]
245    Timeout(String),
246    /// Resource limit exceeded
247    #[error("Resource exceeded: {0}")]
248    ResourceExceeded(String),
249    /// Permission denied
250    #[error("Permission denied: {0}")]
251    PermissionDenied(String),
252    /// IO error
253    #[error("IO error: {0}")]
254    IoError(String),
255}
256
257/// Channel / IM integration error
258#[cfg(feature = "channels")]
259#[derive(Debug, Error)]
260pub enum ChannelError {
261    /// Network error
262    #[error("Network error: {0}")]
263    NetworkError(String),
264    /// API error (status code and message)
265    #[error("API error (status {status}): {message}")]
266    ApiError {
267        /// HTTP status code
268        status: u16,
269        /// Error message
270        message: String,
271    },
272    /// Auth error
273    #[error("Auth error: {0}")]
274    AuthError(String),
275    /// Connection error
276    #[error("Connection error: {0}")]
277    ConnectionError(String),
278    /// Send error
279    #[error("Send error: {0}")]
280    SendError(String),
281    /// Invalid config
282    #[error("Invalid config: {0}")]
283    InvalidConfig(String),
284    /// Other error
285    #[error("Channel error: {0}")]
286    Other(String),
287}
288
289/// Configuration error
290#[derive(Debug, Error)]
291pub enum ConfigError {
292    /// Environment variable parse error
293    #[error("Failed to parse environment variable: {0}")]
294    EnvParseError(String),
295    /// Missing config entry
296    #[error("Model '{0}' missing required config: {1}")]
297    MissingConfig(String, String),
298    /// Invalid environment variable format
299    #[error("Invalid environment variable format: {0}")]
300    EnvFormatError(String),
301    /// Config mismatch
302    #[error("Model '{0}' mismatched config error: {1}")]
303    UnMatchConfigError(String, String),
304    /// Model config not found
305    #[error("No configuration found for model: {0}")]
306    NotFindModelError(String),
307    /// Config file error
308    #[error("Config file error: {0}")]
309    ConfigFileError(String),
310}
311
312// ── From implementation (Box wrapping + custom conversions) ────────────────────────────────────
313
314impl From<LlmError> for ReactError {
315    fn from(err: LlmError) -> Self {
316        ReactError::Llm(Box::new(err))
317    }
318}
319
320impl From<ConfigError> for ReactError {
321    fn from(err: ConfigError) -> Self {
322        ReactError::Config(Box::new(err))
323    }
324}
325
326impl From<MemoryError> for ReactError {
327    fn from(err: MemoryError) -> Self {
328        ReactError::Memory(Box::new(err))
329    }
330}
331
332impl From<serde_json::Error> for ReactError {
333    fn from(err: serde_json::Error) -> Self {
334        ReactError::Parse(ParseError::JsonError(err))
335    }
336}
337
338#[cfg(feature = "reqwest")]
339impl From<reqwest::Error> for ReactError {
340    fn from(err: reqwest::Error) -> Self {
341        if err.is_timeout() {
342            ReactError::Llm(Box::new(LlmError::NetworkError(
343                "Request timeout".to_string(),
344            )))
345        } else if err.is_connect() {
346            ReactError::Llm(Box::new(LlmError::NetworkError(format!(
347                "Connection failed: {}",
348                err
349            ))))
350        } else {
351            ReactError::Llm(Box::new(LlmError::NetworkError(err.to_string())))
352        }
353    }
354}
355
356/// Convenience Result alias
357pub type Result<T> = std::result::Result<T, ReactError>;