Files
agcore/src/agent/error.rs
T

184 lines
5.5 KiB
Rust

//! Agent Runtime 统一错误类型。
//!
//! `AgentError` 聚合 Phase 0-3 各层错误(LlmError / ToolError / MemoryError),
//! 加上 Agent 层特有的错误变体。设计原则:
//!
//! - 聚合而非包装:保留内层错误的类型信息(避免 `Box<dyn Error>` 丢失上下文)
//! - 显式 `From` 实现:让 `?` 运算符能透明传播下层错误
//! - `is_recoverable()`:根据变体类型判定可恢复性,便于上层决策
use thiserror::Error;
use crate::llm::error::LlmError;
use crate::memory::error::MemoryError;
use crate::tools::error::ToolError;
/// Agent Runtime 统一错误枚举。
///
/// **不实现 `Clone`**:透传内层 `LlmError` / `MemoryError`,两者均未派生 `Clone`(保留
/// 完整错误信息,传递所有权)。如需在多 session 间共享错误状态,用 `Arc<AgentError>` 包装。
#[derive(Debug, Error)]
pub enum AgentError {
/// LLM 调用错误(透传 Phase 0)。
#[error("LLM 错误: {0}")]
Llm(#[from] LlmError),
/// 工具调用错误(透传 Phase 2)。
#[error("工具错误: {0}")]
Tool(#[from] ToolError),
/// 记忆系统错误(透传 Phase 3)。
#[error("记忆错误: {0}")]
Memory(#[from] MemoryError),
/// Plan 解析失败(Phase 4b 新增)。
#[error("Plan 解析错误: {0}")]
PlanParse(String),
/// 钩子阻断操作(Agent 层特有)。
#[error("钩子阻断: {0}")]
HookBlocked(String),
/// 达到限制阈值(最大 turn、token 预算等)。
#[error("超过限制: {0}")]
LimitExceeded(String),
/// 配置错误(构建 RuntimeBundle / AgentSession 时校验失败)。
#[error("配置错误: {0}")]
Config(String),
/// 其他未分类错误(兜底)。
#[error("Agent 错误: {0}")]
Other(String),
}
impl AgentError {
/// 判定错误是否可恢复。
///
/// - `Llm` / `Memory`:由内层 `is_recoverable()` 决定
/// - `Tool`:由内层 `is_recoverable()` 决定
/// - `HookBlocked` / `LimitExceeded`:不可恢复(需人工介入或终止循环)
/// - `Config` / `Other`:不可恢复
pub fn is_recoverable(&self) -> bool {
match self {
Self::Llm(e) => matches!(
e,
LlmError::RateLimit { .. } | LlmError::Timeout { .. } | LlmError::Stream(_)
),
Self::Tool(e) => e.is_recoverable(),
Self::Memory(e) => e.is_recoverable(),
Self::PlanParse(_) => false,
Self::HookBlocked(_) | Self::LimitExceeded(_) | Self::Config(_) | Self::Other(_) => {
false
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn llm_recoverable_propagation() {
let err = AgentError::Llm(LlmError::Timeout {
duration: std::time::Duration::from_secs(30),
});
assert!(err.is_recoverable());
}
#[test]
fn llm_non_recoverable_propagation() {
let err = AgentError::Llm(LlmError::Authentication("bad key".into()));
assert!(!err.is_recoverable());
}
#[test]
fn tool_recoverable_propagation() {
let err = AgentError::Tool(ToolError::ExecutionFailed("foo".into(), "boom".into()));
assert!(err.is_recoverable());
}
#[test]
fn tool_non_recoverable_propagation() {
let err = AgentError::Tool(ToolError::NotFound("foo".into()));
assert!(!err.is_recoverable());
}
#[test]
fn memory_recoverable_propagation() {
let err = AgentError::Memory(MemoryError::NotFound("page".into()));
assert!(err.is_recoverable());
}
#[test]
fn memory_non_recoverable_propagation() {
let err = AgentError::Memory(MemoryError::Storage("disk full".into()));
assert!(!err.is_recoverable());
}
#[test]
fn hook_blocked_not_recoverable() {
assert!(!AgentError::HookBlocked("denied".into()).is_recoverable());
}
#[test]
fn limit_exceeded_not_recoverable() {
assert!(!AgentError::LimitExceeded("max turns".into()).is_recoverable());
}
#[test]
fn config_not_recoverable() {
assert!(!AgentError::Config("missing provider".into()).is_recoverable());
}
#[test]
fn other_not_recoverable() {
assert!(!AgentError::Other("unknown".into()).is_recoverable());
}
#[test]
fn plan_parse_not_recoverable() {
assert!(!AgentError::PlanParse("bad json".into()).is_recoverable());
}
#[test]
fn from_llm_via_question_mark() {
fn returns_llm() -> Result<(), LlmError> {
Err(LlmError::Other("test".into()))
}
fn caller() -> Result<(), AgentError> {
returns_llm()?;
Ok(())
}
let err = caller().unwrap_err();
assert!(matches!(err, AgentError::Llm(_)));
}
#[test]
fn from_tool_via_question_mark() {
fn returns_tool() -> Result<(), ToolError> {
Err(ToolError::NotFound("x".into()))
}
fn caller() -> Result<(), AgentError> {
returns_tool()?;
Ok(())
}
let err = caller().unwrap_err();
assert!(matches!(err, AgentError::Tool(_)));
}
#[test]
fn from_memory_via_question_mark() {
fn returns_mem() -> Result<(), MemoryError> {
Err(MemoryError::Storage("x".into()))
}
fn caller() -> Result<(), AgentError> {
returns_mem()?;
Ok(())
}
let err = caller().unwrap_err();
assert!(matches!(err, AgentError::Memory(_)));
}
}