use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use ulid::Ulid;
pub fn now_secs() -> i64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs() as i64)
.unwrap_or(0)
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)]
pub enum MemoryType {
Episodic,
Semantic,
Procedural,
Working,
}
impl MemoryType {
pub fn parse(s: &str) -> Option<MemoryType> {
match s.to_ascii_lowercase().as_str() {
"episodic" => Some(MemoryType::Episodic),
"semantic" => Some(MemoryType::Semantic),
"procedural" => Some(MemoryType::Procedural),
"working" => Some(MemoryType::Working),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
MemoryType::Episodic => "episodic",
MemoryType::Semantic => "semantic",
MemoryType::Procedural => "procedural",
MemoryType::Working => "working",
}
}
}
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
pub enum Scope {
Private,
Shared,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct Memory {
pub id: Ulid,
pub memory_type: MemoryType,
pub vector: Vec<f32>,
pub content: String,
pub metadata: Map<String, Value>,
pub agent_id: String,
pub session_id: Option<String>,
pub scope: Scope,
pub created_at: i64,
pub accessed_at: i64,
pub access_count: u32,
pub importance: f32,
pub ttl_secs: Option<i64>,
}
impl Memory {
pub fn new(content: impl Into<String>, memory_type: MemoryType, vector: Vec<f32>) -> Self {
let now = now_secs();
Memory {
id: Ulid::new(),
memory_type,
vector,
content: content.into(),
metadata: Map::new(),
agent_id: "default".to_string(),
session_id: None,
scope: Scope::Private,
created_at: now,
accessed_at: now,
access_count: 0,
importance: 0.5,
ttl_secs: None,
}
}
pub fn with_agent(mut self, agent_id: impl Into<String>) -> Self {
self.agent_id = agent_id.into();
self
}
pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
self.session_id = Some(session_id.into());
self
}
pub fn with_importance(mut self, importance: f32) -> Self {
self.importance = importance.clamp(0.0, 1.0);
self
}
pub fn with_scope(mut self, scope: Scope) -> Self {
self.scope = scope;
self
}
pub fn with_ttl(mut self, ttl_secs: i64) -> Self {
self.ttl_secs = Some(ttl_secs);
self
}
pub fn with_meta(mut self, key: impl Into<String>, value: Value) -> Self {
self.metadata.insert(key.into(), value);
self
}
pub fn is_expired(&self, now: i64) -> bool {
match self.ttl_secs {
Some(ttl) => now - self.created_at >= ttl,
None => false,
}
}
pub fn scaffold_manifest(dimensions: usize) -> Self {
let content = format!(
"MNEMO MANIFEST (scaffold) — Fresh {}-dimensional database created \
by `mnemo init`. This is a placeholder. Replace it with a memory \
that records your project's embedder (name, dimensions, normalize), \
default agent_id, and metadata conventions; keep the metadata keys \
`area=\"onboarding\"` and `topic=\"manifest\"` so `mnemo about` \
continues to surface it as the headline orientation point. Drop \
`metadata.scaffold` once you've replaced this entry so tooling \
knows the manifest has been curated.",
dimensions
);
let mut metadata = Map::new();
metadata.insert("area".into(), Value::String("onboarding".into()));
metadata.insert("topic".into(), Value::String("manifest".into()));
metadata.insert("scaffold".into(), Value::Bool(true));
metadata.insert(
"engine_version".into(),
Value::String(env!("CARGO_PKG_VERSION").into()),
);
metadata.insert(
"dimensions".into(),
Value::Number(serde_json::Number::from(dimensions as u64)),
);
let now = now_secs();
Memory {
id: Ulid::new(),
memory_type: MemoryType::Semantic,
vector: vec![0.0; dimensions],
content,
metadata,
agent_id: "mnemo".to_string(),
session_id: None,
scope: Scope::Shared,
created_at: now,
accessed_at: now,
access_count: 0,
importance: 1.0,
ttl_secs: None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Metric {
Cosine,
L2,
Dot,
}
pub fn similarity(metric: Metric, a: &[f32], b: &[f32]) -> f32 {
match metric {
Metric::Cosine => cosine(a, b),
Metric::Dot => dot(a, b),
Metric::L2 => {
let d = euclidean(a, b);
1.0 / (1.0 + d)
}
}
}
fn dot(a: &[f32], b: &[f32]) -> f32 {
a.iter().zip(b).map(|(x, y)| x * y).sum()
}
fn cosine(a: &[f32], b: &[f32]) -> f32 {
let na = dot(a, a).sqrt();
let nb = dot(b, b).sqrt();
if na == 0.0 || nb == 0.0 {
0.0
} else {
dot(a, b) / (na * nb)
}
}
fn euclidean(a: &[f32], b: &[f32]) -> f32 {
a.iter()
.zip(b)
.map(|(x, y)| (x - y) * (x - y))
.sum::<f32>()
.sqrt()
}
#[derive(Clone, Copy, Debug)]
pub struct ScoreWeights {
pub alpha: f32,
pub beta: f32,
pub gamma: f32,
pub delta: f32,
pub half_life_secs: f32,
}
impl Default for ScoreWeights {
fn default() -> Self {
Self {
alpha: 1.0,
beta: 0.3,
gamma: 0.2,
delta: 0.1,
half_life_secs: 7.0 * 24.0 * 3600.0,
}
}
}
impl ScoreWeights {
pub fn score(&self, similarity: f32, age_secs: f32, importance: f32, access_count: u32) -> f32 {
let recency = (-std::f32::consts::LN_2 * age_secs.max(0.0) / self.half_life_secs).exp();
let frequency = ((access_count as f32) + 1.0).ln();
self.alpha * similarity
+ self.beta * recency
+ self.gamma * importance
+ self.delta * frequency
}
}