behest-core 0.5.9

Core types for the behest agent runtime: messages, errors, tool definitions, and sans-IO run state machine
Documentation
//! Error types shared across the public API.

use std::time::Duration;

use thiserror::Error;

use crate::id::ProviderId;

/// Crate-wide result type.
pub type Result<T, E = Error> = std::result::Result<T, E>;

/// Top-level crate error.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
    /// Error returned by a model or embedding provider.
    #[error(transparent)]
    Provider(#[from] ProviderError),

    /// Error from tool execution.
    #[error(transparent)]
    Tool(#[from] ToolError),

    /// Error from context construction.
    #[error(transparent)]
    Context(#[from] ContextError),

    /// Error from a storage backend.
    #[error(transparent)]
    Storage(#[from] StorageError),

    /// Configuration loading or validation error.
    #[error("configuration error: {0}")]
    Config(String),
}

/// Errors produced by provider implementations.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ProviderError {
    /// Provider credentials were rejected.
    #[error("authentication failed for provider `{provider}`")]
    Authentication {
        /// Provider that rejected authentication.
        provider: ProviderId,
    },

    /// Request payload was invalid for the target provider.
    #[error("provider `{provider}` rejected request: {message}")]
    BadRequest {
        /// Provider that rejected the request.
        provider: ProviderId,
        /// Provider-facing validation message.
        message: String,
    },

    /// Provider asked the caller to slow down.
    #[error("provider `{provider}` rate limited request")]
    RateLimited {
        /// Provider that rate limited the request.
        provider: ProviderId,
        /// Suggested delay before retrying.
        retry_after: Option<Duration>,
    },

    /// Provider call exceeded the configured timeout.
    #[error("provider `{provider}` timed out")]
    Timeout {
        /// Provider that timed out.
        provider: ProviderId,
    },

    /// Provider is temporarily overloaded.
    #[error("provider `{provider}` is overloaded")]
    Overloaded {
        /// Provider that reported overload.
        provider: ProviderId,
    },

    /// Requested provider feature is unavailable.
    #[error("provider `{provider}` does not support feature `{feature}`")]
    Unsupported {
        /// Provider that cannot serve the feature.
        provider: ProviderId,
        /// Feature name, such as `chat_stream` or `embedding`.
        feature: String,
    },

    /// Transport layer failed before a provider response was decoded.
    #[error("transport error for provider `{provider}`: {source}")]
    Transport {
        /// Provider reached by the transport.
        provider: ProviderId,
        /// Lower-level HTTP client error.
        #[source]
        source: TransportError,
    },

    /// Provider response could not be decoded into the neutral schema.
    #[error("failed to decode response from provider `{provider}`: {message}")]
    Decode {
        /// Provider that returned an invalid response.
        provider: ProviderId,
        /// Decode failure summary.
        message: String,
    },

    /// Provider returned a structured error response.
    #[error("provider `{provider}` returned error: {message}")]
    Provider {
        /// Provider that returned the error.
        provider: ProviderId,
        /// Optional HTTP status or provider-specific status code.
        status: Option<u16>,
        /// Human-readable provider error message.
        message: String,
    },
}

impl ProviderError {
    /// Returns `true` when retrying the same request may succeed.
    #[must_use]
    pub const fn is_retryable(&self) -> bool {
        matches!(
            self,
            Self::RateLimited { .. }
                | Self::Timeout { .. }
                | Self::Overloaded { .. }
                | Self::Transport { .. }
        )
    }

    /// Returns `true` when the error indicates the request exceeded the
    /// model's context window and should be retried after compaction.
    #[must_use]
    pub fn is_context_overflow(&self) -> bool {
        let message = match self {
            Self::BadRequest { message, .. } | Self::Provider { message, .. } => message.as_str(),
            _ => return false,
        };

        message.contains("context_length_exceeded")
            || message.contains("maximum context length")
            || message.contains("too many tokens")
            || message.contains("reduce the length")
            || message.contains("token limit")
            || message.contains("prompt is too long")
            || message.contains("input length exceeds")
            || message.contains("context window")
    }
}

/// A type-erased transport error.
///
/// This replaces the direct `reqwest::Error` dependency so that `behest-core`
/// remains free of HTTP client dependencies.
#[derive(Debug)]
pub struct TransportError(Box<dyn std::error::Error + Send + Sync>);

impl TransportError {
    /// Wraps a transport-level error.
    #[must_use]
    pub fn new(error: impl std::error::Error + Send + Sync + 'static) -> Self {
        Self(Box::new(error))
    }
}

impl std::fmt::Display for TransportError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        self.0.fmt(f)
    }
}

impl std::error::Error for TransportError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        Some(self.0.as_ref())
    }
}

/// Errors produced during tool execution.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ToolError {
    /// Requested tool is not registered.
    #[error("tool `{name}` is not registered")]
    NotFound {
        /// Name of the missing tool.
        name: String,
    },

    /// Tool execution failed with an application error.
    #[error("tool `{name}` failed: {message}")]
    Execution {
        /// Name of the tool that failed.
        name: String,
        /// Human-readable error message.
        message: String,
    },

    /// Tool arguments could not be parsed or validated.
    #[error("tool `{name}` received invalid arguments: {message}")]
    InvalidArguments {
        /// Name of the tool that rejected arguments.
        name: String,
        /// Validation or parsing failure description.
        message: String,
    },

    /// Tool is defined but not yet implemented.
    #[error("tool `{name}` is not implemented")]
    NotImplemented {
        /// Name of the unimplemented tool.
        name: String,
    },
}

/// Errors produced during context construction.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum ContextError {
    /// Context adapter failed to produce its fragment.
    #[error("context adapter `{adapter}` failed: {message}")]
    AdapterFailed {
        /// Name of the adapter that failed.
        adapter: String,
        /// Human-readable error message.
        message: String,
    },

    /// Context input was invalid or incomplete.
    #[error("invalid context input: {message}")]
    InvalidInput {
        /// Description of the validation failure.
        message: String,
    },

    /// Required context adapter is not registered.
    #[error("context adapter `{adapter}` is not registered")]
    AdapterNotFound {
        /// Name of the missing adapter.
        adapter: String,
    },
}

/// Errors produced by storage backends.
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum StorageError {
    /// Requested entity was not found.
    #[error("storage entity not found: `{id}`")]
    NotFound {
        /// Identifier of the missing entity.
        id: String,
    },

    /// Failed to connect to the storage backend.
    #[error("storage connection failed ({backend}): {message}")]
    ConnectionFailed {
        /// Backend identifier (e.g., "postgres", "redis").
        backend: String,
        /// Human-readable failure description.
        message: String,
        /// Lower-level error, if available.
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },

    /// Serialization or deserialization failed.
    #[error("storage serialization failed: {message}")]
    SerializationFailed {
        /// Description of the serialization failure.
        message: String,
        /// Lower-level error, if available.
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },

    /// Backend-specific operational error.
    #[error("storage backend error ({backend}): {message}")]
    BackendError {
        /// Backend identifier.
        backend: String,
        /// Human-readable error description.
        message: String,
        /// Lower-level error, if available.
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },

    /// Schema migration failed.
    #[error("storage migration failed ({backend}): {message}")]
    MigrationFailed {
        /// Backend identifier.
        backend: String,
        /// Human-readable failure description.
        message: String,
        /// Lower-level error, if available.
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },

    /// Stored data is malformed or corrupted and cannot be interpreted.
    #[error("data corruption in `{field}`: {message}")]
    DataCorruption {
        /// Name of the corrupted field (e.g., "session.id", "message.metadata").
        field: String,
        /// Human-readable description of the corruption.
        message: String,
        /// Underlying parse or deserialization error, if available.
        #[source]
        source: Option<Box<dyn std::error::Error + Send + Sync>>,
    },
}