1use async_trait::async_trait;
41use std::collections::{HashMap, VecDeque};
42use std::sync::{Arc, Mutex};
43use std::time::{Duration, SystemTime};
44
45use super::MemoryError;
46
47pub type Clock = Arc<dyn Fn() -> SystemTime + Send + Sync>;
49
50fn real_clock() -> Clock {
51 Arc::new(SystemTime::now)
52}
53
54#[derive(Debug, Clone, PartialEq)]
61pub struct MemoryItem {
62 pub key: String,
64 pub text: String,
66 pub importance: f64,
68 pub metadata: HashMap<String, String>,
70}
71
72impl MemoryItem {
73 pub fn new(key: impl Into<String>, text: impl Into<String>) -> Self {
75 Self {
76 key: key.into(),
77 text: text.into(),
78 importance: 0.5,
79 metadata: HashMap::new(),
80 }
81 }
82
83 pub fn with_importance(mut self, importance: f64) -> Self {
85 self.importance = importance.clamp(0.0, 1.0);
86 self
87 }
88
89 pub fn metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
91 self.metadata.insert(key.into(), value.into());
92 self
93 }
94
95 pub fn with_metadata(mut self, metadata: HashMap<String, String>) -> Self {
97 self.metadata = metadata;
98 self
99 }
100}
101
102#[derive(Debug, Clone)]
104pub struct StoredMemory {
105 pub item: MemoryItem,
107 pub created_at: SystemTime,
109 pub last_access_at: SystemTime,
111 pub access_count: u64,
113}
114
115impl StoredMemory {
116 fn fresh(item: MemoryItem, now: SystemTime) -> Self {
117 Self {
118 item,
119 created_at: now,
120 last_access_at: now,
121 access_count: 1,
122 }
123 }
124
125 fn touch(&mut self, now: SystemTime) {
126 self.last_access_at = now;
127 self.access_count = self.access_count.saturating_add(1);
128 }
129}
130
131#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum MemoryTier {
134 Short,
136 Long,
138}
139
140#[derive(Debug, Clone, PartialEq)]
142pub struct MemoryHit {
143 pub key: String,
145 pub text: String,
147 pub score: f64,
149 pub importance: f64,
151 pub tier: MemoryTier,
153 pub metadata: HashMap<String, String>,
155}
156
157impl MemoryHit {
158 fn from_stored(stored: &StoredMemory, score: f64, tier: MemoryTier) -> Self {
159 Self {
160 key: stored.item.key.clone(),
161 text: stored.item.text.clone(),
162 score,
163 importance: stored.item.importance,
164 tier,
165 metadata: stored.item.metadata.clone(),
166 }
167 }
168}
169
170#[derive(Debug, Clone)]
172pub struct MemoryQuery<'q> {
173 pub namespace: &'q str,
175 pub text: &'q str,
177 pub k: usize,
179 pub min_score: f64,
181}
182
183impl<'q> MemoryQuery<'q> {
184 pub fn new(namespace: &'q str, text: &'q str) -> Self {
186 Self {
187 namespace,
188 text,
189 k: 5,
190 min_score: 0.0,
191 }
192 }
193
194 pub fn k(mut self, k: usize) -> Self {
196 self.k = k.max(1);
197 self
198 }
199
200 pub fn min_score(mut self, min_score: f64) -> Self {
202 self.min_score = min_score;
203 self
204 }
205}
206
207#[async_trait]
212pub trait MemoryStore: Send + Sync {
213 async fn put(&self, namespace: &str, item: MemoryItem) -> Result<(), MemoryError>;
216
217 async fn get(&self, namespace: &str, key: &str) -> Result<Option<MemoryItem>, MemoryError>;
219
220 async fn search(&self, query: &MemoryQuery<'_>) -> Result<Vec<MemoryHit>, MemoryError>;
222
223 async fn forget(&self, namespace: &str, key: &str) -> Result<bool, MemoryError>;
225
226 async fn clear_namespace(&self, namespace: &str) -> Result<usize, MemoryError>;
228
229 async fn len_namespace(&self, namespace: &str) -> Result<usize, MemoryError>;
231}
232
233fn validate(namespace: &str, item: &MemoryItem) -> Result<(), MemoryError> {
234 if namespace.trim().is_empty() {
235 return Err(MemoryError::Other(
236 "memory namespace must not be empty".into(),
237 ));
238 }
239 if item.key.trim().is_empty() {
240 return Err(MemoryError::Other("memory key must not be empty".into()));
241 }
242 if item.text.trim().is_empty() {
243 return Err(MemoryError::Other("memory text must not be empty".into()));
244 }
245 Ok(())
246}
247
248#[async_trait]
255pub trait SemanticScorer: Send + Sync {
256 async fn similarity(&self, query: &str, document: &str) -> f64;
258}
259
260#[derive(Debug, Default, Clone)]
264pub struct LexicalScorer;
265
266impl LexicalScorer {
267 pub fn new() -> Self {
269 Self
270 }
271
272 fn token_vector(text: &str) -> HashMap<String, f64> {
273 let mut v: HashMap<String, f64> = HashMap::new();
274 for token in text
275 .split(|c: char| !c.is_alphanumeric())
276 .filter(|t| !t.is_empty())
277 {
278 *v.entry(token.to_lowercase()).or_insert(0.0) += 1.0;
279 }
280 v
281 }
282
283 pub fn score(query: &str, document: &str) -> f64 {
285 let a = Self::token_vector(query);
286 let b = Self::token_vector(document);
287 if a.is_empty() || b.is_empty() {
288 return 0.0;
289 }
290 let (small, large) = if a.len() <= b.len() {
292 (&a, &b)
293 } else {
294 (&b, &a)
295 };
296 let mut dot = 0.0;
297 for (term, freq) in small {
298 dot += freq * large.get(term).copied().unwrap_or(0.0);
299 }
300 let norm_a: f64 = a.values().map(|v| v * v).sum::<f64>().sqrt();
301 let norm_b: f64 = b.values().map(|v| v * v).sum::<f64>().sqrt();
302 if norm_a == 0.0 || norm_b == 0.0 {
303 return 0.0;
304 }
305 dot / (norm_a * norm_b)
306 }
307}
308
309#[async_trait]
310impl SemanticScorer for LexicalScorer {
311 async fn similarity(&self, query: &str, document: &str) -> f64 {
312 Self::score(query, document)
313 }
314}
315
316#[derive(Debug)]
319struct ShortNamespace {
320 entries: HashMap<String, StoredMemory>,
321 order: VecDeque<String>,
323}
324
325impl ShortNamespace {
326 fn new() -> Self {
327 Self {
328 entries: HashMap::new(),
329 order: VecDeque::new(),
330 }
331 }
332}
333
334pub struct ShortTermMemory {
338 inner: Mutex<HashMap<String, ShortNamespace>>,
339 capacity: usize,
340 scorer: Arc<dyn SemanticScorer>,
341 clock: Clock,
342}
343
344impl std::fmt::Debug for ShortTermMemory {
345 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
346 f.debug_struct("ShortTermMemory")
347 .field("capacity", &self.capacity)
348 .finish_non_exhaustive()
349 }
350}
351
352impl ShortTermMemory {
353 pub fn new(capacity: usize) -> Self {
356 Self::with_scorer(capacity, Arc::new(LexicalScorer::new()))
357 }
358
359 pub fn with_scorer(capacity: usize, scorer: Arc<dyn SemanticScorer>) -> Self {
361 Self {
362 inner: Mutex::new(HashMap::new()),
363 capacity: capacity.max(1),
364 scorer,
365 clock: real_clock(),
366 }
367 }
368
369 #[cfg(test)]
371 fn with_clock(mut self, clock: Clock) -> Self {
372 self.clock = clock;
373 self
374 }
375
376 pub fn capacity(&self) -> usize {
378 self.capacity
379 }
380
381 fn snapshot(&self, namespace: &str) -> Vec<StoredMemory> {
384 let inner = self.inner.lock().unwrap();
385 inner
386 .get(namespace)
387 .map(|ns| ns.entries.values().cloned().collect())
388 .unwrap_or_default()
389 }
390
391 fn touch(&self, namespace: &str, keys: &[String]) {
393 let now = (self.clock)();
394 let mut inner = self.inner.lock().unwrap();
395 if let Some(ns) = inner.get_mut(namespace) {
396 for key in keys {
397 if let Some(stored) = ns.entries.get_mut(key) {
398 stored.touch(now);
399 }
400 }
401 }
402 }
403}
404
405#[async_trait]
406impl MemoryStore for ShortTermMemory {
407 async fn put(&self, namespace: &str, item: MemoryItem) -> Result<(), MemoryError> {
408 validate(namespace, &item)?;
409 let now = (self.clock)();
410 let mut inner = self
411 .inner
412 .lock()
413 .map_err(|e| MemoryError::SaveError(format!("short-term lock poisoned: {e}")))?;
414 let ns = inner
415 .entry(namespace.to_string())
416 .or_insert_with(ShortNamespace::new);
417 let item = MemoryItem {
418 importance: item.importance.clamp(0.0, 1.0),
419 ..item
420 };
421 match ns.entries.get_mut(&item.key) {
422 Some(existing) => {
423 existing.item = item;
424 existing.touch(now);
425 }
426 None => {
427 if ns.entries.len() >= self.capacity {
428 if let Some(oldest) = ns.order.pop_front() {
429 ns.entries.remove(&oldest);
430 }
431 }
432 ns.order.push_back(item.key.clone());
433 ns.entries
434 .insert(item.key.clone(), StoredMemory::fresh(item, now));
435 }
436 }
437 Ok(())
438 }
439
440 async fn get(&self, namespace: &str, key: &str) -> Result<Option<MemoryItem>, MemoryError> {
441 let now = (self.clock)();
442 let mut inner = self
443 .inner
444 .lock()
445 .map_err(|e| MemoryError::LoadError(format!("short-term lock poisoned: {e}")))?;
446 Ok(inner.get_mut(namespace).and_then(|ns| {
447 ns.entries.get_mut(key).map(|stored| {
448 stored.touch(now);
449 stored.item.clone()
450 })
451 }))
452 }
453
454 async fn search(&self, query: &MemoryQuery<'_>) -> Result<Vec<MemoryHit>, MemoryError> {
455 if query.namespace.trim().is_empty() {
456 return Err(MemoryError::Other(
457 "memory namespace must not be empty".into(),
458 ));
459 }
460 let entries = self.snapshot(query.namespace);
461 let mut scored: Vec<MemoryHit> = Vec::with_capacity(entries.len());
462 for stored in &entries {
463 let sim = self.scorer.similarity(query.text, &stored.item.text).await;
464 if sim >= query.min_score {
465 scored.push(MemoryHit::from_stored(stored, sim, MemoryTier::Short));
466 }
467 }
468 scored.sort_by(|a, b| {
469 b.score
470 .partial_cmp(&a.score)
471 .unwrap_or(std::cmp::Ordering::Equal)
472 });
473 let hit_keys: Vec<String> = scored.iter().take(query.k).map(|h| h.key.clone()).collect();
474 scored.truncate(query.k);
475 self.touch(query.namespace, &hit_keys);
476 Ok(scored)
477 }
478
479 async fn forget(&self, namespace: &str, key: &str) -> Result<bool, MemoryError> {
480 let mut inner = self
481 .inner
482 .lock()
483 .map_err(|e| MemoryError::SaveError(format!("short-term lock poisoned: {e}")))?;
484 let removed = inner
485 .get_mut(namespace)
486 .map(|ns| {
487 if ns.entries.remove(key).is_some() {
488 ns.order.retain(|k| k != key);
489 true
490 } else {
491 false
492 }
493 })
494 .unwrap_or(false);
495 Ok(removed)
496 }
497
498 async fn clear_namespace(&self, namespace: &str) -> Result<usize, MemoryError> {
499 let mut inner = self
500 .inner
501 .lock()
502 .map_err(|e| MemoryError::SaveError(format!("short-term lock poisoned: {e}")))?;
503 Ok(inner
504 .remove(namespace)
505 .map(|ns| ns.entries.len())
506 .unwrap_or(0))
507 }
508
509 async fn len_namespace(&self, namespace: &str) -> Result<usize, MemoryError> {
510 let inner = self
511 .inner
512 .lock()
513 .map_err(|e| MemoryError::LoadError(format!("short-term lock poisoned: {e}")))?;
514 Ok(inner.get(namespace).map(|ns| ns.entries.len()).unwrap_or(0))
515 }
516}
517
518#[derive(Debug, Clone, Copy, PartialEq)]
522pub struct DecayWeights {
523 pub similarity: f64,
525 pub recency: f64,
527 pub importance: f64,
529 pub recency_half_life: Duration,
531}
532
533impl Default for DecayWeights {
534 fn default() -> Self {
535 Self {
536 similarity: 0.7,
537 recency: 0.15,
538 importance: 0.15,
539 recency_half_life: Duration::from_secs(7 * 24 * 3600),
540 }
541 }
542}
543
544impl DecayWeights {
545 pub fn new() -> Self {
547 Self::default()
548 }
549
550 pub fn with_weights(mut self, similarity: f64, recency: f64, importance: f64) -> Self {
552 self.similarity = similarity;
553 self.recency = recency;
554 self.importance = importance;
555 self
556 }
557
558 pub fn with_half_life(mut self, half_life: Duration) -> Self {
560 self.recency_half_life = half_life;
561 self
562 }
563
564 pub fn score(&self, similarity: f64, importance: f64, age: Duration) -> f64 {
567 let half = self.recency_half_life.as_secs_f64().max(f64::MIN_POSITIVE);
568 let recency = (-age.as_secs_f64() / half * std::f64::consts::LN_2).exp();
569 let raw =
570 self.similarity * similarity + self.recency * recency + self.importance * importance;
571 raw.clamp(0.0, 1.0)
572 }
573}
574
575pub struct LongTermMemory {
577 inner: Mutex<HashMap<String, HashMap<String, StoredMemory>>>,
578 weights: DecayWeights,
579 scorer: Arc<dyn SemanticScorer>,
580 clock: Clock,
581}
582
583impl std::fmt::Debug for LongTermMemory {
584 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
585 f.debug_struct("LongTermMemory")
586 .field("weights", &self.weights)
587 .finish_non_exhaustive()
588 }
589}
590
591impl LongTermMemory {
592 pub fn new() -> Self {
594 Self::with_config(DecayWeights::default(), Arc::new(LexicalScorer::new()))
595 }
596
597 pub fn with_config(weights: DecayWeights, scorer: Arc<dyn SemanticScorer>) -> Self {
599 Self {
600 inner: Mutex::new(HashMap::new()),
601 weights,
602 scorer,
603 clock: real_clock(),
604 }
605 }
606
607 #[cfg(test)]
609 fn with_clock(mut self, clock: Clock) -> Self {
610 self.clock = clock;
611 self
612 }
613
614 pub fn weights(&self) -> &DecayWeights {
616 &self.weights
617 }
618
619 fn upsert(&self, namespace: &str, incoming: StoredMemory) -> Result<(), MemoryError> {
621 let mut inner = self
622 .inner
623 .lock()
624 .map_err(|e| MemoryError::SaveError(format!("long-term lock poisoned: {e}")))?;
625 let map = inner.entry(namespace.to_string()).or_default();
626 match map.get_mut(&incoming.item.key) {
627 None => {
628 map.insert(incoming.item.key.clone(), incoming);
629 }
630 Some(existing) => {
631 existing.created_at = existing.created_at.min(incoming.created_at);
634 existing.last_access_at = existing.last_access_at.max(incoming.last_access_at);
635 existing.access_count = existing.access_count.saturating_add(incoming.access_count);
636 existing.item.importance = existing.item.importance.max(incoming.item.importance);
637 for (k, v) in incoming.item.metadata {
638 existing.item.metadata.insert(k, v);
639 }
640 existing.item.text = incoming.item.text;
641 }
642 }
643 Ok(())
644 }
645
646 fn snapshot(&self, namespace: &str) -> Vec<StoredMemory> {
647 let inner = self.inner.lock().unwrap();
648 inner
649 .get(namespace)
650 .map(|m| m.values().cloned().collect())
651 .unwrap_or_default()
652 }
653
654 fn touch(&self, namespace: &str, keys: &[String]) {
655 let now = (self.clock)();
656 let mut inner = self.inner.lock().unwrap();
657 if let Some(map) = inner.get_mut(namespace) {
658 for key in keys {
659 if let Some(stored) = map.get_mut(key) {
660 stored.touch(now);
661 }
662 }
663 }
664 }
665}
666
667impl Default for LongTermMemory {
668 fn default() -> Self {
669 Self::new()
670 }
671}
672
673#[async_trait]
674impl MemoryStore for LongTermMemory {
675 async fn put(&self, namespace: &str, item: MemoryItem) -> Result<(), MemoryError> {
676 validate(namespace, &item)?;
677 let now = (self.clock)();
678 self.upsert(
679 namespace,
680 StoredMemory::fresh(
681 MemoryItem {
682 importance: item.importance.clamp(0.0, 1.0),
683 ..item
684 },
685 now,
686 ),
687 )
688 }
689
690 async fn get(&self, namespace: &str, key: &str) -> Result<Option<MemoryItem>, MemoryError> {
691 let now = (self.clock)();
692 let mut inner = self
693 .inner
694 .lock()
695 .map_err(|e| MemoryError::LoadError(format!("long-term lock poisoned: {e}")))?;
696 Ok(inner
697 .get_mut(namespace)
698 .and_then(|m| m.get_mut(key))
699 .map(|stored| {
700 stored.touch(now);
701 stored.item.clone()
702 }))
703 }
704
705 async fn search(&self, query: &MemoryQuery<'_>) -> Result<Vec<MemoryHit>, MemoryError> {
706 if query.namespace.trim().is_empty() {
707 return Err(MemoryError::Other(
708 "memory namespace must not be empty".into(),
709 ));
710 }
711 let now = (self.clock)();
712 let entries = self.snapshot(query.namespace);
713 let mut scored: Vec<MemoryHit> = Vec::with_capacity(entries.len());
714 for stored in entries {
715 let sim = self.scorer.similarity(query.text, &stored.item.text).await;
716 let age = now
717 .duration_since(stored.last_access_at)
718 .unwrap_or(Duration::ZERO);
719 let score = self.weights.score(sim, stored.item.importance, age);
720 if score >= query.min_score {
721 scored.push(MemoryHit::from_stored(&stored, score, MemoryTier::Long));
722 }
723 }
724 scored.sort_by(|a, b| {
725 b.score
726 .partial_cmp(&a.score)
727 .unwrap_or(std::cmp::Ordering::Equal)
728 });
729 let hit_keys: Vec<String> = scored.iter().take(query.k).map(|h| h.key.clone()).collect();
730 scored.truncate(query.k);
731 self.touch(query.namespace, &hit_keys);
732 Ok(scored)
733 }
734
735 async fn forget(&self, namespace: &str, key: &str) -> Result<bool, MemoryError> {
736 let mut inner = self
737 .inner
738 .lock()
739 .map_err(|e| MemoryError::SaveError(format!("long-term lock poisoned: {e}")))?;
740 Ok(inner
741 .get_mut(namespace)
742 .map(|m| m.remove(key).is_some())
743 .unwrap_or(false))
744 }
745
746 async fn clear_namespace(&self, namespace: &str) -> Result<usize, MemoryError> {
747 let mut inner = self
748 .inner
749 .lock()
750 .map_err(|e| MemoryError::SaveError(format!("long-term lock poisoned: {e}")))?;
751 Ok(inner.remove(namespace).map(|m| m.len()).unwrap_or(0))
752 }
753
754 async fn len_namespace(&self, namespace: &str) -> Result<usize, MemoryError> {
755 let inner = self
756 .inner
757 .lock()
758 .map_err(|e| MemoryError::LoadError(format!("long-term lock poisoned: {e}")))?;
759 Ok(inner.get(namespace).map(|m| m.len()).unwrap_or(0))
760 }
761}
762
763#[derive(Debug, Clone, Copy, PartialEq)]
770pub struct PromotionPolicy {
771 pub min_importance: f64,
773 pub min_access_count: u64,
775}
776
777impl Default for PromotionPolicy {
778 fn default() -> Self {
779 Self {
780 min_importance: 0.8,
781 min_access_count: 3,
782 }
783 }
784}
785
786impl PromotionPolicy {
787 pub fn new() -> Self {
789 Self::default()
790 }
791
792 pub fn with_min_importance(mut self, min_importance: f64) -> Self {
794 self.min_importance = min_importance.clamp(0.0, 1.0);
795 self
796 }
797
798 pub fn with_min_access_count(mut self, min_access_count: u64) -> Self {
800 self.min_access_count = min_access_count;
801 self
802 }
803
804 pub fn qualifies(&self, stored: &StoredMemory) -> bool {
806 stored.item.importance >= self.min_importance
807 || stored.access_count >= self.min_access_count
808 }
809}
810
811pub struct TwoTierMemory {
821 short: Arc<ShortTermMemory>,
822 long: Arc<LongTermMemory>,
823 policy: Mutex<PromotionPolicy>,
824 scorer: Arc<dyn SemanticScorer>,
825 weights: DecayWeights,
826 clock: Clock,
827}
828
829impl std::fmt::Debug for TwoTierMemory {
830 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
831 f.debug_struct("TwoTierMemory")
832 .field("policy", &self.policy)
833 .field("weights", &self.weights)
834 .finish_non_exhaustive()
835 }
836}
837
838impl TwoTierMemory {
839 pub fn new(short_capacity: usize) -> Self {
841 let scorer: Arc<dyn SemanticScorer> = Arc::new(LexicalScorer::new());
842 let weights = DecayWeights::default();
843 Self {
844 short: Arc::new(ShortTermMemory::with_scorer(short_capacity, scorer.clone())),
845 long: Arc::new(LongTermMemory::with_config(weights, scorer.clone())),
846 policy: Mutex::new(PromotionPolicy::default()),
847 scorer,
848 weights,
849 clock: real_clock(),
850 }
851 }
852
853 pub fn with_policy(self, policy: PromotionPolicy) -> Self {
855 *self.policy.lock().unwrap() = policy;
856 self
857 }
858
859 #[cfg(test)]
864 pub(crate) fn with_clock(mut self, clock: Clock) -> Self {
865 let capacity = self.short.capacity();
866 self.short = Arc::new(
867 ShortTermMemory::with_scorer(capacity, self.scorer.clone()).with_clock(clock.clone()),
868 );
869 self.long = Arc::new(
870 LongTermMemory::with_config(self.weights, self.scorer.clone())
871 .with_clock(clock.clone()),
872 );
873 self.clock = clock;
874 self
875 }
876
877 pub fn short_term(&self) -> Arc<ShortTermMemory> {
879 self.short.clone()
880 }
881
882 pub fn long_term(&self) -> Arc<LongTermMemory> {
884 self.long.clone()
885 }
886
887 pub fn set_policy(&self, policy: PromotionPolicy) {
889 *self.policy.lock().unwrap() = policy;
890 }
891
892 pub async fn consolidate_namespace(&self, namespace: &str) -> Result<Vec<String>, MemoryError> {
895 let policy = *self
896 .policy
897 .lock()
898 .map_err(|e| MemoryError::Other(format!("promotion policy lock poisoned: {e}")))?;
899 let candidates = self.short.snapshot(namespace);
900 let mut promoted = Vec::new();
901 for stored in candidates {
902 if policy.qualifies(&stored) {
903 self.long.upsert(namespace, stored.clone())?;
904 self.short.forget(namespace, &stored.item.key).await?;
905 promoted.push(stored.item.key);
906 }
907 }
908 Ok(promoted)
909 }
910
911 pub async fn consolidate(&self) -> Result<Vec<String>, MemoryError> {
913 let namespaces: Vec<String> = self.short.inner.lock().unwrap().keys().cloned().collect();
914 let mut all = Vec::new();
915 for namespace in namespaces {
916 all.extend(self.consolidate_namespace(&namespace).await?);
917 }
918 Ok(all)
919 }
920
921 async fn rank_one(&self, stored: &StoredMemory, query: &str, now: SystemTime) -> f64 {
923 let sim = self.scorer.similarity(query, &stored.item.text).await;
924 let age = now
925 .duration_since(stored.last_access_at)
926 .unwrap_or(Duration::ZERO);
927 self.weights.score(sim, stored.item.importance, age)
928 }
929}
930
931#[async_trait]
932impl MemoryStore for TwoTierMemory {
933 async fn put(&self, namespace: &str, item: MemoryItem) -> Result<(), MemoryError> {
934 self.short.put(namespace, item).await
935 }
936
937 async fn get(&self, namespace: &str, key: &str) -> Result<Option<MemoryItem>, MemoryError> {
938 if let Some(item) = self.short.get(namespace, key).await? {
939 return Ok(Some(item));
940 }
941 self.long.get(namespace, key).await
942 }
943
944 async fn search(&self, query: &MemoryQuery<'_>) -> Result<Vec<MemoryHit>, MemoryError> {
945 if query.namespace.trim().is_empty() {
946 return Err(MemoryError::Other(
947 "memory namespace must not be empty".into(),
948 ));
949 }
950 let now = (self.clock)();
951
952 let mut candidates: Vec<(StoredMemory, MemoryTier)> = self
954 .short
955 .snapshot(query.namespace)
956 .into_iter()
957 .map(|s| (s, MemoryTier::Short))
958 .collect();
959 candidates.extend(
960 self.long
961 .snapshot(query.namespace)
962 .into_iter()
963 .map(|s| (s, MemoryTier::Long)),
964 );
965
966 let mut hits: Vec<MemoryHit> = Vec::with_capacity(candidates.len());
967 for (stored, tier) in &candidates {
968 let score = self.rank_one(stored, query.text, now).await;
969 if score >= query.min_score {
970 hits.push(MemoryHit::from_stored(stored, score, *tier));
971 }
972 }
973
974 let mut seen = std::collections::HashSet::new();
976 hits.retain(|h| seen.insert(h.key.clone()));
977
978 hits.sort_by(|a, b| {
979 b.score
980 .partial_cmp(&a.score)
981 .unwrap_or(std::cmp::Ordering::Equal)
982 });
983 hits.truncate(query.k);
984
985 let mut short_keys = Vec::new();
987 let mut long_keys = Vec::new();
988 for hit in &hits {
989 match hit.tier {
990 MemoryTier::Short => short_keys.push(hit.key.clone()),
991 MemoryTier::Long => long_keys.push(hit.key.clone()),
992 }
993 }
994 self.short.touch(query.namespace, &short_keys);
995 self.long.touch(query.namespace, &long_keys);
996 Ok(hits)
997 }
998
999 async fn forget(&self, namespace: &str, key: &str) -> Result<bool, MemoryError> {
1000 let in_short = self.short.forget(namespace, key).await?;
1001 let in_long = self.long.forget(namespace, key).await?;
1002 Ok(in_short || in_long)
1003 }
1004
1005 async fn clear_namespace(&self, namespace: &str) -> Result<usize, MemoryError> {
1006 let s = self.short.clear_namespace(namespace).await?;
1007 let l = self.long.clear_namespace(namespace).await?;
1008 Ok(s + l)
1009 }
1010
1011 async fn len_namespace(&self, namespace: &str) -> Result<usize, MemoryError> {
1012 Ok(self.short.len_namespace(namespace).await? + self.long.len_namespace(namespace).await?)
1013 }
1014}
1015
1016#[async_trait]
1024pub trait MemoryExtractor: Send + Sync {
1025 async fn extract(
1027 &self,
1028 namespace: &str,
1029 user_input: &str,
1030 assistant_output: &str,
1031 ) -> Result<Vec<MemoryItem>, MemoryError>;
1032}
1033
1034#[cfg(test)]
1035mod tests {
1036 use super::*;
1037
1038 fn clock_at(secs: u64) -> Clock {
1039 Arc::new(move || std::time::UNIX_EPOCH + Duration::from_secs(secs))
1040 }
1041
1042 fn item(key: &str, text: &str, importance: f64) -> MemoryItem {
1043 MemoryItem::new(key, text).with_importance(importance)
1044 }
1045
1046 #[tokio::test]
1047 async fn lexical_scorer_ranks_shared_terms_first() {
1048 assert!(LexicalScorer::score("rust memory decay", "rust memory decay") > 0.99);
1049 let exact =
1050 LexicalScorer::score("rust agent framework", "the rust agent framework is fast");
1051 let unrelated = LexicalScorer::score("rust agent framework", "banana bread recipe sunday");
1052 assert!(exact > unrelated);
1053 assert_eq!(LexicalScorer::score("", "anything"), 0.0);
1054 }
1055
1056 #[tokio::test]
1057 async fn namespace_isolation_covers_get_search_forget_and_clear() {
1058 let store = ShortTermMemory::new(10);
1059 store
1060 .put("a", item("k1", "shared secret alpha", 0.5))
1061 .await
1062 .unwrap();
1063 store
1064 .put("b", item("k1", "shared secret beta", 0.5))
1065 .await
1066 .unwrap();
1067
1068 assert_eq!(
1070 store.get("a", "k1").await.unwrap().unwrap().text,
1071 "shared secret alpha"
1072 );
1073 assert_eq!(store.len_namespace("a").await.unwrap(), 1);
1074 assert_eq!(store.len_namespace("b").await.unwrap(), 1);
1075 assert_eq!(store.len_namespace("c").await.unwrap(), 0);
1076
1077 let hits_a = store
1079 .search(&MemoryQuery::new("a", "secret alpha").k(5))
1080 .await
1081 .unwrap();
1082 assert_eq!(hits_a.len(), 1);
1083 assert_eq!(hits_a[0].text, "shared secret alpha");
1084 assert!(store
1085 .search(&MemoryQuery::new("c", "secret"))
1086 .await
1087 .unwrap()
1088 .is_empty());
1089
1090 assert!(store.forget("a", "k1").await.unwrap());
1092 assert!(!store.forget("a", "k1").await.unwrap());
1093 assert_eq!(store.len_namespace("b").await.unwrap(), 1);
1094 assert_eq!(store.clear_namespace("b").await.unwrap(), 1);
1095 assert_eq!(store.len_namespace("b").await.unwrap(), 0);
1096 }
1097
1098 #[tokio::test]
1099 async fn short_term_validates_inputs_and_evicts_fifo() {
1100 let store = ShortTermMemory::new(2);
1101 assert!(store.put("ns", MemoryItem::new("", "x")).await.is_err());
1102 assert!(store.put("ns", MemoryItem::new("k", " ")).await.is_err());
1103 assert!(store.put("", MemoryItem::new("k", "x")).await.is_err());
1104
1105 store
1106 .put("ns", item("first", "first entry text", 0.5))
1107 .await
1108 .unwrap();
1109 store
1110 .put("ns", item("second", "second entry text", 0.5))
1111 .await
1112 .unwrap();
1113 store
1114 .put("ns", item("third", "third entry text", 0.5))
1115 .await
1116 .unwrap();
1117 assert_eq!(store.len_namespace("ns").await.unwrap(), 2);
1118 assert!(store.get("ns", "first").await.unwrap().is_none());
1119 assert!(store.get("ns", "second").await.unwrap().is_some());
1120 assert!(store.get("ns", "third").await.unwrap().is_some());
1121
1122 store
1124 .put("ns", item("second", "second updated", 0.9))
1125 .await
1126 .unwrap();
1127 assert_eq!(store.len_namespace("ns").await.unwrap(), 2);
1128 assert_eq!(
1129 store.get("ns", "second").await.unwrap().unwrap().importance,
1130 0.9
1131 );
1132 }
1133
1134 #[tokio::test]
1135 async fn get_counts_as_access_for_promotion() {
1136 let store = ShortTermMemory::new(10);
1137 store
1138 .put("ns", item("k", "watched fact", 0.1))
1139 .await
1140 .unwrap();
1141 store.get("ns", "k").await.unwrap();
1142 store.get("ns", "k").await.unwrap();
1143 let stored = store.snapshot("ns").pop().unwrap();
1145 assert_eq!(stored.access_count, 3);
1146 }
1147
1148 fn moving_clock(secs: Arc<Mutex<u64>>) -> Clock {
1150 Arc::new(move || std::time::UNIX_EPOCH + Duration::from_secs(*secs.lock().unwrap()))
1151 }
1152
1153 #[tokio::test]
1154 async fn long_term_decay_rewards_recency_and_importance() {
1155 let w = DecayWeights::default().with_half_life(Duration::from_secs(10));
1157 let fresh = w.score(1.0, 0.5, Duration::from_secs(0));
1158 let stale = w.score(1.0, 0.5, Duration::from_secs(30));
1159 assert!(fresh > stale);
1160 let important_stale = w.score(1.0, 1.0, Duration::from_secs(30));
1161 assert!(important_stale > stale);
1162 let half = w.score(0.0, 0.0, Duration::from_secs(10));
1164 assert!((half - 0.15 * 0.5).abs() < 1e-9);
1165
1166 let t = Arc::new(Mutex::new(0u64));
1169 let long = LongTermMemory::with_config(
1170 DecayWeights::default().with_half_life(Duration::from_secs(10)),
1171 Arc::new(LexicalScorer::new()),
1172 )
1173 .with_clock(moving_clock(t.clone()));
1174 long.put("ns", item("old", "same fact wording", 0.5))
1175 .await
1176 .unwrap();
1177 *t.lock().unwrap() = 100;
1178 long.put("ns", item("new", "same fact wording", 0.5))
1179 .await
1180 .unwrap();
1181
1182 let hits = long
1183 .search(&MemoryQuery::new("ns", "same fact wording").k(5))
1184 .await
1185 .unwrap();
1186 assert_eq!(hits[0].key, "new");
1187 assert_eq!(hits[0].tier, MemoryTier::Long);
1188 assert!(hits[0].score > hits[1].score);
1189 }
1190
1191 #[tokio::test]
1192 async fn consolidation_promotes_by_importance_or_access_and_merges() {
1193 let mem = TwoTierMemory::new(10).with_clock(clock_at(0));
1194 mem.put("ns", item("hot", "important fact", 0.95))
1195 .await
1196 .unwrap();
1197 mem.put("ns", item("warm", "reaccessed fact", 0.2))
1198 .await
1199 .unwrap();
1200 mem.put("ns", item("cold", "ignored fact", 0.2))
1201 .await
1202 .unwrap();
1203 mem.get("ns", "warm").await.unwrap();
1205 mem.get("ns", "warm").await.unwrap();
1206
1207 let promoted = mem.consolidate_namespace("ns").await.unwrap();
1208 assert!(promoted.contains(&"hot".to_string()));
1209 assert!(promoted.contains(&"warm".to_string()));
1210 assert!(!promoted.contains(&"cold".to_string()));
1211 assert_eq!(promoted.len(), 2);
1212
1213 assert!(mem.short_term().get("ns", "hot").await.unwrap().is_none());
1215 assert_eq!(mem.long_term().len_namespace("ns").await.unwrap(), 2);
1216 assert_eq!(mem.short_term().len_namespace("ns").await.unwrap(), 1);
1217 assert!(mem.get("ns", "hot").await.unwrap().is_some());
1219
1220 mem.put("ns", item("hot", "important fact refined", 0.3))
1223 .await
1224 .unwrap();
1225 mem.short_term().get("ns", "hot").await.unwrap();
1226 mem.short_term().get("ns", "hot").await.unwrap();
1227 let again = mem.consolidate_namespace("ns").await.unwrap();
1228 assert_eq!(again, vec!["hot".to_string()]);
1229 let merged = mem.long_term().get("ns", "hot").await.unwrap().unwrap();
1230 assert_eq!(merged.text, "important fact refined");
1231 assert_eq!(merged.importance, 0.95); assert_eq!(mem.long_term().len_namespace("ns").await.unwrap(), 2);
1233 }
1234
1235 #[tokio::test]
1236 async fn two_tier_search_merges_dedupes_and_ranks_on_one_scale() {
1237 let mem = TwoTierMemory::new(10)
1238 .with_policy(PromotionPolicy::default().with_min_importance(0.0))
1239 .with_clock(clock_at(0));
1240 mem.put("ns", item("short_only", "alpha distinctive tokens", 0.5))
1241 .await
1242 .unwrap();
1243 mem.put("ns", item("both", "gamma shared wording here", 0.5))
1244 .await
1245 .unwrap();
1246 mem.consolidate_namespace("ns").await.unwrap(); mem.put("ns", item("both", "gamma shared wording here fresher", 0.5))
1249 .await
1250 .unwrap();
1251 mem.long_term()
1252 .put("ns", item("long_only", "beta another memory", 0.5))
1253 .await
1254 .unwrap();
1255
1256 let hits = mem
1257 .search(
1258 &MemoryQuery::new("ns", "gamma shared wording")
1259 .k(10)
1260 .min_score(0.3),
1261 )
1262 .await
1263 .unwrap();
1264 let keys: Vec<&str> = hits.iter().map(|h| h.key.as_str()).collect();
1265 assert!(keys.contains(&"both"));
1266 assert!(!keys.contains(&"short_only"));
1268 assert!(!keys.contains(&"long_only"));
1269 assert_eq!(keys.iter().filter(|k| **k == "both").count(), 1);
1271 assert_eq!(hits[0].key, "both");
1273
1274 assert!(mem.forget("ns", "long_only").await.unwrap());
1277 assert!(mem.forget("ns", "both").await.unwrap());
1278 assert_eq!(mem.len_namespace("ns").await.unwrap(), 1);
1280 }
1281
1282 #[tokio::test]
1283 async fn empty_namespace_and_query_validation() {
1284 let mem = TwoTierMemory::new(4);
1285 assert!(mem.search(&MemoryQuery::new(" ", "x")).await.is_err());
1286 assert!(mem.put(" ", item("k", "v", 0.5)).await.is_err());
1287 }
1288}