use std::fmt;
use super::{ParsedLegacyOutput, tool_error_retryable_heuristic};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[allow(dead_code)] pub enum ToolFailureCategory {
InvalidInput,
PolicyDenied,
Workspace,
Timeout,
External,
Internal,
Unknown,
}
impl ToolFailureCategory {
pub const fn as_str(self) -> &'static str {
match self {
Self::InvalidInput => "invalid_input",
Self::PolicyDenied => "policy_denied",
Self::Workspace => "workspace",
Self::Timeout => "timeout",
Self::External => "external",
Self::Internal => "internal",
Self::Unknown => "unknown",
}
}
}
impl fmt::Display for ToolFailureCategory {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct ToolError {
pub category: ToolFailureCategory,
pub code: String,
pub message: String,
#[allow(dead_code)]
pub retryable: bool,
pub legacy_parsed: ParsedLegacyOutput,
}
impl fmt::Display for ToolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let head = self.message.lines().next().unwrap_or("").trim();
if head.is_empty() {
write!(f, "[{}] {}", self.category.as_str(), self.code)
} else {
write!(f, "[{}][{}] {}", self.category.as_str(), self.code, head)
}
}
}
impl std::error::Error for ToolError {}
pub fn failure_category_for_error_code(code: &str) -> ToolFailureCategory {
match code {
"invalid_args" | "missing_command" => ToolFailureCategory::InvalidInput,
"unknown_tool" => ToolFailureCategory::Unknown,
"command_not_allowed"
| "command_denied"
| "approval_denied"
| "approval_required"
| "repeated_tool_failure_short_circuit"
| "repeated_tool_family_failure_short_circuit" => ToolFailureCategory::PolicyDenied,
"workflow_semaphore_closed"
| "workflow_node_missing_result"
| "workflow_tool_join_error" => ToolFailureCategory::External,
"workspace_not_set" | "workspace_no_cargo_toml" | "codebase_semantic_unconfigured" => {
ToolFailureCategory::Workspace
}
"timeout" => ToolFailureCategory::Timeout,
"cancelled" => ToolFailureCategory::Timeout,
"rate_limited" => ToolFailureCategory::PolicyDenied,
"command_not_found" | "permission_denied" | "spawn_failed" | "cargo_spawn_failed" => {
ToolFailureCategory::External
}
"read_file_invalid_range" | "read_file_count_total_too_large" => {
ToolFailureCategory::InvalidInput
}
"read_file_not_file"
| "read_file_io"
| "read_file_utf8_decode"
| "read_file_encoding"
| "read_file_internal" => ToolFailureCategory::External,
c if c.starts_with("read_file_workspace_") => ToolFailureCategory::Workspace,
"search_in_files_invalid_regex" | "search_in_files_invalid_glob" => {
ToolFailureCategory::InvalidInput
}
"search_in_files_workspace_base_resolve_failed"
| "search_in_files_workspace_subpath_resolve_failed"
| "search_in_files_workspace_outside_root"
| "search_in_files_path_absolute_not_allowed" => ToolFailureCategory::Workspace,
c if c.ends_with("_failed") => ToolFailureCategory::External,
_ => ToolFailureCategory::Unknown,
}
}
impl ToolError {
pub fn from_parsed_legacy(
tool_name: &str,
parsed: &ParsedLegacyOutput,
raw_output: String,
) -> Self {
let code = parsed
.error_code
.clone()
.unwrap_or_else(|| format!("{tool_name}_failed"));
let retryable = tool_error_retryable_heuristic(parsed.error_code.as_deref());
let category = failure_category_for_error_code(code.as_str());
Self {
category,
code,
message: raw_output,
retryable,
legacy_parsed: parsed.clone(),
}
}
pub fn invalid_args(message: String) -> Self {
let parsed = ParsedLegacyOutput {
ok: false,
exit_code: None,
stdout: String::new(),
stderr: String::new(),
error_code: Some("invalid_args".to_string()),
};
Self {
category: ToolFailureCategory::InvalidInput,
code: "invalid_args".to_string(),
message,
retryable: false,
legacy_parsed: parsed,
}
}
pub fn workspace(code: &'static str, message: String) -> Self {
let parsed = ParsedLegacyOutput {
ok: false,
exit_code: None,
stdout: String::new(),
stderr: String::new(),
error_code: Some(code.to_string()),
};
let retryable = tool_error_retryable_heuristic(parsed.error_code.as_deref());
Self {
category: failure_category_for_error_code(code),
code: code.to_string(),
message,
retryable,
legacy_parsed: parsed,
}
}
pub fn unknown_tool(name: &str) -> Self {
let message = format!("未知工具:{}", name);
let parsed = super::parse_legacy_output(name, &message);
Self::from_parsed_legacy(name, &parsed, message)
}
pub fn rate_limited(message: String) -> Self {
let parsed = ParsedLegacyOutput {
ok: false,
exit_code: None,
stdout: String::new(),
stderr: String::new(),
error_code: Some("rate_limited".to_string()),
};
Self {
category: ToolFailureCategory::PolicyDenied,
code: "rate_limited".to_string(),
message,
retryable: true,
legacy_parsed: parsed,
}
}
pub fn approval_required(message: String) -> Self {
let parsed = ParsedLegacyOutput {
ok: false,
exit_code: None,
stdout: String::new(),
stderr: String::new(),
error_code: Some("approval_required".to_string()),
};
Self {
category: ToolFailureCategory::PolicyDenied,
code: "approval_required".to_string(),
message,
retryable: false,
legacy_parsed: parsed,
}
}
pub fn cargo_subcommand_failed(tool_code: &str, exit_code: i32, message: String) -> Self {
let code = format!("{tool_code}_failed");
let parsed = ParsedLegacyOutput {
ok: false,
exit_code: Some(exit_code),
stdout: String::new(),
stderr: String::new(),
error_code: Some(code.clone()),
};
let retryable = tool_error_retryable_heuristic(parsed.error_code.as_deref());
Self {
category: ToolFailureCategory::External,
code,
message,
retryable,
legacy_parsed: parsed,
}
}
pub fn subprocess_spawn_error(title: &str, err: std::io::Error) -> Self {
let message = format!("{}: 执行失败({})", title, err);
let parsed = ParsedLegacyOutput {
ok: false,
exit_code: None,
stdout: String::new(),
stderr: String::new(),
error_code: Some("cargo_spawn_failed".to_string()),
};
Self {
category: ToolFailureCategory::External,
code: "cargo_spawn_failed".to_string(),
message,
retryable: false,
legacy_parsed: parsed,
}
}
pub fn external_code(code: &'static str, message: String) -> Self {
let parsed = ParsedLegacyOutput {
ok: false,
exit_code: None,
stdout: String::new(),
stderr: String::new(),
error_code: Some(code.to_string()),
};
let retryable = tool_error_retryable_heuristic(parsed.error_code.as_deref());
Self {
category: failure_category_for_error_code(code),
code: code.to_string(),
message,
retryable,
legacy_parsed: parsed,
}
}
pub fn internal_code(code: &'static str, message: String) -> Self {
let parsed = ParsedLegacyOutput {
ok: false,
exit_code: None,
stdout: String::new(),
stderr: String::new(),
error_code: Some(code.to_string()),
};
Self {
category: ToolFailureCategory::Internal,
code: code.to_string(),
message,
retryable: false,
legacy_parsed: parsed,
}
}
pub fn session_stop(code: &'static str, message: String) -> Self {
let mut parsed = super::parse_legacy_output(code, &message);
parsed.ok = false;
parsed.error_code = Some(code.to_string());
Self {
category: ToolFailureCategory::Timeout,
code: code.to_string(),
message,
retryable: code == "timeout",
legacy_parsed: parsed,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cm_tools::tool_result::parse_legacy_output;
#[test]
fn tool_error_from_unknown_tool_legacy() {
let raw = "未知工具:foo".to_string();
let p = parse_legacy_output("foo", &raw);
assert!(!p.ok);
let e = ToolError::from_parsed_legacy("foo", &p, raw.clone());
assert_eq!(e.code, "unknown_tool");
assert_eq!(e.category, ToolFailureCategory::Unknown);
assert!(!e.retryable);
assert_eq!(e.message, raw);
}
#[test]
fn tool_error_timeout_retryable() {
let raw = "错误:超时\n".to_string();
let p = parse_legacy_output("run_command", &raw);
let e = ToolError::from_parsed_legacy("run_command", &p, raw);
assert_eq!(e.code, "timeout");
assert!(e.retryable);
assert_eq!(e.category, ToolFailureCategory::Timeout);
}
#[test]
fn failure_category_maps_workflow_and_approval_codes() {
assert_eq!(
failure_category_for_error_code("workflow_tool_join_error"),
ToolFailureCategory::External
);
assert_eq!(
failure_category_for_error_code("approval_required"),
ToolFailureCategory::PolicyDenied
);
}
}