reasonkit/
error.rs

1//! Error types for ReasonKit Core.
2//!
3//! This module defines the error types used throughout ReasonKit Core.
4//! All public functions return [`Result<T>`] which is an alias for
5//! `std::result::Result<T, Error>`.
6//!
7//! # Error Handling Philosophy
8//!
9//! ReasonKit follows these error handling principles:
10//!
11//! 1. **Fail Fast, Fail Loud** - Errors are surfaced immediately with actionable messages
12//! 2. **Typed Errors** - Each error variant carries specific context for the failure mode
13//! 3. **Error Chaining** - Use `with_context()` to add context while preserving the source
14//!
15//! # Example
16//!
17//! ```rust
18//! use reasonkit::{Error, Result};
19//!
20//! fn process_document(path: &str) -> Result<()> {
21//!     // Return typed errors
22//!     if path.is_empty() {
23//!         return Err(Error::Validation("Path cannot be empty".to_string()));
24//!     }
25//!
26//!     // Create errors with context
27//!     std::fs::read_to_string(path)
28//!         .map_err(|e| Error::io(format!("Failed to read {}", path)))?;
29//!
30//!     Ok(())
31//! }
32//! ```
33//!
34//! # Error Categories
35//!
36//! | Category | Variants | Typical Cause |
37//! |----------|----------|---------------|
38//! | I/O | `Io`, `IoMessage` | File operations, network I/O |
39//! | Parsing | `Json`, `Parse` | Malformed input, schema violations |
40//! | Resource | `NotFound`, `DocumentNotFound` | Missing documents, chunks |
41//! | Processing | `Embedding`, `Indexing`, `Retrieval` | Pipeline failures |
42//! | Configuration | `Config`, `ConfigError` | Invalid settings |
43//! | M2 Integration | `M2*` variants | MiniMax M2 API issues |
44
45use thiserror::Error;
46
47/// Result type alias for ReasonKit operations.
48///
49/// All fallible operations in ReasonKit return this type.
50///
51/// # Example
52///
53/// ```rust
54/// use reasonkit::Result;
55///
56/// fn do_something() -> Result<String> {
57///     Ok("Success".to_string())
58/// }
59/// ```
60pub type Result<T> = std::result::Result<T, Error>;
61
62/// Error types for ReasonKit Core.
63///
64/// This enum encompasses all error conditions that can occur during
65/// ReasonKit operations. Each variant carries context-specific information
66/// to aid in debugging and error recovery.
67///
68/// # Example
69///
70/// ```rust
71/// use reasonkit::Error;
72///
73/// // Create specific error types
74/// let validation_err = Error::Validation("Invalid input".to_string());
75/// let not_found_err = Error::NotFound { resource: "document:123".to_string() };
76///
77/// // Add context to existing errors
78/// let io_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
79/// let wrapped = Error::with_context("Loading config", io_error);
80/// ```
81#[derive(Error, Debug)]
82pub enum Error {
83    /// I/O error from standard library operations.
84    ///
85    /// Automatically converted from `std::io::Error`.
86    #[error("I/O error: {0}")]
87    Io(#[from] std::io::Error),
88
89    /// JSON serialization/deserialization error.
90    ///
91    /// Automatically converted from `serde_json::Error`.
92    #[error("JSON error: {0}")]
93    Json(#[from] serde_json::Error),
94
95    /// PDF processing error.
96    ///
97    /// Occurs during PDF parsing, text extraction, or page processing.
98    #[error("PDF processing error: {0}")]
99    Pdf(String),
100
101    /// Document not found in the knowledge base.
102    ///
103    /// The document ID does not exist or has been deleted.
104    #[error("Document not found: {0}")]
105    DocumentNotFound(String),
106
107    /// Chunk not found within a document.
108    ///
109    /// The chunk ID does not exist or the document has been re-chunked.
110    #[error("Chunk not found: {0}")]
111    ChunkNotFound(String),
112
113    /// Embedding generation or retrieval error.
114    ///
115    /// Occurs during vector embedding operations.
116    #[error("Embedding error: {0}")]
117    Embedding(String),
118
119    /// Indexing operation error.
120    ///
121    /// Occurs during document indexing or index updates.
122    #[error("Indexing error: {0}")]
123    Indexing(String),
124
125    /// Retrieval operation error.
126    ///
127    /// Occurs during document or chunk retrieval.
128    #[error("Retrieval error: {0}")]
129    Retrieval(String),
130
131    /// Storage operation error.
132    ///
133    /// Occurs during database or file storage operations.
134    #[error("Storage error: {0}")]
135    Storage(String),
136
137    /// Configuration error.
138    ///
139    /// Invalid or missing configuration values.
140    #[error("Configuration error: {0}")]
141    Config(String),
142
143    /// Network/HTTP error.
144    ///
145    /// Occurs during API calls, downloads, or network operations.
146    #[error("Network error: {0}")]
147    Network(String),
148
149    /// MCP Daemon error.
150    ///
151    /// Occurs during daemon lifecycle management (start, stop, IPC).
152    #[error("Daemon error: {0}")]
153    Daemon(String),
154
155    /// Schema validation error.
156    ///
157    /// Input data does not conform to expected schema.
158    #[error("Schema validation error: {0}")]
159    Validation(String),
160
161    /// Generic resource not found error.
162    ///
163    /// Use this when a resource type is dynamic or not covered by specific variants.
164    ///
165    /// # Example
166    ///
167    /// ```rust
168    /// use reasonkit::Error;
169    ///
170    /// let err = Error::NotFound { resource: "protocol:gigathink".to_string() };
171    /// assert!(err.to_string().contains("gigathink"));
172    /// ```
173    #[error("{resource} not found")]
174    NotFound {
175        /// Resource identifier that was not found
176        resource: String,
177    },
178
179    /// I/O error with custom message.
180    ///
181    /// Use when you need to provide additional context beyond what
182    /// the standard I/O error contains.
183    #[error("I/O error: {message}")]
184    IoMessage {
185        /// Descriptive error message
186        message: String,
187    },
188
189    /// Parse error with custom message.
190    ///
191    /// Use for parsing failures that aren't JSON-specific.
192    #[error("Parse error: {message}")]
193    Parse {
194        /// Descriptive error message
195        message: String,
196    },
197
198    /// Qdrant vector database error.
199    ///
200    /// Occurs during Qdrant operations.
201    #[error("Qdrant error: {0}")]
202    Qdrant(String),
203
204    /// Tantivy search engine error.
205    ///
206    /// Occurs during Tantivy indexing or search operations.
207    #[error("Tantivy error: {0}")]
208    Tantivy(String),
209
210    /// ARF (Autonomous Reasoning Framework) error.
211    ///
212    /// Occurs during autonomous reasoning operations.
213    #[cfg(feature = "arf")]
214    #[error("ARF error: {0}")]
215    ArfError(String),
216
217    /// Config library parsing error.
218    ///
219    /// Occurs when configuration files cannot be parsed.
220    #[error("Config library error: {0}")]
221    ConfigError(String),
222
223    /// Memory module error from reasonkit-mem.
224    ///
225    /// Converted from `reasonkit_mem::Error`.
226    #[cfg(feature = "memory")]
227    #[error("Memory error: {0}")]
228    Memory(String),
229
230    /// Generic error with context chain.
231    ///
232    /// Use [`Error::with_context()`] to create this variant.
233    ///
234    /// # Example
235    ///
236    /// ```rust
237    /// use reasonkit::Error;
238    ///
239    /// let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
240    /// let err = Error::with_context("Loading configuration file", io_err);
241    /// assert!(err.to_string().contains("Loading configuration"));
242    /// ```
243    #[error("{context}: {source}")]
244    WithContext {
245        /// Description of what operation was being attempted
246        context: String,
247        /// The underlying error
248        #[source]
249        source: Box<dyn std::error::Error + Send + Sync>,
250    },
251
252    /// MCP (Model Context Protocol) error.
253    ///
254    /// Occurs during MCP server/client operations.
255    #[error("MCP error: {0}")]
256    Mcp(String),
257
258    /// M2 API execution error.
259    ///
260    /// Occurs during MiniMax M2 model API calls.
261    #[error("M2 execution error: {0}")]
262    M2ExecutionError(String),
263
264    /// M2 rate limit exceeded.
265    ///
266    /// The API rate limit has been exceeded. Retry after waiting.
267    #[error("M2 rate limit exceeded")]
268    RateLimitExceeded,
269
270    /// M2 budget exceeded.
271    ///
272    /// The configured budget limit has been reached.
273    /// Contains (actual_cost, budget_limit).
274    #[error("M2 budget exceeded: {0} > {1}")]
275    BudgetExceeded(f64, f64),
276
277    /// M2 protocol validation error.
278    ///
279    /// The protocol definition or output failed validation.
280    #[error("M2 protocol validation error: {0}")]
281    M2ProtocolValidation(String),
282
283    /// M2 constraint violation.
284    ///
285    /// A protocol constraint was not satisfied.
286    #[error("M2 constraint violation: {0}")]
287    M2ConstraintViolation(String),
288
289    /// M2 framework incompatibility.
290    ///
291    /// The requested operation is not compatible with the current framework.
292    #[error("M2 framework incompatibility: {0}")]
293    M2FrameworkIncompatibility(String),
294
295    /// Resource exhausted (memory, connections, etc.).
296    ///
297    /// A system resource has been exhausted.
298    #[error("Resource exhausted: {0}")]
299    ResourceExhausted(String),
300
301    /// Template not found.
302    ///
303    /// The requested protocol or profile template does not exist.
304    #[error("Template not found: {0}")]
305    TemplateNotFound(String),
306
307    /// Code intelligence error.
308    ///
309    /// Occurs during code parsing or analysis.
310    #[error("Code intelligence error: {0}")]
311    CodeIntelligence(String),
312
313    /// M2 integration error.
314    ///
315    /// General M2 integration failure.
316    #[error("M2 integration error: {0}")]
317    M2IntegrationError(String),
318
319    /// Timeout error.
320    ///
321    /// An operation exceeded its time limit.
322    #[error("Timeout error: {0}")]
323    Timeout(String),
324
325    /// Dependency not met.
326    ///
327    /// A required dependency (step, component, etc.) was not satisfied.
328    #[error("Dependency not met: {0}")]
329    DependencyNotMet(String),
330
331    /// Protocol generation error.
332    ///
333    /// Failed to generate a protocol definition.
334    #[error("Protocol generation error: {0}")]
335    ProtocolGenerationError(String),
336
337    /// ThinkTool execution error.
338    ///
339    /// A ThinkTool failed during execution.
340    #[error("ThinkTool execution error: {0}")]
341    ThinkToolExecutionError(String),
342
343    /// Rate limit error.
344    ///
345    /// API rate limiting was triggered.
346    #[error("Rate limit error: {0}")]
347    RateLimit(String),
348
349    /// Authentication error.
350    ///
351    /// Authentication failed (invalid or missing credentials).
352    #[error("Authentication error: {0}")]
353    Authentication(String),
354
355    /// Authorization error.
356    ///
357    /// The authenticated user lacks permission for the operation.
358    #[error("Authorization error: {0}")]
359    Authorization(String),
360}
361
362impl Error {
363    /// Create an I/O error from a message.
364    ///
365    /// # Arguments
366    ///
367    /// * `msg` - Descriptive error message
368    ///
369    /// # Example
370    ///
371    /// ```rust
372    /// use reasonkit::Error;
373    ///
374    /// let err = Error::io("Failed to open file");
375    /// assert!(err.to_string().contains("Failed to open"));
376    /// ```
377    pub fn io(msg: impl Into<String>) -> Self {
378        Self::Io(std::io::Error::other(msg.into()))
379    }
380
381    /// Create a parse error from a message.
382    ///
383    /// # Arguments
384    ///
385    /// * `msg` - Descriptive error message
386    ///
387    /// # Example
388    ///
389    /// ```rust
390    /// use reasonkit::Error;
391    ///
392    /// let err = Error::parse("Invalid JSON syntax at line 42");
393    /// ```
394    pub fn parse(msg: impl Into<String>) -> Self {
395        // Since serde_json::Error doesn't have a simple constructor,
396        // we use io error to create it
397        let io_err = std::io::Error::new(std::io::ErrorKind::InvalidData, msg.into());
398        Self::Json(serde_json::Error::io(io_err))
399    }
400
401    /// Create a PDF processing error from a message.
402    ///
403    /// # Arguments
404    ///
405    /// * `msg` - Descriptive error message
406    pub fn pdf(msg: impl Into<String>) -> Self {
407        Self::Pdf(msg.into())
408    }
409
410    /// Create an embedding error from a message.
411    ///
412    /// # Arguments
413    ///
414    /// * `msg` - Descriptive error message
415    pub fn embedding(msg: impl Into<String>) -> Self {
416        Self::Embedding(msg.into())
417    }
418
419    /// Create an indexing error from a message.
420    ///
421    /// # Arguments
422    ///
423    /// * `msg` - Descriptive error message
424    pub fn indexing(msg: impl Into<String>) -> Self {
425        Self::Indexing(msg.into())
426    }
427
428    /// Create a retrieval error from a message.
429    ///
430    /// # Arguments
431    ///
432    /// * `msg` - Descriptive error message
433    pub fn retrieval(msg: impl Into<String>) -> Self {
434        Self::Retrieval(msg.into())
435    }
436
437    /// Create a query error from a message.
438    ///
439    /// This is an alias for [`retrieval()`](Self::retrieval).
440    ///
441    /// # Arguments
442    ///
443    /// * `msg` - Descriptive error message
444    pub fn query(msg: impl Into<String>) -> Self {
445        Self::Retrieval(msg.into())
446    }
447
448    /// Create a storage error from a message.
449    ///
450    /// # Arguments
451    ///
452    /// * `msg` - Descriptive error message
453    pub fn storage(msg: impl Into<String>) -> Self {
454        Self::Storage(msg.into())
455    }
456
457    /// Create a config error from a message.
458    ///
459    /// # Arguments
460    ///
461    /// * `msg` - Descriptive error message
462    pub fn config(msg: impl Into<String>) -> Self {
463        Self::Config(msg.into())
464    }
465
466    /// Create a network error from a message.
467    ///
468    /// # Arguments
469    ///
470    /// * `msg` - Descriptive error message
471    pub fn network(msg: impl Into<String>) -> Self {
472        Self::Network(msg.into())
473    }
474
475    /// Create a daemon error from a message.
476    ///
477    /// # Arguments
478    ///
479    /// * `msg` - Descriptive error message
480    pub fn daemon(msg: impl Into<String>) -> Self {
481        Self::Daemon(msg.into())
482    }
483
484    /// Create a validation error from a message.
485    ///
486    /// # Arguments
487    ///
488    /// * `msg` - Descriptive error message
489    ///
490    /// # Example
491    ///
492    /// ```rust
493    /// use reasonkit::Error;
494    ///
495    /// let err = Error::validation("Input must not be empty");
496    /// ```
497    pub fn validation(msg: impl Into<String>) -> Self {
498        Self::Validation(msg.into())
499    }
500
501    /// Create an ARF error from a message.
502    ///
503    /// Only available with the `arf` feature.
504    ///
505    /// # Arguments
506    ///
507    /// * `msg` - Descriptive error message
508    #[cfg(feature = "arf")]
509    pub fn arf_error(msg: impl Into<String>) -> Self {
510        Self::ArfError(msg.into())
511    }
512
513    /// Wrap an error with additional context.
514    ///
515    /// This creates an error chain, preserving the original error
516    /// while adding context about what operation was being attempted.
517    ///
518    /// # Arguments
519    ///
520    /// * `context` - Description of the operation that failed
521    /// * `source` - The underlying error
522    ///
523    /// # Example
524    ///
525    /// ```rust
526    /// use reasonkit::Error;
527    ///
528    /// fn load_config(path: &str) -> Result<(), Error> {
529    ///     std::fs::read_to_string(path)
530    ///         .map_err(|e| Error::with_context(
531    ///             format!("Loading config from {}", path),
532    ///             e
533    ///         ))?;
534    ///     Ok(())
535    /// }
536    /// ```
537    pub fn with_context<E>(context: impl Into<String>, source: E) -> Self
538    where
539        E: std::error::Error + Send + Sync + 'static,
540    {
541        Self::WithContext {
542            context: context.into(),
543            source: Box::new(source),
544        }
545    }
546}
547
548/// Convert reqwest errors to our Error type.
549impl From<reqwest::Error> for Error {
550    fn from(err: reqwest::Error) -> Self {
551        Self::Network(err.to_string())
552    }
553}
554
555/// Convert config::ConfigError to our Error type.
556#[cfg(feature = "arf")]
557impl From<config::ConfigError> for Error {
558    fn from(err: config::ConfigError) -> Self {
559        Self::ConfigError(err.to_string())
560    }
561}
562
563/// Convert sled errors to our Error type.
564#[cfg(feature = "arf")]
565impl From<sled::Error> for Error {
566    fn from(err: sled::Error) -> Self {
567        Self::Storage(err.to_string())
568    }
569}
570
571/// Convert anyhow errors to our Error type.
572#[cfg(feature = "arf")]
573impl From<anyhow::Error> for Error {
574    fn from(err: anyhow::Error) -> Self {
575        Self::arf_error(err.to_string())
576    }
577}
578
579/// Convert mpsc send errors to our Error type.
580#[cfg(feature = "arf")]
581impl<T> From<tokio::sync::mpsc::error::SendError<T>> for Error {
582    fn from(err: tokio::sync::mpsc::error::SendError<T>) -> Self {
583        Self::arf_error(err.to_string())
584    }
585}
586
587/// Convert reasonkit-mem errors to core Error type.
588#[cfg(feature = "memory")]
589impl From<reasonkit_mem::MemError> for Error {
590    fn from(err: reasonkit_mem::MemError) -> Self {
591        Error::Memory(err.to_string())
592    }
593}
594
595/// Extension trait for adding context to Results.
596///
597/// This trait provides a convenient way to add context to any `Result`
598/// type, similar to `anyhow::Context`.
599///
600/// # Example
601///
602/// ```rust,ignore
603/// use reasonkit::{Result, error::ResultExt};
604///
605/// fn load_data(path: &str) -> Result<String> {
606///     std::fs::read_to_string(path)
607///         .context(format!("Failed to load data from {}", path))
608/// }
609/// ```
610pub trait ResultExt<T> {
611    /// Add context to an error.
612    ///
613    /// If the result is `Ok`, it is returned unchanged.
614    /// If the result is `Err`, the error is wrapped with the given context.
615    ///
616    /// # Arguments
617    ///
618    /// * `context` - Description of the operation that was being attempted
619    fn context(self, context: impl Into<String>) -> Result<T>;
620}
621
622impl<T, E> ResultExt<T> for std::result::Result<T, E>
623where
624    E: std::error::Error + Send + Sync + 'static,
625{
626    fn context(self, context: impl Into<String>) -> Result<T> {
627        self.map_err(|e| Error::with_context(context, e))
628    }
629}