Skip to main content

kernex_providers/
error.rs

1//! Per-crate error type for `kernex-providers`.
2//!
3//! Replaces the old `kernex_core::KernexError::Provider(String)` shape so
4//! callers can pattern-match on the actual cause (e.g. distinguish a network
5//! failure from a JSON parse error). Foreign errors (`reqwest::Error`,
6//! `serde_json::Error`, `std::io::Error`) are preserved as `#[source]` so
7//! the chain stays intact.
8
9/// Errors produced by AI provider implementations and the shared tool loop.
10#[derive(Debug, thiserror::Error)]
11pub enum ProviderError {
12    /// HTTP transport failure — request build, network error, timeout.
13    /// `context` describes the operation; `source` is the `reqwest::Error`.
14    #[error("{context}: {source}")]
15    Http {
16        /// Human-readable description of the failing operation
17        /// (e.g. "anthropic: stream request", "build HTTP client").
18        context: String,
19        /// The underlying reqwest error.
20        #[source]
21        source: reqwest::Error,
22    },
23
24    /// JSON serialization or deserialization failed.
25    #[error("{context}: {source}")]
26    Serde {
27        /// Human-readable description (e.g. "anthropic: parse response").
28        context: String,
29        /// The underlying serde_json error.
30        #[source]
31        source: serde_json::Error,
32    },
33
34    /// A filesystem operation failed.
35    #[error("{context}: {source}")]
36    Io {
37        /// Human-readable description (e.g. "create .claude dir",
38        /// "write MCP settings").
39        context: String,
40        /// The underlying I/O error.
41        #[source]
42        source: std::io::Error,
43    },
44
45    /// Missing or invalid configuration — env var unset, bad model id, etc.
46    #[error("{0}")]
47    Config(String),
48
49    /// Domain-logic error with no foreign source — provider rejected the
50    /// request, retries exhausted, response shape mismatch, etc.
51    #[error("{0}")]
52    Logic(String),
53}
54
55impl ProviderError {
56    /// Wrap a `reqwest::Error` with operation context.
57    pub fn http(context: impl Into<String>, source: reqwest::Error) -> Self {
58        Self::Http {
59            context: context.into(),
60            source,
61        }
62    }
63
64    /// Wrap a `serde_json::Error` with operation context.
65    pub fn serde(context: impl Into<String>, source: serde_json::Error) -> Self {
66        Self::Serde {
67            context: context.into(),
68            source,
69        }
70    }
71
72    /// Wrap a `std::io::Error` with operation context.
73    pub fn io(context: impl Into<String>, source: std::io::Error) -> Self {
74        Self::Io {
75            context: context.into(),
76            source,
77        }
78    }
79
80    /// Construct a config error from a message.
81    pub fn config(msg: impl Into<String>) -> Self {
82        Self::Config(msg.into())
83    }
84
85    /// Construct a domain-logic error from a message.
86    pub fn logic(msg: impl Into<String>) -> Self {
87        Self::Logic(msg.into())
88    }
89}
90
91/// Bridge to the workspace-level aggregate error.
92///
93/// Boxes the typed `ProviderError` inside `KernexError::Provider` so callers
94/// downstream can recover the structured cause via
95/// `boxed.downcast_ref::<ProviderError>()`. `Config` is hoisted to the
96/// dedicated `KernexError::Config` variant since it's a configuration
97/// failure, not a provider failure.
98impl From<ProviderError> for kernex_core::error::KernexError {
99    fn from(err: ProviderError) -> Self {
100        match err {
101            ProviderError::Config(msg) => kernex_core::error::KernexError::Config(msg),
102            other => kernex_core::error::KernexError::provider(other),
103        }
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn config_display_passthrough() {
113        let err = ProviderError::config("AWS_ACCESS_KEY_ID not set");
114        assert_eq!(format!("{err}"), "AWS_ACCESS_KEY_ID not set");
115    }
116
117    #[test]
118    fn logic_display_passthrough() {
119        let err = ProviderError::logic("retries exhausted");
120        assert_eq!(format!("{err}"), "retries exhausted");
121    }
122}