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: zeph_common::ToolName },
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    /// Returns true if this error is `LlmError::NoProviders` (all configured backends unavailable).
64    #[must_use]
65    pub fn is_no_providers(&self) -> bool {
66        matches!(self, Self::Llm(zeph_llm::LlmError::NoProviders))
67    }
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73
74    #[test]
75    fn agent_error_detects_context_length_from_llm() {
76        let e = AgentError::Llm(zeph_llm::LlmError::ContextLengthExceeded);
77        assert!(e.is_context_length_error());
78    }
79
80    #[test]
81    fn agent_error_detects_context_length_from_other_message() {
82        let e = AgentError::Llm(zeph_llm::LlmError::Other("context length exceeded".into()));
83        assert!(e.is_context_length_error());
84    }
85
86    #[test]
87    fn agent_error_non_llm_variant_not_detected() {
88        let e = AgentError::Other("something went wrong".into());
89        assert!(!e.is_context_length_error());
90    }
91
92    #[test]
93    fn shutdown_variant_display() {
94        let e = AgentError::Shutdown;
95        assert_eq!(e.to_string(), "agent shut down");
96    }
97
98    #[test]
99    fn context_exhausted_variant_display() {
100        let e = AgentError::ContextExhausted("no space left".into());
101        assert!(e.to_string().contains("no space left"));
102    }
103
104    #[test]
105    fn tool_timeout_variant_display() {
106        let e = AgentError::ToolTimeout {
107            tool_name: "bash".into(),
108        };
109        assert!(e.to_string().contains("bash"));
110    }
111
112    #[test]
113    fn schema_validation_variant_display() {
114        let e = AgentError::SchemaValidation("missing field".into());
115        assert!(e.to_string().contains("missing field"));
116    }
117
118    #[test]
119    fn agent_error_detects_beta_header_rejected() {
120        let e = AgentError::Llm(zeph_llm::LlmError::BetaHeaderRejected {
121            header: "compact-2026-01-12".into(),
122        });
123        assert!(e.is_beta_header_rejected());
124    }
125
126    #[test]
127    fn agent_error_non_llm_variant_not_beta_rejected() {
128        let e = AgentError::Other("something went wrong".into());
129        assert!(!e.is_beta_header_rejected());
130    }
131
132    #[test]
133    fn agent_error_detects_no_providers() {
134        let e = AgentError::Llm(zeph_llm::LlmError::NoProviders);
135        assert!(e.is_no_providers());
136    }
137
138    #[test]
139    fn agent_error_non_no_providers_returns_false() {
140        let e = AgentError::Other("other".into());
141        assert!(!e.is_no_providers());
142    }
143}