1#[non_exhaustive]
5#[derive(Debug, thiserror::Error)]
12pub enum OrchestrationFailure {
13 #[error("scheduler error: {0}")]
15 SchedulerInit(String),
16
17 #[error("config verification error: {0}")]
19 VerifyConfig(String),
20
21 #[error("planner error: {0}")]
23 PlannerError(String),
24
25 #[error("retry reset error: {0}")]
27 RetryReset(String),
28
29 #[error("{0}")]
31 Generic(String),
32}
33
34#[non_exhaustive]
35#[derive(Debug, thiserror::Error)]
39pub enum SkillOperationFailure {
40 #[error("invalid skill name: {0}")]
42 InvalidName(String),
43
44 #[error("skill directory not found: {0}")]
46 DirectoryNotFound(String),
47
48 #[error("{0}")]
50 Generic(String),
51}
52
53#[non_exhaustive]
54#[derive(Debug, thiserror::Error)]
61pub enum AgentError {
62 #[error(transparent)]
63 Llm(#[from] zeph_llm::LlmError),
64
65 #[error(transparent)]
66 Channel(#[from] crate::channel::ChannelError),
67
68 #[error(transparent)]
69 Memory(#[from] zeph_memory::MemoryError),
70
71 #[error(transparent)]
72 Skill(#[from] zeph_skills::SkillError),
73
74 #[error(transparent)]
75 Tool(#[from] zeph_tools::executor::ToolError),
76
77 #[error("I/O error: {0}")]
78 Io(#[from] std::io::Error),
79
80 #[error("blocking task failed: {0}")]
82 SpawnBlocking(#[from] tokio::task::JoinError),
83
84 #[error("agent shut down")]
86 Shutdown,
87
88 #[error("context exhausted: {0}")]
90 ContextExhausted(String),
91
92 #[error("tool timed out: {tool_name}")]
94 ToolTimeout { tool_name: zeph_common::ToolName },
95
96 #[error("schema validation failed: {0}")]
98 SchemaValidation(String),
99
100 #[error("orchestration error: {0}")]
102 OrchestrationError(#[from] OrchestrationFailure),
103
104 #[error("unknown command: {0}")]
106 UnknownCommand(String),
107
108 #[error("skill error: {0}")]
110 SkillOperation(#[from] SkillOperationFailure),
111
112 #[error("context error: {0}")]
114 ContextError(String),
115
116 #[error(transparent)]
118 Db(#[from] zeph_db::DbError),
119}
120
121impl AgentError {
122 #[must_use]
124 pub fn is_context_length_error(&self) -> bool {
125 if let Self::Llm(e) = self {
126 return e.is_context_length_error();
127 }
128 false
129 }
130
131 #[must_use]
133 pub fn is_beta_header_rejected(&self) -> bool {
134 if let Self::Llm(e) = self {
135 return e.is_beta_header_rejected();
136 }
137 false
138 }
139
140 #[must_use]
142 pub fn is_no_providers(&self) -> bool {
143 matches!(self, Self::Llm(zeph_llm::LlmError::NoProviders))
144 }
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn agent_error_detects_context_length_from_llm() {
153 let e = AgentError::Llm(zeph_llm::LlmError::ContextLengthExceeded);
154 assert!(e.is_context_length_error());
155 }
156
157 #[test]
158 fn agent_error_detects_context_length_from_typed_variant() {
159 let e = AgentError::Llm(zeph_llm::LlmError::ContextLengthExceeded);
161 assert!(e.is_context_length_error());
162 }
163
164 #[test]
165 fn agent_error_other_with_context_message_not_detected() {
166 let e = AgentError::Llm(zeph_llm::LlmError::Other("context length exceeded".into()));
169 assert!(!e.is_context_length_error());
170 }
171
172 #[test]
173 fn agent_error_non_llm_variant_not_detected() {
174 let e = AgentError::ContextError("something went wrong".into());
175 assert!(!e.is_context_length_error());
176 }
177
178 #[test]
179 fn shutdown_variant_display() {
180 let e = AgentError::Shutdown;
181 assert_eq!(e.to_string(), "agent shut down");
182 }
183
184 #[test]
185 fn context_exhausted_variant_display() {
186 let e = AgentError::ContextExhausted("no space left".into());
187 assert!(e.to_string().contains("no space left"));
188 }
189
190 #[test]
191 fn tool_timeout_variant_display() {
192 let e = AgentError::ToolTimeout {
193 tool_name: "bash".into(),
194 };
195 assert!(e.to_string().contains("bash"));
196 }
197
198 #[test]
199 fn schema_validation_variant_display() {
200 let e = AgentError::SchemaValidation("missing field".into());
201 assert!(e.to_string().contains("missing field"));
202 }
203
204 #[test]
205 fn agent_error_detects_beta_header_rejected() {
206 let e = AgentError::Llm(zeph_llm::LlmError::BetaHeaderRejected {
207 header: "compact-2026-01-12".into(),
208 });
209 assert!(e.is_beta_header_rejected());
210 }
211
212 #[test]
213 fn agent_error_non_llm_variant_not_beta_rejected() {
214 let e = AgentError::ContextError("something went wrong".into());
215 assert!(!e.is_beta_header_rejected());
216 }
217
218 #[test]
219 fn agent_error_detects_no_providers() {
220 let e = AgentError::Llm(zeph_llm::LlmError::NoProviders);
221 assert!(e.is_no_providers());
222 }
223
224 #[test]
225 fn agent_error_non_no_providers_returns_false() {
226 let e = AgentError::ContextError("other".into());
227 assert!(!e.is_no_providers());
228 }
229
230 #[test]
231 fn orchestration_error_display() {
232 let e =
233 AgentError::OrchestrationError(OrchestrationFailure::Generic("planner failed".into()));
234 assert!(e.to_string().contains("planner failed"));
235 }
236
237 #[test]
238 fn orchestration_failure_variants_display() {
239 assert!(
240 OrchestrationFailure::SchedulerInit("dag error".into())
241 .to_string()
242 .contains("dag error")
243 );
244 assert!(
245 OrchestrationFailure::VerifyConfig("bad config".into())
246 .to_string()
247 .contains("bad config")
248 );
249 assert!(
250 OrchestrationFailure::PlannerError("plan failed".into())
251 .to_string()
252 .contains("plan failed")
253 );
254 assert!(
255 OrchestrationFailure::RetryReset("reset failed".into())
256 .to_string()
257 .contains("reset failed")
258 );
259 }
260
261 #[test]
262 fn unknown_command_display() {
263 let e = AgentError::UnknownCommand("/foo".into());
264 assert!(e.to_string().contains("/foo"));
265 }
266
267 #[test]
268 fn skill_operation_display() {
269 let e =
270 AgentError::SkillOperation(SkillOperationFailure::DirectoryNotFound("my-skill".into()));
271 assert!(e.to_string().contains("my-skill"));
272 }
273
274 #[test]
275 fn skill_operation_failure_variants_display() {
276 assert!(
277 SkillOperationFailure::InvalidName("bad/name".into())
278 .to_string()
279 .contains("bad/name")
280 );
281 assert!(
282 SkillOperationFailure::DirectoryNotFound("foo".into())
283 .to_string()
284 .contains("foo")
285 );
286 }
287}