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