Skip to main content

bob_core/
error.rs

1//! # Error Types
2//!
3//! Error types for the Bob Agent Framework.
4//!
5//! This module provides comprehensive error handling with:
6//!
7//! - **`AgentError`**: Top-level error enum wrapping all error types
8//! - **`LlmError`**: Errors from LLM providers
9//! - **`ToolError`**: Errors from tool execution
10//! - **`StoreError`**: Errors from session storage
11//!
12//! ## Error Codes
13//!
14//! Every error variant carries a stable machine-readable **error code**
15//! (e.g. `"BOB_LLM_RATE_LIMITED"`, `"BOB_TOOL_TIMEOUT"`).
16//! Codes are stable across patch releases and can be used for monitoring,
17//! alerting rules, and structured error reporting.
18//!
19//! ## Error Handling Strategy
20//!
21//! All errors use [`thiserror`] for ergonomic error definitions and implement:
22//! - `std::error::Error` for compatibility
23//! - `Display` for user-friendly messages
24//! - `From` for automatic conversion
25//!
26//! ## Example
27//!
28//! ```rust,ignore
29//! use bob_core::error::{AgentError, LlmError};
30//!
31//! fn handle_error(err: AgentError) {
32//!     eprintln!("[{}] {}", err.code(), err);
33//!     match err {
34//!         AgentError::Llm(e) => eprintln!("LLM error: {}", e),
35//!         AgentError::Tool(e) => eprintln!("Tool error: {}", e),
36//!         AgentError::Policy(msg) => eprintln!("Policy violation: {}", msg),
37//!         AgentError::Timeout => eprintln!("Operation timed out"),
38//!         _ => eprintln!("Other error: {}", err),
39//!     }
40//! }
41//! ```
42
43/// Top-level agent error.
44#[derive(thiserror::Error, Debug)]
45pub enum AgentError {
46    #[error("LLM provider error: {0}")]
47    Llm(#[from] LlmError),
48
49    #[error("Tool execution error: {0}")]
50    Tool(#[from] ToolError),
51
52    #[error("Policy violation: {0}")]
53    Policy(String),
54
55    #[error("configuration error: {0}")]
56    Config(String),
57
58    #[error("Store error: {0}")]
59    Store(#[from] StoreError),
60
61    #[error("Cost meter error: {0}")]
62    Cost(#[from] CostError),
63
64    #[error("timeout")]
65    Timeout,
66
67    #[error("guard exceeded: {reason:?}")]
68    GuardExceeded { reason: crate::types::GuardReason },
69
70    #[error("conflict: {0}")]
71    Conflict(String),
72
73    #[error(transparent)]
74    Internal(#[from] Box<dyn std::error::Error + Send + Sync>),
75}
76
77impl AgentError {
78    /// Stable machine-readable error code for monitoring and alerting.
79    #[must_use]
80    pub fn code(&self) -> &'static str {
81        match self {
82            Self::Llm(e) => e.code(),
83            Self::Tool(e) => e.code(),
84            Self::Policy(_) => "BOB_POLICY_VIOLATION",
85            Self::Config(_) => "BOB_CONFIG_ERROR",
86            Self::Store(e) => e.code(),
87            Self::Cost(e) => e.code(),
88            Self::Timeout => "BOB_TIMEOUT",
89            Self::GuardExceeded { .. } => "BOB_GUARD_EXCEEDED",
90            Self::Conflict(_) => "BOB_CONFLICT",
91            Self::Internal(_) => "BOB_INTERNAL",
92        }
93    }
94}
95
96/// LLM adapter errors.
97#[derive(thiserror::Error, Debug)]
98pub enum LlmError {
99    #[error("provider error: {0}")]
100    Provider(String),
101
102    #[error("rate limited")]
103    RateLimited,
104
105    #[error("context length exceeded")]
106    ContextLengthExceeded,
107
108    #[error("stream error: {0}")]
109    Stream(String),
110
111    #[error(transparent)]
112    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
113}
114
115impl LlmError {
116    /// Stable machine-readable error code.
117    #[must_use]
118    pub fn code(&self) -> &'static str {
119        match self {
120            Self::Provider(_) => "BOB_LLM_PROVIDER",
121            Self::RateLimited => "BOB_LLM_RATE_LIMITED",
122            Self::ContextLengthExceeded => "BOB_LLM_CONTEXT_LENGTH",
123            Self::Stream(_) => "BOB_LLM_STREAM",
124            Self::Other(_) => "BOB_LLM_OTHER",
125        }
126    }
127}
128
129/// Tool execution errors.
130#[derive(thiserror::Error, Debug)]
131pub enum ToolError {
132    #[error("tool not found: {name}")]
133    NotFound { name: String },
134
135    #[error("tool execution failed: {0}")]
136    Execution(String),
137
138    #[error("tool timeout: {name}")]
139    Timeout { name: String },
140
141    #[error(transparent)]
142    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
143}
144
145impl ToolError {
146    /// Stable machine-readable error code.
147    #[must_use]
148    pub fn code(&self) -> &'static str {
149        match self {
150            Self::NotFound { .. } => "BOB_TOOL_NOT_FOUND",
151            Self::Execution(_) => "BOB_TOOL_EXECUTION",
152            Self::Timeout { .. } => "BOB_TOOL_TIMEOUT",
153            Self::Other(_) => "BOB_TOOL_OTHER",
154        }
155    }
156}
157
158/// Session store errors.
159#[derive(thiserror::Error, Debug)]
160pub enum StoreError {
161    #[error("serialization error: {0}")]
162    Serialization(String),
163
164    #[error("storage backend error: {0}")]
165    Backend(String),
166
167    #[error("version conflict: expected version {expected}, found {actual}")]
168    VersionConflict { expected: u64, actual: u64 },
169
170    #[error(transparent)]
171    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
172}
173
174impl StoreError {
175    /// Stable machine-readable error code.
176    #[must_use]
177    pub fn code(&self) -> &'static str {
178        match self {
179            Self::Serialization(_) => "BOB_STORE_SERIALIZATION",
180            Self::Backend(_) => "BOB_STORE_BACKEND",
181            Self::VersionConflict { .. } => "BOB_STORE_VERSION_CONFLICT",
182            Self::Other(_) => "BOB_STORE_OTHER",
183        }
184    }
185}
186
187/// Cost meter errors.
188#[derive(thiserror::Error, Debug)]
189pub enum CostError {
190    #[error("budget exceeded: {0}")]
191    BudgetExceeded(String),
192
193    #[error("cost backend error: {0}")]
194    Backend(String),
195
196    #[error(transparent)]
197    Other(#[from] Box<dyn std::error::Error + Send + Sync>),
198}
199
200impl CostError {
201    /// Stable machine-readable error code.
202    #[must_use]
203    pub fn code(&self) -> &'static str {
204        match self {
205            Self::BudgetExceeded(_) => "BOB_COST_BUDGET_EXCEEDED",
206            Self::Backend(_) => "BOB_COST_BACKEND",
207            Self::Other(_) => "BOB_COST_OTHER",
208        }
209    }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn agent_error_codes_are_stable() {
218        assert_eq!(AgentError::Timeout.code(), "BOB_TIMEOUT");
219        assert_eq!(AgentError::Policy("x".into()).code(), "BOB_POLICY_VIOLATION");
220        assert_eq!(AgentError::Config("x".into()).code(), "BOB_CONFIG_ERROR");
221        assert_eq!(AgentError::Conflict("x".into()).code(), "BOB_CONFLICT");
222    }
223
224    #[test]
225    fn llm_error_codes_are_stable() {
226        assert_eq!(LlmError::RateLimited.code(), "BOB_LLM_RATE_LIMITED");
227        assert_eq!(LlmError::ContextLengthExceeded.code(), "BOB_LLM_CONTEXT_LENGTH");
228        assert_eq!(LlmError::Provider("x".into()).code(), "BOB_LLM_PROVIDER");
229    }
230
231    #[test]
232    fn tool_error_codes_are_stable() {
233        assert_eq!(ToolError::NotFound { name: "x".into() }.code(), "BOB_TOOL_NOT_FOUND");
234        assert_eq!(ToolError::Timeout { name: "x".into() }.code(), "BOB_TOOL_TIMEOUT");
235    }
236
237    #[test]
238    fn store_error_codes_are_stable() {
239        assert_eq!(
240            StoreError::VersionConflict { expected: 1, actual: 2 }.code(),
241            "BOB_STORE_VERSION_CONFLICT"
242        );
243        assert_eq!(StoreError::Backend("x".into()).code(), "BOB_STORE_BACKEND");
244    }
245
246    #[test]
247    fn cost_error_codes_are_stable() {
248        assert_eq!(CostError::BudgetExceeded("x".into()).code(), "BOB_COST_BUDGET_EXCEEDED");
249    }
250
251    #[test]
252    fn error_code_wraps_correctly() {
253        let llm_err = AgentError::Llm(LlmError::RateLimited);
254        assert_eq!(llm_err.code(), "BOB_LLM_RATE_LIMITED");
255
256        let tool_err = AgentError::Tool(ToolError::Timeout { name: "t".into() });
257        assert_eq!(tool_err.code(), "BOB_TOOL_TIMEOUT");
258
259        let store_err = AgentError::Store(StoreError::VersionConflict { expected: 1, actual: 2 });
260        assert_eq!(store_err.code(), "BOB_STORE_VERSION_CONFLICT");
261    }
262}