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