vex_llm/
tool_error.rs

1//! Structured error types for tool execution
2//!
3//! This module provides precise, actionable error types for the VEX Tool System.
4//! Uses `thiserror` for automatic `std::error::Error` implementation.
5//!
6//! # Security Considerations
7//!
8//! - Error messages sanitize sensitive data (no raw args in messages)
9//! - Timeout errors prevent DoS from hanging tools
10//! - Validation errors provide safe feedback without exposing internals
11
12use thiserror::Error;
13
14/// Error types for tool execution with precise variants for each failure mode.
15///
16/// # Example
17///
18/// ```
19/// use vex_llm::ToolError;
20///
21/// let err = ToolError::not_found("unknown_tool");
22/// assert!(err.to_string().contains("unknown_tool"));
23/// ```
24#[derive(Debug, Error)]
25pub enum ToolError {
26    /// Tool not found in registry
27    #[error("Tool '{name}' not found in registry")]
28    NotFound {
29        /// Name of the tool that was requested
30        name: String,
31    },
32
33    /// Invalid arguments provided to tool
34    #[error("Invalid arguments for '{tool}': {reason}")]
35    InvalidArguments {
36        /// Name of the tool
37        tool: String,
38        /// Human-readable reason for validation failure
39        reason: String,
40    },
41
42    /// Tool execution failed
43    #[error("Execution of '{tool}' failed: {message}")]
44    ExecutionFailed {
45        /// Name of the tool
46        tool: String,
47        /// Error message (sanitized)
48        message: String,
49    },
50
51    /// Tool execution exceeded timeout
52    #[error("Tool '{tool}' timed out after {timeout_ms}ms")]
53    Timeout {
54        /// Name of the tool
55        tool: String,
56        /// Timeout in milliseconds
57        timeout_ms: u64,
58    },
59
60    /// JSON serialization/deserialization error
61    #[error("Serialization error: {0}")]
62    Serialization(#[from] serde_json::Error),
63
64    /// Audit logging failed (non-fatal, logged but doesn't stop execution)
65    #[error("Audit logging failed: {0}")]
66    AuditFailed(String),
67
68    /// Tool is disabled or unavailable
69    #[error("Tool '{name}' is currently unavailable: {reason}")]
70    Unavailable {
71        /// Name of the tool
72        name: String,
73        /// Reason for unavailability
74        reason: String,
75    },
76}
77
78impl ToolError {
79    /// Create a NotFound error
80    ///
81    /// # Example
82    /// ```
83    /// use vex_llm::ToolError;
84    /// let err = ToolError::not_found("my_tool");
85    /// ```
86    pub fn not_found(name: impl Into<String>) -> Self {
87        Self::NotFound { name: name.into() }
88    }
89
90    /// Create an InvalidArguments error with context
91    ///
92    /// # Security Note
93    /// The `reason` should not contain raw user input to prevent information leakage
94    pub fn invalid_args(tool: impl Into<String>, reason: impl Into<String>) -> Self {
95        Self::InvalidArguments {
96            tool: tool.into(),
97            reason: reason.into(),
98        }
99    }
100
101    /// Create an ExecutionFailed error
102    ///
103    /// # Security Note
104    /// The `message` should be sanitized before passing to this constructor
105    pub fn execution_failed(tool: impl Into<String>, message: impl Into<String>) -> Self {
106        Self::ExecutionFailed {
107            tool: tool.into(),
108            message: message.into(),
109        }
110    }
111
112    /// Create a Timeout error
113    pub fn timeout(tool: impl Into<String>, timeout_ms: u64) -> Self {
114        Self::Timeout {
115            tool: tool.into(),
116            timeout_ms,
117        }
118    }
119
120    /// Create an Unavailable error
121    pub fn unavailable(name: impl Into<String>, reason: impl Into<String>) -> Self {
122        Self::Unavailable {
123            name: name.into(),
124            reason: reason.into(),
125        }
126    }
127
128    /// Check if this error is recoverable (can retry)
129    pub fn is_retryable(&self) -> bool {
130        matches!(self, Self::Timeout { .. } | Self::Unavailable { .. })
131    }
132
133    /// Check if this error should be logged to audit trail
134    pub fn should_audit(&self) -> bool {
135        // All errors except audit failures themselves should be logged
136        !matches!(self, Self::AuditFailed(_))
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn test_error_display() {
146        let err = ToolError::not_found("calculator");
147        assert!(err.to_string().contains("calculator"));
148        assert!(err.to_string().contains("not found"));
149    }
150
151    #[test]
152    fn test_invalid_args() {
153        let err = ToolError::invalid_args("datetime", "Missing timezone field");
154        assert!(err.to_string().contains("datetime"));
155        assert!(err.to_string().contains("Missing timezone"));
156    }
157
158    #[test]
159    fn test_retryable() {
160        assert!(ToolError::timeout("test", 1000).is_retryable());
161        assert!(ToolError::unavailable("test", "maintenance").is_retryable());
162        assert!(!ToolError::not_found("test").is_retryable());
163    }
164
165    #[test]
166    fn test_should_audit() {
167        assert!(ToolError::not_found("test").should_audit());
168        assert!(!ToolError::AuditFailed("db error".into()).should_audit());
169    }
170}