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}