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}