zeph_tools/
error_taxonomy.rs1pub use zeph_common::error_taxonomy::{ErrorDomain, ToolErrorCategory, ToolInvocationPhase};
11
12use crate::executor::ErrorKind;
13
14pub trait ToolErrorCategoryExt {
20 fn error_kind(self) -> ErrorKind;
22}
23
24impl ToolErrorCategoryExt for ToolErrorCategory {
25 fn error_kind(self) -> ErrorKind {
26 if self.is_retryable() {
27 ErrorKind::Transient
28 } else {
29 ErrorKind::Permanent
30 }
31 }
32}
33
34#[derive(Debug, Clone, serde::Serialize)]
39pub struct ToolErrorFeedback {
40 pub category: ToolErrorCategory,
42 pub message: String,
44 pub retryable: bool,
46}
47
48impl ToolErrorFeedback {
49 #[must_use]
51 pub fn format_for_llm(&self) -> String {
52 format!(
53 "[tool_error]\ncategory: {}\nerror: {}\nsuggestion: {}\nretryable: {}",
54 self.category.label(),
55 self.message,
56 self.category.suggestion(),
57 self.retryable,
58 )
59 }
60}
61
62#[must_use]
64pub fn classify_http_status(status: u16) -> ToolErrorCategory {
65 match status {
66 400 | 422 => ToolErrorCategory::InvalidParameters,
67 401 | 403 => ToolErrorCategory::PolicyBlocked,
68 429 => ToolErrorCategory::RateLimited,
69 500..=599 => ToolErrorCategory::ServerError,
70 _ => ToolErrorCategory::PermanentFailure,
72 }
73}
74
75#[must_use]
84pub fn classify_io_error(err: &std::io::Error) -> ToolErrorCategory {
85 match err.kind() {
86 std::io::ErrorKind::TimedOut => ToolErrorCategory::Timeout,
87 std::io::ErrorKind::ConnectionRefused
88 | std::io::ErrorKind::ConnectionReset
89 | std::io::ErrorKind::ConnectionAborted
90 | std::io::ErrorKind::BrokenPipe => ToolErrorCategory::NetworkError,
91 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::Interrupted => {
95 ToolErrorCategory::ServerError
96 }
97 std::io::ErrorKind::PermissionDenied => ToolErrorCategory::PolicyBlocked,
98 _ => ToolErrorCategory::PermanentFailure,
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::ToolErrorCategoryExt as _;
107 use super::*;
108
109 #[test]
110 fn retryable_categories() {
111 assert!(ToolErrorCategory::RateLimited.is_retryable());
112 assert!(ToolErrorCategory::ServerError.is_retryable());
113 assert!(ToolErrorCategory::NetworkError.is_retryable());
114 assert!(ToolErrorCategory::Timeout.is_retryable());
115
116 assert!(!ToolErrorCategory::InvalidParameters.is_retryable());
117 assert!(!ToolErrorCategory::TypeMismatch.is_retryable());
118 assert!(!ToolErrorCategory::ToolNotFound.is_retryable());
119 assert!(!ToolErrorCategory::PolicyBlocked.is_retryable());
120 assert!(!ToolErrorCategory::PermanentFailure.is_retryable());
121 assert!(!ToolErrorCategory::Cancelled.is_retryable());
122 assert!(!ToolErrorCategory::ConfirmationRequired.is_retryable());
123 }
124
125 #[test]
126 fn quality_failure_categories() {
127 assert!(ToolErrorCategory::InvalidParameters.is_quality_failure());
128 assert!(ToolErrorCategory::TypeMismatch.is_quality_failure());
129 assert!(ToolErrorCategory::ToolNotFound.is_quality_failure());
130
131 assert!(!ToolErrorCategory::NetworkError.is_quality_failure());
132 assert!(!ToolErrorCategory::ServerError.is_quality_failure());
133 assert!(!ToolErrorCategory::RateLimited.is_quality_failure());
134 assert!(!ToolErrorCategory::Timeout.is_quality_failure());
135 assert!(!ToolErrorCategory::PolicyBlocked.is_quality_failure());
136 assert!(!ToolErrorCategory::PermanentFailure.is_quality_failure());
137 assert!(!ToolErrorCategory::Cancelled.is_quality_failure());
138 }
139
140 #[test]
141 fn needs_parameter_reformat() {
142 assert!(ToolErrorCategory::InvalidParameters.needs_parameter_reformat());
143 assert!(ToolErrorCategory::TypeMismatch.needs_parameter_reformat());
144 assert!(!ToolErrorCategory::NetworkError.needs_parameter_reformat());
145 assert!(!ToolErrorCategory::ToolNotFound.needs_parameter_reformat());
146 }
147
148 #[test]
149 fn error_kind_backward_compat() {
150 assert_eq!(
151 ToolErrorCategory::NetworkError.error_kind(),
152 ErrorKind::Transient
153 );
154 assert_eq!(
155 ToolErrorCategory::Timeout.error_kind(),
156 ErrorKind::Transient
157 );
158 assert_eq!(
159 ToolErrorCategory::InvalidParameters.error_kind(),
160 ErrorKind::Permanent
161 );
162 assert_eq!(
163 ToolErrorCategory::PolicyBlocked.error_kind(),
164 ErrorKind::Permanent
165 );
166 }
167
168 #[test]
169 fn classify_http_status_codes() {
170 assert_eq!(classify_http_status(403), ToolErrorCategory::PolicyBlocked);
171 assert_eq!(
172 classify_http_status(404),
173 ToolErrorCategory::PermanentFailure
174 );
175 assert_eq!(
176 classify_http_status(422),
177 ToolErrorCategory::InvalidParameters
178 );
179 assert_eq!(classify_http_status(429), ToolErrorCategory::RateLimited);
180 assert_eq!(classify_http_status(500), ToolErrorCategory::ServerError);
181 assert_eq!(classify_http_status(503), ToolErrorCategory::ServerError);
182 assert_eq!(
183 classify_http_status(200),
184 ToolErrorCategory::PermanentFailure
185 );
186 }
187
188 #[test]
189 fn classify_io_not_found_is_permanent_not_tool_not_found() {
190 let err = std::io::Error::new(std::io::ErrorKind::NotFound, "No such file or directory");
191 assert_eq!(classify_io_error(&err), ToolErrorCategory::PermanentFailure);
192 }
193
194 #[test]
195 fn classify_io_connection_errors() {
196 let refused =
197 std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "connection refused");
198 assert_eq!(classify_io_error(&refused), ToolErrorCategory::NetworkError);
199
200 let reset = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
201 assert_eq!(classify_io_error(&reset), ToolErrorCategory::NetworkError);
202
203 let timed_out = std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out");
204 assert_eq!(classify_io_error(&timed_out), ToolErrorCategory::Timeout);
205 }
206
207 #[test]
208 fn tool_error_feedback_format() {
209 let fb = ToolErrorFeedback {
210 category: ToolErrorCategory::InvalidParameters,
211 message: "missing required field: url".to_owned(),
212 retryable: false,
213 };
214 let s = fb.format_for_llm();
215 assert!(s.contains("[tool_error]"));
216 assert!(s.contains("invalid_parameters"));
217 assert!(s.contains("missing required field: url"));
218 assert!(s.contains("retryable: false"));
219 }
220
221 #[test]
222 fn phase_setup_for_tool_not_found() {
223 assert_eq!(
224 ToolErrorCategory::ToolNotFound.phase(),
225 ToolInvocationPhase::Setup
226 );
227 }
228
229 #[test]
230 fn phase_param_handling() {
231 assert_eq!(
232 ToolErrorCategory::InvalidParameters.phase(),
233 ToolInvocationPhase::ParamHandling
234 );
235 assert_eq!(
236 ToolErrorCategory::TypeMismatch.phase(),
237 ToolInvocationPhase::ParamHandling
238 );
239 }
240}