1use crate::execution::ExternalErrorKind;
7use crate::{FailureSource, RunStatus};
8use serde::{Deserialize, Serialize};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
13pub enum RunErrorCode {
14 Cancelled,
15 RunInterrupted,
16 ExecutorTerminated,
17 ThreadExpired,
18 ThreadArchived,
19 AgentAdmissionTimeout,
20 AgentDeadlineExceeded,
21 AgentConnectionLost,
22 AgentUnavailable,
23 ProcessInterrupted,
24 Protocol,
25 InvalidEnvelope,
26 InvalidFrame,
27 InvalidJson,
28 StaleDispatch,
29 StaleSession,
30 UnsupportedVersion,
31 InvalidFamily,
32 InvalidRequest,
33 Unauthorized,
34 Unavailable,
35 RateLimited,
36 ContextLimit,
37 Timeout,
38 Internal,
39 OutputTruncated,
40 CreditBudgetExceeded,
41 ThreadBusy,
42 AdmissionRejected,
43 AgentExecutionFailed,
44 InvalidModelFinish,
45 InvalidModelToolCalls,
46}
47
48impl RunErrorCode {
49 pub const ALL: &[Self] = &[
51 Self::Cancelled,
52 Self::RunInterrupted,
53 Self::ExecutorTerminated,
54 Self::ThreadExpired,
55 Self::ThreadArchived,
56 Self::AgentAdmissionTimeout,
57 Self::AgentDeadlineExceeded,
58 Self::AgentConnectionLost,
59 Self::AgentUnavailable,
60 Self::ProcessInterrupted,
61 Self::Protocol,
62 Self::InvalidEnvelope,
63 Self::InvalidFrame,
64 Self::InvalidJson,
65 Self::StaleDispatch,
66 Self::StaleSession,
67 Self::UnsupportedVersion,
68 Self::InvalidFamily,
69 Self::InvalidRequest,
70 Self::Unauthorized,
71 Self::Unavailable,
72 Self::RateLimited,
73 Self::ContextLimit,
74 Self::Timeout,
75 Self::Internal,
76 Self::OutputTruncated,
77 Self::CreditBudgetExceeded,
78 Self::ThreadBusy,
79 Self::AdmissionRejected,
80 Self::AgentExecutionFailed,
81 Self::InvalidModelFinish,
82 Self::InvalidModelToolCalls,
83 ];
84
85 pub const fn as_str(self) -> &'static str {
86 match self {
87 Self::Cancelled => "CANCELLED",
88 Self::RunInterrupted => "RUN_INTERRUPTED",
89 Self::ExecutorTerminated => "EXECUTOR_TERMINATED",
90 Self::ThreadExpired => "THREAD_EXPIRED",
91 Self::ThreadArchived => "THREAD_ARCHIVED",
92 Self::AgentAdmissionTimeout => "AGENT_ADMISSION_TIMEOUT",
93 Self::AgentDeadlineExceeded => "AGENT_DEADLINE_EXCEEDED",
94 Self::AgentConnectionLost => "AGENT_CONNECTION_LOST",
95 Self::AgentUnavailable => "AGENT_UNAVAILABLE",
96 Self::ProcessInterrupted => "PROCESS_INTERRUPTED",
97 Self::Protocol => "PROTOCOL",
98 Self::InvalidEnvelope => "INVALID_ENVELOPE",
99 Self::InvalidFrame => "INVALID_FRAME",
100 Self::InvalidJson => "INVALID_JSON",
101 Self::StaleDispatch => "STALE_DISPATCH",
102 Self::StaleSession => "STALE_SESSION",
103 Self::UnsupportedVersion => "UNSUPPORTED_VERSION",
104 Self::InvalidFamily => "INVALID_FAMILY",
105 Self::InvalidRequest => "INVALID_REQUEST",
106 Self::Unauthorized => "UNAUTHORIZED",
107 Self::Unavailable => "UNAVAILABLE",
108 Self::RateLimited => "RATE_LIMITED",
109 Self::ContextLimit => "CONTEXT_LIMIT",
110 Self::Timeout => "TIMEOUT",
111 Self::Internal => "INTERNAL",
112 Self::OutputTruncated => "OUTPUT_TRUNCATED",
113 Self::CreditBudgetExceeded => "CREDIT_BUDGET_EXCEEDED",
114 Self::ThreadBusy => "THREAD_BUSY",
115 Self::AdmissionRejected => "ADMISSION_REJECTED",
116 Self::AgentExecutionFailed => "AGENT_EXECUTION_FAILED",
117 Self::InvalidModelFinish => "INVALID_MODEL_FINISH",
118 Self::InvalidModelToolCalls => "INVALID_MODEL_TOOL_CALLS",
119 }
120 }
121
122 pub fn parse(raw: &str) -> Option<Self> {
123 Self::ALL.iter().copied().find(|code| code.as_str() == raw)
124 }
125
126 pub const fn status(self) -> RunStatus {
127 match self {
128 Self::Cancelled
129 | Self::RunInterrupted
130 | Self::ExecutorTerminated
131 | Self::ThreadExpired
132 | Self::ThreadArchived => RunStatus::Interrupted,
133 Self::AgentAdmissionTimeout
134 | Self::AgentDeadlineExceeded
135 | Self::AgentConnectionLost
136 | Self::AgentUnavailable
137 | Self::ProcessInterrupted
138 | Self::Protocol
139 | Self::InvalidEnvelope
140 | Self::InvalidFrame
141 | Self::InvalidJson
142 | Self::StaleDispatch
143 | Self::StaleSession
144 | Self::UnsupportedVersion
145 | Self::InvalidFamily => RunStatus::SystemFailed,
146 Self::InvalidRequest
147 | Self::Unauthorized
148 | Self::Unavailable
149 | Self::RateLimited
150 | Self::ContextLimit
151 | Self::Timeout
152 | Self::Internal
153 | Self::OutputTruncated
154 | Self::CreditBudgetExceeded
155 | Self::ThreadBusy
156 | Self::AdmissionRejected
157 | Self::AgentExecutionFailed
158 | Self::InvalidModelFinish
159 | Self::InvalidModelToolCalls => RunStatus::AgentFailed,
160 }
161 }
162
163 pub const fn failure_source(self) -> Option<FailureSource> {
165 match self.status() {
166 RunStatus::AgentFailed => Some(match self {
167 Self::InvalidRequest
168 | Self::Unauthorized
169 | Self::Unavailable
170 | Self::RateLimited
171 | Self::ContextLimit
172 | Self::Timeout
173 | Self::Internal
174 | Self::OutputTruncated => FailureSource::LlmProvider,
175 _ => FailureSource::AgentRuntime,
176 }),
177 _ => None,
178 }
179 }
180
181 pub fn status_of(code: &str) -> RunStatus {
182 Self::parse(code)
183 .map(Self::status)
184 .unwrap_or(RunStatus::AgentFailed)
185 }
186
187 pub fn failure_source_of(code: &str) -> Option<FailureSource> {
188 Self::parse(code)
189 .and_then(Self::failure_source)
190 .or(if code.is_empty() {
191 None
192 } else {
193 Some(FailureSource::AgentRuntime)
194 })
195 }
196}
197
198impl From<ExternalErrorKind> for RunErrorCode {
199 fn from(kind: ExternalErrorKind) -> Self {
200 match kind {
201 ExternalErrorKind::InvalidRequest => Self::InvalidRequest,
202 ExternalErrorKind::Unauthorized => Self::Unauthorized,
203 ExternalErrorKind::Unavailable => Self::Unavailable,
204 ExternalErrorKind::RateLimited => Self::RateLimited,
205 ExternalErrorKind::ContextLimit => Self::ContextLimit,
206 ExternalErrorKind::CreditBudgetExceeded => Self::CreditBudgetExceeded,
207 ExternalErrorKind::Timeout => Self::Timeout,
208 ExternalErrorKind::Cancelled => Self::Cancelled,
209 ExternalErrorKind::Internal => Self::Internal,
210 }
211 }
212}
213
214impl AsRef<str> for RunErrorCode {
215 fn as_ref(&self) -> &str {
216 self.as_str()
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 #[test]
225 fn catalog_strings_round_trip_and_are_unique() {
226 let mut seen = std::collections::BTreeSet::new();
227 for code in RunErrorCode::ALL {
228 assert!(seen.insert(code.as_str()), "{}", code.as_str());
229 assert_eq!(RunErrorCode::parse(code.as_str()), Some(*code));
230 assert_eq!(
231 serde_json::from_value::<RunErrorCode>(serde_json::json!(code.as_str())).unwrap(),
232 *code
233 );
234 }
235 assert_eq!(seen.len(), RunErrorCode::ALL.len());
236 assert_eq!(RunErrorCode::parse("NOT_A_CATALOG_CODE"), None);
237 }
238
239 #[test]
240 fn status_and_source_are_properties_of_the_code() {
241 assert_eq!(RunErrorCode::Cancelled.status(), RunStatus::Interrupted);
242 assert_eq!(RunErrorCode::Cancelled.failure_source(), None);
243 assert_eq!(
244 RunErrorCode::AgentDeadlineExceeded.status(),
245 RunStatus::SystemFailed
246 );
247 assert_eq!(RunErrorCode::Unauthorized.status(), RunStatus::AgentFailed);
248 assert_eq!(
249 RunErrorCode::Unauthorized.failure_source(),
250 Some(FailureSource::LlmProvider)
251 );
252 assert_eq!(
253 RunErrorCode::CreditBudgetExceeded.failure_source(),
254 Some(FailureSource::AgentRuntime)
255 );
256 assert_eq!(RunErrorCode::status_of("CANCELLED"), RunStatus::Interrupted);
257 assert_eq!(
258 RunErrorCode::status_of("THREAD_BUSY"),
259 RunStatus::AgentFailed
260 );
261 assert_eq!(RunErrorCode::status_of("MADE_UP"), RunStatus::AgentFailed);
262 assert_eq!(
263 RunErrorCode::failure_source_of("MADE_UP"),
264 Some(FailureSource::AgentRuntime)
265 );
266 assert_eq!(
267 RunErrorCode::from(ExternalErrorKind::RateLimited),
268 RunErrorCode::RateLimited
269 );
270 }
271}