1use std::fmt;
7use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15#[serde(rename_all = "snake_case")]
16#[non_exhaustive]
17pub enum ErrorCode {
18 TypesInvalidId,
21 TypesValidation,
23 TypesSerde,
25
26 ToolNotFound,
29 ToolInvalidArgs,
31 ToolExecution,
33 ToolTimeout,
35 ToolCancelled,
37 ToolDenied,
39 ToolApprovalDenied,
41 ToolStreamProtocol,
43 ToolRateLimited,
45 ToolConcurrencyLimit,
47 ToolNetwork,
49 ToolServiceUnavailable,
51
52 LlmProvider,
55 LlmCancelled,
57 LlmInvalidResponse,
59 LlmAuth,
61 LlmRateLimit,
63 LlmIdleTimeout,
65 LlmEmptyResponse,
67 LlmTruncated,
69
70 AgentInvalidDefinition,
73 AgentBuild,
75 AgentNotFound,
77
78 RuntimeMaxSteps,
81 RuntimeCancelled,
83 RuntimeGate,
85 RuntimeStructuredOutput,
87 RuntimeDeadline,
89 RuntimeStationarity,
91
92 HostSpawn,
95 HostBudget,
97 HostDepth,
99 HostConcurrency,
101 HostUnsupported,
103 HostCancelled,
105 HostIsolation,
107
108 WorkflowScript,
111 WorkflowDivergence,
113 WorkflowJournal,
115 WorkflowBudget,
117 WorkflowCancelled,
119 WorkflowValidate,
121
122 StateInvariant,
125 StatePersistence,
127
128 CompactionFailed,
131 CompactionOverflow,
133
134 Internal,
136}
137
138impl ErrorCode {
139 #[must_use]
141 pub const fn as_str(self) -> &'static str {
142 match self {
143 Self::TypesInvalidId => "types.invalid_id",
144 Self::TypesValidation => "types.validation",
145 Self::TypesSerde => "types.serde",
146 Self::ToolNotFound => "tool.not_found",
147 Self::ToolInvalidArgs => "tool.invalid_args",
148 Self::ToolExecution => "tool.execution",
149 Self::ToolTimeout => "tool.timeout",
150 Self::ToolCancelled => "tool.cancelled",
151 Self::ToolDenied => "tool.denied",
152 Self::ToolApprovalDenied => "tool.approval_denied",
153 Self::ToolStreamProtocol => "tool.stream_protocol",
154 Self::ToolRateLimited => "tool.rate_limited",
155 Self::ToolConcurrencyLimit => "tool.concurrency_limit",
156 Self::ToolNetwork => "tool.network",
157 Self::ToolServiceUnavailable => "tool.service_unavailable",
158 Self::LlmProvider => "llm.provider",
159 Self::LlmCancelled => "llm.cancelled",
160 Self::LlmInvalidResponse => "llm.invalid_response",
161 Self::LlmAuth => "llm.auth",
162 Self::LlmRateLimit => "llm.rate_limit",
163 Self::LlmIdleTimeout => "llm.idle_timeout",
164 Self::LlmEmptyResponse => "llm.empty_response",
165 Self::LlmTruncated => "llm.truncated",
166 Self::AgentInvalidDefinition => "agent.invalid_definition",
167 Self::AgentBuild => "agent.build",
168 Self::AgentNotFound => "agent.not_found",
169 Self::RuntimeMaxSteps => "runtime.max_steps",
170 Self::RuntimeCancelled => "runtime.cancelled",
171 Self::RuntimeGate => "runtime.gate",
172 Self::RuntimeStructuredOutput => "runtime.structured_output",
173 Self::RuntimeDeadline => "runtime.deadline",
174 Self::RuntimeStationarity => "runtime.stationarity",
175 Self::HostSpawn => "host.spawn",
176 Self::HostBudget => "host.budget",
177 Self::HostDepth => "host.depth",
178 Self::HostConcurrency => "host.concurrency",
179 Self::HostUnsupported => "host.unsupported",
180 Self::HostCancelled => "host.cancelled",
181 Self::HostIsolation => "host.isolation",
182 Self::WorkflowScript => "workflow.script",
183 Self::WorkflowDivergence => "workflow.divergence",
184 Self::WorkflowJournal => "workflow.journal",
185 Self::WorkflowBudget => "workflow.budget",
186 Self::WorkflowCancelled => "workflow.cancelled",
187 Self::WorkflowValidate => "workflow.validate",
188 Self::StateInvariant => "state.invariant",
189 Self::StatePersistence => "state.persistence",
190 Self::CompactionFailed => "compaction.failed",
191 Self::CompactionOverflow => "compaction.overflow",
192 Self::Internal => "internal",
193 }
194 }
195
196 #[must_use]
198 pub const fn domain(self) -> &'static str {
199 match self {
200 Self::TypesInvalidId | Self::TypesValidation | Self::TypesSerde => "types",
201 Self::ToolNotFound
202 | Self::ToolInvalidArgs
203 | Self::ToolExecution
204 | Self::ToolTimeout
205 | Self::ToolCancelled
206 | Self::ToolDenied
207 | Self::ToolApprovalDenied
208 | Self::ToolStreamProtocol
209 | Self::ToolRateLimited
210 | Self::ToolConcurrencyLimit
211 | Self::ToolNetwork
212 | Self::ToolServiceUnavailable => "tool",
213 Self::LlmProvider
214 | Self::LlmCancelled
215 | Self::LlmInvalidResponse
216 | Self::LlmAuth
217 | Self::LlmRateLimit
218 | Self::LlmIdleTimeout
219 | Self::LlmEmptyResponse
220 | Self::LlmTruncated => "llm",
221 Self::AgentInvalidDefinition | Self::AgentBuild | Self::AgentNotFound => "agent",
222 Self::RuntimeMaxSteps
223 | Self::RuntimeCancelled
224 | Self::RuntimeGate
225 | Self::RuntimeStructuredOutput
226 | Self::RuntimeDeadline
227 | Self::RuntimeStationarity => "runtime",
228 Self::HostSpawn
229 | Self::HostBudget
230 | Self::HostDepth
231 | Self::HostConcurrency
232 | Self::HostUnsupported
233 | Self::HostCancelled
234 | Self::HostIsolation => "host",
235 Self::WorkflowScript
236 | Self::WorkflowDivergence
237 | Self::WorkflowJournal
238 | Self::WorkflowBudget
239 | Self::WorkflowCancelled
240 | Self::WorkflowValidate => "workflow",
241 Self::StateInvariant | Self::StatePersistence => "state",
242 Self::CompactionFailed | Self::CompactionOverflow => "compaction",
243 Self::Internal => "internal",
244 }
245 }
246
247 #[must_use]
252 pub const fn default_retry(self) -> RetryClass {
253 match self {
254 Self::LlmRateLimit | Self::LlmProvider | Self::LlmEmptyResponse => RetryClass::Backoff,
255 Self::LlmAuth => RetryClass::AuthRefresh,
256 Self::ToolTimeout => RetryClass::Immediate,
257 Self::ToolCancelled
258 | Self::LlmCancelled
259 | Self::LlmIdleTimeout
260 | Self::LlmTruncated
261 | Self::RuntimeCancelled
262 | Self::HostCancelled
263 | Self::WorkflowCancelled
264 | Self::ToolDenied
265 | Self::ToolApprovalDenied
266 | Self::ToolNotFound
267 | Self::ToolInvalidArgs
268 | Self::ToolStreamProtocol
269 | Self::TypesInvalidId
270 | Self::TypesValidation
271 | Self::TypesSerde
272 | Self::AgentInvalidDefinition
273 | Self::AgentBuild
274 | Self::AgentNotFound
275 | Self::RuntimeMaxSteps
276 | Self::RuntimeGate
277 | Self::RuntimeStructuredOutput
278 | Self::RuntimeDeadline
279 | Self::RuntimeStationarity
280 | Self::HostBudget
281 | Self::HostDepth
282 | Self::HostConcurrency
283 | Self::HostUnsupported
284 | Self::WorkflowDivergence
285 | Self::WorkflowBudget
286 | Self::WorkflowValidate
287 | Self::StateInvariant
288 | Self::CompactionOverflow
289 | Self::Internal => RetryClass::Never,
290 Self::ToolExecution
291 | Self::ToolRateLimited
292 | Self::ToolConcurrencyLimit
293 | Self::ToolNetwork
294 | Self::ToolServiceUnavailable
295 | Self::LlmInvalidResponse
296 | Self::HostSpawn
297 | Self::HostIsolation
298 | Self::WorkflowScript
299 | Self::WorkflowJournal
300 | Self::StatePersistence
301 | Self::CompactionFailed => RetryClass::Never,
302 }
303 }
304}
305
306impl fmt::Display for ErrorCode {
307 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 f.write_str(self.as_str())
309 }
310}
311
312#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
314#[serde(rename_all = "snake_case")]
315#[non_exhaustive]
316pub enum RetryClass {
317 #[default]
319 Never,
320 Immediate,
322 Backoff,
324 AuthRefresh,
326}
327
328#[derive(Debug, Clone, thiserror::Error)]
330pub struct MachiError {
331 code: ErrorCode,
332 message: String,
333 retry: RetryClass,
334 #[source]
335 source: Option<Arc<dyn std::error::Error + Send + Sync>>,
336}
337
338impl MachiError {
339 #[must_use]
343 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
344 Self {
345 code,
346 message: message.into(),
347 retry: code.default_retry(),
348 source: None,
349 }
350 }
351
352 #[must_use]
354 pub const fn with_retry(mut self, retry: RetryClass) -> Self {
355 self.retry = retry;
356 self
357 }
358
359 #[must_use]
361 pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
362 self.source = Some(Arc::new(source));
363 self
364 }
365
366 #[must_use]
368 pub const fn code(&self) -> ErrorCode {
369 self.code
370 }
371
372 #[must_use]
374 pub const fn retry_class(&self) -> RetryClass {
375 self.retry
376 }
377
378 #[must_use]
380 pub fn message(&self) -> &str {
381 &self.message
382 }
383
384 #[must_use]
386 pub fn cancelled(message: impl Into<String>) -> Self {
387 Self::new(ErrorCode::RuntimeCancelled, message)
388 }
389}
390
391impl fmt::Display for MachiError {
392 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
393 write!(f, "{}: {}", self.code, self.message)
394 }
395}
396
397pub type Result<T> = std::result::Result<T, MachiError>;
399
400#[cfg(test)]
401mod tests {
402 use super::*;
403
404 #[test]
405 fn display_includes_code() {
406 let err = MachiError::new(ErrorCode::ToolTimeout, "exceeded 5s");
407 assert!(err.to_string().contains("tool.timeout"), "{err}");
408 assert_eq!(err.retry_class(), RetryClass::Immediate);
409 }
410
411 #[test]
412 fn rate_limit_defaults_to_backoff() {
413 let err = MachiError::new(ErrorCode::LlmRateLimit, "429");
414 assert_eq!(err.retry_class(), RetryClass::Backoff);
415 assert_eq!(err.code().domain(), "llm");
416 }
417
418 #[test]
419 fn all_codes_have_domain_prefix_in_as_str() {
420 let codes = [
421 ErrorCode::TypesInvalidId,
422 ErrorCode::ToolApprovalDenied,
423 ErrorCode::ToolStreamProtocol,
424 ErrorCode::LlmAuth,
425 ErrorCode::LlmRateLimit,
426 ErrorCode::AgentNotFound,
427 ErrorCode::RuntimeStructuredOutput,
428 ErrorCode::RuntimeDeadline,
429 ErrorCode::HostIsolation,
430 ErrorCode::WorkflowValidate,
431 ErrorCode::StateInvariant,
432 ErrorCode::StatePersistence,
433 ErrorCode::CompactionFailed,
434 ErrorCode::CompactionOverflow,
435 ErrorCode::Internal,
436 ];
437 for code in codes {
438 let s = code.as_str();
439 assert!(
440 s.starts_with(code.domain()) || code == ErrorCode::Internal,
441 "code {s} should start with domain {}",
442 code.domain()
443 );
444 }
445 }
446}
447
448include!("error_code_matrix.rs");