Skip to main content

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}