Skip to main content

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    /// The HTTP status is carried as structured data rather than embedded in the message,
147    /// so retry classification can read it directly instead of parsing prose. `status` is
148    /// `None` for API errors that did not come from an HTTP response.
149    ///
150    /// # Example
151    ///
152    /// ```rust,ignore
153    /// // From an HTTP response
154    /// return Err(Error::api_status(429, "Rate limit exceeded"));
155    ///
156    /// // Without a status
157    /// return Err(Error::api("Model 'gpt-4' not found on server"));
158    /// ```
159    #[error("API error{}: {message}", .status.map(|code| format!(" {code}")).unwrap_or_default())]
160    Api {
161        /// HTTP status code, when the error originated from an HTTP response.
162        status: Option<u16>,
163
164        /// Error message or response body reported by the server.
165        message: String,
166    },
167
168    /// Error occurred while processing the streaming response.
169    ///
170    /// This happens during Server-Sent Events (SSE) parsing or stream processing.
171    /// Common causes:
172    /// - Malformed SSE data
173    /// - Connection interrupted mid-stream
174    /// - Unexpected end of stream
175    /// - Invalid chunk format
176    ///
177    /// # Example
178    ///
179    /// ```rust,ignore
180    /// return Err(Error::stream("Unexpected end of SSE stream"));
181    /// ```
182    #[error("Streaming error: {0}")]
183    Stream(String),
184
185    /// Tool execution or registration failed.
186    ///
187    /// Occurs when there are problems with tool definitions or execution:
188    /// - Tool handler returns an error
189    /// - Tool input validation fails
190    /// - Tool name collision during registration
191    /// - Tool not found when executing
192    /// - Invalid tool schema
193    ///
194    /// # Example
195    ///
196    /// ```rust,ignore
197    /// return Err(Error::tool("Tool 'calculator' not found"));
198    /// ```
199    #[error("Tool execution error: {0}")]
200    Tool(String),
201
202    /// Invalid input provided by the user.
203    ///
204    /// Validation error for user-provided data that doesn't meet requirements:
205    /// - Empty prompt string
206    /// - Invalid parameter format
207    /// - Out of range values
208    /// - Malformed input data
209    ///
210    /// # Example
211    ///
212    /// ```rust,ignore
213    /// return Err(Error::invalid_input("Prompt cannot be empty"));
214    /// ```
215    #[error("Invalid input: {0}")]
216    InvalidInput(String),
217
218    /// Request exceeded the configured timeout duration.
219    ///
220    /// The operation took longer than the timeout specified in AgentOptions.
221    /// This is a dedicated variant (no message needed) because the cause is clear.
222    ///
223    /// # Example
224    ///
225    /// ```rust,ignore
226    /// return Err(Error::timeout());
227    /// ```
228    #[error("Request timeout")]
229    Timeout,
230
231    /// Miscellaneous error that doesn't fit other categories.
232    ///
233    /// Catch-all variant for unexpected errors or edge cases that don't fit
234    /// into the specific categories above. Should be used sparingly.
235    ///
236    /// # Example
237    ///
238    /// ```rust,ignore
239    /// return Err(Error::other("Unexpected condition occurred"));
240    /// ```
241    #[error("Error: {0}")]
242    Other(String),
243}
244
245// ============================================================================
246// CONVENIENCE CONSTRUCTORS
247// ============================================================================
248
249/// Implementation of convenience constructors for creating Error instances.
250///
251/// These methods provide a more ergonomic API for creating errors compared to
252/// directly constructing the enum variants. They accept `impl Into<String>`,
253/// allowing callers to pass `&str`, `String`, or any other type that converts to `String`.
254impl Error {
255    /// Create a new configuration error with a descriptive message.
256    ///
257    /// Use this when validation fails during `AgentOptions` construction or when
258    /// invalid configuration values are detected.
259    ///
260    /// # Arguments
261    ///
262    /// * `msg` - Error description explaining what configuration is invalid
263    ///
264    /// # Example
265    ///
266    /// ```rust
267    /// use open_agent::Error;
268    ///
269    /// let err = Error::config("base_url must be a valid HTTP or HTTPS URL");
270    /// assert_eq!(err.to_string(), "Invalid configuration: base_url must be a valid HTTP or HTTPS URL");
271    /// ```
272    pub fn config(msg: impl Into<String>) -> Self {
273        Error::Config(msg.into())
274    }
275
276    /// Create a new API error with the server's error message.
277    ///
278    /// Use this when the API returns an error response (even if the HTTP request
279    /// itself succeeded). This typically happens when the server rejects the request
280    /// due to invalid parameters, missing resources, or server-side failures.
281    ///
282    /// # Arguments
283    ///
284    /// * `msg` - Error message from the API server
285    ///
286    /// # Example
287    ///
288    /// ```rust
289    /// use open_agent::Error;
290    ///
291    /// let err = Error::api("Model 'invalid-model' not found");
292    /// assert_eq!(err.to_string(), "API error: Model 'invalid-model' not found");
293    /// ```
294    pub fn api(msg: impl Into<String>) -> Self {
295        Error::Api {
296            status: None,
297            message: msg.into(),
298        }
299    }
300
301    /// Create a new API error from an HTTP error response.
302    ///
303    /// Prefer this over [`Error::api`] whenever a status code is available: retry
304    /// classification reads [`Error::status_code`], and an error built without a status is
305    /// treated as non-retryable no matter what its message says.
306    ///
307    /// # Arguments
308    ///
309    /// * `status` - HTTP status code from the response
310    /// * `msg` - Response body or error message from the API server
311    ///
312    /// # Example
313    ///
314    /// ```rust
315    /// use open_agent::Error;
316    ///
317    /// let err = Error::api_status(429, "Rate limit exceeded");
318    /// assert_eq!(err.to_string(), "API error 429: Rate limit exceeded");
319    /// assert_eq!(err.status_code(), Some(429));
320    /// ```
321    pub fn api_status(status: u16, msg: impl Into<String>) -> Self {
322        Error::Api {
323            status: Some(status),
324            message: msg.into(),
325        }
326    }
327
328    /// The HTTP status code this error carries, if any.
329    ///
330    /// Returns `Some` only for [`Error::Api`] values built with [`Error::api_status`], and
331    /// `None` for every other variant and for API errors raised without a response status.
332    ///
333    /// This is the basis for retry classification: transient failures are identified by
334    /// status code, never by searching the error message for a status-shaped substring. The
335    /// difference is not cosmetic — a substring search classifies
336    /// `API error 400 Bad Request: max_tokens 500 too small` as a 500 and retries a request
337    /// that can never succeed.
338    ///
339    /// # Example
340    ///
341    /// ```rust
342    /// use open_agent::Error;
343    ///
344    /// assert_eq!(Error::api_status(429, "slow down").status_code(), Some(429));
345    /// assert_eq!(Error::api("Model 'gpt-4' not found").status_code(), None);
346    /// assert_eq!(Error::timeout().status_code(), None);
347    /// ```
348    pub fn status_code(&self) -> Option<u16> {
349        match self {
350            Error::Api { status, .. } => *status,
351            _ => None,
352        }
353    }
354
355    /// Create a new streaming error for SSE parsing or stream processing failures.
356    ///
357    /// Use this when errors occur during Server-Sent Events stream parsing,
358    /// such as malformed data, unexpected stream termination, or invalid chunks.
359    ///
360    /// # Arguments
361    ///
362    /// * `msg` - Description of the streaming failure
363    ///
364    /// # Example
365    ///
366    /// ```rust
367    /// use open_agent::Error;
368    ///
369    /// let err = Error::stream("Unexpected end of SSE stream");
370    /// assert_eq!(err.to_string(), "Streaming error: Unexpected end of SSE stream");
371    /// ```
372    pub fn stream(msg: impl Into<String>) -> Self {
373        Error::Stream(msg.into())
374    }
375
376    /// Create a new tool execution error.
377    ///
378    /// Use this when tool registration, lookup, or execution fails. This includes
379    /// tool handler errors, missing tools, and invalid tool inputs.
380    ///
381    /// # Arguments
382    ///
383    /// * `msg` - Description of the tool failure
384    ///
385    /// # Example
386    ///
387    /// ```rust
388    /// use open_agent::Error;
389    ///
390    /// let err = Error::tool("Calculator tool failed: division by zero");
391    /// assert_eq!(err.to_string(), "Tool execution error: Calculator tool failed: division by zero");
392    /// ```
393    pub fn tool(msg: impl Into<String>) -> Self {
394        Error::Tool(msg.into())
395    }
396
397    /// Create a new invalid input error for user input validation failures.
398    ///
399    /// Use this when user-provided data doesn't meet requirements, such as
400    /// empty strings, out-of-range values, or malformed data.
401    ///
402    /// # Arguments
403    ///
404    /// * `msg` - Description of why the input is invalid
405    ///
406    /// # Example
407    ///
408    /// ```rust
409    /// use open_agent::Error;
410    ///
411    /// let err = Error::invalid_input("Prompt cannot be empty");
412    /// assert_eq!(err.to_string(), "Invalid input: Prompt cannot be empty");
413    /// ```
414    pub fn invalid_input(msg: impl Into<String>) -> Self {
415        Error::InvalidInput(msg.into())
416    }
417
418    /// Create a new miscellaneous error for cases that don't fit other categories.
419    ///
420    /// Use this sparingly for unexpected conditions that don't fit into the
421    /// more specific error variants.
422    ///
423    /// # Arguments
424    ///
425    /// * `msg` - Description of the error
426    ///
427    /// # Example
428    ///
429    /// ```rust
430    /// use open_agent::Error;
431    ///
432    /// let err = Error::other("Unexpected internal state");
433    /// assert_eq!(err.to_string(), "Error: Unexpected internal state");
434    /// ```
435    pub fn other(msg: impl Into<String>) -> Self {
436        Error::Other(msg.into())
437    }
438
439    /// Create a timeout error indicating the operation exceeded the time limit.
440    ///
441    /// Use this when the request or operation takes longer than the configured
442    /// timeout duration. No message is needed since the cause is self-explanatory.
443    ///
444    /// # Example
445    ///
446    /// ```rust
447    /// use open_agent::Error;
448    ///
449    /// let err = Error::timeout();
450    /// assert_eq!(err.to_string(), "Request timeout");
451    /// ```
452    pub fn timeout() -> Self {
453        Error::Timeout
454    }
455}
456
457// ============================================================================
458// TESTS
459// ============================================================================
460
461#[cfg(test)]
462mod tests {
463    use super::*;
464
465    #[test]
466    fn test_error_config() {
467        let err = Error::config("Invalid model");
468        assert!(matches!(err, Error::Config(_)));
469        assert_eq!(err.to_string(), "Invalid configuration: Invalid model");
470    }
471
472    #[test]
473    fn test_error_api() {
474        let err = Error::api("Internal Server Error");
475        assert!(matches!(err, Error::Api { status: None, .. }));
476        assert_eq!(err.to_string(), "API error: Internal Server Error");
477
478        // A status-carrying error renders the code once, not twice, and exposes it directly.
479        let err = Error::api_status(500, "Internal Server Error");
480        assert!(matches!(
481            err,
482            Error::Api {
483                status: Some(500),
484                ..
485            }
486        ));
487        assert_eq!(err.to_string(), "API error 500: Internal Server Error");
488        assert_eq!(err.status_code(), Some(500));
489    }
490
491    #[test]
492    fn test_error_stream() {
493        let err = Error::stream("Connection lost");
494        assert!(matches!(err, Error::Stream(_)));
495        assert_eq!(err.to_string(), "Streaming error: Connection lost");
496    }
497
498    #[test]
499    fn test_error_tool() {
500        let err = Error::tool("Tool not found");
501        assert!(matches!(err, Error::Tool(_)));
502        assert_eq!(err.to_string(), "Tool execution error: Tool not found");
503    }
504
505    #[test]
506    fn test_error_invalid_input() {
507        let err = Error::invalid_input("Missing parameter");
508        assert!(matches!(err, Error::InvalidInput(_)));
509        assert_eq!(err.to_string(), "Invalid input: Missing parameter");
510    }
511
512    #[test]
513    fn test_error_timeout() {
514        let err = Error::timeout();
515        assert!(matches!(err, Error::Timeout));
516        assert_eq!(err.to_string(), "Request timeout");
517    }
518
519    #[test]
520    fn test_error_other() {
521        let err = Error::other("Something went wrong");
522        assert!(matches!(err, Error::Other(_)));
523        assert_eq!(err.to_string(), "Error: Something went wrong");
524    }
525
526    #[test]
527    fn test_error_from_reqwest() {
528        // Test that reqwest::Error can be converted
529        // This is mostly for compile-time checking
530        fn _test_conversion(_e: reqwest::Error) -> Error {
531            // This function just needs to compile
532            Error::Http(_e)
533        }
534    }
535
536    #[test]
537    fn test_error_from_serde_json() {
538        // Test that serde_json::Error can be converted
539        let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
540        let err: Error = json_err.into();
541        assert!(matches!(err, Error::Json(_)));
542    }
543
544    #[test]
545    fn test_result_type_alias() {
546        // Test that our Result type alias works correctly
547        fn _returns_result() -> Result<i32> {
548            Ok(42)
549        }
550
551        fn _returns_error() -> Result<i32> {
552            Err(Error::timeout())
553        }
554    }
555}