use std::fmt;
use thiserror::Error;
use vtcode_commons::ErrorCategory;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorSeverity {
Transient,
Permanent,
RequiresApproval,
PolicyBlocked,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnifiedErrorKind {
Timeout,
Network,
RateLimit,
ArgumentValidation,
ToolNotFound,
PermissionDenied,
SandboxFailure,
InternalError,
CircuitOpen,
ResourceExhausted,
Cancelled,
PolicyViolation,
PlanModeViolation,
ExecutionFailed,
Unknown,
}
impl UnifiedErrorKind {
#[inline]
pub const fn is_retryable(&self) -> bool {
matches!(
self,
UnifiedErrorKind::Timeout
| UnifiedErrorKind::Network
| UnifiedErrorKind::RateLimit
| UnifiedErrorKind::CircuitOpen
)
}
#[inline]
pub const fn is_llm_mistake(&self) -> bool {
matches!(self, UnifiedErrorKind::ArgumentValidation)
}
}
#[derive(Error, Debug)]
pub struct UnifiedToolError {
pub kind: UnifiedErrorKind,
pub severity: ErrorSeverity,
pub user_message: String,
pub debug_context: Option<DebugContext>,
#[source]
pub source: Option<anyhow::Error>,
}
impl fmt::Display for UnifiedToolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.user_message)
}
}
#[derive(Debug, Clone)]
pub struct DebugContext {
pub tool_name: String,
pub invocation_id: Option<String>,
pub attempt: u32,
pub metadata: Vec<(String, String)>,
}
impl UnifiedToolError {
pub fn new(kind: UnifiedErrorKind, user_message: impl Into<String>) -> Self {
let severity = match kind {
UnifiedErrorKind::Timeout
| UnifiedErrorKind::Network
| UnifiedErrorKind::RateLimit
| UnifiedErrorKind::CircuitOpen => ErrorSeverity::Transient,
UnifiedErrorKind::PermissionDenied => ErrorSeverity::RequiresApproval,
_ => ErrorSeverity::Permanent,
};
Self {
kind,
severity,
user_message: user_message.into(),
debug_context: None,
source: None,
}
}
pub fn with_context(mut self, ctx: DebugContext) -> Self {
self.debug_context = Some(ctx);
self
}
pub fn with_source(mut self, err: anyhow::Error) -> Self {
self.source = Some(err);
self
}
pub fn with_tool_name(mut self, name: &str) -> Self {
if let Some(ref mut ctx) = self.debug_context {
ctx.tool_name = name.to_string();
} else {
self.debug_context = Some(DebugContext {
tool_name: name.to_string(),
invocation_id: None,
attempt: 1,
metadata: Vec::new(),
});
}
self
}
pub fn with_invocation_id(mut self, id: crate::tools::invocation::ToolInvocationId) -> Self {
if let Some(ref mut ctx) = self.debug_context {
ctx.invocation_id = Some(id.to_string());
} else {
self.debug_context = Some(DebugContext {
tool_name: String::new(),
invocation_id: Some(id.to_string()),
attempt: 1,
metadata: Vec::new(),
});
}
self
}
pub fn with_duration(mut self, duration: std::time::Duration) -> Self {
if let Some(ref mut ctx) = self.debug_context {
ctx.metadata
.push(("duration_ms".to_string(), duration.as_millis().to_string()));
} else {
self.debug_context = Some(DebugContext {
tool_name: String::new(),
invocation_id: None,
attempt: 1,
metadata: vec![("duration_ms".to_string(), duration.as_millis().to_string())],
});
}
self
}
#[inline]
pub fn is_retryable(&self) -> bool {
self.kind.is_retryable() && matches!(self.severity, ErrorSeverity::Transient)
}
#[inline]
pub fn is_llm_mistake(&self) -> bool {
self.kind.is_llm_mistake()
}
#[inline]
pub fn category(&self) -> ErrorCategory {
ErrorCategory::from(self.kind)
}
}
pub fn classify_error(err: &anyhow::Error) -> UnifiedErrorKind {
let category = vtcode_commons::classify_anyhow_error(err);
UnifiedErrorKind::from(category)
}
impl From<ErrorCategory> for UnifiedErrorKind {
fn from(cat: ErrorCategory) -> Self {
match cat {
ErrorCategory::Network | ErrorCategory::ServiceUnavailable => UnifiedErrorKind::Network,
ErrorCategory::Timeout => UnifiedErrorKind::Timeout,
ErrorCategory::RateLimit => UnifiedErrorKind::RateLimit,
ErrorCategory::CircuitOpen => UnifiedErrorKind::CircuitOpen,
ErrorCategory::Authentication => UnifiedErrorKind::PermissionDenied,
ErrorCategory::InvalidParameters => UnifiedErrorKind::ArgumentValidation,
ErrorCategory::ToolNotFound => UnifiedErrorKind::ToolNotFound,
ErrorCategory::ResourceNotFound => UnifiedErrorKind::ToolNotFound,
ErrorCategory::PermissionDenied => UnifiedErrorKind::PermissionDenied,
ErrorCategory::PolicyViolation => UnifiedErrorKind::PolicyViolation,
ErrorCategory::PlanModeViolation => UnifiedErrorKind::PlanModeViolation,
ErrorCategory::SandboxFailure => UnifiedErrorKind::SandboxFailure,
ErrorCategory::ResourceExhausted => UnifiedErrorKind::ResourceExhausted,
ErrorCategory::Cancelled => UnifiedErrorKind::Cancelled,
ErrorCategory::ExecutionError => UnifiedErrorKind::ExecutionFailed,
}
}
}
impl From<UnifiedErrorKind> for ErrorCategory {
fn from(kind: UnifiedErrorKind) -> Self {
match kind {
UnifiedErrorKind::Timeout => ErrorCategory::Timeout,
UnifiedErrorKind::Network => ErrorCategory::Network,
UnifiedErrorKind::RateLimit => ErrorCategory::RateLimit,
UnifiedErrorKind::ArgumentValidation => ErrorCategory::InvalidParameters,
UnifiedErrorKind::ToolNotFound => ErrorCategory::ToolNotFound,
UnifiedErrorKind::PermissionDenied => ErrorCategory::PermissionDenied,
UnifiedErrorKind::SandboxFailure => ErrorCategory::SandboxFailure,
UnifiedErrorKind::InternalError => ErrorCategory::ExecutionError,
UnifiedErrorKind::CircuitOpen => ErrorCategory::CircuitOpen,
UnifiedErrorKind::ResourceExhausted => ErrorCategory::ResourceExhausted,
UnifiedErrorKind::Cancelled => ErrorCategory::Cancelled,
UnifiedErrorKind::PolicyViolation => ErrorCategory::PolicyViolation,
UnifiedErrorKind::PlanModeViolation => ErrorCategory::PlanModeViolation,
UnifiedErrorKind::ExecutionFailed => ErrorCategory::ExecutionError,
UnifiedErrorKind::Unknown => ErrorCategory::ExecutionError,
}
}
}
impl From<crate::tools::handlers::ToolCallError> for UnifiedToolError {
fn from(err: crate::tools::handlers::ToolCallError) -> Self {
use crate::tools::handlers::ToolCallError;
match err {
ToolCallError::Rejected(msg) => {
UnifiedToolError::new(UnifiedErrorKind::PermissionDenied, msg)
}
ToolCallError::RespondToModel(msg) => {
UnifiedToolError::new(UnifiedErrorKind::InternalError, msg)
}
ToolCallError::Internal(e) => {
let kind = classify_error(&e);
UnifiedToolError::new(kind, e.to_string()).with_source(e)
}
ToolCallError::Timeout(ms) => {
UnifiedToolError::new(UnifiedErrorKind::Timeout, format!("Timeout after {}ms", ms))
}
}
}
}
impl From<crate::tools::handlers::sandboxing::ToolError> for UnifiedToolError {
fn from(err: crate::tools::handlers::sandboxing::ToolError) -> Self {
use crate::tools::handlers::sandboxing::ToolError;
match err {
ToolError::Rejected(msg) => {
UnifiedToolError::new(UnifiedErrorKind::PermissionDenied, msg)
}
ToolError::Codex(e) => {
let kind = classify_error(&e);
UnifiedToolError::new(kind, e.to_string()).with_source(e)
}
ToolError::SandboxDenied(msg) => {
UnifiedToolError::new(UnifiedErrorKind::SandboxFailure, msg)
}
ToolError::Timeout(ms) => {
UnifiedToolError::new(UnifiedErrorKind::Timeout, format!("Timeout after {}ms", ms))
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_classification() {
assert_eq!(
classify_error(&anyhow::anyhow!("Connection timeout")),
UnifiedErrorKind::Timeout
);
assert_eq!(
classify_error(&anyhow::anyhow!("Rate limit exceeded")),
UnifiedErrorKind::RateLimit
);
assert_eq!(
classify_error(&anyhow::anyhow!("Permission denied")),
UnifiedErrorKind::PermissionDenied
);
assert_eq!(
classify_error(&anyhow::anyhow!("Invalid argument: missing path")),
UnifiedErrorKind::ArgumentValidation
);
}
#[test]
fn test_retryable_errors() {
let timeout_err = UnifiedToolError::new(UnifiedErrorKind::Timeout, "timeout");
assert!(timeout_err.is_retryable());
let perm_err = UnifiedToolError::new(UnifiedErrorKind::PermissionDenied, "denied");
assert!(!perm_err.is_retryable());
}
#[test]
fn test_llm_mistake_classification() {
let arg_err = UnifiedToolError::new(UnifiedErrorKind::ArgumentValidation, "bad args");
assert!(arg_err.is_llm_mistake());
let net_err = UnifiedToolError::new(UnifiedErrorKind::Network, "network down");
assert!(!net_err.is_llm_mistake());
}
}