Skip to main content

serdes_ai_core/
errors.rs

1//! Error types for serdes-ai.
2//!
3//! This module provides a comprehensive error hierarchy that matches pydantic-ai's
4//! error system, enabling proper error handling and retry logic.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::fmt;
9use std::time::Duration;
10use thiserror::Error;
11
12/// The main error type for serdes-ai operations.
13#[derive(Error, Debug)]
14pub enum SerdesAiError {
15    /// Error during agent execution.
16    #[error(transparent)]
17    AgentRun(#[from] AgentRunError),
18
19    /// Model requested a retry.
20    #[error(transparent)]
21    ModelRetry(#[from] ModelRetry),
22
23    /// API error from the model provider.
24    #[error(transparent)]
25    ModelApi(#[from] ModelApiError),
26
27    /// HTTP-level error.
28    #[error(transparent)]
29    ModelHttp(#[from] ModelHttpError),
30
31    /// User-defined error from tools or validators.
32    #[error(transparent)]
33    User(#[from] UserError),
34
35    /// Usage limits exceeded.
36    #[error(transparent)]
37    UsageLimit(#[from] UsageLimitExceeded),
38
39    /// Unexpected model behavior.
40    #[error(transparent)]
41    UnexpectedBehavior(#[from] UnexpectedModelBehavior),
42
43    /// Tool requested a retry.
44    #[error(transparent)]
45    ToolRetry(#[from] ToolRetryError),
46
47    /// Tool requires user approval.
48    #[error(transparent)]
49    ApprovalRequired(#[from] ApprovalRequired),
50
51    /// Tool call was deferred.
52    #[error(transparent)]
53    CallDeferred(#[from] CallDeferred),
54
55    /// Incomplete tool call from model.
56    #[error(transparent)]
57    IncompleteToolCall(#[from] IncompleteToolCall),
58
59    /// Multiple errors occurred.
60    #[error(transparent)]
61    FallbackGroup(#[from] FallbackExceptionGroup),
62
63    /// Serialization/deserialization error.
64    #[error("Serialization error: {0}")]
65    Serialization(#[from] serde_json::Error),
66
67    /// Configuration error.
68    #[error("Configuration error: {0}")]
69    Configuration(String),
70
71    /// Internal error.
72    #[error("Internal error: {0}")]
73    Internal(String),
74}
75
76/// Result type alias using SerdesAiError.
77pub type Result<T> = std::result::Result<T, SerdesAiError>;
78
79/// Error during agent run execution.
80#[derive(Error, Debug, Clone)]
81pub struct AgentRunError {
82    /// Error message.
83    pub message: String,
84    /// The run ID where the error occurred.
85    pub run_id: Option<String>,
86    /// Number of attempts made.
87    pub attempts: u32,
88    /// Original cause (serialized).
89    pub cause: Option<String>,
90}
91
92impl fmt::Display for AgentRunError {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "Agent run error: {}", self.message)?;
95        if let Some(ref run_id) = self.run_id {
96            write!(f, " (run_id: {})", run_id)?;
97        }
98        if self.attempts > 1 {
99            write!(f, " after {} attempts", self.attempts)?;
100        }
101        Ok(())
102    }
103}
104
105impl AgentRunError {
106    /// Create a new agent run error.
107    pub fn new(message: impl Into<String>) -> Self {
108        Self {
109            message: message.into(),
110            run_id: None,
111            attempts: 1,
112            cause: None,
113        }
114    }
115
116    /// Set the run ID.
117    pub fn with_run_id(mut self, run_id: impl Into<String>) -> Self {
118        self.run_id = Some(run_id.into());
119        self
120    }
121
122    /// Set the number of attempts.
123    pub fn with_attempts(mut self, attempts: u32) -> Self {
124        self.attempts = attempts;
125        self
126    }
127
128    /// Set the cause.
129    pub fn with_cause(mut self, cause: impl Into<String>) -> Self {
130        self.cause = Some(cause.into());
131        self
132    }
133}
134
135/// Model requested a retry, typically due to validation failure.
136#[derive(Error, Debug, Clone, Serialize, Deserialize)]
137pub struct ModelRetry {
138    /// Message explaining why retry is needed.
139    pub message: String,
140}
141
142impl fmt::Display for ModelRetry {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(f, "Model retry requested: {}", self.message)
145    }
146}
147
148impl ModelRetry {
149    /// Create a new model retry error.
150    pub fn new(message: impl Into<String>) -> Self {
151        Self {
152            message: message.into(),
153        }
154    }
155}
156
157/// API error from the model provider.
158#[derive(Error, Debug, Clone)]
159pub struct ModelApiError {
160    /// HTTP status code.
161    pub status_code: u16,
162    /// Response body.
163    pub body: String,
164    /// Response headers.
165    pub headers: HashMap<String, String>,
166    /// Error message from the API.
167    pub message: Option<String>,
168    /// Error code from the API.
169    pub error_code: Option<String>,
170    /// Whether this error is retryable.
171    pub retryable: bool,
172    /// Retry-after header value in seconds.
173    pub retry_after: Option<u64>,
174}
175
176impl fmt::Display for ModelApiError {
177    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
178        write!(f, "Model API error (status {})", self.status_code)?;
179        if let Some(ref msg) = self.message {
180            write!(f, ": {}", msg)?;
181        }
182        if let Some(ref code) = self.error_code {
183            write!(f, " [{}]", code)?;
184        }
185        Ok(())
186    }
187}
188
189impl ModelApiError {
190    /// Create a new API error.
191    pub fn new(status_code: u16, body: impl Into<String>) -> Self {
192        Self {
193            status_code,
194            body: body.into(),
195            headers: HashMap::new(),
196            message: None,
197            error_code: None,
198            retryable: status_code == 429 || status_code >= 500,
199            retry_after: None,
200        }
201    }
202
203    /// Set the message.
204    pub fn with_message(mut self, message: impl Into<String>) -> Self {
205        self.message = Some(message.into());
206        self
207    }
208
209    /// Set the error code.
210    pub fn with_error_code(mut self, code: impl Into<String>) -> Self {
211        self.error_code = Some(code.into());
212        self
213    }
214
215    /// Set headers.
216    pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
217        self.headers = headers;
218        // Parse retry-after if present
219        if let Some(retry_after) = self.headers.get("retry-after") {
220            self.retry_after = retry_after.parse().ok();
221        }
222        self
223    }
224
225    /// Check if this is a rate limit error.
226    pub fn is_rate_limit(&self) -> bool {
227        self.status_code == 429
228    }
229
230    /// Check if this is a server error.
231    pub fn is_server_error(&self) -> bool {
232        self.status_code >= 500
233    }
234}
235
236/// Deprecated alias for [`ModelApiError`].
237#[deprecated(note = "Use ModelApiError instead")]
238pub type ModelAPIError = ModelApiError;
239
240/// HTTP error classification for model requests.
241#[derive(Debug, Clone, PartialEq, Eq)]
242pub enum HttpErrorKind {
243    /// Request timed out.
244    Timeout,
245    /// Connection failure.
246    Connection,
247    /// Request construction or transport error.
248    Request,
249    /// Response error with optional status code.
250    Response {
251        /// HTTP status code if available.
252        status: Option<u16>,
253    },
254}
255
256/// HTTP-level error (connection, timeout, etc.).
257#[derive(Error, Debug, Clone)]
258pub struct ModelHttpError {
259    /// Error kind classification.
260    pub kind: HttpErrorKind,
261    /// Error message.
262    pub message: String,
263    /// Retry-after duration if provided by the server.
264    pub retry_after: Option<Duration>,
265}
266
267impl fmt::Display for ModelHttpError {
268    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
269        match self.kind {
270            HttpErrorKind::Timeout => write!(f, "Request timeout: {}", self.message),
271            HttpErrorKind::Connection => write!(f, "Connection error: {}", self.message),
272            HttpErrorKind::Request => write!(f, "HTTP request error: {}", self.message),
273            HttpErrorKind::Response { status } => {
274                if let Some(status) = status {
275                    write!(
276                        f,
277                        "HTTP response error (status {}): {}",
278                        status, self.message
279                    )
280                } else {
281                    write!(f, "HTTP response error: {}", self.message)
282                }
283            }
284        }
285    }
286}
287
288impl ModelHttpError {
289    /// Create a new HTTP error.
290    pub fn new(kind: HttpErrorKind, message: impl Into<String>) -> Self {
291        Self {
292            kind,
293            message: message.into(),
294            retry_after: None,
295        }
296    }
297
298    /// Create a timeout error.
299    pub fn timeout(message: impl Into<String>) -> Self {
300        Self::new(HttpErrorKind::Timeout, message)
301    }
302
303    /// Create a connection error.
304    pub fn connection(message: impl Into<String>) -> Self {
305        Self::new(HttpErrorKind::Connection, message)
306    }
307
308    /// Create a request error.
309    pub fn request(message: impl Into<String>) -> Self {
310        Self::new(HttpErrorKind::Request, message)
311    }
312
313    /// Create a response error with an optional status code.
314    pub fn response(status: Option<u16>, message: impl Into<String>) -> Self {
315        Self::new(HttpErrorKind::Response { status }, message)
316    }
317
318    /// Set the retry-after duration.
319    pub fn with_retry_after(mut self, retry_after: Duration) -> Self {
320        self.retry_after = Some(retry_after);
321        self
322    }
323}
324
325/// Deprecated alias for [`ModelHttpError`].
326#[deprecated(note = "Use ModelHttpError instead")]
327pub type ModelHTTPError = ModelHttpError;
328
329/// User-defined error from tools or validators.
330#[derive(Error, Debug, Clone, Serialize, Deserialize)]
331pub struct UserError {
332    /// Error message.
333    pub message: String,
334    /// Optional details.
335    #[serde(skip_serializing_if = "Option::is_none")]
336    pub details: Option<serde_json::Value>,
337}
338
339impl fmt::Display for UserError {
340    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
341        write!(f, "User error: {}", self.message)
342    }
343}
344
345impl UserError {
346    /// Create a new user error.
347    pub fn new(message: impl Into<String>) -> Self {
348        Self {
349            message: message.into(),
350            details: None,
351        }
352    }
353
354    /// Add details.
355    pub fn with_details(mut self, details: serde_json::Value) -> Self {
356        self.details = Some(details);
357        self
358    }
359}
360
361/// Type of usage limit that was exceeded.
362#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
363#[serde(rename_all = "snake_case")]
364pub enum UsageLimitType {
365    /// Request tokens limit.
366    RequestTokens,
367    /// Response tokens limit.
368    ResponseTokens,
369    /// Total tokens limit.
370    TotalTokens,
371    /// Number of requests limit.
372    Requests,
373}
374
375impl fmt::Display for UsageLimitType {
376    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
377        match self {
378            Self::RequestTokens => write!(f, "request_tokens"),
379            Self::ResponseTokens => write!(f, "response_tokens"),
380            Self::TotalTokens => write!(f, "total_tokens"),
381            Self::Requests => write!(f, "requests"),
382        }
383    }
384}
385
386/// Usage limits exceeded.
387#[derive(Error, Debug, Clone, Serialize, Deserialize)]
388pub struct UsageLimitExceeded {
389    /// Type of limit exceeded.
390    pub limit_type: UsageLimitType,
391    /// Current value.
392    pub current: u64,
393    /// Maximum allowed value.
394    pub max: u64,
395    /// Additional message.
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub message: Option<String>,
398}
399
400impl fmt::Display for UsageLimitExceeded {
401    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
402        write!(
403            f,
404            "Usage limit exceeded: {} is {} but max is {}",
405            self.limit_type, self.current, self.max
406        )
407    }
408}
409
410impl UsageLimitExceeded {
411    /// Create a new usage limit error.
412    pub fn new(limit_type: UsageLimitType, current: u64, max: u64) -> Self {
413        Self {
414            limit_type,
415            current,
416            max,
417            message: None,
418        }
419    }
420
421    /// Add a message.
422    pub fn with_message(mut self, message: impl Into<String>) -> Self {
423        self.message = Some(message.into());
424        self
425    }
426}
427
428/// Unexpected behavior from the model.
429#[derive(Error, Debug, Clone)]
430pub struct UnexpectedModelBehavior {
431    /// Description of the unexpected behavior.
432    pub message: String,
433    /// The response that caused the issue.
434    pub response: Option<String>,
435}
436
437impl fmt::Display for UnexpectedModelBehavior {
438    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
439        write!(f, "Unexpected model behavior: {}", self.message)
440    }
441}
442
443impl UnexpectedModelBehavior {
444    /// Create a new unexpected behavior error.
445    pub fn new(message: impl Into<String>) -> Self {
446        Self {
447            message: message.into(),
448            response: None,
449        }
450    }
451
452    /// Add the response.
453    pub fn with_response(mut self, response: impl Into<String>) -> Self {
454        self.response = Some(response.into());
455        self
456    }
457}
458
459/// Tool requested a retry.
460#[derive(Error, Debug, Clone, Serialize, Deserialize)]
461pub struct ToolRetryError {
462    /// Tool name.
463    pub tool_name: String,
464    /// Retry message.
465    pub message: String,
466    /// Tool call ID.
467    #[serde(skip_serializing_if = "Option::is_none")]
468    pub tool_call_id: Option<String>,
469}
470
471impl fmt::Display for ToolRetryError {
472    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
473        write!(f, "Tool '{}' retry: {}", self.tool_name, self.message)
474    }
475}
476
477impl ToolRetryError {
478    /// Create a new tool retry error.
479    pub fn new(tool_name: impl Into<String>, message: impl Into<String>) -> Self {
480        Self {
481            tool_name: tool_name.into(),
482            message: message.into(),
483            tool_call_id: None,
484        }
485    }
486
487    /// Set the tool call ID.
488    pub fn with_tool_call_id(mut self, id: impl Into<String>) -> Self {
489        self.tool_call_id = Some(id.into());
490        self
491    }
492}
493
494/// Tool requires user approval before execution.
495#[derive(Error, Debug, Clone, Serialize, Deserialize)]
496pub struct ApprovalRequired {
497    /// Tool name.
498    pub tool_name: String,
499    /// Tool arguments.
500    pub args: serde_json::Value,
501    /// Reason approval is required.
502    #[serde(skip_serializing_if = "Option::is_none")]
503    pub reason: Option<String>,
504    /// Tool call ID.
505    #[serde(skip_serializing_if = "Option::is_none")]
506    pub tool_call_id: Option<String>,
507}
508
509impl fmt::Display for ApprovalRequired {
510    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
511        write!(f, "Approval required for tool '{}'", self.tool_name)?;
512        if let Some(ref reason) = self.reason {
513            write!(f, ": {}", reason)?;
514        }
515        Ok(())
516    }
517}
518
519impl ApprovalRequired {
520    /// Create a new approval required error.
521    pub fn new(tool_name: impl Into<String>, args: serde_json::Value) -> Self {
522        Self {
523            tool_name: tool_name.into(),
524            args,
525            reason: None,
526            tool_call_id: None,
527        }
528    }
529
530    /// Set the reason.
531    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
532        self.reason = Some(reason.into());
533        self
534    }
535
536    /// Set the tool call ID.
537    pub fn with_tool_call_id(mut self, id: impl Into<String>) -> Self {
538        self.tool_call_id = Some(id.into());
539        self
540    }
541}
542
543/// Tool call was deferred for later execution.
544#[derive(Error, Debug, Clone, Serialize, Deserialize)]
545pub struct CallDeferred {
546    /// Tool name.
547    pub tool_name: String,
548    /// Tool arguments.
549    pub args: serde_json::Value,
550    /// Reason for deferral.
551    #[serde(skip_serializing_if = "Option::is_none")]
552    pub reason: Option<String>,
553    /// Tool call ID.
554    #[serde(skip_serializing_if = "Option::is_none")]
555    pub tool_call_id: Option<String>,
556}
557
558impl fmt::Display for CallDeferred {
559    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
560        write!(f, "Tool call '{}' deferred", self.tool_name)?;
561        if let Some(ref reason) = self.reason {
562            write!(f, ": {}", reason)?;
563        }
564        Ok(())
565    }
566}
567
568impl CallDeferred {
569    /// Create a new call deferred error.
570    pub fn new(tool_name: impl Into<String>, args: serde_json::Value) -> Self {
571        Self {
572            tool_name: tool_name.into(),
573            args,
574            reason: None,
575            tool_call_id: None,
576        }
577    }
578
579    /// Set the reason.
580    pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
581        self.reason = Some(reason.into());
582        self
583    }
584
585    /// Set the tool call ID.
586    pub fn with_tool_call_id(mut self, id: impl Into<String>) -> Self {
587        self.tool_call_id = Some(id.into());
588        self
589    }
590}
591
592/// Incomplete tool call from model (missing required fields).
593#[derive(Error, Debug, Clone, Serialize, Deserialize)]
594pub struct IncompleteToolCall {
595    /// Tool name (if known).
596    #[serde(skip_serializing_if = "Option::is_none")]
597    pub tool_name: Option<String>,
598    /// What's missing.
599    pub message: String,
600    /// The partial data received.
601    #[serde(skip_serializing_if = "Option::is_none")]
602    pub partial_data: Option<serde_json::Value>,
603}
604
605impl fmt::Display for IncompleteToolCall {
606    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
607        write!(f, "Incomplete tool call")?;
608        if let Some(ref name) = self.tool_name {
609            write!(f, " for '{}'", name)?;
610        }
611        write!(f, ": {}", self.message)
612    }
613}
614
615impl IncompleteToolCall {
616    /// Create a new incomplete tool call error.
617    pub fn new(message: impl Into<String>) -> Self {
618        Self {
619            tool_name: None,
620            message: message.into(),
621            partial_data: None,
622        }
623    }
624
625    /// Set the tool name.
626    pub fn with_tool_name(mut self, name: impl Into<String>) -> Self {
627        self.tool_name = Some(name.into());
628        self
629    }
630
631    /// Set partial data.
632    pub fn with_partial_data(mut self, data: serde_json::Value) -> Self {
633        self.partial_data = Some(data);
634        self
635    }
636}
637
638/// Group of errors from fallback attempts.
639#[derive(Error, Debug, Clone)]
640pub struct FallbackExceptionGroup {
641    /// Message describing the group.
642    pub message: String,
643    /// Individual errors.
644    pub errors: Vec<String>,
645}
646
647impl fmt::Display for FallbackExceptionGroup {
648    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
649        write!(f, "{} ({} errors)", self.message, self.errors.len())
650    }
651}
652
653impl FallbackExceptionGroup {
654    /// Create a new fallback exception group.
655    pub fn new(message: impl Into<String>, errors: Vec<String>) -> Self {
656        Self {
657            message: message.into(),
658            errors,
659        }
660    }
661
662    /// Create from a list of errors.
663    pub fn from_errors<E: std::error::Error>(message: impl Into<String>, errors: Vec<E>) -> Self {
664        Self {
665            message: message.into(),
666            errors: errors.iter().map(|e| e.to_string()).collect(),
667        }
668    }
669
670    /// Check if this group is empty.
671    pub fn is_empty(&self) -> bool {
672        self.errors.is_empty()
673    }
674
675    /// Get the number of errors.
676    pub fn len(&self) -> usize {
677        self.errors.len()
678    }
679}
680
681#[cfg(test)]
682mod tests {
683    use super::*;
684
685    #[test]
686    fn test_model_retry() {
687        let err = ModelRetry::new("Invalid JSON output");
688        assert_eq!(err.message, "Invalid JSON output");
689        assert!(err.to_string().contains("Invalid JSON output"));
690    }
691
692    #[test]
693    fn test_usage_limit_exceeded() {
694        let err = UsageLimitExceeded::new(UsageLimitType::TotalTokens, 5000, 4000);
695        assert_eq!(err.current, 5000);
696        assert_eq!(err.max, 4000);
697        assert!(err.to_string().contains("5000"));
698    }
699
700    #[test]
701    fn test_api_error_is_rate_limit() {
702        let err = ModelApiError::new(429, "Rate limited");
703        assert!(err.is_rate_limit());
704        assert!(err.retryable);
705    }
706
707    #[test]
708    fn test_fallback_group() {
709        let group = FallbackExceptionGroup::new(
710            "All providers failed",
711            vec!["Error 1".to_string(), "Error 2".to_string()],
712        );
713        assert_eq!(group.len(), 2);
714        assert!(!group.is_empty());
715    }
716}