1use std::time::{SystemTime, UNIX_EPOCH};
10
11use serde::{Deserialize, Serialize};
12use serde_json::{Map, Value};
13use ulid::Ulid;
14
15pub fn now_secs() -> i64 {
17 SystemTime::now()
18 .duration_since(UNIX_EPOCH)
19 .map(|d| d.as_secs() as i64)
20 .unwrap_or(0)
21}
22
23#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug, Hash)]
25pub enum MemoryType {
26 Episodic,
28 Semantic,
30 Procedural,
32 Working,
34}
35
36impl MemoryType {
37 pub fn parse(s: &str) -> Option<MemoryType> {
39 match s.to_ascii_lowercase().as_str() {
40 "episodic" => Some(MemoryType::Episodic),
41 "semantic" => Some(MemoryType::Semantic),
42 "procedural" => Some(MemoryType::Procedural),
43 "working" => Some(MemoryType::Working),
44 _ => None,
45 }
46 }
47
48 pub fn as_str(&self) -> &'static str {
50 match self {
51 MemoryType::Episodic => "episodic",
52 MemoryType::Semantic => "semantic",
53 MemoryType::Procedural => "procedural",
54 MemoryType::Working => "working",
55 }
56 }
57}
58
59#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Debug)]
61pub enum Scope {
62 Private,
64 Shared,
66}
67
68#[derive(Serialize, Deserialize, Clone, Debug)]
70pub struct Memory {
71 pub id: Ulid,
73 pub memory_type: MemoryType,
75 pub vector: Vec<f32>,
77 pub content: String,
79 pub metadata: Map<String, Value>,
81 pub agent_id: String,
83 pub session_id: Option<String>,
85 pub scope: Scope,
87 pub created_at: i64,
89 pub accessed_at: i64,
91 pub access_count: u32,
93 pub importance: f32,
95 pub ttl_secs: Option<i64>,
98}
99
100impl Memory {
101 pub fn new(content: impl Into<String>, memory_type: MemoryType, vector: Vec<f32>) -> Self {
104 let now = now_secs();
105 Memory {
106 id: Ulid::new(),
107 memory_type,
108 vector,
109 content: content.into(),
110 metadata: Map::new(),
111 agent_id: "default".to_string(),
112 session_id: None,
113 scope: Scope::Private,
114 created_at: now,
115 accessed_at: now,
116 access_count: 0,
117 importance: 0.5,
118 ttl_secs: None,
119 }
120 }
121
122 pub fn with_agent(mut self, agent_id: impl Into<String>) -> Self {
124 self.agent_id = agent_id.into();
125 self
126 }
127 pub fn with_session(mut self, session_id: impl Into<String>) -> Self {
129 self.session_id = Some(session_id.into());
130 self
131 }
132 pub fn with_importance(mut self, importance: f32) -> Self {
134 self.importance = importance.clamp(0.0, 1.0);
135 self
136 }
137 pub fn with_scope(mut self, scope: Scope) -> Self {
139 self.scope = scope;
140 self
141 }
142 pub fn with_ttl(mut self, ttl_secs: i64) -> Self {
144 self.ttl_secs = Some(ttl_secs);
145 self
146 }
147 pub fn with_meta(mut self, key: impl Into<String>, value: Value) -> Self {
149 self.metadata.insert(key.into(), value);
150 self
151 }
152
153 pub fn is_expired(&self, now: i64) -> bool {
155 match self.ttl_secs {
156 Some(ttl) => now - self.created_at >= ttl,
157 None => false,
158 }
159 }
160
161 pub fn scaffold_manifest(dimensions: usize) -> Self {
175 let content = format!(
176 "MNEMO MANIFEST (scaffold) — Fresh {}-dimensional database created \
177 by `mnemo init`. This is a placeholder. Replace it with a memory \
178 that records your project's embedder (name, dimensions, normalize), \
179 default agent_id, and metadata conventions; keep the metadata keys \
180 `area=\"onboarding\"` and `topic=\"manifest\"` so `mnemo about` \
181 continues to surface it as the headline orientation point. Drop \
182 `metadata.scaffold` once you've replaced this entry so tooling \
183 knows the manifest has been curated.",
184 dimensions
185 );
186 let mut metadata = Map::new();
187 metadata.insert("area".into(), Value::String("onboarding".into()));
188 metadata.insert("topic".into(), Value::String("manifest".into()));
189 metadata.insert("scaffold".into(), Value::Bool(true));
190 metadata.insert(
191 "engine_version".into(),
192 Value::String(env!("CARGO_PKG_VERSION").into()),
193 );
194 metadata.insert(
195 "dimensions".into(),
196 Value::Number(serde_json::Number::from(dimensions as u64)),
197 );
198 let now = now_secs();
199 Memory {
200 id: Ulid::new(),
201 memory_type: MemoryType::Semantic,
202 vector: vec![0.0; dimensions],
203 content,
204 metadata,
205 agent_id: "mnemo".to_string(),
206 session_id: None,
207 scope: Scope::Shared,
208 created_at: now,
209 accessed_at: now,
210 access_count: 0,
211 importance: 1.0,
212 ttl_secs: None,
213 }
214 }
215}
216
217#[derive(Clone, Copy, Debug, PartialEq, Eq)]
219pub enum Metric {
220 Cosine,
222 L2,
224 Dot,
226}
227
228pub fn similarity(metric: Metric, a: &[f32], b: &[f32]) -> f32 {
230 match metric {
231 Metric::Cosine => cosine(a, b),
232 Metric::Dot => dot(a, b),
233 Metric::L2 => {
234 let d = euclidean(a, b);
235 1.0 / (1.0 + d)
236 }
237 }
238}
239
240fn dot(a: &[f32], b: &[f32]) -> f32 {
241 a.iter().zip(b).map(|(x, y)| x * y).sum()
242}
243
244fn cosine(a: &[f32], b: &[f32]) -> f32 {
245 let na = dot(a, a).sqrt();
246 let nb = dot(b, b).sqrt();
247 if na == 0.0 || nb == 0.0 {
248 0.0
249 } else {
250 dot(a, b) / (na * nb)
251 }
252}
253
254fn euclidean(a: &[f32], b: &[f32]) -> f32 {
255 a.iter()
256 .zip(b)
257 .map(|(x, y)| (x - y) * (x - y))
258 .sum::<f32>()
259 .sqrt()
260}
261
262#[derive(Clone, Copy, Debug)]
266pub struct ScoreWeights {
267 pub alpha: f32,
269 pub beta: f32,
271 pub gamma: f32,
273 pub delta: f32,
275 pub half_life_secs: f32,
277}
278
279impl Default for ScoreWeights {
280 fn default() -> Self {
281 Self {
284 alpha: 1.0,
285 beta: 0.3,
286 gamma: 0.2,
287 delta: 0.1,
288 half_life_secs: 7.0 * 24.0 * 3600.0,
289 }
290 }
291}
292
293impl ScoreWeights {
294 pub fn score(&self, similarity: f32, age_secs: f32, importance: f32, access_count: u32) -> f32 {
296 let recency = (-std::f32::consts::LN_2 * age_secs.max(0.0) / self.half_life_secs).exp();
297 let frequency = ((access_count as f32) + 1.0).ln();
298 self.alpha * similarity
299 + self.beta * recency
300 + self.gamma * importance
301 + self.delta * frequency
302 }
303}