#![allow(dead_code)]
#![allow(clippy::doc_lazy_continuation)]
pub mod sqlite;
#[cfg(feature = "sal-postgres")]
pub mod postgres;
use bitflags::bitflags;
use crate::models::{AgentRegistration, Memory, MemoryLink, Tier};
#[non_exhaustive]
#[derive(Debug, thiserror::Error)]
pub enum StoreError {
#[error("memory not found: {id}")]
NotFound { id: String },
#[error("identifier conflict on insert: {id}")]
Conflict { id: String },
#[error("caller lacks permission for {action} on {target}: {reason}")]
PermissionDenied {
action: String,
target: String,
reason: String,
},
#[error("backend unavailable: {backend}: {detail}")]
BackendUnavailable { backend: String, detail: String },
#[error("invalid input: {detail}")]
InvalidInput { detail: String },
#[error("requested capability not supported by this backend: {capability}")]
UnsupportedCapability { capability: String },
#[error("integrity check failed: {detail}")]
IntegrityFailed { detail: String },
#[error("underlying backend error: {0}")]
Backend(#[from] BoxBackendError),
}
#[derive(Debug, thiserror::Error)]
#[error("{0}")]
pub struct BoxBackendError(String);
impl BoxBackendError {
#[must_use]
pub fn new(msg: impl Into<String>) -> Self {
Self(msg.into())
}
}
pub type StoreResult<T> = Result<T, StoreError>;
#[derive(Debug, Clone)]
pub struct CallerContext {
pub agent_id: String,
pub as_agent: Option<String>,
pub request_id: Option<String>,
}
impl CallerContext {
#[must_use]
pub fn for_agent(agent_id: impl Into<String>) -> Self {
Self {
agent_id: agent_id.into(),
as_agent: None,
request_id: None,
}
}
}
bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Capabilities: u32 {
const TRANSACTIONS = 0b0000_0001;
const NATIVE_VECTOR = 0b0000_0010;
const FULLTEXT = 0b0000_0100;
const DURABLE = 0b0000_1000;
const STRONG_CONSISTENCY = 0b0001_0000;
const TTL_NATIVE = 0b0010_0000;
const ATOMIC_MULTI_WRITE = 0b0100_0000;
}
}
#[async_trait::async_trait]
pub trait Transaction: Send {
async fn commit(self: Box<Self>) -> StoreResult<()>;
async fn rollback(self: Box<Self>) -> StoreResult<()>;
}
#[derive(Debug, Default, Clone)]
pub struct Filter {
pub namespace: Option<String>,
pub tier: Option<Tier>,
pub tags_any: Vec<String>,
pub agent_id: Option<String>,
pub since: Option<chrono::DateTime<chrono::Utc>>,
pub until: Option<chrono::DateTime<chrono::Utc>>,
pub limit: usize,
}
#[async_trait::async_trait]
pub trait MemoryStore: Send + Sync {
fn capabilities(&self) -> Capabilities;
async fn store(&self, ctx: &CallerContext, memory: &Memory) -> StoreResult<String>;
async fn get(&self, ctx: &CallerContext, id: &str) -> StoreResult<Memory>;
async fn update(&self, ctx: &CallerContext, id: &str, patch: UpdatePatch) -> StoreResult<()>;
async fn delete(&self, ctx: &CallerContext, id: &str) -> StoreResult<()>;
async fn list(&self, ctx: &CallerContext, filter: &Filter) -> StoreResult<Vec<Memory>>;
async fn search(
&self,
ctx: &CallerContext,
query: &str,
filter: &Filter,
) -> StoreResult<Vec<Memory>>;
async fn verify(&self, ctx: &CallerContext, id: &str) -> StoreResult<VerifyReport>;
async fn begin_transaction(&self, _ctx: &CallerContext) -> StoreResult<Box<dyn Transaction>> {
Err(StoreError::UnsupportedCapability {
capability: "TRANSACTIONS".to_string(),
})
}
async fn link(&self, ctx: &CallerContext, link: &MemoryLink) -> StoreResult<()>;
async fn register_agent(
&self,
ctx: &CallerContext,
agent: &AgentRegistration,
) -> StoreResult<()>;
}
#[derive(Debug, Default, Clone)]
pub struct UpdatePatch {
pub title: Option<String>,
pub content: Option<String>,
pub tier: Option<Tier>,
pub namespace: Option<String>,
pub tags: Option<Vec<String>>,
pub priority: Option<i32>,
pub confidence: Option<f64>,
pub metadata: Option<serde_json::Value>,
}
#[derive(Debug, Clone)]
pub struct VerifyReport {
pub memory_id: String,
pub integrity_ok: bool,
pub findings: Vec<String>,
pub signature_verified: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn caller_context_builder_defaults() {
let ctx = CallerContext::for_agent("alice");
assert_eq!(ctx.agent_id, "alice");
assert!(ctx.as_agent.is_none());
assert!(ctx.request_id.is_none());
}
#[test]
fn capabilities_bitflags_compose() {
let caps = Capabilities::TRANSACTIONS | Capabilities::DURABLE;
assert!(caps.contains(Capabilities::TRANSACTIONS));
assert!(caps.contains(Capabilities::DURABLE));
assert!(!caps.contains(Capabilities::NATIVE_VECTOR));
}
#[test]
fn store_error_display_is_human_readable() {
let err = StoreError::NotFound {
id: "abc".to_string(),
};
assert_eq!(err.to_string(), "memory not found: abc");
let err = StoreError::PermissionDenied {
action: "read".to_string(),
target: "memory/abc".to_string(),
reason: "row-level ACL".to_string(),
};
assert!(err.to_string().contains("read"));
assert!(err.to_string().contains("row-level ACL"));
}
#[test]
fn default_begin_transaction_errors() {
let err = StoreError::UnsupportedCapability {
capability: "TRANSACTIONS".to_string(),
};
assert!(err.to_string().contains("TRANSACTIONS"));
}
#[test]
fn filter_defaults_are_empty() {
let f = Filter::default();
assert!(f.namespace.is_none());
assert!(f.tier.is_none());
assert!(f.tags_any.is_empty());
}
}