agent_framework_core/error.rs
1//! Error types for the agent framework.
2
3use std::fmt;
4
5/// The result type used throughout the framework.
6pub type Result<T> = std::result::Result<T, Error>;
7
8/// The primary error type for the agent framework.
9///
10/// This mirrors the exception hierarchy used by the Python
11/// `agent_framework.exceptions` module while remaining idiomatic Rust.
12#[derive(Debug, thiserror::Error)]
13#[non_exhaustive]
14pub enum Error {
15 /// An error occurred while initializing an agent.
16 #[error("agent initialization error: {0}")]
17 AgentInitialization(String),
18
19 /// An error occurred while executing an agent run.
20 #[error("agent execution error: {0}")]
21 AgentExecution(String),
22
23 /// An error occurred while (de)serializing a value.
24 #[error("serialization error: {0}")]
25 Serialization(String),
26
27 /// A content item could not be parsed or was of an unknown type.
28 #[error("content error: {0}")]
29 Content(String),
30
31 /// A tool/function invocation failed.
32 #[error("tool error: {0}")]
33 Tool(String),
34
35 /// A chat client / service returned an error.
36 ///
37 /// Used for non-HTTP service failures (transport errors, stream-decode
38 /// errors, in-body error payloads on an otherwise-successful response). For
39 /// a non-success HTTP status, prefer [`Error::ServiceStatus`], which also
40 /// carries the status code and any `Retry-After`.
41 #[error("service error: {0}")]
42 Service(String),
43
44 /// A chat client / service returned a non-success HTTP status.
45 ///
46 /// Distinct from [`Error::Service`] so a retry layer can inspect the
47 /// numeric status code and any server-advised `Retry-After` delay (in
48 /// seconds). Displays like [`Error::Service`] (a `service error: ...`
49 /// message), with the status code folded into the message.
50 ///
51 /// This is the fallback classification for a non-success status that
52 /// isn't one of the more specific variants below (notably `408`/`429`/
53 /// `5xx`, which a retry layer treats as transient) — see
54 /// [`Error::ServiceInvalidAuth`], [`Error::ServiceInvalidRequest`], and
55 /// [`Error::ServiceContentFilter`] for statuses a provider client can
56 /// classify more precisely.
57 #[error("service error: {message}")]
58 ServiceStatus {
59 /// The HTTP status code returned by the service.
60 status: u16,
61 /// A human-readable message (typically the response body).
62 message: String,
63 /// The server-advised retry delay in seconds, parsed from the
64 /// `Retry-After` header when present.
65 retry_after: Option<f64>,
66 },
67
68 /// The service rejected the request due to missing or invalid
69 /// credentials (typically HTTP `401`/`403`).
70 ///
71 /// Mirrors upstream's `ServiceInvalidAuthError`. Like [`Error::Service`],
72 /// this carries no status code of its own (the numeric status, when
73 /// known, is folded into the message by the provider client) — it exists
74 /// so callers, and the default retry policy, can treat authentication /
75 /// authorization failures as definitively non-transient without
76 /// inspecting a status code themselves. Never retried by
77 /// [`RetryOn::Default`](crate::client::RetryOn::Default).
78 #[error("service error: {message}")]
79 ServiceInvalidAuth {
80 /// A human-readable message (typically the response body).
81 message: String,
82 },
83
84 /// The service rejected the request as malformed or otherwise invalid
85 /// (typically HTTP `400`/`404`/`422`) for a reason other than content
86 /// filtering.
87 ///
88 /// Mirrors upstream's `ServiceInvalidRequestError`. See
89 /// [`Error::ServiceContentFilter`] for the content-filter-specific case.
90 /// Never retried by [`RetryOn::Default`](crate::client::RetryOn::Default)
91 /// — a request that was rejected as invalid will be rejected again
92 /// unchanged.
93 #[error("service error: {message}")]
94 ServiceInvalidRequest {
95 /// A human-readable message (typically the response body).
96 message: String,
97 },
98
99 /// The service refused the request (or part of a response) because it
100 /// tripped a content filter / moderation policy.
101 ///
102 /// Mirrors upstream's `ServiceContentFilterException`
103 /// (`OpenAIContentFilterException` for OpenAI/Azure OpenAI specifically).
104 /// Never retried by [`RetryOn::Default`](crate::client::RetryOn::Default)
105 /// — the content, not the service, is the problem.
106 #[error("service error: {message}")]
107 ServiceContentFilter {
108 /// A human-readable message (typically the response body).
109 message: String,
110 },
111
112 /// A workflow validation or execution error.
113 #[error("workflow error: {0}")]
114 Workflow(String),
115
116 /// Two streamed content items could not be merged (mismatched ids).
117 #[error("addition item mismatch: {0}")]
118 AdditionItemMismatch(String),
119
120 /// A required configuration value was missing or invalid.
121 #[error("configuration error: {0}")]
122 Configuration(String),
123
124 /// An underlying JSON error.
125 #[error("json error: {0}")]
126 Json(#[from] serde_json::Error),
127
128 /// Any other error, wrapping a boxed source.
129 #[error("{0}")]
130 Other(String),
131}
132
133impl Error {
134 /// Create an [`Error::Other`] from anything displayable.
135 pub fn other(msg: impl fmt::Display) -> Self {
136 Error::Other(msg.to_string())
137 }
138
139 /// Create an [`Error::Service`] from anything displayable.
140 pub fn service(msg: impl fmt::Display) -> Self {
141 Error::Service(msg.to_string())
142 }
143
144 /// Create an [`Error::ServiceStatus`] from an HTTP status code, a message,
145 /// and an optional `Retry-After` delay (in seconds).
146 pub fn service_status(status: u16, msg: impl fmt::Display, retry_after: Option<f64>) -> Self {
147 Error::ServiceStatus {
148 status,
149 message: msg.to_string(),
150 retry_after,
151 }
152 }
153
154 /// Create an [`Error::ServiceInvalidAuth`] from anything displayable.
155 pub fn service_invalid_auth(msg: impl fmt::Display) -> Self {
156 Error::ServiceInvalidAuth {
157 message: msg.to_string(),
158 }
159 }
160
161 /// Create an [`Error::ServiceInvalidRequest`] from anything displayable.
162 pub fn service_invalid_request(msg: impl fmt::Display) -> Self {
163 Error::ServiceInvalidRequest {
164 message: msg.to_string(),
165 }
166 }
167
168 /// Create an [`Error::ServiceContentFilter`] from anything displayable.
169 pub fn service_content_filter(msg: impl fmt::Display) -> Self {
170 Error::ServiceContentFilter {
171 message: msg.to_string(),
172 }
173 }
174
175 /// The HTTP status code carried by this error, if it is an
176 /// [`Error::ServiceStatus`].
177 pub fn status(&self) -> Option<u16> {
178 match self {
179 Error::ServiceStatus { status, .. } => Some(*status),
180 _ => None,
181 }
182 }
183
184 /// The server-advised retry delay in seconds, if this is an
185 /// [`Error::ServiceStatus`] that carried a `Retry-After` header.
186 pub fn retry_after(&self) -> Option<f64> {
187 match self {
188 Error::ServiceStatus { retry_after, .. } => *retry_after,
189 _ => None,
190 }
191 }
192
193 /// Create an [`Error::Tool`] from anything displayable.
194 pub fn tool(msg: impl fmt::Display) -> Self {
195 Error::Tool(msg.to_string())
196 }
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202
203 #[test]
204 fn service_invalid_auth_constructor_and_display() {
205 let err = Error::service_invalid_auth("OpenAI API error 401: unauthorized");
206 assert!(matches!(err, Error::ServiceInvalidAuth { .. }));
207 assert_eq!(
208 err.to_string(),
209 "service error: OpenAI API error 401: unauthorized"
210 );
211 // Not a `ServiceStatus`, so it carries no status/retry_after of its
212 // own (the numeric status lives in the message text instead).
213 assert_eq!(err.status(), None);
214 assert_eq!(err.retry_after(), None);
215 }
216
217 #[test]
218 fn service_invalid_request_constructor_and_display() {
219 let err = Error::service_invalid_request("OpenAI API error 400: bad request");
220 assert!(matches!(err, Error::ServiceInvalidRequest { .. }));
221 assert_eq!(
222 err.to_string(),
223 "service error: OpenAI API error 400: bad request"
224 );
225 }
226
227 #[test]
228 fn service_content_filter_constructor_and_display() {
229 let err = Error::service_content_filter("OpenAI API error 400: content filtered");
230 assert!(matches!(err, Error::ServiceContentFilter { .. }));
231 assert_eq!(
232 err.to_string(),
233 "service error: OpenAI API error 400: content filtered"
234 );
235 }
236
237 /// The new variants must not silently become retryable-looking:
238 /// `status()`/`retry_after()` only ever return `Some` for
239 /// [`Error::ServiceStatus`].
240 #[test]
241 fn new_variants_are_not_service_status() {
242 for err in [
243 Error::service_invalid_auth("x"),
244 Error::service_invalid_request("x"),
245 Error::service_content_filter("x"),
246 ] {
247 assert_eq!(err.status(), None, "{err:?}");
248 assert_eq!(err.retry_after(), None, "{err:?}");
249 }
250 }
251}