open_agent/
error.rs

1//! # Error Types for the Open Agent SDK
2//!
3//! This module defines all error types used throughout the SDK, providing comprehensive
4//! error handling with detailed context for different failure scenarios.
5//!
6//! ## Design Philosophy
7//!
8//! - **Explicit Error Handling**: Uses Rust's `Result<T>` type for all fallible operations
9//! - **No Silent Failures**: All errors are propagated explicitly to the caller
10//! - **Rich Context**: Each error variant provides specific information about what went wrong
11//! - **Easy Conversion**: Automatic conversion from common error types (reqwest, serde_json)
12//!
13//! ## Usage
14//!
15//! ```ignore
16//! use open_agent::{Error, Result};
17//!
18//! fn example() -> Result<()> {
19//!     // Errors can be created using convenience methods
20//!     if some_condition {
21//!         return Err(Error::config("Invalid model name"));
22//!     }
23//!
24//!     // Or automatically converted from reqwest/serde_json errors
25//!     let response = http_client.get(url).send().await?; // Auto-converts to Error::Http
26//!     let json = serde_json::from_str(data)?; // Auto-converts to Error::Json
27//!
28//!     Ok(())
29//! }
30//! ```
31
32use thiserror::Error;
33
34// ============================================================================
35// TYPE ALIASES
36// ============================================================================
37
38/// Type alias for `Result<T, Error>` used throughout the SDK.
39///
40/// This makes function signatures more concise and ensures consistent error handling
41/// across the entire API surface. Instead of writing `std::result::Result<T, Error>`,
42/// you can simply write `Result<T>`.
43///
44/// # Example
45///
46/// ```rust
47/// use open_agent::Result;
48///
49/// async fn send_request() -> Result<String> {
50///     // Function body
51///     Ok("Success".to_string())
52/// }
53/// ```
54pub type Result<T> = std::result::Result<T, Error>;
55
56// ============================================================================
57// ERROR ENUM
58// ============================================================================
59
60/// Comprehensive error type covering all failure modes in the SDK.
61///
62/// This enum uses the `thiserror` crate to automatically implement `std::error::Error`
63/// and provide well-formatted error messages. Each variant represents a different
64/// category of failure that can occur during SDK operation.
65///
66/// ## Error Categories
67///
68/// - **HTTP**: Network communication failures (connection errors, timeouts, etc.)
69/// - **JSON**: Serialization/deserialization failures
70/// - **Config**: Invalid configuration parameters
71/// - **Api**: Error responses from the model server
72/// - **Stream**: Failures during streaming response processing
73/// - **Tool**: Tool execution or registration failures
74/// - **InvalidInput**: User-provided input validation failures
75/// - **Timeout**: Request timeout exceeded
76/// - **Other**: Catch-all for miscellaneous errors
77///
78/// ## Automatic Conversions
79///
80/// The `#[from]` attribute on `Http` and `Json` variants enables automatic conversion
81/// from `reqwest::Error` and `serde_json::Error` using the `?` operator, making
82/// error propagation seamless.
83#[derive(Error, Debug)]
84pub enum Error {
85    /// HTTP request failed due to network issues, connection problems, or HTTP errors.
86    ///
87    /// This variant wraps `reqwest::Error` and is automatically created when using
88    /// the `?` operator on reqwest operations. Common causes include:
89    /// - Connection refused (server not running)
90    /// - DNS resolution failures
91    /// - TLS/SSL certificate errors
92    /// - HTTP status errors (4xx, 5xx)
93    /// - Network timeouts
94    ///
95    /// # Example
96    ///
97    /// ```rust,ignore
98    /// let response = client.post(url).send().await?; // Auto-converts reqwest::Error
99    /// ```
100    #[error("HTTP request failed: {0}")]
101    Http(#[from] reqwest::Error),
102
103    /// JSON serialization or deserialization failed.
104    ///
105    /// This variant wraps `serde_json::Error` and occurs when:
106    /// - Parsing invalid JSON from the API
107    /// - Serializing request data fails
108    /// - JSON structure doesn't match expected schema
109    /// - Required fields are missing in JSON
110    ///
111    /// # Example
112    ///
113    /// ```rust,ignore
114    /// let value: MyType = serde_json::from_str(json_str)?; // Auto-converts serde_json::Error
115    /// ```
116    #[error("JSON error: {0}")]
117    Json(#[from] serde_json::Error),
118
119    /// Invalid configuration provided when building AgentOptions.
120    ///
121    /// Occurs during the builder pattern validation phase when required fields
122    /// are missing or invalid values are provided. Common causes:
123    /// - Missing required fields (model, base_url, system_prompt)
124    /// - Invalid URL format in base_url
125    /// - Invalid timeout values
126    /// - Invalid max_tokens or temperature ranges
127    ///
128    /// # Example
129    ///
130    /// ```rust,ignore
131    /// return Err(Error::config("base_url is required"));
132    /// ```
133    #[error("Invalid configuration: {0}")]
134    Config(String),
135
136    /// Error response received from the model server's API.
137    ///
138    /// This indicates the HTTP request succeeded, but the API returned an error
139    /// response. Common causes:
140    /// - Model not found on the server
141    /// - Invalid API key or authentication failure
142    /// - Rate limiting
143    /// - Server-side errors (500, 502, 503)
144    /// - Invalid request format
145    ///
146    /// # Example
147    ///
148    /// ```rust,ignore
149    /// return Err(Error::api("Model 'gpt-4' not found on server"));
150    /// ```
151    #[error("API error: {0}")]
152    Api(String),
153
154    /// Error occurred while processing the streaming response.
155    ///
156    /// This happens during Server-Sent Events (SSE) parsing or stream processing.
157    /// Common causes:
158    /// - Malformed SSE data
159    /// - Connection interrupted mid-stream
160    /// - Unexpected end of stream
161    /// - Invalid chunk format
162    ///
163    /// # Example
164    ///
165    /// ```rust,ignore
166    /// return Err(Error::stream("Unexpected end of SSE stream"));
167    /// ```
168    #[error("Streaming error: {0}")]
169    Stream(String),
170
171    /// Tool execution or registration failed.
172    ///
173    /// Occurs when there are problems with tool definitions or execution:
174    /// - Tool handler returns an error
175    /// - Tool input validation fails
176    /// - Tool name collision during registration
177    /// - Tool not found when executing
178    /// - Invalid tool schema
179    ///
180    /// # Example
181    ///
182    /// ```rust,ignore
183    /// return Err(Error::tool("Tool 'calculator' not found"));
184    /// ```
185    #[error("Tool execution error: {0}")]
186    Tool(String),
187
188    /// Invalid input provided by the user.
189    ///
190    /// Validation error for user-provided data that doesn't meet requirements:
191    /// - Empty prompt string
192    /// - Invalid parameter format
193    /// - Out of range values
194    /// - Malformed input data
195    ///
196    /// # Example
197    ///
198    /// ```rust,ignore
199    /// return Err(Error::invalid_input("Prompt cannot be empty"));
200    /// ```
201    #[error("Invalid input: {0}")]
202    InvalidInput(String),
203
204    /// Request exceeded the configured timeout duration.
205    ///
206    /// The operation took longer than the timeout specified in AgentOptions.
207    /// This is a dedicated variant (no message needed) because the cause is clear.
208    ///
209    /// # Example
210    ///
211    /// ```rust,ignore
212    /// return Err(Error::timeout());
213    /// ```
214    #[error("Request timeout")]
215    Timeout,
216
217    /// Miscellaneous error that doesn't fit other categories.
218    ///
219    /// Catch-all variant for unexpected errors or edge cases that don't fit
220    /// into the specific categories above. Should be used sparingly.
221    ///
222    /// # Example
223    ///
224    /// ```rust,ignore
225    /// return Err(Error::other("Unexpected condition occurred"));
226    /// ```
227    #[error("Error: {0}")]
228    Other(String),
229}
230
231// ============================================================================
232// CONVENIENCE CONSTRUCTORS
233// ============================================================================
234
235/// Implementation of convenience constructors for creating Error instances.
236///
237/// These methods provide a more ergonomic API for creating errors compared to
238/// directly constructing the enum variants. They accept `impl Into<String>`,
239/// allowing callers to pass `&str`, `String`, or any other type that converts to `String`.
240impl Error {
241    /// Create a new configuration error with a descriptive message.
242    ///
243    /// Use this when validation fails during `AgentOptions` construction or when
244    /// invalid configuration values are detected.
245    ///
246    /// # Arguments
247    ///
248    /// * `msg` - Error description explaining what configuration is invalid
249    ///
250    /// # Example
251    ///
252    /// ```rust
253    /// use open_agent::Error;
254    ///
255    /// let err = Error::config("base_url must be a valid HTTP or HTTPS URL");
256    /// assert_eq!(err.to_string(), "Invalid configuration: base_url must be a valid HTTP or HTTPS URL");
257    /// ```
258    pub fn config(msg: impl Into<String>) -> Self {
259        Error::Config(msg.into())
260    }
261
262    /// Create a new API error with the server's error message.
263    ///
264    /// Use this when the API returns an error response (even if the HTTP request
265    /// itself succeeded). This typically happens when the server rejects the request
266    /// due to invalid parameters, missing resources, or server-side failures.
267    ///
268    /// # Arguments
269    ///
270    /// * `msg` - Error message from the API server
271    ///
272    /// # Example
273    ///
274    /// ```rust
275    /// use open_agent::Error;
276    ///
277    /// let err = Error::api("Model 'invalid-model' not found");
278    /// assert_eq!(err.to_string(), "API error: Model 'invalid-model' not found");
279    /// ```
280    pub fn api(msg: impl Into<String>) -> Self {
281        Error::Api(msg.into())
282    }
283
284    /// Create a new streaming error for SSE parsing or stream processing failures.
285    ///
286    /// Use this when errors occur during Server-Sent Events stream parsing,
287    /// such as malformed data, unexpected stream termination, or invalid chunks.
288    ///
289    /// # Arguments
290    ///
291    /// * `msg` - Description of the streaming failure
292    ///
293    /// # Example
294    ///
295    /// ```rust
296    /// use open_agent::Error;
297    ///
298    /// let err = Error::stream("Unexpected end of SSE stream");
299    /// assert_eq!(err.to_string(), "Streaming error: Unexpected end of SSE stream");
300    /// ```
301    pub fn stream(msg: impl Into<String>) -> Self {
302        Error::Stream(msg.into())
303    }
304
305    /// Create a new tool execution error.
306    ///
307    /// Use this when tool registration, lookup, or execution fails. This includes
308    /// tool handler errors, missing tools, and invalid tool inputs.
309    ///
310    /// # Arguments
311    ///
312    /// * `msg` - Description of the tool failure
313    ///
314    /// # Example
315    ///
316    /// ```rust
317    /// use open_agent::Error;
318    ///
319    /// let err = Error::tool("Calculator tool failed: division by zero");
320    /// assert_eq!(err.to_string(), "Tool execution error: Calculator tool failed: division by zero");
321    /// ```
322    pub fn tool(msg: impl Into<String>) -> Self {
323        Error::Tool(msg.into())
324    }
325
326    /// Create a new invalid input error for user input validation failures.
327    ///
328    /// Use this when user-provided data doesn't meet requirements, such as
329    /// empty strings, out-of-range values, or malformed data.
330    ///
331    /// # Arguments
332    ///
333    /// * `msg` - Description of why the input is invalid
334    ///
335    /// # Example
336    ///
337    /// ```rust
338    /// use open_agent::Error;
339    ///
340    /// let err = Error::invalid_input("Prompt cannot be empty");
341    /// assert_eq!(err.to_string(), "Invalid input: Prompt cannot be empty");
342    /// ```
343    pub fn invalid_input(msg: impl Into<String>) -> Self {
344        Error::InvalidInput(msg.into())
345    }
346
347    /// Create a new miscellaneous error for cases that don't fit other categories.
348    ///
349    /// Use this sparingly for unexpected conditions that don't fit into the
350    /// more specific error variants.
351    ///
352    /// # Arguments
353    ///
354    /// * `msg` - Description of the error
355    ///
356    /// # Example
357    ///
358    /// ```rust
359    /// use open_agent::Error;
360    ///
361    /// let err = Error::other("Unexpected internal state");
362    /// assert_eq!(err.to_string(), "Error: Unexpected internal state");
363    /// ```
364    pub fn other(msg: impl Into<String>) -> Self {
365        Error::Other(msg.into())
366    }
367
368    /// Create a timeout error indicating the operation exceeded the time limit.
369    ///
370    /// Use this when the request or operation takes longer than the configured
371    /// timeout duration. No message is needed since the cause is self-explanatory.
372    ///
373    /// # Example
374    ///
375    /// ```rust
376    /// use open_agent::Error;
377    ///
378    /// let err = Error::timeout();
379    /// assert_eq!(err.to_string(), "Request timeout");
380    /// ```
381    pub fn timeout() -> Self {
382        Error::Timeout
383    }
384}
385
386// ============================================================================
387// TESTS
388// ============================================================================
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393
394    #[test]
395    fn test_error_config() {
396        let err = Error::config("Invalid model");
397        assert!(matches!(err, Error::Config(_)));
398        assert_eq!(err.to_string(), "Invalid configuration: Invalid model");
399    }
400
401    #[test]
402    fn test_error_api() {
403        let err = Error::api("500 Internal Server Error");
404        assert!(matches!(err, Error::Api(_)));
405        assert_eq!(err.to_string(), "API error: 500 Internal Server Error");
406    }
407
408    #[test]
409    fn test_error_stream() {
410        let err = Error::stream("Connection lost");
411        assert!(matches!(err, Error::Stream(_)));
412        assert_eq!(err.to_string(), "Streaming error: Connection lost");
413    }
414
415    #[test]
416    fn test_error_tool() {
417        let err = Error::tool("Tool not found");
418        assert!(matches!(err, Error::Tool(_)));
419        assert_eq!(err.to_string(), "Tool execution error: Tool not found");
420    }
421
422    #[test]
423    fn test_error_invalid_input() {
424        let err = Error::invalid_input("Missing parameter");
425        assert!(matches!(err, Error::InvalidInput(_)));
426        assert_eq!(err.to_string(), "Invalid input: Missing parameter");
427    }
428
429    #[test]
430    fn test_error_timeout() {
431        let err = Error::timeout();
432        assert!(matches!(err, Error::Timeout));
433        assert_eq!(err.to_string(), "Request timeout");
434    }
435
436    #[test]
437    fn test_error_other() {
438        let err = Error::other("Something went wrong");
439        assert!(matches!(err, Error::Other(_)));
440        assert_eq!(err.to_string(), "Error: Something went wrong");
441    }
442
443    #[test]
444    fn test_error_from_reqwest() {
445        // Test that reqwest::Error can be converted
446        // This is mostly for compile-time checking
447        fn _test_conversion(_e: reqwest::Error) -> Error {
448            // This function just needs to compile
449            Error::Http(_e)
450        }
451    }
452
453    #[test]
454    fn test_error_from_serde_json() {
455        // Test that serde_json::Error can be converted
456        let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
457        let err: Error = json_err.into();
458        assert!(matches!(err, Error::Json(_)));
459    }
460
461    #[test]
462    fn test_result_type_alias() {
463        // Test that our Result type alias works correctly
464        fn _returns_result() -> Result<i32> {
465            Ok(42)
466        }
467
468        fn _returns_error() -> Result<i32> {
469            Err(Error::timeout())
470        }
471    }
472}