#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
Network,
Api,
Authentication,
Authorization,
RateLimit,
Timeout,
Model,
Tool,
Policy,
Configuration,
Other,
}
impl ErrorCategory {
pub fn is_retriable(self) -> bool {
matches!(
self,
ErrorCategory::Network | ErrorCategory::Timeout | ErrorCategory::RateLimit
)
}
pub fn is_authentication_error(self) -> bool {
matches!(
self,
ErrorCategory::Authentication | ErrorCategory::Authorization
)
}
pub fn is_client_error(self) -> bool {
matches!(
self,
ErrorCategory::Authentication
| ErrorCategory::Authorization
| ErrorCategory::Configuration
| ErrorCategory::Policy
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ErrorDiagnostic {
pub kind: String,
pub message: String,
pub retriable: bool,
pub source: Option<String>,
pub category: ErrorCategory,
pub status_code: Option<u16>,
pub retry_after: Option<std::time::Duration>,
}
impl Default for ErrorDiagnostic {
fn default() -> Self {
Self {
kind: "unknown".to_string(),
message: String::new(),
retriable: false,
source: None,
category: ErrorCategory::Other,
status_code: None,
retry_after: None,
}
}
}
pub trait DiagnosticError {
fn diagnostic(&self) -> ErrorDiagnostic;
fn is_retriable(&self) -> bool {
self.diagnostic().retriable
}
fn category(&self) -> ErrorCategory {
self.diagnostic().category
}
}
#[derive(thiserror::Error, Debug)]
pub enum ProviderError {
#[error("网络错误:{message}")]
Network {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
retriable: bool,
},
#[error("API 错误 ({status}): {message}")]
Api {
status: u16,
message: String,
code: Option<String>,
},
#[error("认证失败:{message}")]
Authentication { message: String },
#[error("请求频率过高:{message}")]
RateLimit {
message: String,
retry_after: Option<std::time::Duration>,
},
#[error("请求超时:{message}")]
Timeout {
message: String,
elapsed: std::time::Duration,
},
#[error("模型错误:{message}")]
Model { message: String },
#[error("provider error: {0}")]
Message(String),
}
impl ProviderError {
pub fn network(message: impl Into<String>) -> Self {
Self::Network {
message: message.into(),
source: None,
retriable: true,
}
}
pub fn authentication(message: impl Into<String>) -> Self {
Self::Authentication {
message: message.into(),
}
}
pub fn rate_limit(
message: impl Into<String>,
retry_after: Option<std::time::Duration>,
) -> Self {
Self::RateLimit {
message: message.into(),
retry_after,
}
}
pub fn is_retriable(&self) -> bool {
match self {
ProviderError::Network { retriable, .. } => *retriable,
ProviderError::RateLimit { .. } => true,
ProviderError::Timeout { .. } => true,
ProviderError::Model { .. } => true,
ProviderError::Api { status, .. } => {
*status >= 500 && *status < 600
}
_ => false,
}
}
pub fn category(&self) -> ErrorCategory {
match self {
ProviderError::Network { .. } => ErrorCategory::Network,
ProviderError::Api { .. } => ErrorCategory::Api,
ProviderError::Authentication { .. } => ErrorCategory::Authentication,
ProviderError::RateLimit { .. } => ErrorCategory::RateLimit,
ProviderError::Timeout { .. } => ErrorCategory::Timeout,
ProviderError::Model { .. } => ErrorCategory::Model,
ProviderError::Message(_) => ErrorCategory::Other,
}
}
}
impl DiagnosticError for ProviderError {
fn diagnostic(&self) -> ErrorDiagnostic {
match self {
ProviderError::Network {
message, retriable, ..
} => ErrorDiagnostic {
kind: "provider".to_string(),
message: message.clone(),
retriable: *retriable,
source: None,
category: ErrorCategory::Network,
status_code: None,
retry_after: None,
},
ProviderError::Api {
status,
message,
code,
} => ErrorDiagnostic {
kind: "provider".to_string(),
message: message.clone(),
retriable: *status >= 500,
source: code.clone(),
category: ErrorCategory::Api,
status_code: Some(*status),
retry_after: None,
},
ProviderError::Authentication { message } => ErrorDiagnostic {
kind: "provider".to_string(),
message: message.clone(),
retriable: false,
source: None,
category: ErrorCategory::Authentication,
status_code: None,
retry_after: None,
},
ProviderError::RateLimit {
message,
retry_after,
} => ErrorDiagnostic {
kind: "provider".to_string(),
message: message.clone(),
retriable: true,
source: None,
category: ErrorCategory::RateLimit,
status_code: Some(429),
retry_after: *retry_after,
},
ProviderError::Timeout {
message,
elapsed: _,
} => ErrorDiagnostic {
kind: "provider".to_string(),
message: message.clone(),
retriable: true,
source: None,
category: ErrorCategory::Timeout,
status_code: None,
retry_after: None, },
ProviderError::Model { message } => ErrorDiagnostic {
kind: "provider".to_string(),
message: message.clone(),
retriable: false, source: None,
category: ErrorCategory::Model,
status_code: None,
retry_after: None,
},
ProviderError::Message(msg) => ErrorDiagnostic {
kind: "provider".to_string(),
message: msg.clone(),
retriable: true,
source: None,
category: ErrorCategory::Other,
status_code: None,
retry_after: None,
},
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum ToolError {
#[error("tool error: {0}")]
Message(String),
#[error("tool policy denied (rule_id={rule_id}): {reason}")]
PolicyDenied { rule_id: String, reason: String },
#[error("工具不存在:{name}")]
NotFound { name: String },
#[error("输入验证失败:{message}")]
ValidationError { message: String },
#[error("工具执行超时:{message}")]
Timeout { message: String },
}
impl DiagnosticError for ToolError {
fn diagnostic(&self) -> ErrorDiagnostic {
match self {
ToolError::Message(msg) => ErrorDiagnostic {
kind: "tool".to_string(),
message: msg.clone(),
retriable: false,
source: None,
category: ErrorCategory::Tool,
status_code: None,
retry_after: None,
},
ToolError::PolicyDenied { rule_id, reason } => ErrorDiagnostic {
kind: "tool".to_string(),
message: format!("policy denied (rule_id={rule_id}): {reason}"),
retriable: false,
source: Some(rule_id.clone()),
category: ErrorCategory::Policy,
status_code: None,
retry_after: None,
},
ToolError::NotFound { name } => ErrorDiagnostic {
kind: "tool".to_string(),
message: format!("工具不存在:{name}"),
retriable: false,
source: None,
category: ErrorCategory::Configuration,
status_code: None,
retry_after: None,
},
ToolError::ValidationError { message } => ErrorDiagnostic {
kind: "tool".to_string(),
message: format!("输入验证失败:{message}"),
retriable: false,
source: None,
category: ErrorCategory::Configuration,
status_code: None,
retry_after: None,
},
ToolError::Timeout { message } => ErrorDiagnostic {
kind: "tool".to_string(),
message: format!("工具执行超时:{message}"),
retriable: false, source: None,
category: ErrorCategory::Timeout,
status_code: None,
retry_after: None,
},
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum SkillError {
#[error("skill error: {0}")]
Message(String),
#[error("技能不存在:{name}")]
NotFound { name: String },
#[error("技能执行超时:{message}")]
Timeout { message: String },
}
impl DiagnosticError for SkillError {
fn diagnostic(&self) -> ErrorDiagnostic {
match self {
SkillError::Message(msg) => ErrorDiagnostic {
kind: "skill".to_string(),
message: msg.clone(),
retriable: false,
source: None,
category: ErrorCategory::Other,
status_code: None,
retry_after: None,
},
SkillError::NotFound { name } => ErrorDiagnostic {
kind: "skill".to_string(),
message: format!("技能不存在:{name}"),
retriable: false,
source: None,
category: ErrorCategory::Configuration,
status_code: None,
retry_after: None,
},
SkillError::Timeout { message } => ErrorDiagnostic {
kind: "skill".to_string(),
message: format!("技能执行超时:{message}"),
retriable: true,
source: None,
category: ErrorCategory::Timeout,
status_code: None,
retry_after: None,
},
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum AgentError {
#[error("agent error: {0}")]
Message(String),
#[error("超过最大步数限制:{max_steps}")]
MaxStepsExceeded { max_steps: usize },
#[error("Provider 错误:{source}")]
ProviderError {
#[source]
source: ProviderError,
},
#[error("此决策需要 Runtime 支持,请使用 Runtime 模式运行")]
RequiresRuntime,
}
impl DiagnosticError for AgentError {
fn diagnostic(&self) -> ErrorDiagnostic {
match self {
AgentError::Message(msg) => ErrorDiagnostic {
kind: "runtime".to_string(),
message: msg.clone(),
retriable: false,
source: None,
category: ErrorCategory::Other,
status_code: None,
retry_after: None,
},
AgentError::MaxStepsExceeded { max_steps } => ErrorDiagnostic {
kind: "runtime".to_string(),
message: format!("超过最大步数限制:{max_steps}"),
retriable: false,
source: None,
category: ErrorCategory::Configuration,
status_code: None,
retry_after: None,
},
AgentError::ProviderError { source } => {
let mut diag = source.diagnostic();
diag.kind = "runtime".to_string();
diag
}
AgentError::RequiresRuntime => ErrorDiagnostic {
kind: "runtime".to_string(),
message: "此决策需要 Runtime 支持,请使用 Runtime 模式运行".to_string(),
retriable: false,
source: None,
category: ErrorCategory::Configuration,
status_code: None,
retry_after: None,
},
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum MemoryError {
#[error("memory error: {0}")]
Message(String),
#[error("记忆不存在:{id}")]
NotFound { id: String },
}
impl DiagnosticError for MemoryError {
fn diagnostic(&self) -> ErrorDiagnostic {
match self {
MemoryError::Message(msg) => ErrorDiagnostic {
kind: "memory".to_string(),
message: msg.clone(),
retriable: false,
source: None,
category: ErrorCategory::Other,
status_code: None,
retry_after: None,
},
MemoryError::NotFound { id } => ErrorDiagnostic {
kind: "memory".to_string(),
message: format!("记忆不存在:{id}"),
retriable: false,
source: None,
category: ErrorCategory::Configuration,
status_code: None,
retry_after: None,
},
}
}
}
#[derive(thiserror::Error, Debug)]
pub enum ChannelError {
#[error("channel error: {0}")]
Message(String),
}
impl DiagnosticError for ChannelError {
fn diagnostic(&self) -> ErrorDiagnostic {
match self {
ChannelError::Message(msg) => ErrorDiagnostic {
kind: "channel".to_string(),
message: msg.clone(),
retriable: false,
source: None,
category: ErrorCategory::Other,
status_code: None,
retry_after: None,
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provider_error_retriable() {
let network = ProviderError::network("连接失败");
assert!(network.is_retriable());
assert_eq!(network.category(), ErrorCategory::Network);
let auth = ProviderError::authentication("API Key 无效");
assert!(!auth.is_retriable());
assert_eq!(auth.category(), ErrorCategory::Authentication);
let rate_limit =
ProviderError::rate_limit("限流", Some(std::time::Duration::from_secs(60)));
assert!(rate_limit.is_retriable());
assert_eq!(rate_limit.category(), ErrorCategory::RateLimit);
}
#[test]
fn test_error_category() {
assert!(ErrorCategory::Network.is_retriable());
assert!(ErrorCategory::Timeout.is_retriable());
assert!(ErrorCategory::RateLimit.is_retriable());
assert!(!ErrorCategory::Authentication.is_retriable());
assert!(!ErrorCategory::Policy.is_retriable());
}
#[test]
fn test_diagnostic() {
let error = ProviderError::Api {
status: 503,
message: "服务不可用".to_string(),
code: None,
};
let diag = error.diagnostic();
assert_eq!(diag.kind, "provider");
assert_eq!(diag.status_code, Some(503));
assert!(diag.retriable);
assert_eq!(diag.category, ErrorCategory::Api);
}
}