#[derive(thiserror::Error, Debug)]
pub enum AgentError {
#[error("LLM provider error: {0}")]
Llm(#[from] LlmError),
#[error("Tool execution error: {0}")]
Tool(#[from] ToolError),
#[error("Policy violation: {0}")]
Policy(String),
#[error("configuration error: {0}")]
Config(String),
#[error("Store error: {0}")]
Store(#[from] StoreError),
#[error("Cost meter error: {0}")]
Cost(#[from] CostError),
#[error("timeout")]
Timeout,
#[error("guard exceeded: {reason:?}")]
GuardExceeded { reason: crate::types::GuardReason },
#[error("conflict: {0}")]
Conflict(String),
#[error(transparent)]
Internal(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl AgentError {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::Llm(e) => e.code(),
Self::Tool(e) => e.code(),
Self::Policy(_) => "BOB_POLICY_VIOLATION",
Self::Config(_) => "BOB_CONFIG_ERROR",
Self::Store(e) => e.code(),
Self::Cost(e) => e.code(),
Self::Timeout => "BOB_TIMEOUT",
Self::GuardExceeded { .. } => "BOB_GUARD_EXCEEDED",
Self::Conflict(_) => "BOB_CONFLICT",
Self::Internal(_) => "BOB_INTERNAL",
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum LlmError {
#[error("provider error: {0}")]
Provider(String),
#[error("rate limited")]
RateLimited,
#[error("context length exceeded")]
ContextLengthExceeded,
#[error("stream error: {0}")]
Stream(String),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl LlmError {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::Provider(_) => "BOB_LLM_PROVIDER",
Self::RateLimited => "BOB_LLM_RATE_LIMITED",
Self::ContextLengthExceeded => "BOB_LLM_CONTEXT_LENGTH",
Self::Stream(_) => "BOB_LLM_STREAM",
Self::Other(_) => "BOB_LLM_OTHER",
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum ToolError {
#[error("tool not found: {name}")]
NotFound { name: String },
#[error("tool execution failed: {0}")]
Execution(String),
#[error("tool timeout: {name}")]
Timeout { name: String },
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl ToolError {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::NotFound { .. } => "BOB_TOOL_NOT_FOUND",
Self::Execution(_) => "BOB_TOOL_EXECUTION",
Self::Timeout { .. } => "BOB_TOOL_TIMEOUT",
Self::Other(_) => "BOB_TOOL_OTHER",
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum StoreError {
#[error("serialization error: {0}")]
Serialization(String),
#[error("storage backend error: {0}")]
Backend(String),
#[error("version conflict: expected version {expected}, found {actual}")]
VersionConflict { expected: u64, actual: u64 },
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl StoreError {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::Serialization(_) => "BOB_STORE_SERIALIZATION",
Self::Backend(_) => "BOB_STORE_BACKEND",
Self::VersionConflict { .. } => "BOB_STORE_VERSION_CONFLICT",
Self::Other(_) => "BOB_STORE_OTHER",
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum CostError {
#[error("budget exceeded: {0}")]
BudgetExceeded(String),
#[error("cost backend error: {0}")]
Backend(String),
#[error(transparent)]
Other(#[from] Box<dyn std::error::Error + Send + Sync>),
}
impl CostError {
#[must_use]
pub fn code(&self) -> &'static str {
match self {
Self::BudgetExceeded(_) => "BOB_COST_BUDGET_EXCEEDED",
Self::Backend(_) => "BOB_COST_BACKEND",
Self::Other(_) => "BOB_COST_OTHER",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn agent_error_codes_are_stable() {
assert_eq!(AgentError::Timeout.code(), "BOB_TIMEOUT");
assert_eq!(AgentError::Policy("x".into()).code(), "BOB_POLICY_VIOLATION");
assert_eq!(AgentError::Config("x".into()).code(), "BOB_CONFIG_ERROR");
assert_eq!(AgentError::Conflict("x".into()).code(), "BOB_CONFLICT");
}
#[test]
fn llm_error_codes_are_stable() {
assert_eq!(LlmError::RateLimited.code(), "BOB_LLM_RATE_LIMITED");
assert_eq!(LlmError::ContextLengthExceeded.code(), "BOB_LLM_CONTEXT_LENGTH");
assert_eq!(LlmError::Provider("x".into()).code(), "BOB_LLM_PROVIDER");
}
#[test]
fn tool_error_codes_are_stable() {
assert_eq!(ToolError::NotFound { name: "x".into() }.code(), "BOB_TOOL_NOT_FOUND");
assert_eq!(ToolError::Timeout { name: "x".into() }.code(), "BOB_TOOL_TIMEOUT");
}
#[test]
fn store_error_codes_are_stable() {
assert_eq!(
StoreError::VersionConflict { expected: 1, actual: 2 }.code(),
"BOB_STORE_VERSION_CONFLICT"
);
assert_eq!(StoreError::Backend("x".into()).code(), "BOB_STORE_BACKEND");
}
#[test]
fn cost_error_codes_are_stable() {
assert_eq!(CostError::BudgetExceeded("x".into()).code(), "BOB_COST_BUDGET_EXCEEDED");
}
#[test]
fn error_code_wraps_correctly() {
let llm_err = AgentError::Llm(LlmError::RateLimited);
assert_eq!(llm_err.code(), "BOB_LLM_RATE_LIMITED");
let tool_err = AgentError::Tool(ToolError::Timeout { name: "t".into() });
assert_eq!(tool_err.code(), "BOB_TOOL_TIMEOUT");
let store_err = AgentError::Store(StoreError::VersionConflict { expected: 1, actual: 2 });
assert_eq!(store_err.code(), "BOB_STORE_VERSION_CONFLICT");
}
}