Skip to main content

conversation_api/
run_error_code.rs

1//! Closed catalog of public conversation run error codes.
2//!
3//! Host terminalize and Agent `Failed` both emit these strings. `status` and `failure.source`
4//! are properties of the code. Unknown strings classify as `agent_failed` / `agent_runtime`.
5
6use crate::execution::ExternalErrorKind;
7use crate::{FailureSource, RunStatus};
8use serde::{Deserialize, Serialize};
9
10/// Public `runOutcome.errorCode` / Agent `Failed.code` catalog.
11#[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    /// Catalog members in wire order. Tests treat this as the schema source of truth.
50    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    /// `Some` only when the public outcome carries a `failure` object.
164    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}