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}