Skip to main content

open_agent/
error.rs

1//! SDK errors and the shared [`Result`] alias.
2//!
3//! `reqwest::Error` and `serde_json::Error` convert through `From`. Model HTTP
4//! responses retain their status in [`Error::Api`] for retry classification.
5
6use thiserror::Error;
7
8/// A result using the SDK [`enum@Error`] type.
9pub type Result<T> = std::result::Result<T, Error>;
10
11/// Errors from configuration, transport, stream decoding, and tool execution.
12///
13/// `reqwest::Error` and `serde_json::Error` convert automatically through `?`.
14#[derive(Error, Debug)]
15pub enum Error {
16    /// A reqwest transport error, including connection failures and timeouts.
17    #[error("HTTP request failed: {0}")]
18    Http(#[from] reqwest::Error),
19
20    /// A serde JSON serialization or deserialization error.
21    #[error("JSON error: {0}")]
22    Json(#[from] serde_json::Error),
23
24    /// Invalid SDK configuration.
25    #[error("Invalid configuration: {0}")]
26    Config(String),
27
28    /// A model API error with an optional structured HTTP status.
29    ///
30    /// Use [`Error::api_status`] when a status is available. Retry classification uses
31    /// that field, never status-like text in the message.
32    #[error("API error{}: {message}", .status.map(|code| format!(" {code}")).unwrap_or_default())]
33    Api {
34        /// HTTP status code, when the error originated from an HTTP response or from an
35        /// error the server reported inside a stream with an HTTP status as its `code`.
36        status: Option<u16>,
37
38        /// Error message or response body reported by the server.
39        message: String,
40    },
41
42    /// An SSE or stream-processing error.
43    #[error("Streaming error: {0}")]
44    Stream(String),
45
46    /// A tool lookup or execution error.
47    #[error("Tool execution error: {0}")]
48    Tool(String),
49
50    /// Invalid input to an SDK operation.
51    #[error("Invalid input: {0}")]
52    InvalidInput(String),
53
54    /// An explicit SDK timeout error.
55    #[error("Request timeout")]
56    Timeout,
57
58    /// An error without a more specific category.
59    #[error("Error: {0}")]
60    Other(String),
61}
62
63impl Error {
64    /// Creates a configuration error.
65    pub fn config(msg: impl Into<String>) -> Self {
66        Error::Config(msg.into())
67    }
68
69    /// Creates an API error without a status.
70    ///
71    /// It is non-retryable regardless of the message. Use [`api_status`](Self::api_status)
72    /// when the HTTP status is known.
73    pub fn api(msg: impl Into<String>) -> Self {
74        Error::Api {
75            status: None,
76            message: msg.into(),
77        }
78    }
79
80    /// Create a new API error from an HTTP error response.
81    ///
82    /// Prefer this over [`Error::api`] whenever a status code is available: retry
83    /// classification reads [`Error::status_code`], and an error built without a status is
84    /// treated as non-retryable no matter what its message says.
85    ///
86    /// # Arguments
87    ///
88    /// * `status` - HTTP status code from the response
89    /// * `msg` - Response body or error message from the API server
90    ///
91    /// # Example
92    ///
93    /// ```rust
94    /// use open_agent::Error;
95    ///
96    /// let err = Error::api_status(429, "Rate limit exceeded");
97    /// assert_eq!(err.to_string(), "API error 429: Rate limit exceeded");
98    /// assert_eq!(err.status_code(), Some(429));
99    /// ```
100    pub fn api_status(status: u16, msg: impl Into<String>) -> Self {
101        Error::Api {
102            status: Some(status),
103            message: msg.into(),
104        }
105    }
106
107    /// Returns the status carried by [`Error::Api`], or `None`.
108    ///
109    /// No other variant exposes a status through this method.
110    pub fn status_code(&self) -> Option<u16> {
111        match self {
112            Error::Api { status, .. } => *status,
113            _ => None,
114        }
115    }
116
117    /// Creates a stream-processing error.
118    pub fn stream(msg: impl Into<String>) -> Self {
119        Error::Stream(msg.into())
120    }
121
122    /// Creates a tool execution error.
123    pub fn tool(msg: impl Into<String>) -> Self {
124        Error::Tool(msg.into())
125    }
126
127    /// Creates an invalid-input error.
128    pub fn invalid_input(msg: impl Into<String>) -> Self {
129        Error::InvalidInput(msg.into())
130    }
131
132    /// Creates an uncategorized error.
133    pub fn other(msg: impl Into<String>) -> Self {
134        Error::Other(msg.into())
135    }
136
137    /// Creates an explicit timeout error.
138    pub fn timeout() -> Self {
139        Error::Timeout
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146
147    #[test]
148    fn test_error_config() {
149        let err = Error::config("Invalid model");
150        assert!(matches!(err, Error::Config(_)));
151        assert_eq!(err.to_string(), "Invalid configuration: Invalid model");
152    }
153
154    #[test]
155    fn test_error_api() {
156        let err = Error::api("Internal Server Error");
157        assert!(matches!(err, Error::Api { status: None, .. }));
158        assert_eq!(err.to_string(), "API error: Internal Server Error");
159
160        // A status-carrying error renders the code once, not twice, and exposes it directly.
161        let err = Error::api_status(500, "Internal Server Error");
162        assert!(matches!(
163            err,
164            Error::Api {
165                status: Some(500),
166                ..
167            }
168        ));
169        assert_eq!(err.to_string(), "API error 500: Internal Server Error");
170        assert_eq!(err.status_code(), Some(500));
171    }
172
173    #[test]
174    fn test_error_stream() {
175        let err = Error::stream("Connection lost");
176        assert!(matches!(err, Error::Stream(_)));
177        assert_eq!(err.to_string(), "Streaming error: Connection lost");
178    }
179
180    #[test]
181    fn test_error_tool() {
182        let err = Error::tool("Tool not found");
183        assert!(matches!(err, Error::Tool(_)));
184        assert_eq!(err.to_string(), "Tool execution error: Tool not found");
185    }
186
187    #[test]
188    fn test_error_invalid_input() {
189        let err = Error::invalid_input("Missing parameter");
190        assert!(matches!(err, Error::InvalidInput(_)));
191        assert_eq!(err.to_string(), "Invalid input: Missing parameter");
192    }
193
194    #[test]
195    fn test_error_timeout() {
196        let err = Error::timeout();
197        assert!(matches!(err, Error::Timeout));
198        assert_eq!(err.to_string(), "Request timeout");
199    }
200
201    #[test]
202    fn test_error_other() {
203        let err = Error::other("Something went wrong");
204        assert!(matches!(err, Error::Other(_)));
205        assert_eq!(err.to_string(), "Error: Something went wrong");
206    }
207
208    #[test]
209    fn test_error_from_serde_json() {
210        // Test that serde_json::Error can be converted
211        let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
212        let err: Error = json_err.into();
213        assert!(matches!(err, Error::Json(_)));
214    }
215}