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