Skip to main content

zeph_core/agent/
error.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4#[derive(Debug, thiserror::Error)]
5pub enum AgentError {
6    #[error(transparent)]
7    Llm(#[from] zeph_llm::LlmError),
8
9    #[error(transparent)]
10    Channel(#[from] crate::channel::ChannelError),
11
12    #[error(transparent)]
13    Memory(#[from] zeph_memory::MemoryError),
14
15    #[error(transparent)]
16    Skill(#[from] zeph_skills::SkillError),
17
18    #[error(transparent)]
19    Tool(#[from] zeph_tools::executor::ToolError),
20
21    #[error("I/O error: {0}")]
22    Io(#[from] std::io::Error),
23
24    /// Agent received a shutdown signal and exited the run loop cleanly.
25    #[error("agent shut down")]
26    Shutdown,
27
28    /// The context window was exhausted and could not be compacted further.
29    #[error("context exhausted: {0}")]
30    ContextExhausted(String),
31
32    /// A tool call exceeded its configured timeout.
33    #[error("tool timed out: {tool_name}")]
34    ToolTimeout { tool_name: String },
35
36    /// Structured output did not conform to the expected JSON schema.
37    #[error("schema validation failed: {0}")]
38    SchemaValidation(String),
39
40    #[error("{0}")]
41    Other(String),
42}
43
44impl AgentError {
45    /// Returns true if this error originates from a context length exceeded condition.
46    #[must_use]
47    pub fn is_context_length_error(&self) -> bool {
48        if let Self::Llm(e) = self {
49            return e.is_context_length_error();
50        }
51        false
52    }
53
54    /// Returns true if this error indicates that a beta header was rejected by the API.
55    #[must_use]
56    pub fn is_beta_header_rejected(&self) -> bool {
57        if let Self::Llm(e) = self {
58            return e.is_beta_header_rejected();
59        }
60        false
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    #[test]
69    fn agent_error_detects_context_length_from_llm() {
70        let e = AgentError::Llm(zeph_llm::LlmError::ContextLengthExceeded);
71        assert!(e.is_context_length_error());
72    }
73
74    #[test]
75    fn agent_error_detects_context_length_from_other_message() {
76        let e = AgentError::Llm(zeph_llm::LlmError::Other("context length exceeded".into()));
77        assert!(e.is_context_length_error());
78    }
79
80    #[test]
81    fn agent_error_non_llm_variant_not_detected() {
82        let e = AgentError::Other("something went wrong".into());
83        assert!(!e.is_context_length_error());
84    }
85
86    #[test]
87    fn shutdown_variant_display() {
88        let e = AgentError::Shutdown;
89        assert_eq!(e.to_string(), "agent shut down");
90    }
91
92    #[test]
93    fn context_exhausted_variant_display() {
94        let e = AgentError::ContextExhausted("no space left".into());
95        assert!(e.to_string().contains("no space left"));
96    }
97
98    #[test]
99    fn tool_timeout_variant_display() {
100        let e = AgentError::ToolTimeout {
101            tool_name: "bash".into(),
102        };
103        assert!(e.to_string().contains("bash"));
104    }
105
106    #[test]
107    fn schema_validation_variant_display() {
108        let e = AgentError::SchemaValidation("missing field".into());
109        assert!(e.to_string().contains("missing field"));
110    }
111
112    #[test]
113    fn agent_error_detects_beta_header_rejected() {
114        let e = AgentError::Llm(zeph_llm::LlmError::BetaHeaderRejected {
115            header: "compact-2026-01-12".into(),
116        });
117        assert!(e.is_beta_header_rejected());
118    }
119
120    #[test]
121    fn agent_error_non_llm_variant_not_beta_rejected() {
122        let e = AgentError::Other("something went wrong".into());
123        assert!(!e.is_beta_header_rejected());
124    }
125}