Skip to main content

ai_lib_rust/
error.rs

1//! 错误处理模块:提供统一的错误类型和结构化错误上下文。
2//!
3//! # Error Handling Module
4//!
5//! This module provides unified error types and structured error contexts for
6//! comprehensive error handling across the ai-lib-rust library.
7//!
8//! ## Overview
9//!
10//! The error system provides:
11//! - **Unified Error Type**: Single [`Error`] enum for all error conditions
12//! - **Structured Context**: Rich [`ErrorContext`] for debugging information
13//! - **Actionable Hints**: User-friendly suggestions for error resolution
14//! - **Error Classification**: Retryable and fallbackable error marking
15//!
16//! ## Error Categories
17//!
18//! | Variant | Description |
19//! |---------|-------------|
20//! | `Protocol` | Protocol specification errors |
21//! | `Pipeline` | Streaming pipeline errors |
22//! | `Configuration` | Configuration and setup errors |
23//! | `Validation` | Input validation errors |
24//! | `Runtime` | Runtime execution errors |
25//! | `Transport` | Network transport errors |
26//! | `Remote` | Remote API errors (with HTTP status) |
27//!
28//! ## Example
29//!
30//! ```rust
31//! use ai_lib_rust::error::{Error, ErrorContext};
32//!
33//! // Create error with context
34//! let error = Error::validation_with_context(
35//!     "Invalid temperature value",
36//!     ErrorContext::new()
37//!         .with_field_path("request.temperature")
38//!         .with_details("Value must be between 0.0 and 2.0")
39//!         .with_hint("Try setting temperature to 0.7 for balanced output"),
40//! );
41//! ```
42
43use crate::error_code::StandardErrorCode;
44use crate::pipeline::PipelineError;
45use crate::protocol::ProtocolError;
46use std::time::Duration;
47use thiserror::Error;
48
49/// Structured error context for better error handling and debugging.
50///
51/// Provides rich metadata about errors including field paths, details,
52/// hints, and operational flags for retry/fallback decisions.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct ErrorContext {
55    /// Field path or configuration key that caused the error
56    /// (e.g., "manifest.base_url", "request.messages\[0\].content")
57    pub field_path: Option<String>,
58    /// Additional context about the error (e.g., expected type, actual value)
59    pub details: Option<String>,
60    /// Source of the error (e.g., "protocol_loader", "request_validator")
61    pub source: Option<String>,
62    /// Actionable hint or suggestion for the user
63    pub hint: Option<String>,
64    /// Request identifiers for tracking
65    pub request_id: Option<String>,
66    /// HTTP status code if applicable
67    pub status_code: Option<u16>,
68    /// Provider-specific error code
69    pub error_code: Option<String>,
70    /// Flag indicating if the error is retryable
71    pub retryable: Option<bool>,
72    /// Flag indicating if the error should trigger a fallback
73    pub fallbackable: Option<bool>,
74    /// AI-Protocol V2 standard error code
75    pub standard_code: Option<StandardErrorCode>,
76}
77
78impl ErrorContext {
79    pub fn new() -> Self {
80        Self {
81            field_path: None,
82            details: None,
83            source: None,
84            hint: None,
85            request_id: None,
86            status_code: None,
87            error_code: None,
88            retryable: None,
89            fallbackable: None,
90            standard_code: None,
91        }
92    }
93
94    pub fn with_field_path(mut self, path: impl Into<String>) -> Self {
95        self.field_path = Some(path.into());
96        self
97    }
98
99    pub fn with_details(mut self, details: impl Into<String>) -> Self {
100        self.details = Some(details.into());
101        self
102    }
103
104    pub fn with_source(mut self, source: impl Into<String>) -> Self {
105        self.source = Some(source.into());
106        self
107    }
108
109    pub fn with_hint(mut self, hint: impl Into<String>) -> Self {
110        self.hint = Some(hint.into());
111        self
112    }
113
114    pub fn with_request_id(mut self, id: impl Into<String>) -> Self {
115        self.request_id = Some(id.into());
116        self
117    }
118
119    pub fn with_status_code(mut self, code: u16) -> Self {
120        self.status_code = Some(code);
121        self
122    }
123
124    pub fn with_error_code(mut self, code: impl Into<String>) -> Self {
125        self.error_code = Some(code.into());
126        self
127    }
128
129    pub fn with_retryable(mut self, retryable: bool) -> Self {
130        self.retryable = Some(retryable);
131        self
132    }
133
134    pub fn with_fallbackable(mut self, fallbackable: bool) -> Self {
135        self.fallbackable = Some(fallbackable);
136        self
137    }
138
139    pub fn with_standard_code(mut self, code: StandardErrorCode) -> Self {
140        self.standard_code = Some(code);
141        self
142    }
143}
144
145impl Default for ErrorContext {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151/// Unified error type for the AI-Protocol Runtime
152/// This aggregates all low-level errors into actionable, high-level categories
153#[derive(Debug, Error)]
154pub enum Error {
155    #[error("Protocol specification error: {0}")]
156    Protocol(#[from] ProtocolError),
157
158    #[error("Pipeline processing error: {0}")]
159    Pipeline(#[from] PipelineError),
160
161    #[error("Configuration error: {message}{}", format_context(.context))]
162    Configuration {
163        message: String,
164        context: ErrorContext,
165    },
166
167    #[error("Validation error: {message}{}", format_context(.context))]
168    Validation {
169        message: String,
170        context: ErrorContext,
171    },
172
173    #[error("Runtime error: {message}{}", format_context(.context))]
174    Runtime {
175        message: String,
176        context: ErrorContext,
177    },
178
179    #[error("Network transport error: {0}")]
180    Transport(#[from] crate::transport::TransportError),
181
182    #[error("I/O error: {0}")]
183    Io(#[from] std::io::Error),
184
185    #[error("Serialization error: {0}")]
186    Serialization(#[from] serde_json::Error),
187
188    #[error("Remote error: HTTP {status} ({class}): {message}{}", format_optional_context(.context))]
189    Remote {
190        status: u16,
191        class: String,
192        message: String,
193        retryable: bool,
194        fallbackable: bool,
195        retry_after_ms: Option<u32>,
196        context: Option<ErrorContext>,
197    },
198
199    #[error("Unknown error: {message}{}", format_context(.context))]
200    Unknown {
201        message: String,
202        context: ErrorContext,
203    },
204}
205
206// Helper function to format error context for display.
207// Uses a single String buffer to minimize allocations.
208fn format_context(ctx: &ErrorContext) -> String {
209    use std::fmt::Write;
210    let mut buf = String::new();
211
212    // Estimate whether we have any metadata to show
213    let has_meta = ctx.field_path.is_some()
214        || ctx.details.is_some()
215        || ctx.source.is_some()
216        || ctx.request_id.is_some()
217        || ctx.status_code.is_some()
218        || ctx.error_code.is_some()
219        || ctx.retryable.is_some()
220        || ctx.fallbackable.is_some()
221        || ctx.standard_code.is_some();
222
223    if has_meta {
224        buf.push_str(" [");
225        let mut first = true;
226        macro_rules! append_field {
227            ($label:expr, $val:expr) => {
228                if let Some(ref v) = $val {
229                    if !first { buf.push_str(", "); }
230                    let _ = write!(buf, "{}: {}", $label, v);
231                    first = false;
232                }
233            };
234        }
235        append_field!("field", ctx.field_path);
236        append_field!("details", ctx.details);
237        append_field!("source", ctx.source);
238        append_field!("request_id", ctx.request_id);
239        if let Some(code) = ctx.status_code {
240            if !first { buf.push_str(", "); }
241            let _ = write!(buf, "status: {}", code);
242            first = false;
243        }
244        append_field!("error_code", ctx.error_code);
245        if let Some(r) = ctx.retryable {
246            if !first { buf.push_str(", "); }
247            let _ = write!(buf, "retryable: {}", r);
248            first = false;
249        }
250        if let Some(f) = ctx.fallbackable {
251            if !first { buf.push_str(", "); }
252            let _ = write!(buf, "fallbackable: {}", f);
253            #[allow(unused_assignments)]
254            { first = false; }
255        }
256        if let Some(ref std_code) = ctx.standard_code {
257            if !first { buf.push_str(", "); }
258            let _ = write!(buf, "standard_code: {}", std_code.code());
259        }
260        buf.push(']');
261    }
262
263    if let Some(ref hint) = ctx.hint {
264        let _ = write!(buf, "\n💡 Hint: {}", hint);
265    }
266
267    buf
268}
269
270fn format_optional_context(ctx: &Option<ErrorContext>) -> String {
271    ctx.as_ref().map(format_context).unwrap_or_default()
272}
273
274impl Error {
275    /// Create a new runtime error with structured context
276    pub fn runtime_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
277        Error::Runtime {
278            message: msg.into(),
279            context,
280        }
281    }
282
283    /// Create a new validation error with structured context
284    pub fn validation_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
285        Error::Validation {
286            message: msg.into(),
287            context,
288        }
289    }
290
291    /// Create a new configuration error with structured context
292    pub fn configuration_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
293        Error::Configuration {
294            message: msg.into(),
295            context,
296        }
297    }
298
299    /// Create a new unknown error with structured context
300    pub fn unknown_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
301        Error::Unknown {
302            message: msg.into(),
303            context,
304        }
305    }
306
307    /// Create a simple validation error
308    pub fn validation(msg: impl Into<String>) -> Self {
309        Self::validation_with_context(msg, ErrorContext::new())
310    }
311
312    /// Create a simple configuration error
313    pub fn configuration(msg: impl Into<String>) -> Self {
314        Self::configuration_with_context(msg, ErrorContext::new())
315    }
316
317    /// Create a network error with context
318    pub fn network_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
319        Error::Runtime {
320            message: format!("Network error: {}", msg.into()),
321            context,
322        }
323    }
324
325    /// Create an API error with context
326    pub fn api_with_context(msg: impl Into<String>, context: ErrorContext) -> Self {
327        Error::Runtime {
328            message: format!("API error: {}", msg.into()),
329            context,
330        }
331    }
332
333    /// Create a parsing error
334    pub fn parsing(msg: impl Into<String>) -> Self {
335        Error::Validation {
336            message: format!("Parsing error: {}", msg.into()),
337            context: ErrorContext::new().with_source("parsing"),
338        }
339    }
340
341    /// Extract error context if available
342    pub fn context(&self) -> Option<&ErrorContext> {
343        match self {
344            Error::Configuration { context, .. }
345            | Error::Validation { context, .. }
346            | Error::Runtime { context, .. }
347            | Error::Unknown { context, .. } => Some(context),
348            Error::Remote {
349                context: Some(ref c),
350                ..
351            } => Some(c),
352            _ => None,
353        }
354    }
355
356    /// Returns whether this error is retryable.
357    ///
358    /// Checks `Remote.retryable`, `context.retryable`, and `standard_code().retryable()`
359    /// in order of precedence.
360    pub fn is_retryable(&self) -> bool {
361        match self {
362            Error::Remote { retryable, context, .. } => {
363                if *retryable {
364                    return true;
365                }
366                if let Some(ref ctx) = context {
367                    if let Some(r) = ctx.retryable {
368                        return r;
369                    }
370                }
371                self.standard_code().map(|c| c.retryable()).unwrap_or(false)
372            }
373            Error::Configuration { context, .. }
374            | Error::Validation { context, .. }
375            | Error::Runtime { context, .. }
376            | Error::Unknown { context, .. } => context
377                .retryable
378                .or_else(|| context.standard_code.map(|c| c.retryable()))
379                .unwrap_or(false),
380            _ => false,
381        }
382    }
383
384    /// Returns the suggested retry delay when available.
385    ///
386    /// For `Remote` errors, converts `retry_after_ms` to `Duration`.
387    pub fn retry_after(&self) -> Option<Duration> {
388        match self {
389            Error::Remote {
390                retry_after_ms: Some(ms),
391                ..
392            } => Some(Duration::from_millis(*ms as u64)),
393            _ => None,
394        }
395    }
396
397    /// Returns the AI-Protocol V2 standard error code when available.
398    ///
399    /// Alias for [`standard_code`](Self::standard_code) for convenience.
400    #[inline]
401    pub fn error_code(&self) -> Option<StandardErrorCode> {
402        self.standard_code()
403    }
404
405    /// Returns the AI-Protocol V2 standard error code when available.
406    ///
407    /// For `Remote` errors, derives the code from the error class if not set in context.
408    /// Other variants return the standard code from their `ErrorContext` when present.
409    pub fn standard_code(&self) -> Option<StandardErrorCode> {
410        match self {
411            Error::Remote {
412                status,
413                class,
414                context,
415                ..
416            } => context
417                .as_ref()
418                .and_then(|c| c.standard_code)
419                .or_else(|| {
420                    // Derive from class name, or from HTTP status if class unknown
421                    let from_class = StandardErrorCode::from_error_class(class);
422                    if from_class == StandardErrorCode::Unknown {
423                        Some(StandardErrorCode::from_http_status(*status))
424                    } else {
425                        Some(from_class)
426                    }
427                }),
428            Error::Configuration { context, .. }
429            | Error::Validation { context, .. }
430            | Error::Runtime { context, .. }
431            | Error::Unknown { context, .. } => context.standard_code,
432            _ => None,
433        }
434    }
435
436    /// Attach or update context to the error
437    pub fn with_context(mut self, new_ctx: ErrorContext) -> Self {
438        match &mut self {
439            Error::Configuration {
440                ref mut context, ..
441            }
442            | Error::Validation {
443                ref mut context, ..
444            }
445            | Error::Runtime {
446                ref mut context, ..
447            }
448            | Error::Unknown {
449                ref mut context, ..
450            } => {
451                *context = new_ctx;
452            }
453            Error::Remote {
454                ref mut context, ..
455            } => {
456                *context = Some(new_ctx);
457            }
458            _ => {}
459        }
460        self
461    }
462}
463
464// Re-export specific error types for convenience
465pub use crate::pipeline::PipelineError as Pipeline;
466pub use crate::protocol::ProtocolError as Protocol;