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