kiromi-ai-memory 0.2.2

Local-first multi-tenant memory store engine: Markdown/text content on object storage, metadata in SQLite, plugin-shaped embedder/storage/metadata, hybrid text+vector search.
Documentation
// SPDX-License-Identifier: Apache-2.0 OR MIT
//! Public error type. `#[non_exhaustive]` so adding variants is non-breaking.

use std::io;

use crate::partition::{InvalidScheme, PartitionResolveError};
use crate::tenant::InvalidTenantId;

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

/// Public error type. Carries structured context where useful.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// Storage backend failure.
    #[error("storage backend error: {message}")]
    Storage {
        /// Human-readable detail.
        message: String,
        #[source]
        /// Underlying error.
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
    /// Metadata backend failure (SQL, sqlite, migrations).
    #[error("metadata backend error: {message}")]
    Metadata {
        /// Human-readable detail.
        message: String,
        #[source]
        /// Underlying error.
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
    /// Embedder failure.
    #[error("embedder error: {message}")]
    Embedder {
        /// Human-readable detail.
        message: String,
        #[source]
        /// Underlying error.
        source: Box<dyn std::error::Error + Send + Sync + 'static>,
    },
    /// `embedder.id()` does not match the value persisted in `schema_meta`.
    #[error(
        "embedder mismatch: store has {expected:?} ({expected_dims}d), got {got:?} ({got_dims}d)"
    )]
    EmbedderMismatch {
        /// Embedder id stored in `schema_meta`.
        expected: String,
        /// Stored embedder dims.
        expected_dims: usize,
        /// Embedder id passed to `open()`.
        got: String,
        /// Open-time embedder dims.
        got_dims: usize,
    },
    /// Persisted partition scheme differs from the value passed to the builder.
    #[error("partition scheme mismatch: store has {expected:?}, got {got:?}")]
    PartitionSchemeMismatch {
        /// Stored scheme.
        expected: String,
        /// Builder-supplied scheme.
        got: String,
    },
    /// Partition path was malformed or did not match the scheme.
    #[error("invalid partition: {0}")]
    PartitionInvalid(#[from] PartitionResolveError),
    /// Partition scheme template was malformed.
    #[error("invalid partition scheme: {0}")]
    PartitionSchemeInvalid(#[from] InvalidScheme),
    /// Tenant id was invalid.
    #[error("invalid tenant id: {0}")]
    TenantInvalid(#[from] InvalidTenantId),
    /// Lookup miss.
    #[error("memory not found: {0}")]
    MemoryNotFound(String),
    /// Summary lookup miss (Plan 9).
    #[error("summary not found: {0}")]
    SummaryNotFound(String),
    /// Operation rejected because the memory is soft-tombstoned.
    #[error("memory is tombstoned: {0}")]
    Tombstoned(String),
    /// Link request invalid (self-link, missing endpoint, etc.).
    #[error("invalid link request: {0}")]
    LinkInvalid(String),
    /// On-disk index detected as corrupt (Plan 3+).
    #[error("index corrupt: {0}")]
    IndexCorrupt(String),
    /// Recovery walker failed to replay an audit-log entry (Plan 3+).
    #[error("recovery error: {0}")]
    Recovery(String),
    /// Builder / config validation error.
    #[error("config error: {0}")]
    Config(String),
    /// A configured plugin reported it does not support a capability the engine
    /// requires.
    #[error("capability missing on {plugin}: required={required:?}, got={got:?}")]
    CapabilityMissing {
        /// Which plugin (Storage / Metadata / Embedder).
        plugin: &'static str,
        /// The capability flag the engine required.
        required: &'static str,
        /// What the plugin reported.
        got: bool,
    },
    /// I/O error (filesystem-side mostly).
    #[error("io error: {0}")]
    Io(#[from] io::Error),
    /// Caller passed an [`crate::attribute::AttributeValue`] that violates a
    /// constraint — kind mismatch on a range query, non-orderable kind on a
    /// range query, or a decoding failure on read.
    #[error("invalid attribute: {reason}")]
    InvalidAttribute {
        /// Human-readable reason.
        reason: String,
    },
    /// Anchor / citation URI failed to parse. See [`crate::Memory::resolve_anchor`].
    #[error("invalid anchor: {0}")]
    InvalidAnchor(String),
    /// Snapshot id was not found in `snapshot`. Reserved for the
    /// deferred Plan 12 `Memory::at` / `Memory::restore` paths;
    /// [`crate::Memory::delete_snapshot`] is currently idempotent on
    /// missing rows and does not return this variant.
    #[error("snapshot not found: {id}")]
    SnapshotNotFound {
        /// Snapshot id (ULID).
        id: String,
    },
    /// `regenerate_embeddings` rejected an embedder whose dim differs
    /// from the persisted store dim.
    #[error("embedder dim mismatch: store has {old}d, got {new}d")]
    EmbedderDimMismatch {
        /// Stored dim.
        old: usize,
        /// New embedder's dim.
        new: usize,
    },
    /// `migrate_scheme` could not proceed (e.g. previous run is still
    /// `in_progress` with a different mapper).
    #[error("migration conflict: {reason}")]
    MigrationConflict {
        /// Human-readable reason.
        reason: String,
    },
}

impl Error {
    /// Convenience constructor.
    pub fn storage<E>(message: impl Into<String>, source: E) -> Self
    where
        E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
    {
        Error::Storage {
            message: message.into(),
            source: source.into(),
        }
    }

    /// Convenience constructor.
    pub fn metadata<E>(message: impl Into<String>, source: E) -> Self
    where
        E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
    {
        Error::Metadata {
            message: message.into(),
            source: source.into(),
        }
    }

    /// Convenience constructor.
    pub fn embedder<E>(message: impl Into<String>, source: E) -> Self
    where
        E: Into<Box<dyn std::error::Error + Send + Sync + 'static>>,
    {
        Error::Embedder {
            message: message.into(),
            source: source.into(),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn display_carries_context() {
        let e = Error::EmbedderMismatch {
            expected: "onnx:e5-small:v1".into(),
            expected_dims: 384,
            got: "onnx:e5-large:v1".into(),
            got_dims: 1024,
        };
        let s = format!("{e}");
        assert!(s.contains("e5-small"));
        assert!(s.contains("384"));
        assert!(s.contains("e5-large"));
    }

    #[test]
    fn from_partition_resolve_error() {
        let e: Error = PartitionResolveError::MissingKey { key: "user".into() }.into();
        assert!(matches!(e, Error::PartitionInvalid(_)));
    }
}