Error Handling
Note: This page covers the error type design. Usage examples and error recovery patterns are planned for a future update.
Design pattern
skelegent uses thiserror for all error types. Each protocol has its own error enum in layer0::error. Error types are #[non_exhaustive] so new variants can be added without breaking downstream code.
Every error enum includes an Other variant with #[from] Box<dyn std::error::Error + Send + Sync> for wrapping arbitrary errors. This provides an escape hatch for implementation-specific errors that do not fit the named variants.
Error types by protocol
OperatorError
Errors from operator execution (Layer 0, layer0::error::OperatorError):
#![allow(unused)]
fn main() {
pub enum OperatorError {
Model(String), // LLM provider error
SubDispatch { operator, message }, // Sub-dispatch execution error
ContextAssembly(String), // Context assembly failed
Retryable(String), // Transient, may succeed on retry
NonRetryable(String), // Permanent failure (budget, safety, invalid input)
Other(Box<dyn Error>), // Catch-all
}
}
The Retryable / NonRetryable distinction lets orchestrators make retry decisions without inspecting error details.
OrchError
Errors from orchestration (Layer 0, layer0::error::OrchError):
#![allow(unused)]
fn main() {
pub enum OrchError {
OperatorNotFound(String), // Operator ID not registered
WorkflowNotFound(String), // Workflow ID not found
DispatchFailed(String), // Dispatch failed
SignalFailed(String), // Signal delivery failed
OperatorError(OperatorError), // Propagated from operator
Other(Box<dyn Error>), // Catch-all
}
}
OperatorError propagates into OrchError via the From trait. If an operator fails during dispatch, the error is wrapped automatically.
StateError
Errors from state operations (Layer 0, layer0::error::StateError):
#![allow(unused)]
fn main() {
pub enum StateError {
NotFound { scope, key }, // Key expected to exist but doesn't
WriteFailed(String), // Write operation failed
Serialization(String), // Serde error
Other(Box<dyn Error>), // Catch-all
}
}
Note: StateStore::read returns Ok(None) for missing keys. NotFound is for higher-level APIs that expect a key to exist.
EnvError
Errors from environment operations (Layer 0, layer0::error::EnvError):
#![allow(unused)]
fn main() {
pub enum EnvError {
ProvisionFailed(String), // Failed to set up the environment
IsolationViolation(String), // Isolation boundary violated
CredentialFailed(String), // Credential injection failed
ResourceExceeded(String), // Resource limit exceeded
OperatorError(OperatorError), // Propagated from operator
Other(Box<dyn Error>), // Catch-all
}
}
Like OrchError, OperatorError propagates into EnvError via From.
ProviderError
Errors from LLM providers (Layer 1, skg_turn::provider::ProviderError):
#![allow(unused)]
fn main() {
pub enum ProviderError {
TransientError { message: String, status: Option<u16> }, // 5xx / network failure
RateLimited { retry_after: Option<Duration> }, // 429 response
InvalidRequest { message: String, status: Option<u16> }, // 4xx client error (not retryable)
ContentBlocked { message: String }, // Content blocked by provider
AuthFailed(String), // 401/403 response
InvalidResponse(String), // Response parse failure
Other(Box<dyn Error>), // Catch-all
}
}
ProviderError::is_retryable() returns true for RateLimited and TransientError. InvalidRequest is not retryable — 4xx client errors indicate malformed requests that will never succeed on retry.
ToolError
Errors from tool operations (Layer 1, skg_tool::ToolError):
#![allow(unused)]
fn main() {
pub enum ToolError {
NotFound(String), // Tool not in registry
ExecutionFailed(String), // Tool execution failed
InvalidInput(String), // Input didn't match schema
Other(Box<dyn Error>), // Catch-all
}
}
Error propagation
Errors propagate upward through the layer stack:
ProviderError / ToolError
↓ (mapped by operator implementation)
OperatorError
↓ (From impl)
OrchError / EnvError
Provider and tool errors are mapped to OperatorError by the operator implementation (e.g., the react_loop-based operator maps ProviderError::RateLimited to OperatorError::Model { retryable: true }). Operator errors propagate into orchestration and environment errors automatically via From impls. The RetryMiddleware checks OperatorError::is_retryable() to determine whether an OrchError::OperatorError should be retried.
This layered propagation ensures that callers at each level see errors appropriate to their abstraction. An orchestrator sees OrchError, never ProviderError.