a3s-memory 0.1.4

A3S Memory - Pluggable memory storage for AI agents
Documentation
use super::super::in_memory::PartitionBlock;
use super::super::{VectorIndexError, VectorRecord, VectorResult};
use sha2::{Digest, Sha256};
use std::collections::BTreeMap;

const PARTITION_DIGEST_DOMAIN: &str = "a3s.memory.sqlite-vector-partition.v1";

pub(super) fn encode_vector(vector: &[f32]) -> Vec<u8> {
    vector
        .iter()
        .flat_map(|value| value.to_le_bytes())
        .collect()
}

pub(super) fn decode_vector(blob: &[u8], dimension: usize) -> VectorResult<Vec<f32>> {
    let expected_bytes = dimension
        .checked_mul(std::mem::size_of::<f32>())
        .ok_or(VectorIndexError::SizeOverflow)?;
    if blob.len() != expected_bytes {
        return Err(VectorIndexError::StorageCorrupted(
            "stored vector has an invalid byte length".to_string(),
        ));
    }
    let mut vector = Vec::with_capacity(dimension);
    for bytes in blob.chunks_exact(std::mem::size_of::<f32>()) {
        vector.push(f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]));
    }
    Ok(vector)
}

pub(super) fn digest_block(block: &PartitionBlock, dimension: usize) -> String {
    let mut hasher = partition_hasher(&block.name, block.record_count());
    for ((id, labels), vector) in block
        .ids
        .iter()
        .zip(&block.labels)
        .zip(block.vectors.chunks_exact(dimension))
    {
        digest_record(&mut hasher, id, labels, vector);
    }
    format!("sha256:{:x}", hasher.finalize())
}

pub(super) fn digest_records(partition: &str, records: &[VectorRecord]) -> String {
    let mut hasher = partition_hasher(partition, records.len());
    for record in records {
        digest_record(&mut hasher, &record.id, &record.labels, &record.embedding);
    }
    format!("sha256:{:x}", hasher.finalize())
}

fn partition_hasher(partition: &str, record_count: usize) -> Sha256 {
    let mut hasher = Sha256::new();
    hasher.update(PARTITION_DIGEST_DOMAIN.as_bytes());
    digest_bytes(&mut hasher, partition.as_bytes());
    hasher.update((record_count as u64).to_le_bytes());
    hasher
}

fn digest_record(hasher: &mut Sha256, id: &str, labels: &BTreeMap<String, String>, vector: &[f32]) {
    digest_bytes(hasher, id.as_bytes());
    hasher.update((labels.len() as u64).to_le_bytes());
    for (key, value) in labels {
        digest_bytes(hasher, key.as_bytes());
        digest_bytes(hasher, value.as_bytes());
    }
    hasher.update((vector.len() as u64).to_le_bytes());
    for value in vector {
        hasher.update(value.to_le_bytes());
    }
}

fn digest_bytes(hasher: &mut Sha256, value: &[u8]) {
    hasher.update((value.len() as u64).to_le_bytes());
    hasher.update(value);
}