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 #[error(transparent)]
123 Session(#[from] zeph_session::SessionError),
124
125 #[error(transparent)]
128 Worktree(#[from] zeph_worktree::WorktreeError),
129}
130
131impl AgentError {
132 #[must_use]
134 pub fn is_context_length_error(&self) -> bool {
135 if let Self::Llm(e) = self {
136 return e.is_context_length_error();
137 }
138 false
139 }
140
141 #[must_use]
143 pub fn is_beta_header_rejected(&self) -> bool {
144 if let Self::Llm(e) = self {
145 return e.is_beta_header_rejected();
146 }
147 false
148 }
149
150 #[must_use]
152 pub fn is_no_providers(&self) -> bool {
153 matches!(self, Self::Llm(zeph_llm::LlmError::NoProviders))
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn agent_error_detects_context_length_from_llm() {
163 let e = AgentError::Llm(zeph_llm::LlmError::ContextLengthExceeded);
164 assert!(e.is_context_length_error());
165 }
166
167 #[test]
168 fn agent_error_detects_context_length_from_typed_variant() {
169 let e = AgentError::Llm(zeph_llm::LlmError::ContextLengthExceeded);
171 assert!(e.is_context_length_error());
172 }
173
174 #[test]
175 fn agent_error_other_with_context_message_not_detected() {
176 let e = AgentError::Llm(zeph_llm::LlmError::Other("context length exceeded".into()));
179 assert!(!e.is_context_length_error());
180 }
181
182 #[test]
183 fn agent_error_non_llm_variant_not_detected() {
184 let e = AgentError::ContextError("something went wrong".into());
185 assert!(!e.is_context_length_error());
186 }
187
188 #[test]
189 fn shutdown_variant_display() {
190 let e = AgentError::Shutdown;
191 assert_eq!(e.to_string(), "agent shut down");
192 }
193
194 #[test]
195 fn context_exhausted_variant_display() {
196 let e = AgentError::ContextExhausted("no space left".into());
197 assert!(e.to_string().contains("no space left"));
198 }
199
200 #[test]
201 fn tool_timeout_variant_display() {
202 let e = AgentError::ToolTimeout {
203 tool_name: "bash".into(),
204 };
205 assert!(e.to_string().contains("bash"));
206 }
207
208 #[test]
209 fn schema_validation_variant_display() {
210 let e = AgentError::SchemaValidation("missing field".into());
211 assert!(e.to_string().contains("missing field"));
212 }
213
214 #[test]
215 fn agent_error_detects_beta_header_rejected() {
216 let e = AgentError::Llm(zeph_llm::LlmError::BetaHeaderRejected {
217 header: "compact-2026-01-12".into(),
218 });
219 assert!(e.is_beta_header_rejected());
220 }
221
222 #[test]
223 fn agent_error_non_llm_variant_not_beta_rejected() {
224 let e = AgentError::ContextError("something went wrong".into());
225 assert!(!e.is_beta_header_rejected());
226 }
227
228 #[test]
229 fn agent_error_detects_no_providers() {
230 let e = AgentError::Llm(zeph_llm::LlmError::NoProviders);
231 assert!(e.is_no_providers());
232 }
233
234 #[test]
235 fn agent_error_non_no_providers_returns_false() {
236 let e = AgentError::ContextError("other".into());
237 assert!(!e.is_no_providers());
238 }
239
240 #[test]
241 fn orchestration_error_display() {
242 let e =
243 AgentError::OrchestrationError(OrchestrationFailure::Generic("planner failed".into()));
244 assert!(e.to_string().contains("planner failed"));
245 }
246
247 #[test]
248 fn orchestration_failure_variants_display() {
249 assert!(
250 OrchestrationFailure::SchedulerInit("dag error".into())
251 .to_string()
252 .contains("dag error")
253 );
254 assert!(
255 OrchestrationFailure::VerifyConfig("bad config".into())
256 .to_string()
257 .contains("bad config")
258 );
259 assert!(
260 OrchestrationFailure::PlannerError("plan failed".into())
261 .to_string()
262 .contains("plan failed")
263 );
264 assert!(
265 OrchestrationFailure::RetryReset("reset failed".into())
266 .to_string()
267 .contains("reset failed")
268 );
269 }
270
271 #[test]
272 fn unknown_command_display() {
273 let e = AgentError::UnknownCommand("/foo".into());
274 assert!(e.to_string().contains("/foo"));
275 }
276
277 #[test]
278 fn skill_operation_display() {
279 let e =
280 AgentError::SkillOperation(SkillOperationFailure::DirectoryNotFound("my-skill".into()));
281 assert!(e.to_string().contains("my-skill"));
282 }
283
284 #[test]
285 fn skill_operation_failure_variants_display() {
286 assert!(
287 SkillOperationFailure::InvalidName("bad/name".into())
288 .to_string()
289 .contains("bad/name")
290 );
291 assert!(
292 SkillOperationFailure::DirectoryNotFound("foo".into())
293 .to_string()
294 .contains("foo")
295 );
296 }
297}