use std::fmt;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum ErrorCode {
TypesInvalidId,
TypesValidation,
TypesSerde,
ToolNotFound,
ToolInvalidArgs,
ToolExecution,
ToolTimeout,
ToolCancelled,
ToolDenied,
ToolApprovalDenied,
ToolStreamProtocol,
ToolRateLimited,
ToolConcurrencyLimit,
ToolNetwork,
ToolServiceUnavailable,
LlmProvider,
LlmCancelled,
LlmInvalidResponse,
LlmAuth,
LlmRateLimit,
LlmIdleTimeout,
LlmEmptyResponse,
LlmTruncated,
AgentInvalidDefinition,
AgentBuild,
AgentNotFound,
RuntimeMaxSteps,
RuntimeCancelled,
RuntimeGate,
RuntimeStructuredOutput,
RuntimeDeadline,
RuntimeStationarity,
HostSpawn,
HostBudget,
HostDepth,
HostConcurrency,
HostUnsupported,
HostCancelled,
HostIsolation,
WorkflowScript,
WorkflowDivergence,
WorkflowJournal,
WorkflowBudget,
WorkflowCancelled,
WorkflowValidate,
StateInvariant,
StatePersistence,
CompactionFailed,
CompactionOverflow,
Internal,
}
impl ErrorCode {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::TypesInvalidId => "types.invalid_id",
Self::TypesValidation => "types.validation",
Self::TypesSerde => "types.serde",
Self::ToolNotFound => "tool.not_found",
Self::ToolInvalidArgs => "tool.invalid_args",
Self::ToolExecution => "tool.execution",
Self::ToolTimeout => "tool.timeout",
Self::ToolCancelled => "tool.cancelled",
Self::ToolDenied => "tool.denied",
Self::ToolApprovalDenied => "tool.approval_denied",
Self::ToolStreamProtocol => "tool.stream_protocol",
Self::ToolRateLimited => "tool.rate_limited",
Self::ToolConcurrencyLimit => "tool.concurrency_limit",
Self::ToolNetwork => "tool.network",
Self::ToolServiceUnavailable => "tool.service_unavailable",
Self::LlmProvider => "llm.provider",
Self::LlmCancelled => "llm.cancelled",
Self::LlmInvalidResponse => "llm.invalid_response",
Self::LlmAuth => "llm.auth",
Self::LlmRateLimit => "llm.rate_limit",
Self::LlmIdleTimeout => "llm.idle_timeout",
Self::LlmEmptyResponse => "llm.empty_response",
Self::LlmTruncated => "llm.truncated",
Self::AgentInvalidDefinition => "agent.invalid_definition",
Self::AgentBuild => "agent.build",
Self::AgentNotFound => "agent.not_found",
Self::RuntimeMaxSteps => "runtime.max_steps",
Self::RuntimeCancelled => "runtime.cancelled",
Self::RuntimeGate => "runtime.gate",
Self::RuntimeStructuredOutput => "runtime.structured_output",
Self::RuntimeDeadline => "runtime.deadline",
Self::RuntimeStationarity => "runtime.stationarity",
Self::HostSpawn => "host.spawn",
Self::HostBudget => "host.budget",
Self::HostDepth => "host.depth",
Self::HostConcurrency => "host.concurrency",
Self::HostUnsupported => "host.unsupported",
Self::HostCancelled => "host.cancelled",
Self::HostIsolation => "host.isolation",
Self::WorkflowScript => "workflow.script",
Self::WorkflowDivergence => "workflow.divergence",
Self::WorkflowJournal => "workflow.journal",
Self::WorkflowBudget => "workflow.budget",
Self::WorkflowCancelled => "workflow.cancelled",
Self::WorkflowValidate => "workflow.validate",
Self::StateInvariant => "state.invariant",
Self::StatePersistence => "state.persistence",
Self::CompactionFailed => "compaction.failed",
Self::CompactionOverflow => "compaction.overflow",
Self::Internal => "internal",
}
}
#[must_use]
pub const fn domain(self) -> &'static str {
match self {
Self::TypesInvalidId | Self::TypesValidation | Self::TypesSerde => "types",
Self::ToolNotFound
| Self::ToolInvalidArgs
| Self::ToolExecution
| Self::ToolTimeout
| Self::ToolCancelled
| Self::ToolDenied
| Self::ToolApprovalDenied
| Self::ToolStreamProtocol
| Self::ToolRateLimited
| Self::ToolConcurrencyLimit
| Self::ToolNetwork
| Self::ToolServiceUnavailable => "tool",
Self::LlmProvider
| Self::LlmCancelled
| Self::LlmInvalidResponse
| Self::LlmAuth
| Self::LlmRateLimit
| Self::LlmIdleTimeout
| Self::LlmEmptyResponse
| Self::LlmTruncated => "llm",
Self::AgentInvalidDefinition | Self::AgentBuild | Self::AgentNotFound => "agent",
Self::RuntimeMaxSteps
| Self::RuntimeCancelled
| Self::RuntimeGate
| Self::RuntimeStructuredOutput
| Self::RuntimeDeadline
| Self::RuntimeStationarity => "runtime",
Self::HostSpawn
| Self::HostBudget
| Self::HostDepth
| Self::HostConcurrency
| Self::HostUnsupported
| Self::HostCancelled
| Self::HostIsolation => "host",
Self::WorkflowScript
| Self::WorkflowDivergence
| Self::WorkflowJournal
| Self::WorkflowBudget
| Self::WorkflowCancelled
| Self::WorkflowValidate => "workflow",
Self::StateInvariant | Self::StatePersistence => "state",
Self::CompactionFailed | Self::CompactionOverflow => "compaction",
Self::Internal => "internal",
}
}
#[must_use]
pub const fn default_retry(self) -> RetryClass {
match self {
Self::LlmRateLimit | Self::LlmProvider | Self::LlmEmptyResponse => RetryClass::Backoff,
Self::LlmAuth => RetryClass::AuthRefresh,
Self::ToolTimeout => RetryClass::Immediate,
Self::ToolCancelled
| Self::LlmCancelled
| Self::LlmIdleTimeout
| Self::LlmTruncated
| Self::RuntimeCancelled
| Self::HostCancelled
| Self::WorkflowCancelled
| Self::ToolDenied
| Self::ToolApprovalDenied
| Self::ToolNotFound
| Self::ToolInvalidArgs
| Self::ToolStreamProtocol
| Self::TypesInvalidId
| Self::TypesValidation
| Self::TypesSerde
| Self::AgentInvalidDefinition
| Self::AgentBuild
| Self::AgentNotFound
| Self::RuntimeMaxSteps
| Self::RuntimeGate
| Self::RuntimeStructuredOutput
| Self::RuntimeDeadline
| Self::RuntimeStationarity
| Self::HostBudget
| Self::HostDepth
| Self::HostConcurrency
| Self::HostUnsupported
| Self::WorkflowDivergence
| Self::WorkflowBudget
| Self::WorkflowValidate
| Self::StateInvariant
| Self::CompactionOverflow
| Self::Internal => RetryClass::Never,
Self::ToolExecution
| Self::ToolRateLimited
| Self::ToolConcurrencyLimit
| Self::ToolNetwork
| Self::ToolServiceUnavailable
| Self::LlmInvalidResponse
| Self::HostSpawn
| Self::HostIsolation
| Self::WorkflowScript
| Self::WorkflowJournal
| Self::StatePersistence
| Self::CompactionFailed => RetryClass::Never,
}
}
}
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum RetryClass {
#[default]
Never,
Immediate,
Backoff,
AuthRefresh,
}
#[derive(Debug, Clone, thiserror::Error)]
pub struct MachiError {
code: ErrorCode,
message: String,
retry: RetryClass,
#[source]
source: Option<Arc<dyn std::error::Error + Send + Sync>>,
}
impl MachiError {
#[must_use]
pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
retry: code.default_retry(),
source: None,
}
}
#[must_use]
pub const fn with_retry(mut self, retry: RetryClass) -> Self {
self.retry = retry;
self
}
#[must_use]
pub fn with_source(mut self, source: impl std::error::Error + Send + Sync + 'static) -> Self {
self.source = Some(Arc::new(source));
self
}
#[must_use]
pub const fn code(&self) -> ErrorCode {
self.code
}
#[must_use]
pub const fn retry_class(&self) -> RetryClass {
self.retry
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
#[must_use]
pub fn cancelled(message: impl Into<String>) -> Self {
Self::new(ErrorCode::RuntimeCancelled, message)
}
}
impl fmt::Display for MachiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}: {}", self.code, self.message)
}
}
pub type Result<T> = std::result::Result<T, MachiError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn display_includes_code() {
let err = MachiError::new(ErrorCode::ToolTimeout, "exceeded 5s");
assert!(err.to_string().contains("tool.timeout"), "{err}");
assert_eq!(err.retry_class(), RetryClass::Immediate);
}
#[test]
fn rate_limit_defaults_to_backoff() {
let err = MachiError::new(ErrorCode::LlmRateLimit, "429");
assert_eq!(err.retry_class(), RetryClass::Backoff);
assert_eq!(err.code().domain(), "llm");
}
#[test]
fn all_codes_have_domain_prefix_in_as_str() {
let codes = [
ErrorCode::TypesInvalidId,
ErrorCode::ToolApprovalDenied,
ErrorCode::ToolStreamProtocol,
ErrorCode::LlmAuth,
ErrorCode::LlmRateLimit,
ErrorCode::AgentNotFound,
ErrorCode::RuntimeStructuredOutput,
ErrorCode::RuntimeDeadline,
ErrorCode::HostIsolation,
ErrorCode::WorkflowValidate,
ErrorCode::StateInvariant,
ErrorCode::StatePersistence,
ErrorCode::CompactionFailed,
ErrorCode::CompactionOverflow,
ErrorCode::Internal,
];
for code in codes {
let s = code.as_str();
assert!(
s.starts_with(code.domain()) || code == ErrorCode::Internal,
"code {s} should start with domain {}",
code.domain()
);
}
}
}
include!("error_code_matrix.rs");