//! Agent Runtime 统一错误类型。 //! //! `AgentError` 聚合 Phase 0-3 各层错误(LlmError / ToolError / MemoryError), //! 加上 Agent 层特有的错误变体。设计原则: //! //! - 聚合而非包装:保留内层错误的类型信息(避免 `Box` 丢失上下文) //! - 显式 `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` 包装。 #[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(_))); } }