pub use zeph_common::error_taxonomy::{ErrorDomain, ToolErrorCategory, ToolInvocationPhase};
use crate::executor::ErrorKind;
pub trait ToolErrorCategoryExt {
fn error_kind(self) -> ErrorKind;
}
impl ToolErrorCategoryExt for ToolErrorCategory {
fn error_kind(self) -> ErrorKind {
if self.is_retryable() {
ErrorKind::Transient
} else {
ErrorKind::Permanent
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ToolErrorFeedback {
pub category: ToolErrorCategory,
pub message: String,
pub retryable: bool,
}
impl ToolErrorFeedback {
#[must_use]
pub fn format_for_llm(&self) -> String {
format!(
"[tool_error]\ncategory: {}\nerror: {}\nsuggestion: {}\nretryable: {}",
self.category.label(),
self.message,
self.category.suggestion(),
self.retryable,
)
}
}
#[must_use]
pub fn classify_http_status(status: u16) -> ToolErrorCategory {
match status {
400 | 422 => ToolErrorCategory::InvalidParameters,
401 | 403 => ToolErrorCategory::PolicyBlocked,
429 => ToolErrorCategory::RateLimited,
500..=599 => ToolErrorCategory::ServerError,
_ => ToolErrorCategory::PermanentFailure,
}
}
#[must_use]
pub fn classify_io_error(err: &std::io::Error) -> ToolErrorCategory {
match err.kind() {
std::io::ErrorKind::TimedOut => ToolErrorCategory::Timeout,
std::io::ErrorKind::ConnectionRefused
| std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::BrokenPipe => ToolErrorCategory::NetworkError,
std::io::ErrorKind::WouldBlock | std::io::ErrorKind::Interrupted => {
ToolErrorCategory::ServerError
}
std::io::ErrorKind::PermissionDenied => ToolErrorCategory::PolicyBlocked,
_ => ToolErrorCategory::PermanentFailure,
}
}
#[cfg(test)]
mod tests {
use super::ToolErrorCategoryExt as _;
use super::*;
#[test]
fn retryable_categories() {
assert!(ToolErrorCategory::RateLimited.is_retryable());
assert!(ToolErrorCategory::ServerError.is_retryable());
assert!(ToolErrorCategory::NetworkError.is_retryable());
assert!(ToolErrorCategory::Timeout.is_retryable());
assert!(!ToolErrorCategory::InvalidParameters.is_retryable());
assert!(!ToolErrorCategory::TypeMismatch.is_retryable());
assert!(!ToolErrorCategory::ToolNotFound.is_retryable());
assert!(!ToolErrorCategory::PolicyBlocked.is_retryable());
assert!(!ToolErrorCategory::PermanentFailure.is_retryable());
assert!(!ToolErrorCategory::Cancelled.is_retryable());
assert!(!ToolErrorCategory::ConfirmationRequired.is_retryable());
}
#[test]
fn quality_failure_categories() {
assert!(ToolErrorCategory::InvalidParameters.is_quality_failure());
assert!(ToolErrorCategory::TypeMismatch.is_quality_failure());
assert!(ToolErrorCategory::ToolNotFound.is_quality_failure());
assert!(!ToolErrorCategory::NetworkError.is_quality_failure());
assert!(!ToolErrorCategory::ServerError.is_quality_failure());
assert!(!ToolErrorCategory::RateLimited.is_quality_failure());
assert!(!ToolErrorCategory::Timeout.is_quality_failure());
assert!(!ToolErrorCategory::PolicyBlocked.is_quality_failure());
assert!(!ToolErrorCategory::PermanentFailure.is_quality_failure());
assert!(!ToolErrorCategory::Cancelled.is_quality_failure());
}
#[test]
fn needs_parameter_reformat() {
assert!(ToolErrorCategory::InvalidParameters.needs_parameter_reformat());
assert!(ToolErrorCategory::TypeMismatch.needs_parameter_reformat());
assert!(!ToolErrorCategory::NetworkError.needs_parameter_reformat());
assert!(!ToolErrorCategory::ToolNotFound.needs_parameter_reformat());
}
#[test]
fn error_kind_backward_compat() {
assert_eq!(
ToolErrorCategory::NetworkError.error_kind(),
ErrorKind::Transient
);
assert_eq!(
ToolErrorCategory::Timeout.error_kind(),
ErrorKind::Transient
);
assert_eq!(
ToolErrorCategory::InvalidParameters.error_kind(),
ErrorKind::Permanent
);
assert_eq!(
ToolErrorCategory::PolicyBlocked.error_kind(),
ErrorKind::Permanent
);
}
#[test]
fn classify_http_status_codes() {
assert_eq!(classify_http_status(403), ToolErrorCategory::PolicyBlocked);
assert_eq!(
classify_http_status(404),
ToolErrorCategory::PermanentFailure
);
assert_eq!(
classify_http_status(422),
ToolErrorCategory::InvalidParameters
);
assert_eq!(classify_http_status(429), ToolErrorCategory::RateLimited);
assert_eq!(classify_http_status(500), ToolErrorCategory::ServerError);
assert_eq!(classify_http_status(503), ToolErrorCategory::ServerError);
assert_eq!(
classify_http_status(200),
ToolErrorCategory::PermanentFailure
);
}
#[test]
fn classify_io_not_found_is_permanent_not_tool_not_found() {
let err = std::io::Error::new(std::io::ErrorKind::NotFound, "No such file or directory");
assert_eq!(classify_io_error(&err), ToolErrorCategory::PermanentFailure);
}
#[test]
fn classify_io_connection_errors() {
let refused =
std::io::Error::new(std::io::ErrorKind::ConnectionRefused, "connection refused");
assert_eq!(classify_io_error(&refused), ToolErrorCategory::NetworkError);
let reset = std::io::Error::new(std::io::ErrorKind::ConnectionReset, "reset");
assert_eq!(classify_io_error(&reset), ToolErrorCategory::NetworkError);
let timed_out = std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out");
assert_eq!(classify_io_error(&timed_out), ToolErrorCategory::Timeout);
}
#[test]
fn tool_error_feedback_format() {
let fb = ToolErrorFeedback {
category: ToolErrorCategory::InvalidParameters,
message: "missing required field: url".to_owned(),
retryable: false,
};
let s = fb.format_for_llm();
assert!(s.contains("[tool_error]"));
assert!(s.contains("invalid_parameters"));
assert!(s.contains("missing required field: url"));
assert!(s.contains("retryable: false"));
}
#[test]
fn phase_setup_for_tool_not_found() {
assert_eq!(
ToolErrorCategory::ToolNotFound.phase(),
ToolInvocationPhase::Setup
);
}
#[test]
fn phase_param_handling() {
assert_eq!(
ToolErrorCategory::InvalidParameters.phase(),
ToolInvocationPhase::ParamHandling
);
assert_eq!(
ToolErrorCategory::TypeMismatch.phase(),
ToolInvocationPhase::ParamHandling
);
}
}