Skip to main content

zai_rs/toolkits/
error.rs

1//! Error types and context builders for tool operations.
2
3use std::borrow::Cow;
4
5use thiserror::Error;
6
7/// Result type for tool operations
8pub type ToolResult<T> = Result<T, ToolError>;
9
10/// Error returned while registering, validating, or executing a tool.
11#[derive(Error, Debug)]
12#[non_exhaustive]
13pub enum ToolError {
14    /// The requested tool is not registered.
15    #[error("Tool '{name}' not found")]
16    ToolNotFound {
17        /// Name of the missing tool.
18        name: Cow<'static, str>,
19    },
20
21    /// The supplied parameters were invalid for the tool.
22    #[error("Invalid parameters for tool '{tool}': {message}")]
23    InvalidParameters {
24        /// Name of the tool.
25        tool: Cow<'static, str>,
26        /// Why the parameters were invalid.
27        message: Cow<'static, str>,
28    },
29
30    /// Tool execution failed at runtime.
31    #[error("Tool '{tool}' execution failed: {message}")]
32    ExecutionFailed {
33        /// Name of the tool.
34        tool: Cow<'static, str>,
35        /// What went wrong during execution.
36        message: Cow<'static, str>,
37    },
38
39    /// The tool's JSON schema validation failed.
40    #[error("Schema validation failed for tool '{tool}': {message}")]
41    SchemaValidation {
42        /// Name of the tool.
43        tool: Cow<'static, str>,
44        /// Schema-validation error detail.
45        message: Cow<'static, str>,
46    },
47
48    /// Tool registration failed (duplicate name, …).
49    #[error("Tool registration failed: {message}")]
50    RegistrationError {
51        /// Why registration failed.
52        message: Cow<'static, str>,
53    },
54
55    /// (De)serialization for a tool payload failed.
56    #[error("Serialization error for tool '{tool}': {source}")]
57    SerializationError {
58        /// Name of the tool.
59        tool: Cow<'static, str>,
60        /// Underlying serde error.
61        #[source]
62        source: serde_json::Error,
63    },
64
65    /// Tool execution exceeded its timeout.
66    ///
67    /// Note: the timeout cancels only the *local* execution future. For a tool
68    /// backed by a remote service (e.g. an MCP tool, or any tool that performs
69    /// I/O), the request may already be on the wire and the remote side effect
70    /// may still occur. A timed-out call is not cached, and because
71    /// `TimeoutError` is retryable the same call may be issued more than once —
72    /// so keep per-call timeouts conservative for non-idempotent tools.
73    #[error("Timeout error for tool '{tool}': execution exceeded {timeout:?}")]
74    TimeoutError {
75        /// Name of the tool.
76        tool: Cow<'static, str>,
77        /// Configured timeout that was exceeded.
78        timeout: std::time::Duration,
79    },
80
81    /// A tool handler panicked while it was being polled.
82    ///
83    /// Panic payloads are deliberately omitted because they may contain
84    /// sensitive data and are not a stable error interface.
85    #[error("Tool '{tool}' execution panicked")]
86    ExecutionPanicked {
87        /// Name of the tool whose handler panicked.
88        tool: Cow<'static, str>,
89    },
90}
91
92impl ToolError {
93    /// Determine if the error is retryable
94    pub fn is_retryable(&self) -> bool {
95        matches!(
96            self,
97            ToolError::TimeoutError { .. } | ToolError::ExecutionFailed { .. }
98        )
99    }
100}
101
102/// Builder that attaches tool and operation context to a [`ToolError`].
103pub struct ErrorContext {
104    tool_name: Option<String>,
105}
106
107impl ErrorContext {
108    /// Create a new empty error context.
109    pub fn new() -> Self {
110        Self { tool_name: None }
111    }
112
113    /// Attach a tool name to this context.
114    pub fn with_tool(mut self, tool_name: impl Into<String>) -> Self {
115        self.tool_name = Some(tool_name.into());
116        self
117    }
118
119    fn get_tool_name(&self) -> String {
120        self.tool_name
121            .clone()
122            .unwrap_or_else(|| "unknown".to_string())
123    }
124
125    /// Build a [`ToolError::ToolNotFound`] from this context.
126    pub fn tool_not_found(self) -> ToolError {
127        ToolError::ToolNotFound {
128            name: Cow::Owned(self.get_tool_name()),
129        }
130    }
131
132    /// Build a [`ToolError::InvalidParameters`] from this context.
133    pub fn invalid_parameters(self, message: impl Into<String>) -> ToolError {
134        ToolError::InvalidParameters {
135            tool: Cow::Owned(self.get_tool_name()),
136            message: Cow::Owned(message.into()),
137        }
138    }
139
140    /// Build a [`ToolError::ExecutionFailed`] from this context.
141    pub fn execution_failed(self, message: impl Into<String>) -> ToolError {
142        ToolError::ExecutionFailed {
143            tool: Cow::Owned(self.get_tool_name()),
144            message: Cow::Owned(message.into()),
145        }
146    }
147
148    /// Build a [`ToolError::SchemaValidation`] from this context.
149    pub fn schema_validation(self, message: impl Into<String>) -> ToolError {
150        ToolError::SchemaValidation {
151            tool: Cow::Owned(self.get_tool_name()),
152            message: Cow::Owned(message.into()),
153        }
154    }
155
156    /// Build a [`ToolError::SerializationError`] from this context.
157    pub fn serialization_error(self, source: serde_json::Error) -> ToolError {
158        ToolError::SerializationError {
159            tool: Cow::Owned(self.get_tool_name()),
160            source,
161        }
162    }
163
164    /// Build a [`ToolError::TimeoutError`] from this context.
165    pub fn timeout_error(self, timeout: std::time::Duration) -> ToolError {
166        ToolError::TimeoutError {
167            tool: Cow::Owned(self.get_tool_name()),
168            timeout,
169        }
170    }
171}
172
173impl Default for ErrorContext {
174    fn default() -> Self {
175        Self::new()
176    }
177}
178
179/// Convenience function to create error context
180pub fn error_context() -> ErrorContext {
181    ErrorContext::new()
182}