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
//! Storage trait + helpers. Concrete impls live in submodules.

mod in_memory;
mod local_fs;

use std::fmt;

use async_trait::async_trait;
use bytes::Bytes;

use crate::error::Result;

pub use in_memory::InMemoryBackend;
pub use local_fs::LocalFsBackend;

/// Hierarchical storage key (forward-slash-separated). Treated opaquely by core.
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct StorageKey(String);

impl StorageKey {
    /// Construct from a path-like string. Leading `/` is stripped.
    pub fn new(s: impl Into<String>) -> Self {
        let mut s = s.into();
        if s.starts_with('/') {
            s.remove(0);
        }
        StorageKey(s)
    }

    /// Borrow as `&str`.
    #[must_use]
    pub fn as_str(&self) -> &str {
        &self.0
    }

    /// Append a child segment.
    #[must_use]
    pub fn join(&self, child: &str) -> StorageKey {
        let child = child.trim_start_matches('/');
        if self.0.is_empty() {
            StorageKey(child.to_string())
        } else {
            StorageKey(format!("{}/{}", self.0, child))
        }
    }
}

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

/// One entry returned by `list_prefix`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StorageEntry {
    /// Full key.
    pub key: StorageKey,
    /// Size in bytes.
    pub size: u64,
}

/// Storage backend trait. Plain object-storage semantics: keyed blobs, prefix
/// list, no random-access writes.
#[async_trait]
pub trait Storage: Send + Sync + fmt::Debug + 'static {
    /// Stable identifier (e.g. `"local-fs:/var/kiromi-ai-memory"`, `"in-memory"`).
    fn id(&self) -> String;

    /// Write or overwrite a blob.
    async fn put(&self, key: &StorageKey, body: Bytes) -> Result<()>;

    /// Read a blob.
    async fn get(&self, key: &StorageKey) -> Result<Bytes>;

    /// Check existence cheaply.
    async fn exists(&self, key: &StorageKey) -> Result<bool>;

    /// Delete a blob. Idempotent — deleting a missing key succeeds.
    async fn delete(&self, key: &StorageKey) -> Result<()>;

    /// List entries under a prefix. Order is implementation-defined but stable
    /// within one call.
    async fn list_prefix(&self, prefix: &StorageKey) -> Result<Vec<StorageEntry>>;

    /// Self-declared capabilities. Default: every slice-1 capability supported.
    fn capabilities(&self) -> crate::capabilities::StorageCapabilities {
        crate::capabilities::StorageCapabilities::default()
    }

    /// Optional on-disk root the engine can use for index files. Returns `Some`
    /// for `LocalFsBackend` and `None` for backends that aren't filesystem-backed.
    fn root_hint(&self) -> Option<std::path::PathBuf> {
        None
    }

    /// Move a blob from `src` to `dst` within the same backend. Default impl
    /// is `get` → `put` → `delete`; backends with native rename should
    /// override for atomicity. Used by the storage-layout migrator (Plan 9).
    async fn rename_within(&self, src: &StorageKey, dst: &StorageKey) -> Result<()> {
        if src == dst {
            return Ok(());
        }
        if !self.exists(src).await? {
            return Ok(());
        }
        let body = self.get(src).await?;
        self.put(dst, body).await?;
        self.delete(src).await?;
        Ok(())
    }
}