use std::fmt::Debug;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::core::{Digest, Label, Sensitivity, SourceId, StoreError, Timestamp, Trust};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MemoryItem {
pub id: String,
pub subject: String,
pub purpose: String,
pub content: Value,
pub provenance: Vec<SourceId>,
pub sensitivity: Sensitivity,
pub trust: Trust,
pub written_by: String,
pub version: u64,
pub created_at: Timestamp,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expires_at: Option<Timestamp>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub access_retention_seconds: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub superseded_at: Option<Timestamp>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub derived_from: Vec<Selected>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MemoryWrite {
pub id: String,
pub subject: String,
pub purpose: String,
pub expires_at: Option<Timestamp>,
pub access_retention_seconds: Option<u64>,
}
impl MemoryWrite {
#[must_use]
pub fn new(
id: impl Into<String>,
subject: impl Into<String>,
purpose: impl Into<String>,
) -> Self {
Self {
id: id.into(),
subject: subject.into(),
purpose: purpose.into(),
expires_at: None,
access_retention_seconds: None,
}
}
#[must_use]
pub const fn expires_at(mut self, at: Timestamp) -> Self {
self.expires_at = Some(at);
self
}
#[must_use]
pub const fn retain_after_access(mut self, seconds: u64) -> Self {
self.access_retention_seconds = Some(seconds);
self
}
}
impl MemoryItem {
#[must_use]
pub fn label(&self) -> Label {
let mut label = if self.trust == Trust::Trusted {
Label::trusted()
} else {
Label::untrusted(SourceId::new(format!("memory:{}", self.id)))
};
for source in &self.provenance {
label.provenance.insert(source.clone());
}
label.sensitivity = self.sensitivity;
label
}
#[must_use]
pub fn digest(&self) -> Digest {
Digest::of(&crate::core::canon::value_bytes(&self.content))
}
#[must_use]
pub fn selection_digest(&self) -> Digest {
Digest::of(&crate::core::canon::value_bytes(&serde_json::json!({
"subject": self.subject,
"purpose": self.purpose,
"content": self.digest().to_hex(),
"provenance": self.provenance,
"sensitivity": self.sensitivity,
"trust": self.trust,
"written_by": self.written_by,
"created_at": self.created_at,
"expires_at": self.expires_at,
"access_retention_seconds": self.access_retention_seconds,
"derived_from": self.derived_from,
})))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Recall {
pub subject: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub purpose: Option<String>,
pub limit: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub as_of: Option<Timestamp>,
#[serde(default)]
pub refresh_access: bool,
}
impl Recall {
#[must_use]
pub fn about(subject: impl Into<String>) -> Self {
Self {
subject: subject.into(),
purpose: None,
limit: 10,
as_of: None,
refresh_access: false,
}
}
#[must_use]
pub fn for_purpose(mut self, purpose: impl Into<String>) -> Self {
self.purpose = Some(purpose.into());
self
}
#[must_use]
pub const fn limit(mut self, n: usize) -> Self {
self.limit = n;
self
}
#[must_use]
pub const fn at(mut self, at: Timestamp) -> Self {
self.as_of = Some(at);
self
}
#[must_use]
pub const fn refresh_access(mut self) -> Self {
self.refresh_access = true;
self
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SemanticQuery {
pub subject: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub purpose: Option<String>,
pub text: String,
pub embedding: Vec<f32>,
pub embedding_model: String,
pub index_snapshot: String,
pub limit: usize,
pub max_sensitivity: Sensitivity,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SemanticHit {
pub selected: Selected,
pub score: f32,
}
#[async_trait]
pub trait SemanticRetriever: Send + Sync + Debug {
fn profile(&self) -> Value;
async fn search(&self, query: &SemanticQuery) -> Result<Vec<SemanticHit>, StoreError>;
}
#[async_trait]
pub trait Embedder: Send + Sync + Debug {
fn revision(&self) -> String;
async fn embed(&self, text: &str) -> Result<Vec<f32>, StoreError>;
}
#[derive(Debug, Clone)]
pub struct SemanticVector {
pub subject: String,
pub purpose: String,
pub selected: Selected,
pub embedding: Vec<f32>,
}
#[derive(Debug, Clone)]
pub struct InMemorySemanticRetriever {
identity: String,
snapshot: String,
vectors: Vec<SemanticVector>,
}
impl InMemorySemanticRetriever {
#[must_use]
pub fn new(
identity: impl Into<String>,
snapshot: impl Into<String>,
vectors: Vec<SemanticVector>,
) -> Self {
Self {
identity: identity.into(),
snapshot: snapshot.into(),
vectors,
}
}
}
#[async_trait]
impl SemanticRetriever for InMemorySemanticRetriever {
fn profile(&self) -> Value {
serde_json::json!({
"driver": "in-memory-exact-cosine/v1",
"identity": self.identity,
"snapshot": self.snapshot,
})
}
async fn search(&self, query: &SemanticQuery) -> Result<Vec<SemanticHit>, StoreError> {
if query.index_snapshot != self.snapshot {
return Err(StoreError::Backend(format!(
"semantic query names index snapshot '{}' but retriever holds '{}'",
query.index_snapshot, self.snapshot
)));
}
validate_vector(&query.embedding)?;
let mut hits = Vec::new();
for candidate in &self.vectors {
if candidate.subject != query.subject
|| query
.purpose
.as_ref()
.is_some_and(|purpose| purpose != &candidate.purpose)
{
continue;
}
validate_vector(&candidate.embedding)?;
if candidate.embedding.len() != query.embedding.len() {
return Err(StoreError::Backend(format!(
"semantic vector dimension {} does not match query dimension {}",
candidate.embedding.len(),
query.embedding.len()
)));
}
let dot: f32 = candidate
.embedding
.iter()
.zip(&query.embedding)
.map(|(a, b)| *a * *b)
.sum();
let left = candidate
.embedding
.iter()
.map(|value| value.powi(2))
.sum::<f32>()
.sqrt();
let right = query
.embedding
.iter()
.map(|value| value.powi(2))
.sum::<f32>()
.sqrt();
let score = if left == 0.0 || right == 0.0 {
0.0
} else {
dot / (left * right)
};
hits.push(SemanticHit {
selected: candidate.selected.clone(),
score,
});
}
hits.sort_by(|a, b| {
b.score
.total_cmp(&a.score)
.then_with(|| a.selected.id.cmp(&b.selected.id))
.then_with(|| a.selected.version.cmp(&b.selected.version))
});
hits.truncate(query.limit);
Ok(hits)
}
}
fn validate_vector(vector: &[f32]) -> Result<(), StoreError> {
if vector.is_empty() || vector.iter().any(|value| !value.is_finite()) {
return Err(StoreError::Backend(
"semantic vectors must be non-empty and finite".to_owned(),
));
}
Ok(())
}
#[derive(Debug, Clone, PartialEq)]
pub struct Compaction {
pub id: String,
pub subject: String,
pub purpose: String,
pub at: Timestamp,
pub instruction: String,
pub max_sensitivity: Sensitivity,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Formation {
pub subject: String,
pub purpose: String,
pub instruction: String,
pub max_items: usize,
pub expires_at: Option<Timestamp>,
pub access_retention_seconds: Option<u64>,
pub max_sensitivity: Sensitivity,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Selected {
pub id: String,
pub version: u64,
pub digest: Digest,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum MemoryError {
#[error(
"memory '{id}' version {version} was recalled by this run and is no longer \
stored — it was forgotten, so this history cannot be replayed as it happened"
)]
Forgotten { id: String, version: u64 },
#[error(
"memory '{id}' version {version} has different content or security metadata \
than when it was recalled — a version is supposed to be immutable, so this \
store cannot reproduce its own history"
)]
Rewritten { id: String, version: u64 },
#[error("the memory store could not be reached: {0}")]
Unavailable(String),
}
impl From<StoreError> for MemoryError {
fn from(e: StoreError) -> Self {
Self::Unavailable(e.to_string())
}
}
#[async_trait]
pub trait MemoryStore: Send + Sync + Debug {
async fn remember(&self, item: &MemoryItem) -> Result<u64, StoreError>;
async fn recall(&self, query: &Recall) -> Result<Vec<MemoryItem>, StoreError>;
async fn version(&self, id: &str, version: u64) -> Result<Option<MemoryItem>, StoreError>;
async fn forget(&self, id: &str) -> Result<(), StoreError>;
async fn forget_subject(&self, subject: &str) -> Result<usize, StoreError>;
async fn derivatives(&self, id: &str) -> Result<Vec<MemoryItem>, StoreError>;
async fn forget_cascading(&self, id: &str) -> Result<usize, StoreError>;
async fn set_legal_hold(&self, id: &str, held: bool) -> Result<(), StoreError>;
async fn legal_hold(&self, id: &str) -> Result<bool, StoreError>;
async fn sweep_expired(&self, at: Timestamp) -> Result<usize, StoreError>;
async fn touch(&self, ids: &[String], at: Timestamp) -> Result<(), StoreError>;
}
#[cfg(test)]
mod write_tests {
use super::*;
#[test]
fn each_lifecycle_builder_sets_only_what_it_names() {
let plain = MemoryWrite::new("m-1", "account-1", "support");
assert_eq!(plain.expires_at, None, "neither is set by default");
assert_eq!(plain.access_retention_seconds, None);
let sliding = MemoryWrite::new("m-1", "account-1", "support").retain_after_access(600);
assert_eq!(sliding.access_retention_seconds, Some(600));
assert_eq!(
sliding.expires_at, None,
"a sliding window is not also a fixed expiry"
);
let at = Timestamp::UNIX_EPOCH;
let fixed = MemoryWrite::new("m-1", "account-1", "support").expires_at(at);
assert_eq!(fixed.expires_at, Some(at));
assert_eq!(
fixed.access_retention_seconds, None,
"a fixed expiry is not also a sliding window"
);
for built in [&sliding, &fixed] {
assert_eq!(built.id, plain.id);
assert_eq!(built.subject, plain.subject);
assert_eq!(built.purpose, plain.purpose);
}
}
}
#[cfg(test)]
mod semantic_tests {
use super::*;
fn selected(id: &str) -> Selected {
Selected {
id: id.to_owned(),
version: 1,
digest: Digest::of(id.as_bytes()),
}
}
#[tokio::test]
async fn exact_cosine_retrieval_is_scoped_ranked_and_snapshot_bound() {
let retriever = InMemorySemanticRetriever::new(
"reference",
"snapshot-7",
vec![
SemanticVector {
subject: "account-1".to_owned(),
purpose: "support".to_owned(),
selected: selected("near"),
embedding: vec![1.0, 0.0],
},
SemanticVector {
subject: "account-1".to_owned(),
purpose: "support".to_owned(),
selected: selected("far"),
embedding: vec![0.0, 1.0],
},
SemanticVector {
subject: "account-2".to_owned(),
purpose: "support".to_owned(),
selected: selected("wrong-subject"),
embedding: vec![1.0, 0.0],
},
],
);
let query = SemanticQuery {
subject: "account-1".to_owned(),
purpose: Some("support".to_owned()),
text: "query".to_owned(),
embedding: vec![1.0, 0.0],
embedding_model: "embed-v3@2026-07-01".to_owned(),
index_snapshot: "snapshot-7".to_owned(),
limit: 2,
max_sensitivity: Sensitivity::Internal,
};
let hits = retriever.search(&query).await.expect("semantic search");
assert_eq!(
hits.iter()
.map(|hit| hit.selected.id.as_str())
.collect::<Vec<_>>(),
vec!["near", "far"]
);
assert!(hits[0].score > hits[1].score);
let mut stale = query;
stale.index_snapshot = "snapshot-8".to_owned();
assert!(retriever.search(&stale).await.is_err());
}
}