Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.