1use super::VmValue;
2
3#[derive(Debug, Clone)]
4pub enum VmError {
5 StackUnderflow,
6 StackOverflow,
7 UndefinedVariable(String),
8 UndefinedBuiltin(String),
9 ImmutableAssignment(String),
10 TypeError(String),
11 Runtime(String),
12 DivisionByZero,
13 Thrown(VmValue),
14 CategorizedError {
16 message: String,
17 category: ErrorCategory,
18 },
19 DaemonQueueFull {
20 daemon_id: String,
21 capacity: usize,
22 },
23 Return(VmValue),
24 InvalidInstruction(u8),
25}
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum ErrorCategory {
30 Timeout,
32 Auth,
34 RateLimit,
36 Overloaded,
40 ServerError,
42 TransientNetwork,
45 SchemaValidation,
47 ToolError,
49 ToolRejected,
51 EgressBlocked,
53 Cancelled,
55 NotFound,
57 CircuitOpen,
59 BudgetExceeded,
61 Generic,
63}
64
65impl ErrorCategory {
66 pub fn as_str(&self) -> &'static str {
67 match self {
68 ErrorCategory::Timeout => "timeout",
69 ErrorCategory::Auth => "auth",
70 ErrorCategory::RateLimit => "rate_limit",
71 ErrorCategory::Overloaded => "overloaded",
72 ErrorCategory::ServerError => "server_error",
73 ErrorCategory::TransientNetwork => "transient_network",
74 ErrorCategory::SchemaValidation => "schema_validation",
75 ErrorCategory::ToolError => "tool_error",
76 ErrorCategory::ToolRejected => "tool_rejected",
77 ErrorCategory::EgressBlocked => "egress_blocked",
78 ErrorCategory::Cancelled => "cancelled",
79 ErrorCategory::NotFound => "not_found",
80 ErrorCategory::CircuitOpen => "circuit_open",
81 ErrorCategory::BudgetExceeded => "budget_exceeded",
82 ErrorCategory::Generic => "generic",
83 }
84 }
85
86 pub fn parse(s: &str) -> Self {
87 match s {
88 "timeout" => ErrorCategory::Timeout,
89 "auth" => ErrorCategory::Auth,
90 "rate_limit" => ErrorCategory::RateLimit,
91 "overloaded" => ErrorCategory::Overloaded,
92 "server_error" => ErrorCategory::ServerError,
93 "transient_network" => ErrorCategory::TransientNetwork,
94 "schema_validation" => ErrorCategory::SchemaValidation,
95 "tool_error" => ErrorCategory::ToolError,
96 "tool_rejected" => ErrorCategory::ToolRejected,
97 "egress_blocked" => ErrorCategory::EgressBlocked,
98 "cancelled" => ErrorCategory::Cancelled,
99 "not_found" => ErrorCategory::NotFound,
100 "circuit_open" => ErrorCategory::CircuitOpen,
101 "budget_exceeded" => ErrorCategory::BudgetExceeded,
102 _ => ErrorCategory::Generic,
103 }
104 }
105
106 pub fn is_transient(&self) -> bool {
110 matches!(
111 self,
112 ErrorCategory::Timeout
113 | ErrorCategory::RateLimit
114 | ErrorCategory::Overloaded
115 | ErrorCategory::ServerError
116 | ErrorCategory::TransientNetwork
117 )
118 }
119}
120
121pub fn categorized_error(message: impl Into<String>, category: ErrorCategory) -> VmError {
123 VmError::CategorizedError {
124 message: message.into(),
125 category,
126 }
127}
128
129pub fn error_to_category(err: &VmError) -> ErrorCategory {
138 match err {
139 VmError::CategorizedError { category, .. } => category.clone(),
140 VmError::Thrown(VmValue::Dict(d)) => d
141 .get("category")
142 .map(|v| ErrorCategory::parse(&v.display()))
143 .unwrap_or(ErrorCategory::Generic),
144 VmError::Thrown(VmValue::String(s)) => classify_error_message(s),
145 VmError::Runtime(msg) => classify_error_message(msg),
146 _ => ErrorCategory::Generic,
147 }
148}
149
150pub fn classify_error_message(msg: &str) -> ErrorCategory {
153 if let Some(cat) = classify_by_http_status(msg) {
155 return cat;
156 }
157 if msg.contains("Deadline exceeded") || msg.contains("context deadline exceeded") {
160 return ErrorCategory::Timeout;
161 }
162 if msg.contains("overloaded_error") {
163 return ErrorCategory::Overloaded;
165 }
166 if msg.contains("api_error") {
167 return ErrorCategory::ServerError;
169 }
170 if msg.contains("insufficient_quota") || msg.contains("billing_hard_limit_reached") {
171 return ErrorCategory::RateLimit;
173 }
174 if msg.contains("invalid_api_key") || msg.contains("authentication_error") {
175 return ErrorCategory::Auth;
176 }
177 if msg.contains("not_found_error") || msg.contains("model_not_found") {
178 return ErrorCategory::NotFound;
179 }
180 if msg.contains("circuit_open") {
181 return ErrorCategory::CircuitOpen;
182 }
183 let lower = msg.to_lowercase();
185 if lower.contains("connection reset")
186 || lower.contains("connection refused")
187 || lower.contains("connection closed")
188 || lower.contains("broken pipe")
189 || lower.contains("dns error")
190 || lower.contains("stream error")
191 || lower.contains("unexpected eof")
192 {
193 return ErrorCategory::TransientNetwork;
194 }
195 ErrorCategory::Generic
196}
197
198fn classify_by_http_status(msg: &str) -> Option<ErrorCategory> {
202 for code in extract_http_status_codes(msg) {
205 return Some(match code {
206 401 | 403 => ErrorCategory::Auth,
207 404 | 410 => ErrorCategory::NotFound,
208 408 | 504 | 522 | 524 => ErrorCategory::Timeout,
209 429 => ErrorCategory::RateLimit,
210 503 | 529 => ErrorCategory::Overloaded,
211 500 | 502 => ErrorCategory::ServerError,
212 _ => continue,
213 });
214 }
215 None
216}
217
218fn extract_http_status_codes(msg: &str) -> Vec<u16> {
220 let mut codes = Vec::new();
221 let bytes = msg.as_bytes();
222 for i in 0..bytes.len().saturating_sub(2) {
223 if bytes[i].is_ascii_digit()
225 && bytes[i + 1].is_ascii_digit()
226 && bytes[i + 2].is_ascii_digit()
227 {
228 let before_ok = i == 0 || !bytes[i - 1].is_ascii_digit();
230 let after_ok = i + 3 >= bytes.len() || !bytes[i + 3].is_ascii_digit();
231 if before_ok && after_ok {
232 if let Ok(code) = msg[i..i + 3].parse::<u16>() {
233 if (400..=599).contains(&code) {
234 codes.push(code);
235 }
236 }
237 }
238 }
239 }
240 codes
241}
242
243impl std::fmt::Display for VmError {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 match self {
246 VmError::StackUnderflow => write!(f, "Stack underflow"),
247 VmError::StackOverflow => write!(f, "Stack overflow: too many nested calls"),
248 VmError::UndefinedVariable(n) => write!(f, "Undefined variable: {n}"),
249 VmError::UndefinedBuiltin(n) => write!(f, "Undefined builtin: {n}"),
250 VmError::ImmutableAssignment(n) => {
251 write!(f, "Cannot assign to immutable binding: {n}")
252 }
253 VmError::TypeError(msg) => write!(f, "Type error: {msg}"),
254 VmError::Runtime(msg) => write!(f, "Runtime error: {msg}"),
255 VmError::DivisionByZero => write!(f, "Division by zero"),
256 VmError::Thrown(v) => write!(f, "Thrown: {}", v.display()),
257 VmError::CategorizedError { message, category } => {
258 write!(f, "Error [{}]: {}", category.as_str(), message)
259 }
260 VmError::DaemonQueueFull {
261 daemon_id,
262 capacity,
263 } => write!(
264 f,
265 "Daemon queue full: daemon '{daemon_id}' reached its event_queue_capacity of {capacity}"
266 ),
267 VmError::Return(_) => write!(f, "Return from function"),
268 VmError::InvalidInstruction(op) => write!(f, "Invalid instruction: 0x{op:02x}"),
269 }
270 }
271}
272
273impl std::error::Error for VmError {}