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 PlanModeViolation,
61 ExecutionFailed,
63 Unknown,
65}
66
67impl UnifiedErrorKind {
68 #[inline]
70 pub const fn is_retryable(&self) -> bool {
71 matches!(
72 self,
73 UnifiedErrorKind::Timeout
74 | UnifiedErrorKind::Network
75 | UnifiedErrorKind::RateLimit
76 | UnifiedErrorKind::CircuitOpen
77 )
78 }
79
80 #[inline]
82 pub const fn is_llm_mistake(&self) -> bool {
83 matches!(self, UnifiedErrorKind::ArgumentValidation)
84 }
85}
86
87#[derive(Error, Debug)]
89pub struct UnifiedToolError {
90 pub kind: UnifiedErrorKind,
92 pub severity: ErrorSeverity,
94 pub user_message: String,
96 pub debug_context: Option<DebugContext>,
98 #[source]
100 pub source: Option<anyhow::Error>,
101}
102
103impl fmt::Display for UnifiedToolError {
104 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 write!(f, "{}", self.user_message)
106 }
107}
108
109#[derive(Debug, Clone)]
111pub struct DebugContext {
112 pub tool_name: String,
114 pub invocation_id: Option<String>,
116 pub attempt: u32,
118 pub metadata: Vec<(String, String)>,
120}
121
122impl UnifiedToolError {
123 fn debug_context_mut(&mut self) -> &mut DebugContext {
125 self.debug_context.get_or_insert_with(|| DebugContext {
126 tool_name: String::new(),
127 invocation_id: None,
128 attempt: 1,
129 metadata: Vec::new(),
130 })
131 }
132
133 #[must_use]
135 pub fn new(kind: UnifiedErrorKind, user_message: impl Into<String>) -> Self {
136 let severity = match kind {
137 UnifiedErrorKind::Timeout
138 | UnifiedErrorKind::Network
139 | UnifiedErrorKind::RateLimit
140 | UnifiedErrorKind::CircuitOpen => ErrorSeverity::Transient,
141 UnifiedErrorKind::PermissionDenied => ErrorSeverity::RequiresApproval,
142 _ => ErrorSeverity::Permanent,
143 };
144
145 Self {
146 kind,
147 severity,
148 user_message: user_message.into(),
149 debug_context: None,
150 source: None,
151 }
152 }
153
154 #[must_use]
156 pub fn with_context(mut self, ctx: DebugContext) -> Self {
157 self.debug_context = Some(ctx);
158 self
159 }
160
161 #[must_use]
163 pub fn with_source(mut self, err: anyhow::Error) -> Self {
164 self.source = Some(err);
165 self
166 }
167
168 #[must_use]
170 pub fn with_tool_name(mut self, name: &str) -> Self {
171 self.debug_context_mut().tool_name = name.to_string();
172 self
173 }
174
175 #[must_use]
177 pub fn with_invocation_id(mut self, id: crate::tools::invocation::ToolInvocationId) -> Self {
178 self.debug_context_mut().invocation_id = Some(id.to_string());
179 self
180 }
181
182 #[must_use]
184 pub fn with_duration(mut self, duration: std::time::Duration) -> Self {
185 self.debug_context_mut()
186 .metadata
187 .push(("duration_ms".to_string(), duration.as_millis().to_string()));
188 self
189 }
190
191 #[inline]
193 #[must_use]
194 pub fn is_retryable(&self) -> bool {
195 self.kind.is_retryable() && matches!(self.severity, ErrorSeverity::Transient)
196 }
197
198 #[inline]
200 #[must_use]
201 pub fn is_llm_mistake(&self) -> bool {
202 self.kind.is_llm_mistake()
203 }
204
205 #[inline]
207 #[must_use]
208 pub fn category(&self) -> ErrorCategory {
209 ErrorCategory::from(self.kind)
210 }
211}
212
213#[cold]
221pub fn classify_error(err: &anyhow::Error) -> UnifiedErrorKind {
222 let category = vtcode_commons::classify_anyhow_error(err);
223 UnifiedErrorKind::from(category)
224}
225
226impl From<ErrorCategory> for UnifiedErrorKind {
229 fn from(cat: ErrorCategory) -> Self {
230 match cat {
231 ErrorCategory::Network | ErrorCategory::ServiceUnavailable => UnifiedErrorKind::Network,
232 ErrorCategory::Timeout => UnifiedErrorKind::Timeout,
233 ErrorCategory::RateLimit => UnifiedErrorKind::RateLimit,
234 ErrorCategory::CircuitOpen => UnifiedErrorKind::CircuitOpen,
235 ErrorCategory::Authentication => UnifiedErrorKind::PermissionDenied,
236 ErrorCategory::InvalidParameters => UnifiedErrorKind::ArgumentValidation,
237 ErrorCategory::ToolNotFound => UnifiedErrorKind::ToolNotFound,
238 ErrorCategory::ResourceNotFound => UnifiedErrorKind::ToolNotFound,
239 ErrorCategory::PermissionDenied => UnifiedErrorKind::PermissionDenied,
240 ErrorCategory::PolicyViolation => UnifiedErrorKind::PolicyViolation,
241 ErrorCategory::PlanModeViolation => UnifiedErrorKind::PlanModeViolation,
242 ErrorCategory::SandboxFailure => UnifiedErrorKind::SandboxFailure,
243 ErrorCategory::ResourceExhausted => UnifiedErrorKind::ResourceExhausted,
244 ErrorCategory::Cancelled => UnifiedErrorKind::Cancelled,
245 ErrorCategory::ExecutionError => UnifiedErrorKind::ExecutionFailed,
246 }
247 }
248}
249
250impl From<UnifiedErrorKind> for ErrorCategory {
251 fn from(kind: UnifiedErrorKind) -> Self {
252 match kind {
253 UnifiedErrorKind::Timeout => ErrorCategory::Timeout,
254 UnifiedErrorKind::Network => ErrorCategory::Network,
255 UnifiedErrorKind::RateLimit => ErrorCategory::RateLimit,
256 UnifiedErrorKind::ArgumentValidation => ErrorCategory::InvalidParameters,
257 UnifiedErrorKind::ToolNotFound => ErrorCategory::ToolNotFound,
258 UnifiedErrorKind::PermissionDenied => ErrorCategory::PermissionDenied,
259 UnifiedErrorKind::SandboxFailure => ErrorCategory::SandboxFailure,
260 UnifiedErrorKind::InternalError => ErrorCategory::ExecutionError,
261 UnifiedErrorKind::CircuitOpen => ErrorCategory::CircuitOpen,
262 UnifiedErrorKind::ResourceExhausted => ErrorCategory::ResourceExhausted,
263 UnifiedErrorKind::Cancelled => ErrorCategory::Cancelled,
264 UnifiedErrorKind::PolicyViolation => ErrorCategory::PolicyViolation,
265 UnifiedErrorKind::PlanModeViolation => ErrorCategory::PlanModeViolation,
266 UnifiedErrorKind::ExecutionFailed => ErrorCategory::ExecutionError,
267 UnifiedErrorKind::Unknown => ErrorCategory::ExecutionError,
268 }
269 }
270}
271
272impl From<crate::tools::handlers::ToolCallError> for UnifiedToolError {
274 fn from(err: crate::tools::handlers::ToolCallError) -> Self {
275 use crate::tools::handlers::ToolCallError;
276 match err {
277 ToolCallError::Rejected(msg) => {
278 UnifiedToolError::new(UnifiedErrorKind::PermissionDenied, msg)
279 }
280 ToolCallError::RespondToModel(msg) => {
281 UnifiedToolError::new(UnifiedErrorKind::InternalError, msg)
282 }
283 ToolCallError::Internal(e) => {
284 let kind = classify_error(&e);
285 UnifiedToolError::new(kind, e.to_string()).with_source(e)
286 }
287 ToolCallError::Timeout(ms) => {
288 UnifiedToolError::new(UnifiedErrorKind::Timeout, format!("Timeout after {}ms", ms))
289 }
290 }
291 }
292}
293
294impl From<crate::tools::handlers::sandboxing::ToolError> for UnifiedToolError {
296 fn from(err: crate::tools::handlers::sandboxing::ToolError) -> Self {
297 use crate::tools::handlers::sandboxing::ToolError;
298 match err {
299 ToolError::Rejected(msg) => {
300 UnifiedToolError::new(UnifiedErrorKind::PermissionDenied, msg)
301 }
302 ToolError::Codex(e) => {
303 let kind = classify_error(&e);
304 UnifiedToolError::new(kind, e.to_string()).with_source(e)
305 }
306 ToolError::SandboxDenied(msg) => {
307 UnifiedToolError::new(UnifiedErrorKind::SandboxFailure, msg)
308 }
309 ToolError::Timeout(ms) => {
310 UnifiedToolError::new(UnifiedErrorKind::Timeout, format!("Timeout after {}ms", ms))
311 }
312 }
313 }
314}
315
316#[cfg(test)]
317mod tests {
318 use super::*;
319
320 #[test]
321 fn test_error_classification() {
322 assert_eq!(
323 classify_error(&anyhow::anyhow!("Connection timeout")),
324 UnifiedErrorKind::Timeout
325 );
326 assert_eq!(
327 classify_error(&anyhow::anyhow!("Rate limit exceeded")),
328 UnifiedErrorKind::RateLimit
329 );
330 assert_eq!(
331 classify_error(&anyhow::anyhow!("Permission denied")),
332 UnifiedErrorKind::PermissionDenied
333 );
334 assert_eq!(
335 classify_error(&anyhow::anyhow!("Invalid argument: missing path")),
336 UnifiedErrorKind::ArgumentValidation
337 );
338 }
339
340 #[test]
341 fn test_retryable_errors() {
342 let timeout_err = UnifiedToolError::new(UnifiedErrorKind::Timeout, "timeout");
343 assert!(timeout_err.is_retryable());
344
345 let perm_err = UnifiedToolError::new(UnifiedErrorKind::PermissionDenied, "denied");
346 assert!(!perm_err.is_retryable());
347 }
348
349 #[test]
350 fn test_llm_mistake_classification() {
351 let arg_err = UnifiedToolError::new(UnifiedErrorKind::ArgumentValidation, "bad args");
352 assert!(arg_err.is_llm_mistake());
353
354 let net_err = UnifiedToolError::new(UnifiedErrorKind::Network, "network down");
355 assert!(!net_err.is_llm_mistake());
356 }
357}