use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
pub type MemoryId = String;
pub type ProposalId = String;
pub const MAX_RECORD_TITLE_BYTES: usize = 200;
pub const MAX_RECORD_DESCRIPTION_BYTES: usize = 400;
pub const MAX_RECORD_BODY_BYTES: usize = 64 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(tag = "scope", rename_all = "snake_case")]
pub enum MemoryScope {
Identity { realm: String, identity: String },
Mob { realm: String, mob: String },
Operator { realm: String, operator: String },
Realm { realm: String },
}
impl MemoryScope {
pub fn realm(&self) -> &str {
match self {
Self::Identity { realm, .. }
| Self::Mob { realm, .. }
| Self::Operator { realm, .. }
| Self::Realm { realm } => realm,
}
}
pub fn kind_str(&self) -> &'static str {
match self {
Self::Identity { .. } => "identity",
Self::Mob { .. } => "mob",
Self::Operator { .. } => "operator",
Self::Realm { .. } => "realm",
}
}
pub fn key(&self) -> &str {
match self {
Self::Identity { identity, .. } => identity,
Self::Mob { mob, .. } => mob,
Self::Operator { operator, .. } => operator,
Self::Realm { .. } => "",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MemoryKind {
Preference,
Fact,
Gotcha,
Procedure,
Relationship,
OpenLoop,
Reference,
}
impl MemoryKind {
pub fn as_str(&self) -> &'static str {
match self {
Self::Preference => "preference",
Self::Fact => "fact",
Self::Gotcha => "gotcha",
Self::Procedure => "procedure",
Self::Relationship => "relationship",
Self::OpenLoop => "open_loop",
Self::Reference => "reference",
}
}
pub fn parse(value: &str) -> Option<Self> {
match value {
"preference" => Some(Self::Preference),
"fact" => Some(Self::Fact),
"gotcha" => Some(Self::Gotcha),
"procedure" => Some(Self::Procedure),
"relationship" => Some(Self::Relationship),
"open_loop" => Some(Self::OpenLoop),
"reference" => Some(Self::Reference),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum TrustTier {
Untrusted,
AgentObserved,
AgentVerified,
Application,
Operator,
}
impl TrustTier {
pub fn as_str(&self) -> &'static str {
match self {
Self::Untrusted => "untrusted",
Self::AgentObserved => "agent_observed",
Self::AgentVerified => "agent_verified",
Self::Application => "application",
Self::Operator => "operator",
}
}
pub fn parse(value: &str) -> Option<Self> {
match value {
"untrusted" => Some(Self::Untrusted),
"agent_observed" => Some(Self::AgentObserved),
"agent_verified" => Some(Self::AgentVerified),
"application" => Some(Self::Application),
"operator" => Some(Self::Operator),
_ => None,
}
}
pub fn assignable_via_staged_batch(&self) -> bool {
!matches!(self, Self::Operator | Self::Application)
}
pub fn llm_write_ceiling() -> Self {
Self::AgentObserved
}
pub fn capped_for_tainted_provenance(self) -> Self {
self.min(Self::AgentObserved)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "status", rename_all = "snake_case")]
pub enum RecordStatus {
Active,
Superseded { by: MemoryId },
Quarantined { reason: String },
Tombstoned,
}
impl RecordStatus {
pub fn kind_str(&self) -> &'static str {
match self {
Self::Active => "active",
Self::Superseded { .. } => "superseded",
Self::Quarantined { .. } => "quarantined",
Self::Tombstoned => "tombstoned",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "author", rename_all = "snake_case")]
pub enum MemoryAuthor {
Operator,
Application,
Agent { identity: String },
Steward { run_id: String },
Distiller { run_id: String },
}
impl MemoryAuthor {
pub fn is_llm(&self) -> bool {
matches!(
self,
Self::Agent { .. } | Self::Steward { .. } | Self::Distiller { .. }
)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvidenceRef {
pub session_id: String,
pub generation: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub revision: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub range: Option<(u64, u64)>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CalibrationRef {
pub stage: String,
pub bundle: String,
pub version: String,
pub model: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VerificationClaim {
pub checked: String,
#[serde(default)]
pub evidence: Vec<EvidenceRef>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemoryProvenance {
#[serde(default)]
pub evidence: Vec<EvidenceRef>,
pub author: MemoryAuthor,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub profile: Option<CalibrationRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verification: Option<VerificationClaim>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct UsageStats {
#[serde(default)]
pub injected_count: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_injected_at_ms: Option<u64>,
#[serde(default)]
pub explicit_recall_count: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_recalled_at_ms: Option<u64>,
#[serde(default)]
pub judged_useful_count: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_useful_at_ms: Option<u64>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum UsageEvent {
Injected,
ExplicitRecall,
JudgedUseful,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum InjectionSurface {
Build,
Turn,
}
impl InjectionSurface {
pub fn as_str(&self) -> &'static str {
match self {
Self::Build => "build",
Self::Turn => "turn",
}
}
pub fn parse(value: &str) -> Option<Self> {
match value {
"build" => Some(Self::Build),
"turn" => Some(Self::Turn),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct InjectionLogEntry {
pub record_id: MemoryId,
pub identity: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_key: Option<String>,
pub surface: InjectionSurface,
pub at_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemoryRecord {
pub id: MemoryId,
pub scope: MemoryScope,
pub kind: MemoryKind,
pub title: String,
#[serde(default)]
pub description: String,
pub body: String,
#[serde(default)]
pub tags: Vec<String>,
pub provenance: MemoryProvenance,
pub trust: TrustTier,
pub status: RecordStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supersedes: Option<MemoryId>,
#[serde(default)]
pub derived_from: Vec<MemoryId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub working_set_rank: Option<u32>,
pub created_at_ms: u64,
pub updated_at_ms: u64,
#[serde(default)]
pub usage: UsageStats,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct NewMemoryRecord {
pub kind: MemoryKind,
pub title: String,
#[serde(default)]
pub description: String,
pub body: String,
#[serde(default)]
pub tags: Vec<String>,
#[serde(default)]
pub evidence: Vec<EvidenceRef>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verification: Option<VerificationClaim>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RecordMeta {
pub id: MemoryId,
pub kind: MemoryKind,
pub title: String,
#[serde(default)]
pub description: String,
pub age_days: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rank: Option<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ManifestTier {
WorkingSet(usize),
Full,
}
pub fn content_hash(title: &str, body: &str) -> String {
let mut hasher = Sha256::new();
hasher.update((title.len() as u64).to_le_bytes());
hasher.update(title.as_bytes());
hasher.update(body.as_bytes());
let digest = hasher.finalize();
let mut out = String::with_capacity(64);
for byte in digest {
out.push_str(&format!("{byte:02x}"));
}
out
}
pub fn validate_record_fields(title: &str, description: &str, body: &str) -> Result<(), String> {
if title.trim().is_empty() {
return Err("title must not be empty".to_string());
}
if title.len() > MAX_RECORD_TITLE_BYTES {
return Err(format!(
"title must be at most {MAX_RECORD_TITLE_BYTES} bytes"
));
}
if description.len() > MAX_RECORD_DESCRIPTION_BYTES {
return Err(format!(
"description must be at most {MAX_RECORD_DESCRIPTION_BYTES} bytes"
));
}
if body.trim().is_empty() {
return Err("body must not be empty".to_string());
}
if body.len() > MAX_RECORD_BODY_BYTES {
return Err(format!(
"body must be at most {MAX_RECORD_BODY_BYTES} bytes"
));
}
Ok(())
}
pub fn age_days(updated_at_ms: u64, now_ms: u64) -> u64 {
now_ms.saturating_sub(updated_at_ms) / (24 * 60 * 60 * 1000)
}
#[cfg(test)]
#[allow(clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn trust_tier_order_matches_lattice_authority() {
assert!(TrustTier::Untrusted < TrustTier::AgentObserved);
assert!(TrustTier::AgentObserved < TrustTier::AgentVerified);
assert!(TrustTier::AgentVerified < TrustTier::Application);
assert!(TrustTier::Application < TrustTier::Operator);
}
#[test]
fn operator_and_application_tiers_never_staged_assignable() {
assert!(!TrustTier::Operator.assignable_via_staged_batch());
assert!(!TrustTier::Application.assignable_via_staged_batch());
assert!(TrustTier::AgentVerified.assignable_via_staged_batch());
assert!(TrustTier::AgentObserved.assignable_via_staged_batch());
assert!(TrustTier::Untrusted.assignable_via_staged_batch());
}
#[test]
fn tainted_provenance_caps_at_agent_observed() {
assert_eq!(
TrustTier::AgentVerified.capped_for_tainted_provenance(),
TrustTier::AgentObserved
);
assert_eq!(
TrustTier::Operator.capped_for_tainted_provenance(),
TrustTier::AgentObserved
);
assert_eq!(
TrustTier::Untrusted.capped_for_tainted_provenance(),
TrustTier::Untrusted
);
}
#[test]
fn content_hash_is_stable_and_boundary_safe() {
assert_eq!(content_hash("a", "b"), content_hash("a", "b"));
assert_ne!(content_hash("ab", "c"), content_hash("a", "bc"));
assert_eq!(content_hash("t", "b").len(), 64);
}
#[test]
fn record_serde_round_trips() {
let record = MemoryRecord {
id: "mem-1".to_string(),
scope: MemoryScope::Identity {
realm: "family".to_string(),
identity: "identity:luka".to_string(),
},
kind: MemoryKind::OpenLoop,
title: "Try the staging DB".to_string(),
description: "When smoke tests need a database".to_string(),
body: "Next time try the staging DB first. Resolved when tried.".to_string(),
tags: vec!["staging".to_string()],
provenance: MemoryProvenance {
evidence: vec![EvidenceRef {
session_id: "sess-1".to_string(),
generation: 2,
revision: None,
range: Some((3, 9)),
}],
author: MemoryAuthor::Agent {
identity: "identity:luka".to_string(),
},
profile: None,
verification: None,
},
trust: TrustTier::AgentObserved,
status: RecordStatus::Superseded {
by: "mem-2".to_string(),
},
supersedes: None,
derived_from: Vec::new(),
working_set_rank: Some(4),
created_at_ms: 10,
updated_at_ms: 20,
usage: UsageStats::default(),
};
let json = serde_json::to_string(&record).expect("serialize");
let back: MemoryRecord = serde_json::from_str(&json).expect("deserialize");
assert_eq!(back, record);
}
#[test]
fn field_caps_reject_oversized_and_empty() {
assert!(validate_record_fields("t", "", "b").is_ok());
assert!(validate_record_fields("", "", "b").is_err());
assert!(validate_record_fields("t", "", " ").is_err());
assert!(validate_record_fields(&"t".repeat(201), "", "b").is_err());
assert!(validate_record_fields("t", &"d".repeat(401), "b").is_err());
assert!(validate_record_fields("t", "", &"b".repeat(64 * 1024 + 1)).is_err());
}
}