1use crate::error::AgentRuntimeError;
20use crate::util::recover_lock;
21use chrono::{DateTime, Utc};
22use serde::{Deserialize, Serialize};
23use std::collections::{HashMap, VecDeque};
24use std::sync::{Arc, Mutex};
25use uuid::Uuid;
26
27fn cosine_similarity(a: &[f32], b: &[f32]) -> f32 {
30 if a.len() != b.len() || a.is_empty() {
31 return 0.0;
32 }
33 let dot: f32 = a.iter().zip(b.iter()).map(|(x, y)| x * y).sum();
34 let norm_a: f32 = a.iter().map(|x| x * x).sum::<f32>().sqrt();
35 let norm_b: f32 = b.iter().map(|x| x * x).sum::<f32>().sqrt();
36 if norm_a == 0.0 || norm_b == 0.0 {
37 return 0.0;
38 }
39 (dot / (norm_a * norm_b)).clamp(-1.0, 1.0)
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
46pub struct AgentId(pub String);
47
48impl AgentId {
49 pub fn new(id: impl Into<String>) -> Self {
51 let id = id.into();
52 debug_assert!(!id.is_empty(), "AgentId must not be empty");
53 Self(id)
54 }
55
56 pub fn random() -> Self {
58 Self(Uuid::new_v4().to_string())
59 }
60
61 pub fn as_str(&self) -> &str {
63 &self.0
64 }
65}
66
67impl AsRef<str> for AgentId {
68 fn as_ref(&self) -> &str {
69 &self.0
70 }
71}
72
73impl std::fmt::Display for AgentId {
74 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75 write!(f, "{}", self.0)
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
81pub struct MemoryId(pub String);
82
83impl MemoryId {
84 pub fn new(id: impl Into<String>) -> Self {
86 let id = id.into();
87 debug_assert!(!id.is_empty(), "MemoryId must not be empty");
88 Self(id)
89 }
90
91 pub fn random() -> Self {
93 Self(Uuid::new_v4().to_string())
94 }
95}
96
97impl AsRef<str> for MemoryId {
98 fn as_ref(&self) -> &str {
99 &self.0
100 }
101}
102
103impl std::fmt::Display for MemoryId {
104 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105 write!(f, "{}", self.0)
106 }
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct MemoryItem {
114 pub id: MemoryId,
116 pub agent_id: AgentId,
118 pub content: String,
120 pub importance: f32,
122 pub timestamp: DateTime<Utc>,
124 pub tags: Vec<String>,
126 #[serde(default)]
128 pub recall_count: u64,
129}
130
131impl MemoryItem {
132 pub fn new(
134 agent_id: AgentId,
135 content: impl Into<String>,
136 importance: f32,
137 tags: Vec<String>,
138 ) -> Self {
139 Self {
140 id: MemoryId::random(),
141 agent_id,
142 content: content.into(),
143 importance: importance.clamp(0.0, 1.0),
144 timestamp: Utc::now(),
145 tags,
146 recall_count: 0,
147 }
148 }
149}
150
151#[derive(Debug, Clone)]
155pub struct DecayPolicy {
156 half_life_hours: f64,
158}
159
160impl DecayPolicy {
161 pub fn exponential(half_life_hours: f64) -> Result<Self, AgentRuntimeError> {
170 if half_life_hours <= 0.0 {
171 return Err(AgentRuntimeError::Memory(
172 "half_life_hours must be positive".into(),
173 ));
174 }
175 Ok(Self { half_life_hours })
176 }
177
178 pub fn apply(&self, importance: f32, age_hours: f64) -> f32 {
187 let decay = (-age_hours * std::f64::consts::LN_2 / self.half_life_hours).exp();
188 (importance as f64 * decay).clamp(0.0, 1.0) as f32
189 }
190
191 pub fn decay_item(&self, item: &mut MemoryItem) {
193 let age_hours = (Utc::now() - item.timestamp).num_seconds().max(0) as f64 / 3600.0;
194 item.importance = self.apply(item.importance, age_hours);
195 }
196}
197
198#[derive(Debug, Clone)]
226pub enum RecallPolicy {
227 Importance,
229 Hybrid {
235 recency_weight: f32,
237 frequency_weight: f32,
239 },
240}
241
242impl Default for RecallPolicy {
243 fn default() -> Self {
244 RecallPolicy::Importance
245 }
246}
247
248fn compute_hybrid_score(
251 item: &MemoryItem,
252 recency_weight: f32,
253 frequency_weight: f32,
254 max_recall: u64,
255 now: chrono::DateTime<Utc>,
256) -> f32 {
257 let age_hours = (now - item.timestamp).num_seconds().max(0) as f64 / 3600.0;
258 let recency_score = (-age_hours / 24.0).exp() as f32;
259 let frequency_score = item.recall_count as f32 / (max_recall as f32 + 1.0);
260 item.importance + recency_score * recency_weight + frequency_score * frequency_weight
261}
262
263#[derive(Debug, Clone, PartialEq, Eq, Default)]
267pub enum EvictionPolicy {
268 #[default]
270 LowestImportance,
271 Oldest,
273}
274
275#[derive(Default)]
292pub struct EpisodicStoreBuilder {
293 decay: Option<DecayPolicy>,
294 recall_policy: Option<RecallPolicy>,
295 per_agent_capacity: Option<usize>,
296 max_age_hours: Option<f64>,
297 eviction_policy: Option<EvictionPolicy>,
298}
299
300impl EpisodicStoreBuilder {
301 pub fn decay(mut self, policy: DecayPolicy) -> Self {
303 self.decay = Some(policy);
304 self
305 }
306
307 pub fn recall_policy(mut self, policy: RecallPolicy) -> Self {
309 self.recall_policy = Some(policy);
310 self
311 }
312
313 pub fn per_agent_capacity(mut self, capacity: usize) -> Self {
315 assert!(capacity > 0, "per_agent_capacity must be > 0");
316 self.per_agent_capacity = Some(capacity);
317 self
318 }
319
320 pub fn max_age_hours(mut self, hours: f64) -> Result<Self, crate::error::AgentRuntimeError> {
322 if hours <= 0.0 {
323 return Err(crate::error::AgentRuntimeError::Memory(
324 "max_age_hours must be positive".into(),
325 ));
326 }
327 self.max_age_hours = Some(hours);
328 Ok(self)
329 }
330
331 pub fn eviction_policy(mut self, policy: EvictionPolicy) -> Self {
333 self.eviction_policy = Some(policy);
334 self
335 }
336
337 pub fn build(self) -> EpisodicStore {
339 EpisodicStore {
340 inner: Arc::new(Mutex::new(EpisodicInner {
341 items: HashMap::new(),
342 decay: self.decay,
343 recall_policy: self.recall_policy.unwrap_or(RecallPolicy::Importance),
344 per_agent_capacity: self.per_agent_capacity,
345 max_age_hours: self.max_age_hours,
346 eviction_policy: self.eviction_policy.unwrap_or_default(),
347 })),
348 }
349 }
350}
351
352#[derive(Debug, Clone)]
363pub struct EpisodicStore {
364 inner: Arc<Mutex<EpisodicInner>>,
365}
366
367#[derive(Debug)]
368struct EpisodicInner {
369 items: HashMap<AgentId, Vec<MemoryItem>>,
371 decay: Option<DecayPolicy>,
372 recall_policy: RecallPolicy,
373 per_agent_capacity: Option<usize>,
375 max_age_hours: Option<f64>,
377 eviction_policy: EvictionPolicy,
379}
380
381impl EpisodicInner {
382 fn purge_stale(&mut self, agent_id: &AgentId) {
384 if let Some(max_age_h) = self.max_age_hours {
385 let cutoff = Utc::now()
386 - chrono::Duration::seconds((max_age_h * 3600.0) as i64);
387 if let Some(agent_items) = self.items.get_mut(agent_id) {
388 agent_items.retain(|i| i.timestamp >= cutoff);
389 }
390 }
391 }
392}
393
394fn evict_if_over_capacity(
399 agent_items: &mut Vec<MemoryItem>,
400 cap: usize,
401 policy: &EvictionPolicy,
402) {
403 if agent_items.len() <= cap {
404 return;
405 }
406 let pos = match policy {
407 EvictionPolicy::LowestImportance => {
408 let len = agent_items.len();
409 agent_items[..len - 1]
410 .iter()
411 .enumerate()
412 .min_by(|(_, a), (_, b)| {
413 a.importance
414 .partial_cmp(&b.importance)
415 .unwrap_or(std::cmp::Ordering::Equal)
416 })
417 .map(|(pos, _)| pos)
418 }
419 EvictionPolicy::Oldest => {
420 let len = agent_items.len();
421 agent_items[..len - 1]
422 .iter()
423 .enumerate()
424 .min_by_key(|(_, item)| item.timestamp)
425 .map(|(pos, _)| pos)
426 }
427 };
428 if let Some(pos) = pos {
429 agent_items.remove(pos);
430 }
431}
432
433impl EpisodicStore {
434 pub fn new() -> Self {
436 Self {
437 inner: Arc::new(Mutex::new(EpisodicInner {
438 items: HashMap::new(),
439 decay: None,
440 recall_policy: RecallPolicy::Importance,
441 per_agent_capacity: None,
442 max_age_hours: None,
443 eviction_policy: EvictionPolicy::LowestImportance,
444 })),
445 }
446 }
447
448 pub fn builder() -> EpisodicStoreBuilder {
450 EpisodicStoreBuilder::default()
451 }
452
453 pub fn with_decay(policy: DecayPolicy) -> Self {
455 Self {
456 inner: Arc::new(Mutex::new(EpisodicInner {
457 items: HashMap::new(),
458 decay: Some(policy),
459 recall_policy: RecallPolicy::Importance,
460 per_agent_capacity: None,
461 max_age_hours: None,
462 eviction_policy: EvictionPolicy::LowestImportance,
463 })),
464 }
465 }
466
467 pub fn with_decay_and_recall_policy(decay: DecayPolicy, recall: RecallPolicy) -> Self {
469 Self {
470 inner: Arc::new(Mutex::new(EpisodicInner {
471 items: HashMap::new(),
472 decay: Some(decay),
473 recall_policy: recall,
474 per_agent_capacity: None,
475 max_age_hours: None,
476 eviction_policy: EvictionPolicy::LowestImportance,
477 })),
478 }
479 }
480
481 pub fn with_recall_policy(policy: RecallPolicy) -> Self {
483 Self {
484 inner: Arc::new(Mutex::new(EpisodicInner {
485 items: HashMap::new(),
486 decay: None,
487 recall_policy: policy,
488 per_agent_capacity: None,
489 max_age_hours: None,
490 eviction_policy: EvictionPolicy::LowestImportance,
491 })),
492 }
493 }
494
495 pub fn with_per_agent_capacity(capacity: usize) -> Self {
514 assert!(capacity > 0, "per_agent_capacity must be > 0");
515 Self {
516 inner: Arc::new(Mutex::new(EpisodicInner {
517 items: HashMap::new(),
518 decay: None,
519 recall_policy: RecallPolicy::Importance,
520 per_agent_capacity: Some(capacity),
521 max_age_hours: None,
522 eviction_policy: EvictionPolicy::LowestImportance,
523 })),
524 }
525 }
526
527 pub fn with_max_age(max_age_hours: f64) -> Result<Self, AgentRuntimeError> {
535 if max_age_hours <= 0.0 {
536 return Err(AgentRuntimeError::Memory(
537 "max_age_hours must be positive".into(),
538 ));
539 }
540 Ok(Self {
541 inner: Arc::new(Mutex::new(EpisodicInner {
542 items: HashMap::new(),
543 decay: None,
544 recall_policy: RecallPolicy::Importance,
545 per_agent_capacity: None,
546 max_age_hours: Some(max_age_hours),
547 eviction_policy: EvictionPolicy::LowestImportance,
548 })),
549 })
550 }
551
552 pub fn with_eviction_policy(policy: EvictionPolicy) -> Self {
554 Self {
555 inner: Arc::new(Mutex::new(EpisodicInner {
556 items: HashMap::new(),
557 decay: None,
558 recall_policy: RecallPolicy::Importance,
559 per_agent_capacity: None,
560 max_age_hours: None,
561 eviction_policy: policy,
562 })),
563 }
564 }
565
566 #[tracing::instrument(skip(self))]
584 pub fn add_episode(
585 &self,
586 agent_id: AgentId,
587 content: impl Into<String> + std::fmt::Debug,
588 importance: f32,
589 ) -> Result<MemoryId, AgentRuntimeError> {
590 let item = MemoryItem::new(agent_id.clone(), content, importance, Vec::new());
591 let id = item.id.clone();
592 let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::add_episode");
593
594 inner.purge_stale(&agent_id);
595 let cap = inner.per_agent_capacity; let eviction_policy = inner.eviction_policy.clone();
597 let agent_items = inner.items.entry(agent_id).or_default();
598 agent_items.push(item);
599
600 if let Some(cap) = cap {
601 evict_if_over_capacity(agent_items, cap, &eviction_policy);
602 }
603 Ok(id)
604 }
605
606 #[tracing::instrument(skip(self))]
608 pub fn add_episode_at(
609 &self,
610 agent_id: AgentId,
611 content: impl Into<String> + std::fmt::Debug,
612 importance: f32,
613 timestamp: chrono::DateTime<chrono::Utc>,
614 ) -> Result<MemoryId, AgentRuntimeError> {
615 let mut item = MemoryItem::new(agent_id.clone(), content, importance, Vec::new());
616 item.timestamp = timestamp;
617 let id = item.id.clone();
618 let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::add_episode_at");
619
620 inner.purge_stale(&agent_id);
621 let cap = inner.per_agent_capacity; let eviction_policy = inner.eviction_policy.clone();
623 let agent_items = inner.items.entry(agent_id).or_default();
624 agent_items.push(item);
625
626 if let Some(cap) = cap {
627 evict_if_over_capacity(agent_items, cap, &eviction_policy);
628 }
629 Ok(id)
630 }
631
632 #[tracing::instrument(skip(self))]
642 pub fn recall(
643 &self,
644 agent_id: &AgentId,
645 limit: usize,
646 ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
647 let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::recall");
648
649 let decay = inner.decay.clone();
651 let max_age = inner.max_age_hours;
652 let recall_policy = inner.recall_policy.clone();
653
654 if !inner.items.contains_key(agent_id) {
656 return Ok(Vec::new());
657 }
658 let agent_items = inner.items.get_mut(agent_id).unwrap();
659
660 if let Some(ref policy) = decay {
662 for item in agent_items.iter_mut() {
663 policy.decay_item(item);
664 }
665 }
666
667 if let Some(max_age_h) = max_age {
669 let cutoff =
670 Utc::now() - chrono::Duration::seconds((max_age_h * 3600.0) as i64);
671 agent_items.retain(|i| i.timestamp >= cutoff);
672 }
673
674 let mut indices: Vec<usize> = (0..agent_items.len()).collect();
676
677 match recall_policy {
678 RecallPolicy::Importance => {
679 indices.sort_by(|&a, &b| {
680 agent_items[b]
681 .importance
682 .partial_cmp(&agent_items[a].importance)
683 .unwrap_or(std::cmp::Ordering::Equal)
684 });
685 }
686 RecallPolicy::Hybrid {
687 recency_weight,
688 frequency_weight,
689 } => {
690 let max_recall = agent_items
691 .iter()
692 .map(|i| i.recall_count)
693 .max()
694 .unwrap_or(1)
695 .max(1);
696 let now = Utc::now();
697 indices.sort_by(|&a, &b| {
698 let score_a = compute_hybrid_score(
699 &agent_items[a],
700 recency_weight,
701 frequency_weight,
702 max_recall,
703 now,
704 );
705 let score_b = compute_hybrid_score(
706 &agent_items[b],
707 recency_weight,
708 frequency_weight,
709 max_recall,
710 now,
711 );
712 score_b
713 .partial_cmp(&score_a)
714 .unwrap_or(std::cmp::Ordering::Equal)
715 });
716 }
717 }
718
719 indices.truncate(limit);
720
721 for &idx in &indices {
723 agent_items[idx].recall_count += 1;
724 }
725
726 let items: Vec<MemoryItem> = indices.iter().map(|&idx| agent_items[idx].clone()).collect();
728
729 tracing::debug!("recalled {} items", items.len());
730 Ok(items)
731 }
732
733 pub fn len(&self) -> Result<usize, AgentRuntimeError> {
735 let inner = recover_lock(self.inner.lock(), "EpisodicStore::len");
736 Ok(inner.items.values().map(|v| v.len()).sum())
737 }
738
739 pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
741 Ok(self.len()? == 0)
742 }
743
744 pub fn agent_memory_count(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
748 let inner = recover_lock(self.inner.lock(), "EpisodicStore::agent_memory_count");
749 Ok(inner.items.get(agent_id).map_or(0, |v| v.len()))
750 }
751
752 pub fn list_agents(&self) -> Result<Vec<AgentId>, AgentRuntimeError> {
756 let inner = recover_lock(self.inner.lock(), "EpisodicStore::list_agents");
757 Ok(inner.items.keys().cloned().collect())
758 }
759
760 pub fn purge_agent_memories(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
764 let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::purge_agent_memories");
765 let removed = inner.items.remove(agent_id).map_or(0, |v| v.len());
766 Ok(removed)
767 }
768
769 pub fn clear_agent_memory(&self, agent_id: &AgentId) -> Result<(), AgentRuntimeError> {
773 let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::clear_agent_memory");
774 inner.items.remove(agent_id);
775 Ok(())
776 }
777
778 pub fn export_agent_memory(&self, agent_id: &AgentId) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
782 let inner = recover_lock(self.inner.lock(), "EpisodicStore::export_agent_memory");
783 Ok(inner.items.get(agent_id).cloned().unwrap_or_default())
784 }
785
786 pub fn import_agent_memory(&self, agent_id: &AgentId, items: Vec<MemoryItem>) -> Result<(), AgentRuntimeError> {
790 let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::import_agent_memory");
791 inner.items.insert(agent_id.clone(), items);
792 Ok(())
793 }
794
795 #[doc(hidden)]
800 pub fn bump_recall_count_by_content(&self, content: &str, amount: u64) {
801 let mut inner = recover_lock(
802 self.inner.lock(),
803 "EpisodicStore::bump_recall_count_by_content",
804 );
805 for agent_items in inner.items.values_mut() {
806 for item in agent_items.iter_mut() {
807 if item.content == content {
808 item.recall_count = item.recall_count.saturating_add(amount);
809 }
810 }
811 }
812 }
813}
814
815impl Default for EpisodicStore {
816 fn default() -> Self {
817 Self::new()
818 }
819}
820
821#[derive(Debug, Clone)]
830pub struct SemanticStore {
831 inner: Arc<Mutex<SemanticInner>>,
832}
833
834#[derive(Debug)]
835struct SemanticInner {
836 entries: Vec<SemanticEntry>,
837 expected_dim: Option<usize>,
838}
839
840#[derive(Debug, Clone)]
841struct SemanticEntry {
842 key: String,
843 value: String,
844 tags: Vec<String>,
845 embedding: Option<Vec<f32>>,
846}
847
848impl SemanticStore {
849 pub fn new() -> Self {
851 Self {
852 inner: Arc::new(Mutex::new(SemanticInner {
853 entries: Vec::new(),
854 expected_dim: None,
855 })),
856 }
857 }
858
859 #[tracing::instrument(skip(self))]
861 pub fn store(
862 &self,
863 key: impl Into<String> + std::fmt::Debug,
864 value: impl Into<String> + std::fmt::Debug,
865 tags: Vec<String>,
866 ) -> Result<(), AgentRuntimeError> {
867 let mut inner = recover_lock(self.inner.lock(), "SemanticStore::store");
868 inner.entries.push(SemanticEntry {
869 key: key.into(),
870 value: value.into(),
871 tags,
872 embedding: None,
873 });
874 Ok(())
875 }
876
877 #[tracing::instrument(skip(self))]
882 pub fn store_with_embedding(
883 &self,
884 key: impl Into<String> + std::fmt::Debug,
885 value: impl Into<String> + std::fmt::Debug,
886 tags: Vec<String>,
887 embedding: Vec<f32>,
888 ) -> Result<(), AgentRuntimeError> {
889 if embedding.is_empty() {
890 return Err(AgentRuntimeError::Memory(
891 "embedding vector must not be empty".into(),
892 ));
893 }
894 let mut inner = recover_lock(self.inner.lock(), "SemanticStore::store_with_embedding");
895 if let Some(expected) = inner.expected_dim {
897 if expected != embedding.len() {
898 return Err(AgentRuntimeError::Memory(format!(
899 "embedding dimension mismatch: expected {expected}, got {}",
900 embedding.len()
901 )));
902 }
903 } else {
904 inner.expected_dim = Some(embedding.len());
905 }
906 inner.entries.push(SemanticEntry {
907 key: key.into(),
908 value: value.into(),
909 tags,
910 embedding: Some(embedding),
911 });
912 Ok(())
913 }
914
915 #[tracing::instrument(skip(self))]
919 pub fn retrieve(&self, tags: &[&str]) -> Result<Vec<(String, String)>, AgentRuntimeError> {
920 let inner = recover_lock(self.inner.lock(), "SemanticStore::retrieve");
921
922 let results = inner
923 .entries
924 .iter()
925 .filter(|entry| {
926 tags.iter()
927 .all(|t| entry.tags.iter().any(|et| et.as_str() == *t))
928 })
929 .map(|e| (e.key.clone(), e.value.clone()))
930 .collect();
931
932 Ok(results)
933 }
934
935 #[tracing::instrument(skip(self, query_embedding))]
945 pub fn retrieve_similar(
946 &self,
947 query_embedding: &[f32],
948 top_k: usize,
949 ) -> Result<Vec<(String, String, f32)>, AgentRuntimeError> {
950 let inner = recover_lock(self.inner.lock(), "SemanticStore::retrieve_similar");
951
952 if let Some(expected) = inner.expected_dim {
954 if expected != query_embedding.len() {
955 return Err(AgentRuntimeError::Memory(format!(
956 "query embedding dimension mismatch: expected {expected}, got {}",
957 query_embedding.len()
958 )));
959 }
960 }
961
962 let mut scored: Vec<(String, String, f32)> = inner
963 .entries
964 .iter()
965 .filter_map(|entry| {
966 entry.embedding.as_ref().map(|emb| {
967 let sim = cosine_similarity(query_embedding, emb);
968 (entry.key.clone(), entry.value.clone(), sim)
969 })
970 })
971 .collect();
972
973 scored.sort_by(|a, b| b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal));
974 scored.truncate(top_k);
975 Ok(scored)
976 }
977
978 pub fn len(&self) -> Result<usize, AgentRuntimeError> {
980 let inner = recover_lock(self.inner.lock(), "SemanticStore::len");
981 Ok(inner.entries.len())
982 }
983
984 pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
986 Ok(self.len()? == 0)
987 }
988}
989
990impl Default for SemanticStore {
991 fn default() -> Self {
992 Self::new()
993 }
994}
995
996#[derive(Debug, Clone)]
1007pub struct WorkingMemory {
1008 capacity: usize,
1009 inner: Arc<Mutex<WorkingInner>>,
1010}
1011
1012#[derive(Debug)]
1013struct WorkingInner {
1014 map: HashMap<String, String>,
1015 order: VecDeque<String>,
1016}
1017
1018impl WorkingMemory {
1019 pub fn new(capacity: usize) -> Result<Self, AgentRuntimeError> {
1025 if capacity == 0 {
1026 return Err(AgentRuntimeError::Memory(
1027 "WorkingMemory capacity must be > 0".into(),
1028 ));
1029 }
1030 Ok(Self {
1031 capacity,
1032 inner: Arc::new(Mutex::new(WorkingInner {
1033 map: HashMap::new(),
1034 order: VecDeque::new(),
1035 })),
1036 })
1037 }
1038
1039 #[tracing::instrument(skip(self))]
1041 pub fn set(
1042 &self,
1043 key: impl Into<String> + std::fmt::Debug,
1044 value: impl Into<String> + std::fmt::Debug,
1045 ) -> Result<(), AgentRuntimeError> {
1046 let key = key.into();
1047 let value = value.into();
1048 let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::set");
1049
1050 if inner.map.contains_key(&key) {
1052 inner.order.retain(|k| k != &key);
1053 } else if inner.map.len() >= self.capacity {
1054 if let Some(oldest) = inner.order.pop_front() {
1056 inner.map.remove(&oldest);
1057 }
1058 }
1059
1060 inner.order.push_back(key.clone());
1061 inner.map.insert(key, value);
1062 Ok(())
1063 }
1064
1065 #[tracing::instrument(skip(self))]
1071 pub fn get(&self, key: &str) -> Result<Option<String>, AgentRuntimeError> {
1072 let inner = recover_lock(self.inner.lock(), "WorkingMemory::get");
1073 Ok(inner.map.get(key).cloned())
1074 }
1075
1076 pub fn clear(&self) -> Result<(), AgentRuntimeError> {
1078 let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::clear");
1079 inner.map.clear();
1080 inner.order.clear();
1081 Ok(())
1082 }
1083
1084 pub fn len(&self) -> Result<usize, AgentRuntimeError> {
1086 let inner = recover_lock(self.inner.lock(), "WorkingMemory::len");
1087 Ok(inner.map.len())
1088 }
1089
1090 pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
1092 Ok(self.len()? == 0)
1093 }
1094
1095 pub fn iter(&self) -> Result<Vec<(String, String)>, AgentRuntimeError> {
1102 self.entries()
1103 }
1104
1105 pub fn entries(&self) -> Result<Vec<(String, String)>, AgentRuntimeError> {
1107 let inner = recover_lock(self.inner.lock(), "WorkingMemory::entries");
1108 let entries = inner
1109 .order
1110 .iter()
1111 .filter_map(|k| inner.map.get(k).map(|v| (k.clone(), v.clone())))
1112 .collect();
1113 Ok(entries)
1114 }
1115}
1116
1117#[cfg(test)]
1120mod tests {
1121 use super::*;
1122
1123 #[test]
1126 fn test_agent_id_new_stores_string() {
1127 let id = AgentId::new("agent-1");
1128 assert_eq!(id.0, "agent-1");
1129 }
1130
1131 #[test]
1132 fn test_agent_id_random_is_unique() {
1133 let a = AgentId::random();
1134 let b = AgentId::random();
1135 assert_ne!(a, b);
1136 }
1137
1138 #[test]
1139 fn test_memory_id_new_stores_string() {
1140 let id = MemoryId::new("mem-1");
1141 assert_eq!(id.0, "mem-1");
1142 }
1143
1144 #[test]
1145 fn test_memory_id_random_is_unique() {
1146 let a = MemoryId::random();
1147 let b = MemoryId::random();
1148 assert_ne!(a, b);
1149 }
1150
1151 #[test]
1154 fn test_memory_item_new_clamps_importance_above_one() {
1155 let item = MemoryItem::new(AgentId::new("a"), "test", 1.5, vec![]);
1156 assert_eq!(item.importance, 1.0);
1157 }
1158
1159 #[test]
1160 fn test_memory_item_new_clamps_importance_below_zero() {
1161 let item = MemoryItem::new(AgentId::new("a"), "test", -0.5, vec![]);
1162 assert_eq!(item.importance, 0.0);
1163 }
1164
1165 #[test]
1166 fn test_memory_item_new_preserves_valid_importance() {
1167 let item = MemoryItem::new(AgentId::new("a"), "test", 0.7, vec![]);
1168 assert!((item.importance - 0.7).abs() < 1e-6);
1169 }
1170
1171 #[test]
1174 fn test_decay_policy_rejects_zero_half_life() {
1175 assert!(DecayPolicy::exponential(0.0).is_err());
1176 }
1177
1178 #[test]
1179 fn test_decay_policy_rejects_negative_half_life() {
1180 assert!(DecayPolicy::exponential(-1.0).is_err());
1181 }
1182
1183 #[test]
1184 fn test_decay_policy_no_decay_at_age_zero() {
1185 let p = DecayPolicy::exponential(24.0).unwrap();
1186 let decayed = p.apply(1.0, 0.0);
1187 assert!((decayed - 1.0).abs() < 1e-5);
1188 }
1189
1190 #[test]
1191 fn test_decay_policy_half_importance_at_half_life() {
1192 let p = DecayPolicy::exponential(24.0).unwrap();
1193 let decayed = p.apply(1.0, 24.0);
1194 assert!((decayed - 0.5).abs() < 1e-5);
1195 }
1196
1197 #[test]
1198 fn test_decay_policy_quarter_importance_at_two_half_lives() {
1199 let p = DecayPolicy::exponential(24.0).unwrap();
1200 let decayed = p.apply(1.0, 48.0);
1201 assert!((decayed - 0.25).abs() < 1e-5);
1202 }
1203
1204 #[test]
1205 fn test_decay_policy_result_is_clamped_to_zero_one() {
1206 let p = DecayPolicy::exponential(1.0).unwrap();
1207 let decayed = p.apply(0.0, 1000.0);
1208 assert!(decayed >= 0.0 && decayed <= 1.0);
1209 }
1210
1211 #[test]
1214 fn test_episodic_store_add_episode_returns_id() {
1215 let store = EpisodicStore::new();
1216 let id = store.add_episode(AgentId::new("a"), "event", 0.8).unwrap();
1217 assert!(!id.0.is_empty());
1218 }
1219
1220 #[test]
1221 fn test_episodic_store_recall_returns_stored_item() {
1222 let store = EpisodicStore::new();
1223 let agent = AgentId::new("agent-1");
1224 store
1225 .add_episode(agent.clone(), "hello world", 0.9)
1226 .unwrap();
1227 let items = store.recall(&agent, 10).unwrap();
1228 assert_eq!(items.len(), 1);
1229 assert_eq!(items[0].content, "hello world");
1230 }
1231
1232 #[test]
1233 fn test_episodic_store_recall_filters_by_agent() {
1234 let store = EpisodicStore::new();
1235 let a = AgentId::new("agent-a");
1236 let b = AgentId::new("agent-b");
1237 store.add_episode(a.clone(), "for a", 0.5).unwrap();
1238 store.add_episode(b.clone(), "for b", 0.5).unwrap();
1239 let items = store.recall(&a, 10).unwrap();
1240 assert_eq!(items.len(), 1);
1241 assert_eq!(items[0].content, "for a");
1242 }
1243
1244 #[test]
1245 fn test_episodic_store_recall_sorted_by_descending_importance() {
1246 let store = EpisodicStore::new();
1247 let agent = AgentId::new("agent-1");
1248 store.add_episode(agent.clone(), "low", 0.1).unwrap();
1249 store.add_episode(agent.clone(), "high", 0.9).unwrap();
1250 store.add_episode(agent.clone(), "mid", 0.5).unwrap();
1251 let items = store.recall(&agent, 10).unwrap();
1252 assert_eq!(items[0].content, "high");
1253 assert_eq!(items[1].content, "mid");
1254 assert_eq!(items[2].content, "low");
1255 }
1256
1257 #[test]
1258 fn test_episodic_store_recall_respects_limit() {
1259 let store = EpisodicStore::new();
1260 let agent = AgentId::new("agent-1");
1261 for i in 0..5 {
1262 store
1263 .add_episode(agent.clone(), format!("item {i}"), 0.5)
1264 .unwrap();
1265 }
1266 let items = store.recall(&agent, 3).unwrap();
1267 assert_eq!(items.len(), 3);
1268 }
1269
1270 #[test]
1271 fn test_episodic_store_len_tracks_insertions() {
1272 let store = EpisodicStore::new();
1273 let agent = AgentId::new("a");
1274 store.add_episode(agent.clone(), "a", 0.5).unwrap();
1275 store.add_episode(agent.clone(), "b", 0.5).unwrap();
1276 assert_eq!(store.len().unwrap(), 2);
1277 }
1278
1279 #[test]
1280 fn test_episodic_store_is_empty_initially() {
1281 let store = EpisodicStore::new();
1282 assert!(store.is_empty().unwrap());
1283 }
1284
1285 #[test]
1286 fn test_episodic_store_with_decay_reduces_importance() {
1287 let policy = DecayPolicy::exponential(0.001).unwrap(); let store = EpisodicStore::with_decay(policy);
1289 let agent = AgentId::new("a");
1290
1291 let old_ts = Utc::now() - chrono::Duration::hours(1);
1293 store
1294 .add_episode_at(agent.clone(), "old event", 1.0, old_ts)
1295 .unwrap();
1296
1297 let items = store.recall(&agent, 10).unwrap();
1298 assert_eq!(items.len(), 1);
1300 assert!(
1301 items[0].importance < 0.01,
1302 "expected near-zero importance, got {}",
1303 items[0].importance
1304 );
1305 }
1306
1307 #[test]
1310 fn test_max_age_rejects_zero() {
1311 assert!(EpisodicStore::with_max_age(0.0).is_err());
1312 }
1313
1314 #[test]
1315 fn test_max_age_rejects_negative() {
1316 assert!(EpisodicStore::with_max_age(-1.0).is_err());
1317 }
1318
1319 #[test]
1320 fn test_max_age_evicts_old_items_on_recall() {
1321 let store = EpisodicStore::with_max_age(0.001).unwrap();
1323 let agent = AgentId::new("a");
1324
1325 let old_ts = Utc::now() - chrono::Duration::hours(1);
1326 store
1327 .add_episode_at(agent.clone(), "old", 0.9, old_ts)
1328 .unwrap();
1329 store.add_episode(agent.clone(), "new", 0.5).unwrap();
1330
1331 let items = store.recall(&agent, 10).unwrap();
1332 assert_eq!(items.len(), 1, "old item should be evicted by max_age");
1333 assert_eq!(items[0].content, "new");
1334 }
1335
1336 #[test]
1337 fn test_max_age_evicts_old_items_on_add() {
1338 let store = EpisodicStore::with_max_age(0.001).unwrap();
1339 let agent = AgentId::new("a");
1340
1341 let old_ts = Utc::now() - chrono::Duration::hours(1);
1342 store
1343 .add_episode_at(agent.clone(), "old", 0.9, old_ts)
1344 .unwrap();
1345 store.add_episode(agent.clone(), "new", 0.5).unwrap();
1347
1348 assert_eq!(store.len().unwrap(), 1);
1349 }
1350
1351 #[test]
1354 fn test_recall_increments_recall_count() {
1355 let store = EpisodicStore::new();
1356 let agent = AgentId::new("agent-rc");
1357 store.add_episode(agent.clone(), "memory", 0.5).unwrap();
1358
1359 let items = store.recall(&agent, 10).unwrap();
1361 assert_eq!(items[0].recall_count, 1);
1362
1363 let items = store.recall(&agent, 10).unwrap();
1365 assert_eq!(items[0].recall_count, 2);
1366 }
1367
1368 #[test]
1369 fn test_hybrid_recall_policy_prefers_recently_used() {
1370 let store = EpisodicStore::with_recall_policy(RecallPolicy::Hybrid {
1371 recency_weight: 0.1,
1372 frequency_weight: 2.0,
1373 });
1374 let agent = AgentId::new("agent-hybrid");
1375
1376 let old_ts = Utc::now() - chrono::Duration::hours(48);
1377 store
1378 .add_episode_at(agent.clone(), "old_frequent", 0.5, old_ts)
1379 .unwrap();
1380 store.add_episode(agent.clone(), "new_never", 0.5).unwrap();
1381
1382 store.bump_recall_count_by_content("old_frequent", 100);
1384
1385 let items = store.recall(&agent, 10).unwrap();
1386 assert_eq!(items.len(), 2);
1387 assert_eq!(
1388 items[0].content, "old_frequent",
1389 "hybrid policy should rank the frequently-recalled item first"
1390 );
1391 }
1392
1393 #[test]
1394 fn test_per_agent_capacity_evicts_lowest_importance() {
1395 let store = EpisodicStore::with_per_agent_capacity(2);
1396 let agent = AgentId::new("agent-cap");
1397
1398 store.add_episode(agent.clone(), "low", 0.1).unwrap();
1400 store.add_episode(agent.clone(), "high", 0.9).unwrap();
1401 store.add_episode(agent.clone(), "new", 0.5).unwrap();
1404
1405 assert_eq!(
1406 store.len().unwrap(),
1407 2,
1408 "store should hold exactly 2 items after eviction"
1409 );
1410
1411 let items = store.recall(&agent, 10).unwrap();
1412 let contents: Vec<&str> = items.iter().map(|i| i.content.as_str()).collect();
1413 assert!(
1414 !contents.contains(&"low"),
1415 "the pre-existing lowest-importance item should have been evicted; remaining: {:?}",
1416 contents
1417 );
1418 assert!(
1419 contents.contains(&"new"),
1420 "the newly added item must never be evicted; remaining: {:?}",
1421 contents
1422 );
1423 }
1424
1425 #[test]
1428 fn test_many_agents_do_not_see_each_others_memories() {
1429 let store = EpisodicStore::new();
1430 let n_agents = 20usize;
1431 for i in 0..n_agents {
1432 let agent = AgentId::new(format!("agent-{i}"));
1433 for j in 0..5 {
1434 store
1435 .add_episode(agent.clone(), format!("item-{i}-{j}"), 0.5)
1436 .unwrap();
1437 }
1438 }
1439 for i in 0..n_agents {
1441 let agent = AgentId::new(format!("agent-{i}"));
1442 let items = store.recall(&agent, 100).unwrap();
1443 assert_eq!(
1444 items.len(),
1445 5,
1446 "agent {i} should see exactly 5 items, got {}",
1447 items.len()
1448 );
1449 for item in &items {
1450 assert!(
1451 item.content.starts_with(&format!("item-{i}-")),
1452 "agent {i} saw foreign item: {}",
1453 item.content
1454 );
1455 }
1456 }
1457 }
1458
1459 #[test]
1462 fn test_agent_memory_count_returns_zero_for_unknown_agent() {
1463 let store = EpisodicStore::new();
1464 let count = store.agent_memory_count(&AgentId::new("ghost")).unwrap();
1465 assert_eq!(count, 0);
1466 }
1467
1468 #[test]
1469 fn test_agent_memory_count_tracks_insertions() {
1470 let store = EpisodicStore::new();
1471 let agent = AgentId::new("a");
1472 store.add_episode(agent.clone(), "e1", 0.5).unwrap();
1473 store.add_episode(agent.clone(), "e2", 0.5).unwrap();
1474 assert_eq!(store.agent_memory_count(&agent).unwrap(), 2);
1475 }
1476
1477 #[test]
1478 fn test_list_agents_returns_all_known_agents() {
1479 let store = EpisodicStore::new();
1480 let a = AgentId::new("agent-a");
1481 let b = AgentId::new("agent-b");
1482 store.add_episode(a.clone(), "x", 0.5).unwrap();
1483 store.add_episode(b.clone(), "y", 0.5).unwrap();
1484 let agents = store.list_agents().unwrap();
1485 assert_eq!(agents.len(), 2);
1486 assert!(agents.contains(&a));
1487 assert!(agents.contains(&b));
1488 }
1489
1490 #[test]
1491 fn test_list_agents_empty_when_no_episodes() {
1492 let store = EpisodicStore::new();
1493 let agents = store.list_agents().unwrap();
1494 assert!(agents.is_empty());
1495 }
1496
1497 #[test]
1498 fn test_purge_agent_memories_removes_all_for_agent() {
1499 let store = EpisodicStore::new();
1500 let a = AgentId::new("a");
1501 let b = AgentId::new("b");
1502 store.add_episode(a.clone(), "ep1", 0.5).unwrap();
1503 store.add_episode(a.clone(), "ep2", 0.5).unwrap();
1504 store.add_episode(b.clone(), "ep-b", 0.5).unwrap();
1505
1506 let removed = store.purge_agent_memories(&a).unwrap();
1507 assert_eq!(removed, 2);
1508 assert_eq!(store.agent_memory_count(&a).unwrap(), 0);
1509 assert_eq!(store.agent_memory_count(&b).unwrap(), 1);
1510 assert_eq!(store.len().unwrap(), 1);
1511 }
1512
1513 #[test]
1514 fn test_purge_agent_memories_returns_zero_for_unknown_agent() {
1515 let store = EpisodicStore::new();
1516 let removed = store.purge_agent_memories(&AgentId::new("ghost")).unwrap();
1517 assert_eq!(removed, 0);
1518 }
1519
1520 #[test]
1523 fn test_recall_returns_empty_when_all_items_are_stale() {
1524 let store = EpisodicStore::with_max_age(0.001).unwrap();
1526 let agent = AgentId::new("stale-agent");
1527
1528 let old_ts = Utc::now() - chrono::Duration::hours(1);
1529 store
1530 .add_episode_at(agent.clone(), "stale-1", 0.9, old_ts)
1531 .unwrap();
1532 store
1533 .add_episode_at(agent.clone(), "stale-2", 0.7, old_ts)
1534 .unwrap();
1535
1536 let items = store.recall(&agent, 100).unwrap();
1537 assert!(
1538 items.is_empty(),
1539 "all stale items should be evicted on recall, got {}",
1540 items.len()
1541 );
1542 }
1543
1544 #[test]
1547 fn test_concurrent_add_and_recall_are_consistent() {
1548 use std::sync::Arc;
1549 use std::thread;
1550
1551 let store = Arc::new(EpisodicStore::new());
1552 let agent = AgentId::new("concurrent-agent");
1553 let n_threads = 8;
1554 let items_per_thread = 25;
1555
1556 let mut handles = Vec::new();
1558 for t in 0..n_threads {
1559 let s = Arc::clone(&store);
1560 let a = agent.clone();
1561 handles.push(thread::spawn(move || {
1562 for i in 0..items_per_thread {
1563 s.add_episode(a.clone(), format!("t{t}-i{i}"), 0.5).unwrap();
1564 }
1565 }));
1566 }
1567 for h in handles {
1568 h.join().unwrap();
1569 }
1570
1571 let mut read_handles = Vec::new();
1573 for _ in 0..n_threads {
1574 let s = Arc::clone(&store);
1575 let a = agent.clone();
1576 read_handles.push(thread::spawn(move || {
1577 let items = s.recall(&a, 1000).unwrap();
1578 assert!(items.len() <= n_threads * items_per_thread);
1579 }));
1580 }
1581 for h in read_handles {
1582 h.join().unwrap();
1583 }
1584 }
1585
1586 #[test]
1589 fn test_concurrent_capacity_eviction_never_exceeds_cap() {
1590 use std::sync::Arc;
1591 use std::thread;
1592
1593 let cap = 5usize;
1594 let store = Arc::new(EpisodicStore::with_per_agent_capacity(cap));
1595 let agent = AgentId::new("cap-agent");
1596 let n_threads = 8;
1597 let items_per_thread = 10;
1598
1599 let mut handles = Vec::new();
1600 for t in 0..n_threads {
1601 let s = Arc::clone(&store);
1602 let a = agent.clone();
1603 handles.push(thread::spawn(move || {
1604 for i in 0..items_per_thread {
1605 let importance = (t * items_per_thread + i) as f32 / 100.0;
1606 s.add_episode(a.clone(), format!("t{t}-i{i}"), importance)
1607 .unwrap();
1608 }
1609 }));
1610 }
1611 for h in handles {
1612 h.join().unwrap();
1613 }
1614
1615 let count = store.agent_memory_count(&agent).unwrap();
1620 assert!(
1621 count <= cap + n_threads,
1622 "expected at most {} items, got {}",
1623 cap + n_threads,
1624 count
1625 );
1626 }
1627
1628 #[test]
1631 fn test_semantic_store_store_and_retrieve_all() {
1632 let store = SemanticStore::new();
1633 store.store("key1", "value1", vec!["tag-a".into()]).unwrap();
1634 store.store("key2", "value2", vec!["tag-b".into()]).unwrap();
1635 let results = store.retrieve(&[]).unwrap();
1636 assert_eq!(results.len(), 2);
1637 }
1638
1639 #[test]
1640 fn test_semantic_store_retrieve_filters_by_tag() {
1641 let store = SemanticStore::new();
1642 store
1643 .store("k1", "v1", vec!["rust".into(), "async".into()])
1644 .unwrap();
1645 store.store("k2", "v2", vec!["rust".into()]).unwrap();
1646 let results = store.retrieve(&["async"]).unwrap();
1647 assert_eq!(results.len(), 1);
1648 assert_eq!(results[0].0, "k1");
1649 }
1650
1651 #[test]
1652 fn test_semantic_store_retrieve_requires_all_tags() {
1653 let store = SemanticStore::new();
1654 store
1655 .store("k1", "v1", vec!["a".into(), "b".into()])
1656 .unwrap();
1657 store.store("k2", "v2", vec!["a".into()]).unwrap();
1658 let results = store.retrieve(&["a", "b"]).unwrap();
1659 assert_eq!(results.len(), 1);
1660 }
1661
1662 #[test]
1663 fn test_semantic_store_is_empty_initially() {
1664 let store = SemanticStore::new();
1665 assert!(store.is_empty().unwrap());
1666 }
1667
1668 #[test]
1669 fn test_semantic_store_len_tracks_insertions() {
1670 let store = SemanticStore::new();
1671 store.store("k", "v", vec![]).unwrap();
1672 assert_eq!(store.len().unwrap(), 1);
1673 }
1674
1675 #[test]
1676 fn test_semantic_store_empty_embedding_is_rejected() {
1677 let store = SemanticStore::new();
1678 let result = store.store_with_embedding("k", "v", vec![], vec![]);
1679 assert!(result.is_err(), "empty embedding should be rejected");
1680 }
1681
1682 #[test]
1683 fn test_semantic_store_dimension_mismatch_is_rejected() {
1684 let store = SemanticStore::new();
1685 store
1686 .store_with_embedding("k1", "v1", vec![], vec![1.0, 0.0])
1687 .unwrap();
1688 let result = store.store_with_embedding("k2", "v2", vec![], vec![1.0, 0.0, 0.0]);
1690 assert!(
1691 result.is_err(),
1692 "embedding dimension mismatch should be rejected"
1693 );
1694 }
1695
1696 #[test]
1697 fn test_semantic_store_retrieve_similar_returns_closest() {
1698 let store = SemanticStore::new();
1699 store
1700 .store_with_embedding("close", "close value", vec![], vec![1.0, 0.0, 0.0])
1701 .unwrap();
1702 store
1703 .store_with_embedding("far", "far value", vec![], vec![0.0, 1.0, 0.0])
1704 .unwrap();
1705
1706 let query = vec![1.0, 0.0, 0.0];
1707 let results = store.retrieve_similar(&query, 2).unwrap();
1708 assert_eq!(results.len(), 2);
1709 assert_eq!(results[0].0, "close");
1710 assert!(
1711 (results[0].2 - 1.0).abs() < 1e-5,
1712 "expected similarity ~1.0, got {}",
1713 results[0].2
1714 );
1715 assert!(
1716 (results[1].2).abs() < 1e-5,
1717 "expected similarity ~0.0, got {}",
1718 results[1].2
1719 );
1720 }
1721
1722 #[test]
1723 fn test_semantic_store_retrieve_similar_ignores_unembedded_entries() {
1724 let store = SemanticStore::new();
1725 store.store("no-emb", "no embedding value", vec![]).unwrap();
1726 store
1727 .store_with_embedding("with-emb", "with embedding value", vec![], vec![1.0, 0.0])
1728 .unwrap();
1729
1730 let query = vec![1.0, 0.0];
1731 let results = store.retrieve_similar(&query, 10).unwrap();
1732 assert_eq!(results.len(), 1, "only the embedded entry should appear");
1733 assert_eq!(results[0].0, "with-emb");
1734 }
1735
1736 #[test]
1737 fn test_cosine_similarity_orthogonal_vectors_return_zero() {
1738 let store = SemanticStore::new();
1739 store
1740 .store_with_embedding("a", "va", vec![], vec![1.0, 0.0])
1741 .unwrap();
1742 store
1743 .store_with_embedding("b", "vb", vec![], vec![0.0, 1.0])
1744 .unwrap();
1745
1746 let query = vec![1.0, 0.0];
1747 let results = store.retrieve_similar(&query, 2).unwrap();
1748 assert_eq!(results.len(), 2);
1749 let b_result = results.iter().find(|(k, _, _)| k == "b").unwrap();
1750 assert!(
1751 b_result.2.abs() < 1e-5,
1752 "expected cosine similarity 0.0 for orthogonal vectors, got {}",
1753 b_result.2
1754 );
1755 }
1756
1757 #[test]
1760 fn test_working_memory_new_rejects_zero_capacity() {
1761 assert!(WorkingMemory::new(0).is_err());
1762 }
1763
1764 #[test]
1765 fn test_working_memory_set_and_get() {
1766 let wm = WorkingMemory::new(10).unwrap();
1767 wm.set("foo", "bar").unwrap();
1768 let val = wm.get("foo").unwrap();
1769 assert_eq!(val, Some("bar".into()));
1770 }
1771
1772 #[test]
1773 fn test_working_memory_get_missing_key_returns_none() {
1774 let wm = WorkingMemory::new(10).unwrap();
1775 assert_eq!(wm.get("missing").unwrap(), None);
1776 }
1777
1778 #[test]
1779 fn test_working_memory_bounded_evicts_oldest() {
1780 let wm = WorkingMemory::new(3).unwrap();
1781 wm.set("k1", "v1").unwrap();
1782 wm.set("k2", "v2").unwrap();
1783 wm.set("k3", "v3").unwrap();
1784 wm.set("k4", "v4").unwrap(); assert_eq!(wm.get("k1").unwrap(), None);
1786 assert_eq!(wm.get("k4").unwrap(), Some("v4".into()));
1787 }
1788
1789 #[test]
1790 fn test_working_memory_update_existing_key_no_eviction() {
1791 let wm = WorkingMemory::new(2).unwrap();
1792 wm.set("k1", "v1").unwrap();
1793 wm.set("k2", "v2").unwrap();
1794 wm.set("k1", "v1-updated").unwrap(); assert_eq!(wm.len().unwrap(), 2);
1796 assert_eq!(wm.get("k1").unwrap(), Some("v1-updated".into()));
1797 assert_eq!(wm.get("k2").unwrap(), Some("v2".into()));
1798 }
1799
1800 #[test]
1801 fn test_working_memory_clear_removes_all() {
1802 let wm = WorkingMemory::new(10).unwrap();
1803 wm.set("a", "1").unwrap();
1804 wm.set("b", "2").unwrap();
1805 wm.clear().unwrap();
1806 assert!(wm.is_empty().unwrap());
1807 }
1808
1809 #[test]
1810 fn test_working_memory_is_empty_initially() {
1811 let wm = WorkingMemory::new(5).unwrap();
1812 assert!(wm.is_empty().unwrap());
1813 }
1814
1815 #[test]
1816 fn test_working_memory_len_tracks_entries() {
1817 let wm = WorkingMemory::new(10).unwrap();
1818 wm.set("a", "1").unwrap();
1819 wm.set("b", "2").unwrap();
1820 assert_eq!(wm.len().unwrap(), 2);
1821 }
1822
1823 #[test]
1824 fn test_working_memory_capacity_never_exceeded() {
1825 let cap = 5usize;
1826 let wm = WorkingMemory::new(cap).unwrap();
1827 for i in 0..20 {
1828 wm.set(format!("key-{i}"), format!("val-{i}")).unwrap();
1829 assert!(wm.len().unwrap() <= cap);
1830 }
1831 }
1832
1833 #[test]
1836 fn test_semantic_dimension_mismatch_on_retrieve_returns_error() {
1837 let store = SemanticStore::new();
1838 store
1839 .store_with_embedding("k1", "v1", vec![], vec![1.0, 0.0, 0.0])
1840 .unwrap();
1841 let result = store.retrieve_similar(&[1.0, 0.0], 10);
1843 assert!(result.is_err(), "dimension mismatch on retrieve should error");
1844 }
1845
1846 #[test]
1851 fn test_clear_agent_memory_removes_all_episodes() {
1852 let store = EpisodicStore::new();
1853 let agent = AgentId::new("a");
1854 store.add_episode(agent.clone(), "ep1", 0.5).unwrap();
1855 store.add_episode(agent.clone(), "ep2", 0.9).unwrap();
1856 store.clear_agent_memory(&agent).unwrap();
1857 let items = store.recall(&agent, 10).unwrap();
1858 assert!(items.is_empty(), "all memories should be cleared");
1859 }
1860
1861 #[test]
1864 fn test_agent_id_as_str() {
1865 let id = AgentId::new("hello");
1866 assert_eq!(id.as_str(), "hello");
1867 }
1868
1869 #[test]
1872 fn test_export_import_agent_memory_round_trip() {
1873 let store = EpisodicStore::new();
1874 let agent = AgentId::new("export-agent");
1875 store.add_episode(agent.clone(), "fact1", 0.8).unwrap();
1876 store.add_episode(agent.clone(), "fact2", 0.6).unwrap();
1877
1878 let exported = store.export_agent_memory(&agent).unwrap();
1879 assert_eq!(exported.len(), 2);
1880
1881 let new_store = EpisodicStore::new();
1882 new_store.import_agent_memory(&agent, exported).unwrap();
1883 let recalled = new_store.recall(&agent, 10).unwrap();
1884 assert_eq!(recalled.len(), 2);
1885 }
1886
1887 #[test]
1890 fn test_working_memory_iter_matches_entries() {
1891 let wm = WorkingMemory::new(10).unwrap();
1892 wm.set("a", "1").unwrap();
1893 wm.set("b", "2").unwrap();
1894 let via_iter = wm.iter().unwrap();
1895 let via_entries = wm.entries().unwrap();
1896 assert_eq!(via_iter, via_entries);
1897 }
1898
1899 #[test]
1902 fn test_agent_id_as_ref_str() {
1903 let id = AgentId::new("ref-test");
1904 let s: &str = id.as_ref();
1905 assert_eq!(s, "ref-test");
1906 }
1907
1908 #[test]
1909 fn test_eviction_policy_oldest_evicts_first_inserted() {
1910 let store = EpisodicStore::with_eviction_policy(EvictionPolicy::Oldest);
1911 let store = {
1917 let inner = EpisodicInner {
1919 items: std::collections::HashMap::new(),
1920 decay: None,
1921 recall_policy: RecallPolicy::Importance,
1922 per_agent_capacity: Some(2),
1923 max_age_hours: None,
1924 eviction_policy: EvictionPolicy::Oldest,
1925 };
1926 EpisodicStore {
1927 inner: std::sync::Arc::new(std::sync::Mutex::new(inner)),
1928 }
1929 };
1930
1931 let agent = AgentId::new("agent");
1932 let t1 = chrono::Utc::now() - chrono::Duration::seconds(100);
1934 let t2 = chrono::Utc::now() - chrono::Duration::seconds(50);
1935 store.add_episode_at(agent.clone(), "oldest", 0.9, t1).unwrap();
1936 store.add_episode_at(agent.clone(), "newer", 0.8, t2).unwrap();
1937 store.add_episode(agent.clone(), "newest", 0.5).unwrap();
1939
1940 let items = store.recall(&agent, 10).unwrap();
1941 assert_eq!(items.len(), 2);
1942 let contents: Vec<&str> = items.iter().map(|i| i.content.as_str()).collect();
1943 assert!(!contents.contains(&"oldest"), "oldest item should have been evicted; got: {contents:?}");
1944 }
1945}