use super::VmValue;
#[derive(Debug, Clone)]
pub enum VmError {
StackUnderflow,
StackOverflow,
UndefinedVariable(String),
UndefinedBuiltin(String),
ImmutableAssignment(String),
TypeError(String),
Runtime(String),
DivisionByZero,
Thrown(VmValue),
CategorizedError {
message: String,
category: ErrorCategory,
},
DaemonQueueFull {
daemon_id: String,
capacity: usize,
},
Return(VmValue),
InvalidInstruction(u8),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorCategory {
Timeout,
Auth,
RateLimit,
Overloaded,
ServerError,
TransientNetwork,
SchemaValidation,
ToolError,
ToolRejected,
EgressBlocked,
Cancelled,
NotFound,
CircuitOpen,
Generic,
}
impl ErrorCategory {
pub fn as_str(&self) -> &'static str {
match self {
ErrorCategory::Timeout => "timeout",
ErrorCategory::Auth => "auth",
ErrorCategory::RateLimit => "rate_limit",
ErrorCategory::Overloaded => "overloaded",
ErrorCategory::ServerError => "server_error",
ErrorCategory::TransientNetwork => "transient_network",
ErrorCategory::SchemaValidation => "schema_validation",
ErrorCategory::ToolError => "tool_error",
ErrorCategory::ToolRejected => "tool_rejected",
ErrorCategory::EgressBlocked => "egress_blocked",
ErrorCategory::Cancelled => "cancelled",
ErrorCategory::NotFound => "not_found",
ErrorCategory::CircuitOpen => "circuit_open",
ErrorCategory::Generic => "generic",
}
}
pub fn parse(s: &str) -> Self {
match s {
"timeout" => ErrorCategory::Timeout,
"auth" => ErrorCategory::Auth,
"rate_limit" => ErrorCategory::RateLimit,
"overloaded" => ErrorCategory::Overloaded,
"server_error" => ErrorCategory::ServerError,
"transient_network" => ErrorCategory::TransientNetwork,
"schema_validation" => ErrorCategory::SchemaValidation,
"tool_error" => ErrorCategory::ToolError,
"tool_rejected" => ErrorCategory::ToolRejected,
"egress_blocked" => ErrorCategory::EgressBlocked,
"cancelled" => ErrorCategory::Cancelled,
"not_found" => ErrorCategory::NotFound,
"circuit_open" => ErrorCategory::CircuitOpen,
_ => ErrorCategory::Generic,
}
}
pub fn is_transient(&self) -> bool {
matches!(
self,
ErrorCategory::Timeout
| ErrorCategory::RateLimit
| ErrorCategory::Overloaded
| ErrorCategory::ServerError
| ErrorCategory::TransientNetwork
)
}
}
pub fn categorized_error(message: impl Into<String>, category: ErrorCategory) -> VmError {
VmError::CategorizedError {
message: message.into(),
category,
}
}
pub fn error_to_category(err: &VmError) -> ErrorCategory {
match err {
VmError::CategorizedError { category, .. } => category.clone(),
VmError::Thrown(VmValue::Dict(d)) => d
.get("category")
.map(|v| ErrorCategory::parse(&v.display()))
.unwrap_or(ErrorCategory::Generic),
VmError::Thrown(VmValue::String(s)) => classify_error_message(s),
VmError::Runtime(msg) => classify_error_message(msg),
_ => ErrorCategory::Generic,
}
}
pub fn classify_error_message(msg: &str) -> ErrorCategory {
if let Some(cat) = classify_by_http_status(msg) {
return cat;
}
if msg.contains("Deadline exceeded") || msg.contains("context deadline exceeded") {
return ErrorCategory::Timeout;
}
if msg.contains("overloaded_error") {
return ErrorCategory::Overloaded;
}
if msg.contains("api_error") {
return ErrorCategory::ServerError;
}
if msg.contains("insufficient_quota") || msg.contains("billing_hard_limit_reached") {
return ErrorCategory::RateLimit;
}
if msg.contains("invalid_api_key") || msg.contains("authentication_error") {
return ErrorCategory::Auth;
}
if msg.contains("not_found_error") || msg.contains("model_not_found") {
return ErrorCategory::NotFound;
}
if msg.contains("circuit_open") {
return ErrorCategory::CircuitOpen;
}
let lower = msg.to_lowercase();
if lower.contains("connection reset")
|| lower.contains("connection refused")
|| lower.contains("connection closed")
|| lower.contains("broken pipe")
|| lower.contains("dns error")
|| lower.contains("stream error")
|| lower.contains("unexpected eof")
{
return ErrorCategory::TransientNetwork;
}
ErrorCategory::Generic
}
fn classify_by_http_status(msg: &str) -> Option<ErrorCategory> {
for code in extract_http_status_codes(msg) {
return Some(match code {
401 | 403 => ErrorCategory::Auth,
404 | 410 => ErrorCategory::NotFound,
408 | 504 | 522 | 524 => ErrorCategory::Timeout,
429 => ErrorCategory::RateLimit,
503 | 529 => ErrorCategory::Overloaded,
500 | 502 => ErrorCategory::ServerError,
_ => continue,
});
}
None
}
fn extract_http_status_codes(msg: &str) -> Vec<u16> {
let mut codes = Vec::new();
let bytes = msg.as_bytes();
for i in 0..bytes.len().saturating_sub(2) {
if bytes[i].is_ascii_digit()
&& bytes[i + 1].is_ascii_digit()
&& bytes[i + 2].is_ascii_digit()
{
let before_ok = i == 0 || !bytes[i - 1].is_ascii_digit();
let after_ok = i + 3 >= bytes.len() || !bytes[i + 3].is_ascii_digit();
if before_ok && after_ok {
if let Ok(code) = msg[i..i + 3].parse::<u16>() {
if (400..=599).contains(&code) {
codes.push(code);
}
}
}
}
}
codes
}
impl std::fmt::Display for VmError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VmError::StackUnderflow => write!(f, "Stack underflow"),
VmError::StackOverflow => write!(f, "Stack overflow: too many nested calls"),
VmError::UndefinedVariable(n) => write!(f, "Undefined variable: {n}"),
VmError::UndefinedBuiltin(n) => write!(f, "Undefined builtin: {n}"),
VmError::ImmutableAssignment(n) => {
write!(f, "Cannot assign to immutable binding: {n}")
}
VmError::TypeError(msg) => write!(f, "Type error: {msg}"),
VmError::Runtime(msg) => write!(f, "Runtime error: {msg}"),
VmError::DivisionByZero => write!(f, "Division by zero"),
VmError::Thrown(v) => write!(f, "Thrown: {}", v.display()),
VmError::CategorizedError { message, category } => {
write!(f, "Error [{}]: {}", category.as_str(), message)
}
VmError::DaemonQueueFull {
daemon_id,
capacity,
} => write!(
f,
"Daemon queue full: daemon '{daemon_id}' reached its event_queue_capacity of {capacity}"
),
VmError::Return(_) => write!(f, "Return from function"),
VmError::InvalidInstruction(op) => write!(f, "Invalid instruction: 0x{op:02x}"),
}
}
}
impl std::error::Error for VmError {}