1#[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 #[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#[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 #[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#[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 #[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#[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 #[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#[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 #[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}