1use std::fmt;
16use thiserror::Error;
17use vtcode_commons::ErrorCategory;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ErrorSeverity {
22 Transient,
24 Permanent,
26 RequiresApproval,
28 PolicyBlocked,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum UnifiedErrorKind {
35 Timeout,
37 Network,
39 RateLimit,
41 ArgumentValidation,
43 ToolNotFound,
45 PermissionDenied,
47 SandboxFailure,
49 InternalError,
51 CircuitOpen,
53 ResourceExhausted,
55 Cancelled,
57 PolicyViolation,
59 PlanningPolicyViolation,
61 ExecutionFailed,
63 Unknown,
65}
66
67impl UnifiedErrorKind {
68 #[inline]
73 pub fn is_retryable(&self) -> bool {
74 ErrorCategory::from(*self).is_retryable()
75 }
76
77 #[inline]
79 pub const fn is_llm_mistake(&self) -> bool {
80 matches!(self, UnifiedErrorKind::ArgumentValidation)
81 }
82}
83
84#[derive(Error, Debug)]
86pub struct UnifiedToolError {
87 pub kind: UnifiedErrorKind,
89 pub severity: ErrorSeverity,
91 pub user_message: String,
93 pub debug_context: Option<DebugContext>,
95 #[source]
97 pub source: Option<anyhow::Error>,
98}
99
100impl fmt::Display for UnifiedToolError {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(f, "{}", self.user_message)
103 }
104}
105
106#[derive(Debug, Clone)]
108pub struct DebugContext {
109 pub tool_name: String,
111 pub invocation_id: Option<String>,
113 pub attempt: u32,
115 pub metadata: Vec<(String, String)>,
117}
118
119impl UnifiedToolError {
120 fn debug_context_mut(&mut self) -> &mut DebugContext {
122 self.debug_context.get_or_insert_with(|| DebugContext {
123 tool_name: String::new(),
124 invocation_id: None,
125 attempt: 1,
126 metadata: Vec::new(),
127 })
128 }
129
130 #[must_use]
132 pub fn new(kind: UnifiedErrorKind, user_message: impl Into<String>) -> Self {
133 let severity = match kind {
134 UnifiedErrorKind::Timeout
135 | UnifiedErrorKind::Network
136 | UnifiedErrorKind::RateLimit
137 | UnifiedErrorKind::CircuitOpen => ErrorSeverity::Transient,
138 UnifiedErrorKind::PermissionDenied => ErrorSeverity::RequiresApproval,
139 _ => ErrorSeverity::Permanent,
140 };
141
142 Self {
143 kind,
144 severity,
145 user_message: user_message.into(),
146 debug_context: None,
147 source: None,
148 }
149 }
150
151 #[must_use]
153 pub fn with_context(mut self, ctx: DebugContext) -> Self {
154 self.debug_context = Some(ctx);
155 self
156 }
157
158 #[must_use]
160 pub fn with_source(mut self, err: anyhow::Error) -> Self {
161 self.source = Some(err);
162 self
163 }
164
165 #[must_use]
167 pub fn with_tool_name(mut self, name: &str) -> Self {
168 self.debug_context_mut().tool_name = name.to_string();
169 self
170 }
171
172 #[must_use]
174 pub fn with_invocation_id(mut self, id: crate::tools::invocation::ToolInvocationId) -> Self {
175 self.debug_context_mut().invocation_id = Some(id.to_string());
176 self
177 }
178
179 #[must_use]
181 pub fn with_duration(mut self, duration: std::time::Duration) -> Self {
182 self.debug_context_mut()
183 .metadata
184 .push(("duration_ms".to_string(), duration.as_millis().to_string()));
185 self
186 }
187
188 #[inline]
190 #[must_use]
191 pub fn is_retryable(&self) -> bool {
192 self.kind.is_retryable() && matches!(self.severity, ErrorSeverity::Transient)
193 }
194
195 #[inline]
197 #[must_use]
198 pub fn is_llm_mistake(&self) -> bool {
199 self.kind.is_llm_mistake()
200 }
201
202 #[inline]
204 #[must_use]
205 pub fn category(&self) -> ErrorCategory {
206 ErrorCategory::from(self.kind)
207 }
208}
209
210impl From<ErrorCategory> for UnifiedErrorKind {
221 fn from(cat: ErrorCategory) -> Self {
222 match cat {
223 ErrorCategory::Network | ErrorCategory::ServiceUnavailable => UnifiedErrorKind::Network,
224 ErrorCategory::Timeout => UnifiedErrorKind::Timeout,
225 ErrorCategory::RateLimit => UnifiedErrorKind::RateLimit,
226 ErrorCategory::CircuitOpen => UnifiedErrorKind::CircuitOpen,
227 ErrorCategory::Authentication => UnifiedErrorKind::PermissionDenied,
228 ErrorCategory::InvalidParameters => UnifiedErrorKind::ArgumentValidation,
229 ErrorCategory::ToolNotFound => UnifiedErrorKind::ToolNotFound,
230 ErrorCategory::ResourceNotFound => UnifiedErrorKind::ToolNotFound,
231 ErrorCategory::PermissionDenied => UnifiedErrorKind::PermissionDenied,
232 ErrorCategory::PolicyViolation => UnifiedErrorKind::PolicyViolation,
233 ErrorCategory::PlanningPolicyViolation => UnifiedErrorKind::PlanningPolicyViolation,
234 ErrorCategory::SandboxFailure => UnifiedErrorKind::SandboxFailure,
235 ErrorCategory::ResourceExhausted => UnifiedErrorKind::ResourceExhausted,
236 ErrorCategory::Cancelled => UnifiedErrorKind::Cancelled,
237 ErrorCategory::ExecutionError => UnifiedErrorKind::ExecutionFailed,
238 }
239 }
240}
241
242impl From<UnifiedErrorKind> for ErrorCategory {
245 fn from(kind: UnifiedErrorKind) -> Self {
246 match kind {
247 UnifiedErrorKind::Timeout => ErrorCategory::Timeout,
248 UnifiedErrorKind::Network => ErrorCategory::Network,
249 UnifiedErrorKind::RateLimit => ErrorCategory::RateLimit,
250 UnifiedErrorKind::ArgumentValidation => ErrorCategory::InvalidParameters,
251 UnifiedErrorKind::ToolNotFound => ErrorCategory::ToolNotFound,
252 UnifiedErrorKind::PermissionDenied => ErrorCategory::PermissionDenied,
253 UnifiedErrorKind::SandboxFailure => ErrorCategory::SandboxFailure,
254 UnifiedErrorKind::InternalError => ErrorCategory::ExecutionError,
255 UnifiedErrorKind::CircuitOpen => ErrorCategory::CircuitOpen,
256 UnifiedErrorKind::ResourceExhausted => ErrorCategory::ResourceExhausted,
257 UnifiedErrorKind::Cancelled => ErrorCategory::Cancelled,
258 UnifiedErrorKind::PolicyViolation => ErrorCategory::PolicyViolation,
259 UnifiedErrorKind::PlanningPolicyViolation => ErrorCategory::PlanningPolicyViolation,
260 UnifiedErrorKind::ExecutionFailed => ErrorCategory::ExecutionError,
261 UnifiedErrorKind::Unknown => ErrorCategory::ExecutionError,
262 }
263 }
264}
265
266impl From<crate::tools::handlers::ToolCallError> for UnifiedToolError {
268 fn from(err: crate::tools::handlers::ToolCallError) -> Self {
269 use crate::tools::handlers::ToolCallError;
270 match err {
271 ToolCallError::Rejected(msg) => UnifiedToolError::new(UnifiedErrorKind::PermissionDenied, msg),
272 ToolCallError::RespondToModel(msg) => UnifiedToolError::new(UnifiedErrorKind::InternalError, msg),
273 ToolCallError::Internal(e) => {
274 let kind = UnifiedErrorKind::from(vtcode_commons::classify_anyhow_error(&e));
275 UnifiedToolError::new(kind, e.to_string()).with_source(e)
276 }
277 ToolCallError::Timeout(ms) => {
278 UnifiedToolError::new(UnifiedErrorKind::Timeout, format!("Timeout after {ms}ms"))
279 }
280 }
281 }
282}
283
284impl From<crate::tools::handlers::sandboxing::ToolError> for UnifiedToolError {
286 fn from(err: crate::tools::handlers::sandboxing::ToolError) -> Self {
287 use crate::tools::handlers::sandboxing::ToolError;
288 match err {
289 ToolError::Rejected(msg) => UnifiedToolError::new(UnifiedErrorKind::PermissionDenied, msg),
290 ToolError::Codex(e) => {
291 let kind = UnifiedErrorKind::from(vtcode_commons::classify_anyhow_error(&e));
292 UnifiedToolError::new(kind, e.to_string()).with_source(e)
293 }
294 ToolError::SandboxDenied(msg) => UnifiedToolError::new(UnifiedErrorKind::SandboxFailure, msg),
295 ToolError::Timeout(ms) => UnifiedToolError::new(UnifiedErrorKind::Timeout, format!("Timeout after {ms}ms")),
296 }
297 }
298}
299
300#[cfg(test)]
301mod tests {
302 use super::*;
303
304 fn classify(err: &anyhow::Error) -> UnifiedErrorKind {
305 UnifiedErrorKind::from(vtcode_commons::classify_anyhow_error(err))
306 }
307
308 #[test]
309 fn test_error_classification() {
310 assert_eq!(classify(&anyhow::anyhow!("Connection timeout")), UnifiedErrorKind::Timeout);
311 assert_eq!(classify(&anyhow::anyhow!("Rate limit exceeded")), UnifiedErrorKind::RateLimit);
312 assert_eq!(classify(&anyhow::anyhow!("Permission denied")), UnifiedErrorKind::PermissionDenied);
313 assert_eq!(classify(&anyhow::anyhow!("Invalid argument: missing path")), UnifiedErrorKind::ArgumentValidation);
314 }
315
316 #[test]
317 fn test_retryable_parity_with_error_category() {
318 let kinds = [
319 UnifiedErrorKind::Timeout,
320 UnifiedErrorKind::Network,
321 UnifiedErrorKind::RateLimit,
322 UnifiedErrorKind::ArgumentValidation,
323 UnifiedErrorKind::ToolNotFound,
324 UnifiedErrorKind::PermissionDenied,
325 UnifiedErrorKind::SandboxFailure,
326 UnifiedErrorKind::InternalError,
327 UnifiedErrorKind::CircuitOpen,
328 UnifiedErrorKind::ResourceExhausted,
329 UnifiedErrorKind::Cancelled,
330 UnifiedErrorKind::PolicyViolation,
331 UnifiedErrorKind::PlanningPolicyViolation,
332 UnifiedErrorKind::ExecutionFailed,
333 UnifiedErrorKind::Unknown,
334 ];
335 for kind in kinds {
336 let expected = matches!(
337 kind,
338 UnifiedErrorKind::Timeout
339 | UnifiedErrorKind::Network
340 | UnifiedErrorKind::RateLimit
341 | UnifiedErrorKind::CircuitOpen
342 );
343 assert_eq!(kind.is_retryable(), expected, "kind: {kind:?}");
344 }
345 }
346
347 #[test]
348 fn test_retryable_errors() {
349 let timeout_err = UnifiedToolError::new(UnifiedErrorKind::Timeout, "timeout");
350 assert!(timeout_err.is_retryable());
351
352 let perm_err = UnifiedToolError::new(UnifiedErrorKind::PermissionDenied, "denied");
353 assert!(!perm_err.is_retryable());
354 }
355
356 #[test]
357 fn test_llm_mistake_classification() {
358 let arg_err = UnifiedToolError::new(UnifiedErrorKind::ArgumentValidation, "bad args");
359 assert!(arg_err.is_llm_mistake());
360
361 let net_err = UnifiedToolError::new(UnifiedErrorKind::Network, "network down");
362 assert!(!net_err.is_llm_mistake());
363 }
364}