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}