agentyk_core/error.rs
1//! Crate error type.
2
3use std::fmt;
4
5/// Coarse classification of an LLM driver failure, so a host (in particular
6/// a durable executor retrying activities) can tell a transient failure
7/// worth retrying apart from a terminal one that will never succeed by
8/// itself. Mirrors the intent of everruns' `LlmErrorKind`.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10#[non_exhaustive]
11pub enum LlmErrorKind {
12 /// The provider rejected the request for being over its rate limit
13 /// (HTTP 429). Retry after a backoff.
14 RateLimited,
15 /// The provider is temporarily overloaded (HTTP 503, Anthropic 529).
16 /// Retry after a backoff.
17 Overloaded,
18 /// The request timed out before a response arrived. Retry.
19 Timeout,
20 /// A transport-level failure (connection refused, DNS, TLS, a stream
21 /// read that dropped mid-response). Retry.
22 Network,
23 /// Generic server-side failure (HTTP 5xx not otherwise classified).
24 /// Usually worth one retry.
25 ServerError,
26 /// Missing or rejected credentials (HTTP 401/403, or no api key was
27 /// configured at all). Will never succeed without a config change —
28 /// do not retry.
29 Authentication,
30 /// The request itself was malformed (HTTP 400/404/422 or similar).
31 /// Will never succeed unchanged — do not retry.
32 InvalidRequest,
33 /// Unclassified — an unexpected response shape, a decode failure, or a
34 /// status code with no clear category. Conservative default: do not
35 /// retry blindly.
36 Unknown,
37}
38
39impl LlmErrorKind {
40 /// Whether a host should consider retrying the request after a backoff.
41 pub fn is_retryable(&self) -> bool {
42 matches!(
43 self,
44 LlmErrorKind::RateLimited
45 | LlmErrorKind::Overloaded
46 | LlmErrorKind::Timeout
47 | LlmErrorKind::Network
48 | LlmErrorKind::ServerError
49 )
50 }
51}
52
53impl fmt::Display for LlmErrorKind {
54 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55 let label = match self {
56 LlmErrorKind::RateLimited => "rate_limited",
57 LlmErrorKind::Overloaded => "overloaded",
58 LlmErrorKind::Timeout => "timeout",
59 LlmErrorKind::Network => "network",
60 LlmErrorKind::ServerError => "server_error",
61 LlmErrorKind::Authentication => "authentication",
62 LlmErrorKind::InvalidRequest => "invalid_request",
63 LlmErrorKind::Unknown => "unknown",
64 };
65 f.write_str(label)
66 }
67}
68
69/// Errors surfaced by agentyk.
70#[derive(Debug, thiserror::Error)]
71#[non_exhaustive]
72pub enum Error {
73 /// The agent was composed without a model, or is otherwise invalid.
74 #[error("invalid agent: {0}")]
75 InvalidAgent(String),
76
77 /// No driver registered for the model's `DriverId`.
78 #[error("no chat driver registered for `{0}`")]
79 UnknownDriver(String),
80
81 /// A turn hit the iteration ceiling without producing a final response.
82 #[error("turn exceeded max iterations ({0})")]
83 MaxIterations(usize),
84
85 /// The turn was cancelled via a [`crate::cancellation::CancellationToken`].
86 #[error("turn cancelled")]
87 Cancelled,
88
89 /// The turn was deliberately sealed — see [`crate::turn::SealReason`].
90 #[error("turn sealed: {0:?}")]
91 Sealed(crate::turn::SealReason),
92
93 /// A blockable user hook refused the prompt or tool action.
94 #[error("hook blocked execution: {reason}")]
95 HookBlocked {
96 /// Diagnostic reason supplied by the hook.
97 reason: String,
98 /// Optional message suitable for a user-facing surface.
99 user_message: Option<String>,
100 },
101
102 /// An LLM driver failed. `kind` classifies whether retrying is worth it
103 /// — see [`Error::is_retryable`].
104 #[error("driver error ({kind}): {message}")]
105 Driver {
106 /// Whether retrying is worth it.
107 kind: LlmErrorKind,
108 /// What the provider or transport reported.
109 message: String,
110 },
111
112 /// An MCP server or transport failed.
113 #[error("mcp error: {0}")]
114 Mcp(String),
115
116 /// The event log failed to append or read.
117 #[error("event log error: {0}")]
118 EventLog(String),
119
120 /// An append expected a different end-of-stream sequence.
121 #[error("event stream conflict: expected version {expected}, found {actual}")]
122 EventConflict {
123 /// Version supplied by the writer.
124 expected: u64,
125 /// Version currently stored.
126 actual: u64,
127 },
128
129 /// Local I/O failed — an event log file, a workspace read.
130 #[error(transparent)]
131 Io(#[from] std::io::Error),
132
133 /// Serialization or deserialization failed.
134 #[error(transparent)]
135 Json(#[from] serde_json::Error),
136
137 /// Anything without a more specific variant.
138 #[error("{0}")]
139 Other(String),
140}
141
142impl Error {
143 /// Shorthand for `Error::Driver { kind, message: message.into() }`.
144 pub fn driver(kind: LlmErrorKind, message: impl Into<String>) -> Self {
145 Error::Driver {
146 kind,
147 message: message.into(),
148 }
149 }
150
151 /// Whether a host should consider retrying the operation that produced
152 /// this error. Only [`Error::Driver`] carries enough information to say
153 /// yes; every other variant is a local/config problem retrying can't fix.
154 pub fn is_retryable(&self) -> bool {
155 match self {
156 Error::Driver { kind, .. } => kind.is_retryable(),
157 _ => false,
158 }
159 }
160}
161
162/// `Result` with this crate's [`Error`].
163pub type Result<T> = std::result::Result<T, Error>;
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn retryable_kinds_match_the_documented_set() {
171 assert!(LlmErrorKind::RateLimited.is_retryable());
172 assert!(LlmErrorKind::Overloaded.is_retryable());
173 assert!(LlmErrorKind::Timeout.is_retryable());
174 assert!(LlmErrorKind::Network.is_retryable());
175 assert!(LlmErrorKind::ServerError.is_retryable());
176 assert!(!LlmErrorKind::Authentication.is_retryable());
177 assert!(!LlmErrorKind::InvalidRequest.is_retryable());
178 assert!(!LlmErrorKind::Unknown.is_retryable());
179 }
180
181 #[test]
182 fn error_is_retryable_delegates_to_driver_kind_only() {
183 assert!(Error::driver(LlmErrorKind::RateLimited, "429").is_retryable());
184 assert!(!Error::driver(LlmErrorKind::Authentication, "401").is_retryable());
185 assert!(!Error::Cancelled.is_retryable());
186 assert!(!Error::MaxIterations(4).is_retryable());
187 }
188}