Skip to main content

whycodes_core/
error.rs

1use thiserror::Error;
2
3/// Matchable LLM / HTTP transport failure class.
4///
5/// Retry and TUI copy should match this, not parse [`Error`] display strings.
6/// Wire bodies still go through `whycodes_llm::classify_message` when `kind`
7/// is [`ErrorKind::Unknown`].
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9pub enum ErrorKind {
10    RateLimited,
11    Server,
12    Network,
13    Timeout,
14    Auth,
15    Client,
16    ContextOverflow,
17    Cancelled,
18    #[default]
19    Unknown,
20}
21
22impl ErrorKind {
23    pub fn as_str(self) -> &'static str {
24        match self {
25            Self::RateLimited => "rate_limited",
26            Self::Server => "server",
27            Self::Network => "network",
28            Self::Timeout => "timeout",
29            Self::Auth => "auth",
30            Self::Client => "client",
31            Self::ContextOverflow => "context_overflow",
32            Self::Cancelled => "cancelled",
33            Self::Unknown => "unknown",
34        }
35    }
36
37    /// Whether another attempt may succeed without changing the request.
38    pub fn retryable(self) -> bool {
39        matches!(
40            self,
41            Self::RateLimited | Self::Server | Self::Network | Self::Timeout
42        )
43    }
44}
45
46impl std::fmt::Display for ErrorKind {
47    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48        f.write_str(self.as_str())
49    }
50}
51
52/// LLM or HTTP payload with an optional structured kind.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct TransportError {
55    pub kind: ErrorKind,
56    pub message: String,
57}
58
59impl TransportError {
60    pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
61        Self {
62            kind,
63            message: message.into(),
64        }
65    }
66
67    pub fn unknown(message: impl Into<String>) -> Self {
68        Self::new(ErrorKind::Unknown, message)
69    }
70}
71
72impl std::fmt::Display for TransportError {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.write_str(&self.message)
75    }
76}
77
78impl From<String> for TransportError {
79    fn from(message: String) -> Self {
80        Self::unknown(message)
81    }
82}
83
84impl From<&str> for TransportError {
85    fn from(message: &str) -> Self {
86        Self::unknown(message)
87    }
88}
89
90#[derive(Error, Debug)]
91pub enum Error {
92    #[error("Configuration error: {0}")]
93    Config(String),
94
95    #[error("IO error: {0}")]
96    Io(#[from] std::io::Error),
97
98    #[error("Serialization error: {0}")]
99    Serde(String),
100
101    #[error("LLM error: {0}")]
102    Llm(TransportError),
103
104    #[error("Tool error: {0}")]
105    Tool(String),
106
107    #[error("Session error: {0}")]
108    Session(String),
109
110    #[error("Agent error: {0}")]
111    Agent(String),
112
113    #[error("Provider error: {0}")]
114    Provider(String),
115
116    #[error("HTTP error: {0}")]
117    Http(TransportError),
118
119    #[error("{0}")]
120    Other(String),
121}
122
123impl Error {
124    pub fn llm(message: impl Into<String>) -> Self {
125        Self::Llm(TransportError::unknown(message))
126    }
127
128    pub fn llm_kind(kind: ErrorKind, message: impl Into<String>) -> Self {
129        Self::Llm(TransportError::new(kind, message))
130    }
131
132    pub fn http(message: impl Into<String>) -> Self {
133        Self::Http(TransportError::unknown(message))
134    }
135
136    pub fn http_kind(kind: ErrorKind, message: impl Into<String>) -> Self {
137        Self::Http(TransportError::new(kind, message))
138    }
139
140    /// Structured kind when this is an LLM/HTTP transport error.
141    pub fn transport_kind(&self) -> Option<ErrorKind> {
142        match self {
143            Self::Llm(e) | Self::Http(e) => Some(e.kind),
144            _ => None,
145        }
146    }
147}
148
149impl From<serde_json::Error> for Error {
150    fn from(err: serde_json::Error) -> Self {
151        Self::Serde(err.to_string())
152    }
153}
154
155impl Clone for Error {
156    fn clone(&self) -> Self {
157        match self {
158            Self::Config(s) => Self::Config(s.clone()),
159            Self::Io(e) => Self::Io(std::io::Error::new(e.kind(), e.to_string())),
160            Self::Serde(s) => Self::Serde(s.clone()),
161            Self::Llm(e) => Self::Llm(e.clone()),
162            Self::Tool(s) => Self::Tool(s.clone()),
163            Self::Session(s) => Self::Session(s.clone()),
164            Self::Agent(s) => Self::Agent(s.clone()),
165            Self::Provider(s) => Self::Provider(s.clone()),
166            Self::Http(e) => Self::Http(e.clone()),
167            Self::Other(s) => Self::Other(s.clone()),
168        }
169    }
170}
171
172pub type Result<T> = std::result::Result<T, Error>;
173
174#[cfg(test)]
175mod tests {
176    use super::*;
177
178    #[test]
179    fn error_kind_transport_constructors_and_clone() {
180        let kinds = [
181            ErrorKind::RateLimited,
182            ErrorKind::Server,
183            ErrorKind::Network,
184            ErrorKind::Timeout,
185            ErrorKind::Auth,
186            ErrorKind::Client,
187            ErrorKind::ContextOverflow,
188            ErrorKind::Cancelled,
189            ErrorKind::Unknown,
190        ];
191        for k in kinds {
192            assert!(!k.as_str().is_empty());
193            assert_eq!(k.to_string(), k.as_str());
194            let _ = k.retryable();
195        }
196        assert_eq!(ErrorKind::default(), ErrorKind::Unknown);
197        assert!(ErrorKind::Timeout.retryable());
198        assert!(!ErrorKind::Auth.retryable());
199
200        let te = TransportError::new(ErrorKind::Server, "s");
201        assert_eq!(te.to_string(), "s");
202        assert_eq!(TransportError::unknown("u").kind, ErrorKind::Unknown);
203        let from_string: TransportError = String::from("x").into();
204        assert_eq!(from_string.message, "x");
205        let from_str: TransportError = "y".into();
206        assert_eq!(from_str.message, "y");
207
208        let errs = vec![
209            Error::Config("c".into()),
210            Error::Io(std::io::Error::other("i")),
211            Error::Serde("s".into()),
212            Error::llm("l"),
213            Error::llm_kind(ErrorKind::RateLimited, "r"),
214            Error::Tool("t".into()),
215            Error::Session("se".into()),
216            Error::Agent("a".into()),
217            Error::Provider("p".into()),
218            Error::http("h"),
219            Error::http_kind(ErrorKind::Timeout, "ht"),
220            Error::Other("o".into()),
221        ];
222        for e in &errs {
223            let cloned = e.clone();
224            assert!(!cloned.to_string().is_empty());
225            let _ = format!("{e:?}");
226        }
227        assert_eq!(Error::llm("l").transport_kind(), Some(ErrorKind::Unknown));
228        assert_eq!(
229            Error::http_kind(ErrorKind::Timeout, "ht").transport_kind(),
230            Some(ErrorKind::Timeout)
231        );
232        assert_eq!(Error::Config("c".into()).transport_kind(), None);
233        let json_err = serde_json::from_str::<u8>("not-json").unwrap_err();
234        let e = Error::from(json_err);
235        assert!(matches!(e, Error::Serde(_)));
236    }
237}