Skip to main content

llm_agent_runtime/
memory.rs

1//! # Module: Memory
2//!
3//! ## Responsibility
4//! Provides episodic, semantic, and working memory stores for agents.
5//! Mirrors the public API of `tokio-agent-memory` and `tokio-memory`.
6//!
7//! ## Guarantees
8//! - Thread-safe: all stores wrap their state in `Arc<Mutex<_>>`
9//! - Bounded: WorkingMemory evicts the oldest entry when capacity is exceeded
10//! - Decaying: DecayPolicy reduces importance scores over time
11//! - Non-panicking: all operations return `Result`
12//! - Lock-poisoning resilient: a panicking thread does not permanently break a store
13//! - O(1) agent lookup: EpisodicStore indexes items per-agent for efficient recall
14//!
15//! ## NOT Responsible For
16//! - Cross-agent shared memory (see runtime.rs coordinator)
17//! - Persistence to disk or external store
18
19use 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};
25
26// Re-export the core ID types so callers can import from either module.
27pub use crate::types::{AgentId, MemoryId};
28
29// ── Cosine similarity ─────────────────────────────────────────────────────────
30
31/// Normalize `v` to unit length in-place.  Does nothing if the norm is below
32/// `f32::EPSILON` (zero or near-zero vector).
33fn normalize_in_place(v: &mut Vec<f32>) {
34    let norm: f32 = v.iter().map(|x| x * x).sum::<f32>().sqrt();
35    if norm > f32::EPSILON {
36        for x in v.iter_mut() {
37            *x /= norm;
38        }
39    }
40}
41
42// ── MemoryItem ────────────────────────────────────────────────────────────────
43
44/// A single memory record stored for an agent.
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct MemoryItem {
47    /// Unique identifier for this memory.
48    pub id: MemoryId,
49    /// The agent this memory belongs to.
50    pub agent_id: AgentId,
51    /// Textual content of the memory.
52    pub content: String,
53    /// Importance score in `[0.0, 1.0]`. Higher = more important.
54    pub importance: f32,
55    /// UTC timestamp when this memory was recorded.
56    pub timestamp: DateTime<Utc>,
57    /// Searchable tags attached to this memory.
58    pub tags: Vec<String>,
59    /// Number of times this memory has been recalled. Updated in-place by `recall`.
60    #[serde(default)]
61    pub recall_count: u64,
62}
63
64impl MemoryItem {
65    /// Construct a new `MemoryItem` with the current timestamp and a random ID.
66    pub fn new(
67        agent_id: AgentId,
68        content: impl Into<String>,
69        importance: f32,
70        tags: Vec<String>,
71    ) -> Self {
72        Self {
73            id: MemoryId::random(),
74            agent_id,
75            content: content.into(),
76            importance: importance.clamp(0.0, 1.0),
77            timestamp: Utc::now(),
78            tags,
79            recall_count: 0,
80        }
81    }
82
83    /// Return the age of this memory in fractional hours since it was recorded.
84    ///
85    /// Returns `0.0` if the current time is somehow before `timestamp`.
86    pub fn age_hours(&self) -> f64 {
87        let now = Utc::now();
88        let elapsed = now.signed_duration_since(self.timestamp);
89        elapsed.num_milliseconds().max(0) as f64 / 3_600_000.0
90    }
91
92    /// Return `true` if this memory item has the given tag.
93    pub fn has_tag(&self, tag: &str) -> bool {
94        self.tags.iter().any(|t| t == tag)
95    }
96
97    /// Return the approximate number of whitespace-separated words in the content.
98    pub fn word_count(&self) -> usize {
99        self.content.split_whitespace().count()
100    }
101}
102
103impl std::fmt::Display for MemoryItem {
104    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
105        write!(
106            f,
107            "[{}] importance={:.2} recalls={} content=\"{}\"",
108            self.id,
109            self.importance,
110            self.recall_count,
111            self.content
112        )
113    }
114}
115
116// ── DecayPolicy ───────────────────────────────────────────────────────────────
117
118/// Governs how memory importance decays over time.
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct DecayPolicy {
121    /// The half-life duration in hours. After this many hours, importance is halved.
122    half_life_hours: f64,
123}
124
125impl DecayPolicy {
126    /// Create an exponential decay policy with the given half-life in hours.
127    ///
128    /// # Arguments
129    /// * `half_life_hours` — time after which importance is halved; must be > 0
130    ///
131    /// # Returns
132    /// - `Ok(DecayPolicy)` — on success
133    /// - `Err(AgentRuntimeError::Memory)` — if `half_life_hours <= 0`
134    pub fn exponential(half_life_hours: f64) -> Result<Self, AgentRuntimeError> {
135        if half_life_hours <= 0.0 {
136            return Err(AgentRuntimeError::Memory(
137                "half_life_hours must be positive".into(),
138            ));
139        }
140        Ok(Self { half_life_hours })
141    }
142
143    /// Apply decay to an importance score based on elapsed time.
144    ///
145    /// # Arguments
146    /// * `importance` — original importance in `[0.0, 1.0]`
147    /// * `age_hours` — how many hours have passed since the memory was recorded
148    ///
149    /// # Returns
150    /// Decayed importance clamped to `[0.0, 1.0]`.
151    pub fn apply(&self, importance: f32, age_hours: f64) -> f32 {
152        let decay = (-age_hours * std::f64::consts::LN_2 / self.half_life_hours).exp();
153        (importance as f64 * decay).clamp(0.0, 1.0) as f32
154    }
155
156    /// Return the configured half-life in hours.
157    pub fn half_life_hours(&self) -> f64 {
158        self.half_life_hours
159    }
160
161    /// Apply decay in-place to a mutable `MemoryItem`.
162    pub fn decay_item(&self, item: &mut MemoryItem) {
163        let age_hours = (Utc::now() - item.timestamp).num_seconds().max(0) as f64 / 3600.0;
164        item.importance = self.apply(item.importance, age_hours);
165    }
166}
167
168// ── RecallPolicy ──────────────────────────────────────────────────────────────
169
170/// Controls how memories are scored and ranked during recall.
171///
172/// # Interaction with `DecayPolicy`
173///
174/// When both a `DecayPolicy` and a `RecallPolicy` are configured, decay is
175/// applied **before** scoring.  This means that for `RecallPolicy::Importance`,
176/// an old high-importance memory may rank lower than a fresh low-importance
177/// memory after decay has reduced its score.
178///
179/// For `RecallPolicy::Hybrid`, the `recency_weight` term already captures
180/// temporal distance; combining it with a `DecayPolicy` therefore applies a
181/// *double* time penalty — set one or the other, not both, unless the double
182/// penalty is intentional.
183///
184/// ## Score Calculation Example
185///
186/// Given two memories, each with `importance = 0.5`:
187/// - Memory A: `recall_count = 0`, inserted 1 hour ago
188/// - Memory B: `recall_count = 10`, inserted 10 hours ago
189///
190/// With `recency_weight = 1.0` and `frequency_weight = 0.1`:
191/// - Score A = `0.5 + 1.0 × 1.0 + 0.1 × 0` = `1.5` (recency wins)
192/// - Score B = `0.5 + 1.0 × (−10.0) + 0.1 × 10` = `−8.5` (old → ranked lower)
193///
194/// Note: the recency term uses negative hours-since-creation so older items score lower.
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub enum RecallPolicy {
197    /// Rank purely by importance score (default).
198    Importance,
199    /// Hybrid score: blends importance, recency, and recall frequency.
200    ///
201    /// `score = importance + recency_score * recency_weight + frequency_score * frequency_weight`
202    /// where `recency_score = exp(-age_hours / 24.0)` and
203    /// `frequency_score = recall_count / (max_recall_count + 1)` (normalized).
204    Hybrid {
205        /// Weight applied to the recency component of the hybrid score.
206        recency_weight: f32,
207        /// Weight applied to the recall-frequency component of the hybrid score.
208        frequency_weight: f32,
209    },
210}
211
212impl Default for RecallPolicy {
213    fn default() -> Self {
214        RecallPolicy::Importance
215    }
216}
217
218// ── Hybrid scoring helper ─────────────────────────────────────────────────────
219
220fn compute_hybrid_score(
221    item: &MemoryItem,
222    recency_weight: f32,
223    frequency_weight: f32,
224    max_recall: u64,
225    now: chrono::DateTime<Utc>,
226) -> f32 {
227    let age_hours = (now - item.timestamp).num_seconds().max(0) as f64 / 3600.0;
228    let recency_score = (-age_hours / 24.0).exp() as f32;
229    let frequency_score = item.recall_count as f32 / (max_recall as f32 + 1.0);
230    item.importance + recency_score * recency_weight + frequency_score * frequency_weight
231}
232
233// ── EvictionPolicy ────────────────────────────────────────────────────────────
234
235/// Policy controlling which item is evicted when the per-agent capacity is exceeded.
236#[derive(Debug, Clone, PartialEq, Eq, Default)]
237pub enum EvictionPolicy {
238    /// Evict the item with the lowest importance score (default).
239    #[default]
240    LowestImportance,
241    /// Evict the oldest item (by insertion order / timestamp).
242    Oldest,
243}
244
245// ── EpisodicStoreBuilder ──────────────────────────────────────────────────────
246
247/// Fluent builder for [`EpisodicStore`].
248///
249/// Allows combining any set of options — decay, recall policy, per-agent
250/// capacity, max age, and eviction policy — before creating the store.
251///
252/// # Example
253/// ```rust
254/// use llm_agent_runtime::memory::{EpisodicStore, EvictionPolicy, RecallPolicy, DecayPolicy};
255///
256/// let store = EpisodicStore::builder()
257///     .per_agent_capacity(50)
258///     .eviction_policy(EvictionPolicy::Oldest)
259///     .build();
260/// ```
261#[derive(Default)]
262pub struct EpisodicStoreBuilder {
263    decay: Option<DecayPolicy>,
264    recall_policy: Option<RecallPolicy>,
265    per_agent_capacity: Option<usize>,
266    max_age_hours: Option<f64>,
267    eviction_policy: Option<EvictionPolicy>,
268}
269
270impl EpisodicStoreBuilder {
271    /// Set the decay policy.
272    pub fn decay(mut self, policy: DecayPolicy) -> Self {
273        self.decay = Some(policy);
274        self
275    }
276
277    /// Set the recall policy.
278    pub fn recall_policy(mut self, policy: RecallPolicy) -> Self {
279        self.recall_policy = Some(policy);
280        self
281    }
282
283    /// Set the per-agent capacity. Panics if `capacity == 0`.
284    pub fn per_agent_capacity(mut self, capacity: usize) -> Self {
285        assert!(capacity > 0, "per_agent_capacity must be > 0");
286        self.per_agent_capacity = Some(capacity);
287        self
288    }
289
290    /// Set the per-agent capacity without panicking.
291    ///
292    /// Returns `Err` if `capacity == 0`. Prefer this over [`per_agent_capacity`]
293    /// in library/user-facing code where a misconfigured capacity should be
294    /// handled gracefully rather than aborting the process.
295    ///
296    /// [`per_agent_capacity`]: EpisodicStoreBuilder::per_agent_capacity
297    pub fn try_per_agent_capacity(
298        mut self,
299        capacity: usize,
300    ) -> Result<Self, crate::error::AgentRuntimeError> {
301        if capacity == 0 {
302            return Err(crate::error::AgentRuntimeError::Memory(
303                "per_agent_capacity must be > 0".into(),
304            ));
305        }
306        self.per_agent_capacity = Some(capacity);
307        Ok(self)
308    }
309
310    /// Set the maximum memory age in hours. Returns `Err` if `max_age_hours <= 0`.
311    pub fn max_age_hours(mut self, hours: f64) -> Result<Self, crate::error::AgentRuntimeError> {
312        if hours <= 0.0 {
313            return Err(crate::error::AgentRuntimeError::Memory(
314                "max_age_hours must be positive".into(),
315            ));
316        }
317        self.max_age_hours = Some(hours);
318        Ok(self)
319    }
320
321    /// Set the eviction policy.
322    pub fn eviction_policy(mut self, policy: EvictionPolicy) -> Self {
323        self.eviction_policy = Some(policy);
324        self
325    }
326
327    /// Consume the builder and create an [`EpisodicStore`].
328    pub fn build(self) -> EpisodicStore {
329        // Warn when both a DecayPolicy and RecallPolicy::Hybrid are active:
330        // decay is applied before scoring, so Hybrid's recency term produces a
331        // double time penalty.  Set one or the other unless the double penalty
332        // is intentional.
333        if self.decay.is_some() {
334            if let Some(RecallPolicy::Hybrid { .. }) = &self.recall_policy {
335                tracing::warn!(
336                    "EpisodicStore configured with both DecayPolicy and RecallPolicy::Hybrid \
337                     — time-based decay is applied before hybrid scoring, resulting in a \
338                     double time penalty.  Set one or the other unless this is intentional."
339                );
340            }
341        }
342        EpisodicStore {
343            inner: Arc::new(Mutex::new(EpisodicInner {
344                items: HashMap::new(),
345                decay: self.decay,
346                recall_policy: self.recall_policy.unwrap_or(RecallPolicy::Importance),
347                per_agent_capacity: self.per_agent_capacity,
348                max_age_hours: self.max_age_hours,
349                eviction_policy: self.eviction_policy.unwrap_or_default(),
350            })),
351        }
352    }
353}
354
355// ── EpisodicStore ─────────────────────────────────────────────────────────────
356
357/// Stores episodic (event-based) memories for agents, ordered by insertion time.
358///
359/// ## Guarantees
360/// - Thread-safe via `Arc<Mutex<_>>`
361/// - Ordered: recall returns items in descending importance order
362/// - Bounded by optional per-agent capacity
363/// - O(1) agent lookup via per-agent `HashMap` index
364/// - Automatic expiry via optional `max_age_hours`
365#[derive(Debug, Clone)]
366pub struct EpisodicStore {
367    inner: Arc<Mutex<EpisodicInner>>,
368}
369
370#[derive(Debug)]
371struct EpisodicInner {
372    /// Items stored per-agent for O(1) lookup. The key is the agent ID.
373    items: HashMap<AgentId, Vec<MemoryItem>>,
374    decay: Option<DecayPolicy>,
375    recall_policy: RecallPolicy,
376    /// Maximum items stored per agent. Oldest (lowest-importance) items evicted when exceeded.
377    per_agent_capacity: Option<usize>,
378    /// Maximum age in hours. Items older than this are purged on the next recall or add.
379    max_age_hours: Option<f64>,
380    /// Eviction policy when per_agent_capacity is exceeded.
381    eviction_policy: EvictionPolicy,
382}
383
384impl EpisodicInner {
385    /// Purge items for `agent_id` that exceed `max_age_hours`, if configured.
386    fn purge_stale(&mut self, agent_id: &AgentId) {
387        if let Some(max_age_h) = self.max_age_hours {
388            let cutoff = Utc::now()
389                - chrono::Duration::seconds((max_age_h * 3600.0) as i64);
390            if let Some(agent_items) = self.items.get_mut(agent_id) {
391                agent_items.retain(|i| i.timestamp >= cutoff);
392            }
393        }
394    }
395}
396
397/// Evict one item from `agent_items` if `len > cap`, according to `policy`.
398///
399/// The last element (the just-inserted item) is excluded from the
400/// `LowestImportance` scan so that newly added items are never evicted.
401fn evict_if_over_capacity(
402    agent_items: &mut Vec<MemoryItem>,
403    cap: usize,
404    policy: &EvictionPolicy,
405) {
406    if agent_items.len() <= cap {
407        return;
408    }
409    let pos = match policy {
410        EvictionPolicy::LowestImportance => {
411            let len = agent_items.len();
412            agent_items[..len - 1]
413                .iter()
414                .enumerate()
415                .min_by(|(_, a), (_, b)| {
416                    a.importance
417                        .partial_cmp(&b.importance)
418                        .unwrap_or(std::cmp::Ordering::Equal)
419                })
420                .map(|(pos, _)| pos)
421        }
422        EvictionPolicy::Oldest => {
423            let len = agent_items.len();
424            agent_items[..len - 1]
425                .iter()
426                .enumerate()
427                .min_by_key(|(_, item)| item.timestamp)
428                .map(|(pos, _)| pos)
429        }
430    };
431    if let Some(pos) = pos {
432        agent_items.remove(pos);
433    }
434}
435
436impl EpisodicStore {
437    /// Create a new unbounded episodic store without decay.
438    pub fn new() -> Self {
439        Self {
440            inner: Arc::new(Mutex::new(EpisodicInner {
441                items: HashMap::new(),
442                decay: None,
443                recall_policy: RecallPolicy::Importance,
444                per_agent_capacity: None,
445                max_age_hours: None,
446                eviction_policy: EvictionPolicy::LowestImportance,
447            })),
448        }
449    }
450
451    /// Return a fluent builder to construct an `EpisodicStore` with any combination of options.
452    pub fn builder() -> EpisodicStoreBuilder {
453        EpisodicStoreBuilder::default()
454    }
455
456    /// Create a new episodic store with the given decay policy.
457    pub fn with_decay(policy: DecayPolicy) -> Self {
458        Self {
459            inner: Arc::new(Mutex::new(EpisodicInner {
460                items: HashMap::new(),
461                decay: Some(policy),
462                recall_policy: RecallPolicy::Importance,
463                per_agent_capacity: None,
464                max_age_hours: None,
465                eviction_policy: EvictionPolicy::LowestImportance,
466            })),
467        }
468    }
469
470    /// Create a new episodic store with both a decay policy and a recall policy.
471    ///
472    /// # Warning
473    ///
474    /// When `recall` is [`RecallPolicy::Hybrid`], decay is applied **before**
475    /// scoring, producing a double time penalty.  See [`RecallPolicy`] docs for
476    /// details.  Consider using the [`builder`](EpisodicStore::builder) to
477    /// configure only one of these time-based mechanisms.
478    pub fn with_decay_and_recall_policy(decay: DecayPolicy, recall: RecallPolicy) -> Self {
479        if let RecallPolicy::Hybrid { .. } = &recall {
480            tracing::warn!(
481                "EpisodicStore::with_decay_and_recall_policy called with RecallPolicy::Hybrid \
482                 — this applies a double time penalty.  Set DecayPolicy OR Hybrid recency \
483                 weighting, not both, unless the double penalty is intentional."
484            );
485        }
486        Self {
487            inner: Arc::new(Mutex::new(EpisodicInner {
488                items: HashMap::new(),
489                decay: Some(decay),
490                recall_policy: recall,
491                per_agent_capacity: None,
492                max_age_hours: None,
493                eviction_policy: EvictionPolicy::LowestImportance,
494            })),
495        }
496    }
497
498    /// Create a new episodic store with the given recall policy.
499    pub fn with_recall_policy(policy: RecallPolicy) -> Self {
500        Self {
501            inner: Arc::new(Mutex::new(EpisodicInner {
502                items: HashMap::new(),
503                decay: None,
504                recall_policy: policy,
505                per_agent_capacity: None,
506                max_age_hours: None,
507                eviction_policy: EvictionPolicy::LowestImportance,
508            })),
509        }
510    }
511
512    /// Create a new episodic store with the given per-agent capacity limit.
513    ///
514    /// When an agent exceeds this capacity, the lowest-importance item for that
515    /// agent is evicted.
516    ///
517    /// # Soft-limit semantics
518    ///
519    /// The capacity is a *soft* limit.  During each [`add_episode`] call the
520    /// new item is inserted first, and only then is the lowest-importance item
521    /// evicted if the count exceeds `capacity`.  This means the store
522    /// momentarily holds `capacity + 1` items per agent while eviction is in
523    /// progress.  The newly added item is **never** the one evicted regardless
524    /// of its importance score.
525    ///
526    /// Concurrent calls to `add_episode` may briefly exceed the cap by more
527    /// than one item before each call performs its own eviction sweep.
528    ///
529    /// [`add_episode`]: EpisodicStore::add_episode
530    pub fn with_per_agent_capacity(capacity: usize) -> Self {
531        assert!(capacity > 0, "per_agent_capacity must be > 0");
532        Self {
533            inner: Arc::new(Mutex::new(EpisodicInner {
534                items: HashMap::new(),
535                decay: None,
536                recall_policy: RecallPolicy::Importance,
537                per_agent_capacity: Some(capacity),
538                max_age_hours: None,
539                eviction_policy: EvictionPolicy::LowestImportance,
540            })),
541        }
542    }
543
544    /// Create a new episodic store with a per-agent capacity limit, without panicking.
545    ///
546    /// Returns `Err` if `capacity == 0`. Prefer this over [`with_per_agent_capacity`]
547    /// in user-facing code where a zero capacity should be a recoverable error
548    /// rather than a panic.
549    ///
550    /// [`with_per_agent_capacity`]: EpisodicStore::with_per_agent_capacity
551    pub fn try_with_per_agent_capacity(
552        capacity: usize,
553    ) -> Result<Self, AgentRuntimeError> {
554        if capacity == 0 {
555            return Err(AgentRuntimeError::Memory(
556                "per_agent_capacity must be > 0".into(),
557            ));
558        }
559        Ok(Self {
560            inner: Arc::new(Mutex::new(EpisodicInner {
561                items: HashMap::new(),
562                decay: None,
563                recall_policy: RecallPolicy::Importance,
564                per_agent_capacity: Some(capacity),
565                max_age_hours: None,
566                eviction_policy: EvictionPolicy::LowestImportance,
567            })),
568        })
569    }
570
571    /// Create a new episodic store with an absolute age limit.
572    ///
573    /// Items older than `max_age_hours` are automatically purged on the next
574    /// `recall` or `add_episode` call for the owning agent.
575    ///
576    /// # Arguments
577    /// * `max_age_hours` — maximum memory age in hours; must be > 0
578    pub fn with_max_age(max_age_hours: f64) -> Result<Self, AgentRuntimeError> {
579        if max_age_hours <= 0.0 {
580            return Err(AgentRuntimeError::Memory(
581                "max_age_hours must be positive".into(),
582            ));
583        }
584        Ok(Self {
585            inner: Arc::new(Mutex::new(EpisodicInner {
586                items: HashMap::new(),
587                decay: None,
588                recall_policy: RecallPolicy::Importance,
589                per_agent_capacity: None,
590                max_age_hours: Some(max_age_hours),
591                eviction_policy: EvictionPolicy::LowestImportance,
592            })),
593        })
594    }
595
596    /// Create a new episodic store with the given eviction policy.
597    pub fn with_eviction_policy(policy: EvictionPolicy) -> Self {
598        Self {
599            inner: Arc::new(Mutex::new(EpisodicInner {
600                items: HashMap::new(),
601                decay: None,
602                recall_policy: RecallPolicy::Importance,
603                per_agent_capacity: None,
604                max_age_hours: None,
605                eviction_policy: policy,
606            })),
607        }
608    }
609
610    /// Record a new episode for the given agent.
611    ///
612    /// # Returns
613    /// The `MemoryId` of the newly created memory item.
614    ///
615    /// # Errors
616    /// Returns `Err(AgentRuntimeError::Memory)` only if the internal mutex is
617    /// poisoned (extremely unlikely in normal operation; see [`recover_lock`]).
618    ///
619    /// # Capacity enforcement
620    ///
621    /// If the store was created with [`with_per_agent_capacity`], the item is
622    /// always inserted first.  If the agent's item count then exceeds the cap,
623    /// the single lowest-importance item for that agent is evicted.  See
624    /// [`with_per_agent_capacity`] for the full soft-limit semantics.
625    ///
626    /// [`with_per_agent_capacity`]: EpisodicStore::with_per_agent_capacity
627    #[tracing::instrument(skip(self))]
628    pub fn add_episode(
629        &self,
630        agent_id: AgentId,
631        content: impl Into<String> + std::fmt::Debug,
632        importance: f32,
633    ) -> Result<MemoryId, AgentRuntimeError> {
634        let item = MemoryItem::new(agent_id.clone(), content, importance, Vec::new());
635        let id = item.id.clone();
636        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::add_episode");
637
638        inner.purge_stale(&agent_id);
639        let cap = inner.per_agent_capacity; // read before mutable borrow
640        let eviction_policy = inner.eviction_policy.clone();
641        let agent_items = inner.items.entry(agent_id).or_default();
642        agent_items.push(item);
643
644        if let Some(cap) = cap {
645            evict_if_over_capacity(agent_items, cap, &eviction_policy);
646        }
647        Ok(id)
648    }
649
650    /// Add an episode with associated tags.
651    ///
652    /// Convenience wrapper around [`add_episode`] that accepts a tag list in the
653    /// same call.  Episode capacity eviction follows the same rules as `add_episode`.
654    ///
655    /// [`add_episode`]: EpisodicStore::add_episode
656    #[tracing::instrument(skip(self))]
657    pub fn add_episode_with_tags(
658        &self,
659        agent_id: AgentId,
660        content: impl Into<String> + std::fmt::Debug,
661        importance: f32,
662        tags: Vec<String>,
663    ) -> Result<MemoryId, AgentRuntimeError> {
664        let item = MemoryItem::new(agent_id.clone(), content, importance, tags);
665        let id = item.id.clone();
666        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::add_episode_with_tags");
667        inner.purge_stale(&agent_id);
668        let cap = inner.per_agent_capacity;
669        let eviction_policy = inner.eviction_policy.clone();
670        let agent_items = inner.items.entry(agent_id).or_default();
671        agent_items.push(item);
672        if let Some(cap) = cap {
673            evict_if_over_capacity(agent_items, cap, &eviction_policy);
674        }
675        Ok(id)
676    }
677
678    /// Remove a specific episode by its `MemoryId`.
679    ///
680    /// Returns `Ok(true)` if the episode was found and removed, `Ok(false)` if
681    /// no episode with that `id` exists for `agent_id`.
682    pub fn remove_by_id(
683        &self,
684        agent_id: &AgentId,
685        id: &MemoryId,
686    ) -> Result<bool, AgentRuntimeError> {
687        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::remove_by_id");
688        if let Some(items) = inner.items.get_mut(agent_id) {
689            if let Some(pos) = items.iter().position(|i| &i.id == id) {
690                items.remove(pos);
691                return Ok(true);
692            }
693        }
694        Ok(false)
695    }
696
697    /// Update the `tags` of an episode identified by its `MemoryId`.
698    ///
699    /// Returns `Ok(true)` if found and updated, `Ok(false)` otherwise.
700    pub fn update_tags_by_id(
701        &self,
702        agent_id: &AgentId,
703        id: &MemoryId,
704        new_tags: Vec<String>,
705    ) -> Result<bool, AgentRuntimeError> {
706        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::update_tags_by_id");
707        if let Some(items) = inner.items.get_mut(agent_id) {
708            if let Some(item) = items.iter_mut().find(|i| &i.id == id) {
709                item.tags = new_tags;
710                return Ok(true);
711            }
712        }
713        Ok(false)
714    }
715
716    /// Return the highest importance score across all episodes for `agent_id`.
717    ///
718    /// Returns `None` if the agent has no stored episodes.
719    pub fn max_importance_for(
720        &self,
721        agent_id: &AgentId,
722    ) -> Result<Option<f32>, AgentRuntimeError> {
723        let inner = recover_lock(self.inner.lock(), "EpisodicStore::max_importance_for");
724        let max = inner
725            .items
726            .get(agent_id)
727            .and_then(|items| {
728                items
729                    .iter()
730                    .map(|i| i.importance)
731                    .reduce(f32::max)
732            });
733        Ok(max)
734    }
735
736    /// Return the number of episodes stored for `agent_id`.
737    ///
738    /// Cheaper than `recall(agent, usize::MAX)?.len()` because it does not
739    /// sort, clone, or increment recall counts.
740    pub fn count_for(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
741        let inner = recover_lock(self.inner.lock(), "EpisodicStore::count_for");
742        Ok(inner.items.get(agent_id).map_or(0, |v| v.len()))
743    }
744
745    /// Return `true` if the store contains at least one episode for `agent_id`.
746    ///
747    /// Cheaper than `count_for(agent_id)? > 0` because no heap allocation occurs.
748    pub fn has_agent(&self, agent_id: &AgentId) -> Result<bool, AgentRuntimeError> {
749        let inner = recover_lock(self.inner.lock(), "EpisodicStore::has_agent");
750        Ok(inner.items.get(agent_id).map_or(false, |v| !v.is_empty()))
751    }
752
753    /// Return the IDs of agents that have at least `min` episodes.
754    pub fn agents_with_min_episodes(&self, min: usize) -> Result<Vec<AgentId>, AgentRuntimeError> {
755        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agents_with_min_episodes");
756        let mut ids: Vec<AgentId> = inner
757            .items
758            .iter()
759            .filter(|(_, v)| v.len() >= min)
760            .map(|(id, _)| id.clone())
761            .collect();
762        ids.sort_by(|a, b| a.0.cmp(&b.0));
763        Ok(ids)
764    }
765
766    /// Return the total number of episodes stored across all agents.
767    pub fn total_episode_count(&self) -> Result<usize, AgentRuntimeError> {
768        let inner = recover_lock(self.inner.lock(), "EpisodicStore::total_episode_count");
769        Ok(inner.items.values().map(|v| v.len()).sum())
770    }
771
772    /// Return the number of episodes stored for the given agent.
773    ///
774    /// Returns `0` if the agent has no recorded episodes.
775    pub fn episode_count_for(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
776        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episode_count_for");
777        Ok(inner.items.get(agent_id).map_or(0, |v| v.len()))
778    }
779
780    /// Return the `AgentId` of the agent with the most stored episodes, or
781    /// `None` if the store is empty.
782    pub fn agent_with_most_episodes(&self) -> Result<Option<AgentId>, AgentRuntimeError> {
783        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agent_with_most_episodes");
784        Ok(inner
785            .items
786            .iter()
787            .max_by_key(|(_, v)| v.len())
788            .map(|(id, _)| id.clone()))
789    }
790
791    /// Return all agent IDs that have at least one stored episode, sorted.
792    pub fn agents(&self) -> Result<Vec<AgentId>, AgentRuntimeError> {
793        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agents");
794        let mut ids: Vec<AgentId> = inner
795            .items
796            .keys()
797            .filter(|id| !inner.items[id].is_empty())
798            .cloned()
799            .collect();
800        ids.sort_unstable_by(|a, b| a.0.cmp(&b.0));
801        Ok(ids)
802    }
803
804    /// Return the highest importance value across all stored episodes,
805    /// or `None` if the store is empty.
806    pub fn max_importance_overall(&self) -> Result<Option<f32>, AgentRuntimeError> {
807        let inner = recover_lock(self.inner.lock(), "EpisodicStore::max_importance_overall");
808        let max = inner
809            .items
810            .values()
811            .flat_map(|v| v.iter())
812            .map(|e| e.importance)
813            .reduce(f32::max);
814        Ok(max)
815    }
816
817    /// Return the variance of importance scores for the given agent's episodes.
818    ///
819    /// Returns `0.0` when the agent has fewer than two episodes.
820    pub fn importance_variance_for(&self, agent_id: &AgentId) -> Result<f64, AgentRuntimeError> {
821        let inner = recover_lock(self.inner.lock(), "EpisodicStore::importance_variance_for");
822        let vals: Vec<f64> = inner
823            .items
824            .get(agent_id)
825            .map(|v| v.iter().map(|e| e.importance as f64).collect())
826            .unwrap_or_default();
827        if vals.len() < 2 {
828            return Ok(0.0);
829        }
830        let mean = vals.iter().sum::<f64>() / vals.len() as f64;
831        let variance = vals.iter().map(|x| (x - mean).powi(2)).sum::<f64>() / vals.len() as f64;
832        Ok(variance)
833    }
834
835    /// Return up to `n` episodes for `agent_id` sorted by descending importance
836    /// without incrementing recall counts or applying decay.
837    ///
838    /// Use this for read-only importance-ranked snapshots.
839    pub fn recall_top_n(
840        &self,
841        agent_id: &AgentId,
842        n: usize,
843    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
844        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_top_n");
845        let mut items: Vec<MemoryItem> = inner
846            .items
847            .get(agent_id)
848            .cloned()
849            .unwrap_or_default();
850        items.sort_unstable_by(|a, b| {
851            b.importance
852                .partial_cmp(&a.importance)
853                .unwrap_or(std::cmp::Ordering::Equal)
854        });
855        items.truncate(n);
856        Ok(items)
857    }
858
859    /// Return all episodes for `agent_id` whose importance is within
860    /// `[min_inclusive, max_inclusive]`, sorted by descending importance.
861    pub fn filter_by_importance(
862        &self,
863        agent_id: &AgentId,
864        min: f32,
865        max: f32,
866    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
867        let inner = recover_lock(self.inner.lock(), "EpisodicStore::filter_by_importance");
868        let mut items: Vec<MemoryItem> = inner
869            .items
870            .get(agent_id)
871            .map(|v| {
872                v.iter()
873                    .filter(|i| i.importance >= min && i.importance <= max)
874                    .cloned()
875                    .collect()
876            })
877            .unwrap_or_default();
878        items.sort_unstable_by(|a, b| {
879            b.importance
880                .partial_cmp(&a.importance)
881                .unwrap_or(std::cmp::Ordering::Equal)
882        });
883        Ok(items)
884    }
885
886    /// Keep only the `n` most-important episodes for `agent_id`, removing
887    /// the rest.  Returns the number of episodes that were removed.
888    ///
889    /// If the agent has `n` or fewer episodes already, nothing is removed.
890    pub fn retain_top_n(&self, agent_id: &AgentId, n: usize) -> Result<usize, AgentRuntimeError> {
891        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::retain_top_n");
892        let items = inner.items.entry(agent_id.clone()).or_default();
893        if items.len() <= n {
894            return Ok(0);
895        }
896        items.sort_unstable_by(|a, b| {
897            b.importance
898                .partial_cmp(&a.importance)
899                .unwrap_or(std::cmp::Ordering::Equal)
900        });
901        let removed = items.len() - n;
902        items.truncate(n);
903        Ok(removed)
904    }
905
906    /// Return the most recently stored episode for `agent_id`, or `None` if
907    /// the agent has no episodes.
908    ///
909    /// "Most recent" is defined as the last element in the stored list, which
910    /// matches insertion order.
911    pub fn most_recent(&self, agent_id: &AgentId) -> Result<Option<MemoryItem>, AgentRuntimeError> {
912        let inner = recover_lock(self.inner.lock(), "EpisodicStore::most_recent");
913        Ok(inner
914            .items
915            .get(agent_id)
916            .and_then(|v| v.last().cloned()))
917    }
918
919    /// Return the highest importance score for `agent_id`, or `None` if the
920    /// agent has no episodes.
921    pub fn max_importance(&self, agent_id: &AgentId) -> Result<Option<f32>, AgentRuntimeError> {
922        let inner = recover_lock(self.inner.lock(), "EpisodicStore::max_importance");
923        Ok(inner
924            .items
925            .get(agent_id)
926            .and_then(|v| {
927                v.iter()
928                    .map(|i| i.importance)
929                    .reduce(f32::max)
930            }))
931    }
932
933    /// Return the lowest importance score for `agent_id`, or `None` if the
934    /// agent has no episodes.
935    pub fn min_importance(&self, agent_id: &AgentId) -> Result<Option<f32>, AgentRuntimeError> {
936        let inner = recover_lock(self.inner.lock(), "EpisodicStore::min_importance");
937        Ok(inner
938            .items
939            .get(agent_id)
940            .and_then(|v| {
941                v.iter()
942                    .map(|i| i.importance)
943                    .reduce(f32::min)
944            }))
945    }
946
947    /// Count episodes for `agent_id` whose importance is strictly greater than
948    /// `threshold`.
949    ///
950    /// Returns `0` if the agent has no episodes.
951    pub fn count_above_importance(
952        &self,
953        agent_id: &AgentId,
954        threshold: f32,
955    ) -> Result<usize, AgentRuntimeError> {
956        let inner = recover_lock(self.inner.lock(), "EpisodicStore::count_above_importance");
957        Ok(inner
958            .items
959            .get(agent_id)
960            .map(|v| v.iter().filter(|i| i.importance > threshold).count())
961            .unwrap_or(0))
962    }
963
964    /// Return the episode with the highest `recall_count` for `agent_id`.
965    ///
966    /// Returns `None` if the agent has no stored episodes.  When multiple
967    /// episodes tie for the maximum recall count, any one of them may be returned.
968    pub fn most_recalled(&self, agent_id: &AgentId) -> Result<Option<MemoryItem>, AgentRuntimeError> {
969        let inner = recover_lock(self.inner.lock(), "EpisodicStore::most_recalled");
970        Ok(inner
971            .items
972            .get(agent_id)
973            .and_then(|v| v.iter().max_by_key(|i| i.recall_count))
974            .cloned())
975    }
976
977    /// Return the arithmetic mean importance for `agent_id`, or `0.0` if the
978    /// agent has no stored episodes.
979    pub fn importance_avg(&self, agent_id: &AgentId) -> Result<f32, AgentRuntimeError> {
980        let inner = recover_lock(self.inner.lock(), "EpisodicStore::importance_avg");
981        match inner.items.get(agent_id) {
982            None => Ok(0.0),
983            Some(items) if items.is_empty() => Ok(0.0),
984            Some(items) => {
985                let sum: f32 = items.iter().map(|i| i.importance).sum();
986                Ok(sum / items.len() as f32)
987            }
988        }
989    }
990
991    /// Remove duplicate episodes (same `content`) for `agent_id`, keeping only
992    /// the episode with the highest importance for each distinct content string.
993    ///
994    /// Returns the number of episodes removed.
995    pub fn deduplicate_content(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
996        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::deduplicate_content");
997        let Some(items) = inner.items.get_mut(agent_id) else {
998            return Ok(0);
999        };
1000        let before = items.len();
1001        // For each unique content keep the item with the highest importance.
1002        let mut seen: std::collections::HashMap<String, usize> = std::collections::HashMap::new();
1003        let mut keepers: Vec<usize> = Vec::new();
1004        for (idx, item) in items.iter().enumerate() {
1005            match seen.get(&item.content) {
1006                None => {
1007                    seen.insert(item.content.clone(), keepers.len());
1008                    keepers.push(idx);
1009                }
1010                Some(&pos) => {
1011                    let kept_idx = keepers[pos];
1012                    if item.importance > items[kept_idx].importance {
1013                        keepers[pos] = idx;
1014                    }
1015                }
1016            }
1017        }
1018        let kept: std::collections::HashSet<usize> = keepers.into_iter().collect();
1019        let mut i = 0;
1020        items.retain(|_| {
1021            let keep = kept.contains(&i);
1022            i += 1;
1023            keep
1024        });
1025        Ok(before - items.len())
1026    }
1027
1028    /// Return all `AgentId`s that have at least one stored episode.
1029    pub fn agent_ids(&self) -> Result<Vec<AgentId>, AgentRuntimeError> {
1030        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agent_ids");
1031        Ok(inner.items.keys().cloned().collect())
1032    }
1033
1034    /// Return all episodes for `agent_id` whose `content` contains `pattern`
1035    /// (case-sensitive substring match), sorted by descending importance.
1036    pub fn find_by_content(
1037        &self,
1038        agent_id: &AgentId,
1039        pattern: &str,
1040    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1041        let inner = recover_lock(self.inner.lock(), "EpisodicStore::find_by_content");
1042        let mut matches: Vec<MemoryItem> = inner
1043            .items
1044            .get(agent_id)
1045            .map(|items| {
1046                items
1047                    .iter()
1048                    .filter(|i| i.content.contains(pattern))
1049                    .cloned()
1050                    .collect()
1051            })
1052            .unwrap_or_default();
1053        matches.sort_unstable_by(|a, b| {
1054            b.importance
1055                .partial_cmp(&a.importance)
1056                .unwrap_or(std::cmp::Ordering::Equal)
1057        });
1058        Ok(matches)
1059    }
1060
1061    /// Remove all episodes stored for `agent_id`.
1062    ///
1063    /// Returns the number of episodes that were removed.
1064    pub fn clear_for(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1065        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::clear_for");
1066        let count = inner.items.remove(agent_id).map_or(0, |v| v.len());
1067        Ok(count)
1068    }
1069
1070    /// Return the number of episodes for `agent_id` that carry the given tag.
1071    pub fn count_episodes_with_tag(
1072        &self,
1073        agent_id: &AgentId,
1074        tag: &str,
1075    ) -> Result<usize, AgentRuntimeError> {
1076        let inner = recover_lock(self.inner.lock(), "EpisodicStore::count_episodes_with_tag");
1077        let count = inner
1078            .items
1079            .get(agent_id)
1080            .map_or(0, |items| items.iter().filter(|i| i.has_tag(tag)).count());
1081        Ok(count)
1082    }
1083
1084    /// Return the content strings of all episodes for `agent_id` whose content contains `substring`.
1085    pub fn episodes_with_content(
1086        &self,
1087        agent_id: &AgentId,
1088        substring: &str,
1089    ) -> Result<Vec<String>, AgentRuntimeError> {
1090        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_with_content");
1091        let items = inner
1092            .items
1093            .get(agent_id)
1094            .map_or_else(Vec::new, |v| {
1095                v.iter()
1096                    .filter(|i| i.content.contains(substring))
1097                    .map(|i| i.content.clone())
1098                    .collect()
1099            });
1100        Ok(items)
1101    }
1102
1103    /// Return the byte length of the longest episode content for `agent_id`.
1104    ///
1105    /// Returns `0` if the agent has no episodes.
1106    pub fn max_content_length(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1107        let inner = recover_lock(self.inner.lock(), "EpisodicStore::max_content_length");
1108        Ok(inner
1109            .items
1110            .get(agent_id)
1111            .and_then(|v| v.iter().map(|i| i.content.len()).max())
1112            .unwrap_or(0))
1113    }
1114
1115    /// Return the byte length of the shortest episode content for `agent_id`.
1116    ///
1117    /// Returns `0` if the agent has no episodes.
1118    pub fn min_content_length(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1119        let inner = recover_lock(self.inner.lock(), "EpisodicStore::min_content_length");
1120        Ok(inner
1121            .items
1122            .get(agent_id)
1123            .and_then(|v| v.iter().map(|i| i.content.len()).min())
1124            .unwrap_or(0))
1125    }
1126
1127    /// Return the content strings of all episodes for `agent_id`, sorted by
1128    /// descending importance (most important first).
1129    pub fn episodes_by_importance(
1130        &self,
1131        agent_id: &AgentId,
1132    ) -> Result<Vec<String>, AgentRuntimeError> {
1133        let inner = recover_lock(self.inner.lock(), "EpisodicStore::episodes_by_importance");
1134        let mut items: Vec<(f32, String)> = inner
1135            .items
1136            .get(agent_id)
1137            .map_or_else(Vec::new, |v| {
1138                v.iter().map(|i| (i.importance, i.content.clone())).collect()
1139            });
1140        items.sort_unstable_by(|a, b| b.0.partial_cmp(&a.0).unwrap_or(std::cmp::Ordering::Equal));
1141        Ok(items.into_iter().map(|(_, c)| c).collect())
1142    }
1143
1144    /// Return the count of episodes for `agent_id` whose content contains `substring`.
1145    ///
1146    /// The match is case-sensitive.  Returns `0` if the agent has no episodes.
1147    pub fn content_contains_count(
1148        &self,
1149        agent_id: &AgentId,
1150        substring: &str,
1151    ) -> Result<usize, AgentRuntimeError> {
1152        let inner = recover_lock(self.inner.lock(), "EpisodicStore::content_contains_count");
1153        Ok(inner
1154            .items
1155            .get(agent_id)
1156            .map_or(0, |v| v.iter().filter(|i| i.content.contains(substring)).count()))
1157    }
1158
1159    /// Return a sorted list of all `AgentId`s that have at least one episode.
1160    pub fn agents_with_episodes(&self) -> Result<Vec<AgentId>, AgentRuntimeError> {
1161        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agents_with_episodes");
1162        let mut ids: Vec<AgentId> = inner
1163            .items
1164            .iter()
1165            .filter(|(_, v)| !v.is_empty())
1166            .map(|(k, _)| k.clone())
1167            .collect();
1168        ids.sort_unstable_by(|a, b| a.as_str().cmp(b.as_str()));
1169        Ok(ids)
1170    }
1171
1172    /// Return the count of episodes whose importance is strictly greater than `threshold`.
1173    pub fn high_importance_count(
1174        &self,
1175        agent_id: &AgentId,
1176        threshold: f32,
1177    ) -> Result<usize, AgentRuntimeError> {
1178        let inner = recover_lock(self.inner.lock(), "EpisodicStore::high_importance_count");
1179        Ok(inner
1180            .items
1181            .get(agent_id)
1182            .map_or(0, |v| v.iter().filter(|i| i.importance > threshold).count()))
1183    }
1184
1185    /// Return a list of content byte lengths for all episodes belonging to `agent_id`, in storage order.
1186    pub fn content_lengths(&self, agent_id: &AgentId) -> Result<Vec<usize>, AgentRuntimeError> {
1187        let inner = recover_lock(self.inner.lock(), "EpisodicStore::content_lengths");
1188        let lengths = inner
1189            .items
1190            .get(agent_id)
1191            .map_or_else(Vec::new, |v| v.iter().map(|i| i.content.len()).collect());
1192        Ok(lengths)
1193    }
1194
1195    /// Return the total byte length of all episode content strings for `agent_id`.
1196    ///
1197    /// Returns `0` if the agent has no stored episodes.
1198    pub fn total_content_bytes(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1199        let inner = recover_lock(self.inner.lock(), "EpisodicStore::total_content_bytes");
1200        let total = inner
1201            .items
1202            .get(agent_id)
1203            .map_or(0, |items| items.iter().map(|i| i.content.len()).sum());
1204        Ok(total)
1205    }
1206
1207    /// Return the average byte length of episode content strings for `agent_id`.
1208    ///
1209    /// Returns `0.0` if the agent has no stored episodes.
1210    pub fn avg_content_length(&self, agent_id: &AgentId) -> Result<f64, AgentRuntimeError> {
1211        let inner = recover_lock(self.inner.lock(), "EpisodicStore::avg_content_length");
1212        let items = match inner.items.get(agent_id) {
1213            Some(v) if !v.is_empty() => v,
1214            _ => return Ok(0.0),
1215        };
1216        let total: usize = items.iter().map(|i| i.content.len()).sum();
1217        Ok(total as f64 / items.len() as f64)
1218    }
1219
1220    /// Return the sum of all importance scores for `agent_id`.
1221    ///
1222    /// Returns `0.0` if the agent has no stored episodes.
1223    pub fn importance_sum(&self, agent_id: &AgentId) -> Result<f32, AgentRuntimeError> {
1224        let inner = recover_lock(self.inner.lock(), "EpisodicStore::importance_sum");
1225        let sum = inner
1226            .items
1227            .get(agent_id)
1228            .map_or(0.0, |items| items.iter().map(|i| i.importance).sum());
1229        Ok(sum)
1230    }
1231
1232    /// Recall up to `limit` episodes for `agent_id` that carry `tag`,
1233    /// sorted by descending importance.  `limit = 0` returns all matches.
1234    pub fn recall_by_tag(
1235        &self,
1236        agent_id: &AgentId,
1237        tag: &str,
1238        limit: usize,
1239    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1240        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_by_tag");
1241        let mut matches: Vec<MemoryItem> = inner
1242            .items
1243            .get(agent_id)
1244            .map(|items| {
1245                items
1246                    .iter()
1247                    .filter(|i| i.tags.iter().any(|t| t == tag))
1248                    .cloned()
1249                    .collect()
1250            })
1251            .unwrap_or_default();
1252        matches.sort_unstable_by(|a, b| {
1253            b.importance
1254                .partial_cmp(&a.importance)
1255                .unwrap_or(std::cmp::Ordering::Equal)
1256        });
1257        if limit > 0 {
1258            matches.truncate(limit);
1259        }
1260        Ok(matches)
1261    }
1262
1263    /// Add an episode with an explicit timestamp.
1264    #[tracing::instrument(skip(self))]
1265    pub fn add_episode_at(
1266        &self,
1267        agent_id: AgentId,
1268        content: impl Into<String> + std::fmt::Debug,
1269        importance: f32,
1270        timestamp: chrono::DateTime<chrono::Utc>,
1271    ) -> Result<MemoryId, AgentRuntimeError> {
1272        let mut item = MemoryItem::new(agent_id.clone(), content, importance, Vec::new());
1273        item.timestamp = timestamp;
1274        let id = item.id.clone();
1275        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::add_episode_at");
1276
1277        inner.purge_stale(&agent_id);
1278        let cap = inner.per_agent_capacity; // read before mutable borrow
1279        let eviction_policy = inner.eviction_policy.clone();
1280        let agent_items = inner.items.entry(agent_id).or_default();
1281        agent_items.push(item);
1282
1283        if let Some(cap) = cap {
1284            evict_if_over_capacity(agent_items, cap, &eviction_policy);
1285        }
1286        Ok(id)
1287    }
1288
1289    /// Add multiple episodes for the same agent in a single lock acquisition.
1290    ///
1291    /// More efficient than calling [`add_episode`] in a loop when inserting
1292    /// many items at once.  Returns the generated [`MemoryId`]s in the same
1293    /// order as `episodes`.
1294    ///
1295    /// [`add_episode`]: EpisodicStore::add_episode
1296    pub fn add_episodes_batch(
1297        &self,
1298        agent_id: AgentId,
1299        episodes: impl IntoIterator<Item = (impl Into<String>, f32)>,
1300    ) -> Result<Vec<MemoryId>, AgentRuntimeError> {
1301        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::add_episodes_batch");
1302        inner.purge_stale(&agent_id);
1303        let cap = inner.per_agent_capacity;
1304        let eviction_policy = inner.eviction_policy.clone();
1305        let agent_items = inner.items.entry(agent_id.clone()).or_default();
1306
1307        let mut ids = Vec::new();
1308        for (content, importance) in episodes {
1309            let item = MemoryItem::new(agent_id.clone(), content, importance, Vec::new());
1310            ids.push(item.id.clone());
1311            agent_items.push(item);
1312        }
1313        if let Some(cap) = cap {
1314            evict_if_over_capacity(agent_items, cap, &eviction_policy);
1315        }
1316        Ok(ids)
1317    }
1318
1319    /// Recall up to `limit` memories for the given agent.
1320    ///
1321    /// Applies decay if configured, purges stale items if `max_age` is set,
1322    /// increments `recall_count` for each recalled item, then returns items
1323    /// sorted according to the configured `RecallPolicy`.
1324    ///
1325    /// # Errors
1326    /// Returns `Err(AgentRuntimeError::Memory)` only if the internal mutex is
1327    /// poisoned (extremely unlikely in normal operation).
1328    #[tracing::instrument(skip(self))]
1329    pub fn recall(
1330        &self,
1331        agent_id: &AgentId,
1332        limit: usize,
1333    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1334        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::recall");
1335
1336        // Clone policy values to avoid borrow conflicts.
1337        let decay = inner.decay.clone();
1338        let max_age = inner.max_age_hours;
1339        let recall_policy = inner.recall_policy.clone();
1340
1341        // Use get_mut to avoid creating ghost entries for unknown agents.
1342        if !inner.items.contains_key(agent_id) {
1343            return Ok(Vec::new());
1344        }
1345        let agent_items = inner.items.get_mut(agent_id).unwrap();
1346
1347        // Apply decay in-place.
1348        if let Some(ref policy) = decay {
1349            for item in agent_items.iter_mut() {
1350                policy.decay_item(item);
1351            }
1352        }
1353
1354        // Purge stale items.
1355        if let Some(max_age_h) = max_age {
1356            let cutoff =
1357                Utc::now() - chrono::Duration::seconds((max_age_h * 3600.0) as i64);
1358            agent_items.retain(|i| i.timestamp >= cutoff);
1359        }
1360
1361        // Build a sorted index list (descending by score) without cloning all items first.
1362        //
1363        // When `limit < indices.len()` we use a two-phase partial sort:
1364        //   1. `select_nth_unstable_by(limit - 1, cmp)` — O(n) — partitions the
1365        //      slice so that indices[0..limit] are the top-limit elements (unordered)
1366        //      and indices[limit..] are the remaining (discarded) elements.
1367        //   2. Sort only indices[0..limit] — O(limit log limit).
1368        // Total: O(n + limit log limit) vs O(n log n) for a full sort.
1369        let mut indices: Vec<usize> = (0..agent_items.len()).collect();
1370
1371        match recall_policy {
1372            RecallPolicy::Importance => {
1373                let cmp = |&a: &usize, &b: &usize| {
1374                    agent_items[b]
1375                        .importance
1376                        .partial_cmp(&agent_items[a].importance)
1377                        .unwrap_or(std::cmp::Ordering::Equal)
1378                };
1379                if limit > 0 && limit < indices.len() {
1380                    indices.select_nth_unstable_by(limit - 1, cmp);
1381                    indices[..limit].sort_unstable_by(cmp);
1382                } else {
1383                    indices.sort_unstable_by(cmp);
1384                }
1385            }
1386            RecallPolicy::Hybrid {
1387                recency_weight,
1388                frequency_weight,
1389            } => {
1390                let max_recall = agent_items
1391                    .iter()
1392                    .map(|i| i.recall_count)
1393                    .max()
1394                    .unwrap_or(1)
1395                    .max(1);
1396                let now = Utc::now();
1397                let cmp = |&a: &usize, &b: &usize| {
1398                    let score_a = compute_hybrid_score(
1399                        &agent_items[a],
1400                        recency_weight,
1401                        frequency_weight,
1402                        max_recall,
1403                        now,
1404                    );
1405                    let score_b = compute_hybrid_score(
1406                        &agent_items[b],
1407                        recency_weight,
1408                        frequency_weight,
1409                        max_recall,
1410                        now,
1411                    );
1412                    score_b
1413                        .partial_cmp(&score_a)
1414                        .unwrap_or(std::cmp::Ordering::Equal)
1415                };
1416                if limit > 0 && limit < indices.len() {
1417                    indices.select_nth_unstable_by(limit - 1, cmp);
1418                    indices[..limit].sort_unstable_by(cmp);
1419                } else {
1420                    indices.sort_unstable_by(cmp);
1421                }
1422            }
1423        }
1424
1425        indices.truncate(limit);
1426
1427        // Increment recall_count only for the surviving items.
1428        for &idx in &indices {
1429            agent_items[idx].recall_count += 1;
1430        }
1431
1432        // Clone only the surviving items, with already-incremented counts.
1433        let items: Vec<MemoryItem> = indices.iter().map(|&idx| agent_items[idx].clone()).collect();
1434
1435        tracing::debug!("recalled {} items", items.len());
1436        Ok(items)
1437    }
1438
1439    /// Recall episodes for `agent_id` that contain **all** of the specified `tags`.
1440    ///
1441    /// Returns at most `limit` items ordered by descending importance, consistent
1442    /// with [`recall`].  Pass an empty `tags` slice to match all episodes.
1443    ///
1444    /// [`recall`]: EpisodicStore::recall
1445    pub fn recall_tagged(
1446        &self,
1447        agent_id: &AgentId,
1448        tags: &[&str],
1449        limit: usize,
1450    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1451        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_tagged");
1452        let items = inner.items.get(agent_id).cloned().unwrap_or_default();
1453        drop(inner);
1454        let mut matched: Vec<MemoryItem> = items
1455            .into_iter()
1456            .filter(|item| {
1457                tags.iter()
1458                    .all(|t| item.tags.iter().any(|it| it.as_str() == *t))
1459            })
1460            .collect();
1461        matched.sort_unstable_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal));
1462        if limit > 0 {
1463            matched.truncate(limit);
1464        }
1465        Ok(matched)
1466    }
1467
1468    /// Retrieve a single episode by its `MemoryId`.
1469    ///
1470    /// Returns `Ok(Some(item))` if found, `Ok(None)` if no episode with that ID
1471    /// exists for `agent_id`.
1472    pub fn recall_by_id(
1473        &self,
1474        agent_id: &AgentId,
1475        id: &MemoryId,
1476    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
1477        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_by_id");
1478        Ok(inner
1479            .items
1480            .get(agent_id)
1481            .and_then(|items| items.iter().find(|i| &i.id == id).cloned()))
1482    }
1483
1484    /// Import all episodes from `other` for `agent_id` into this store.
1485    ///
1486    /// Episodes are appended without deduplication.  Capacity eviction is applied
1487    /// per-episode exactly as in [`add_episode`].
1488    ///
1489    /// [`add_episode`]: EpisodicStore::add_episode
1490    pub fn merge_from(
1491        &self,
1492        other: &EpisodicStore,
1493        agent_id: &AgentId,
1494    ) -> Result<usize, AgentRuntimeError> {
1495        let other_items = {
1496            let inner = recover_lock(other.inner.lock(), "EpisodicStore::merge_from:read");
1497            inner.items.get(agent_id).cloned().unwrap_or_default()
1498        };
1499        let count = other_items.len();
1500        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::merge_from:write");
1501        inner.purge_stale(agent_id);
1502        let cap = inner.per_agent_capacity;
1503        let bucket = inner.items.entry(agent_id.clone()).or_default();
1504        for item in other_items {
1505            if let Some(cap) = cap {
1506                while bucket.len() >= cap {
1507                    bucket.remove(0);
1508                }
1509            }
1510            bucket.push(item);
1511        }
1512        Ok(count)
1513    }
1514
1515    /// Update the importance score of a specific episode in-place.
1516    ///
1517    /// Returns `Ok(true)` if the episode was found and updated, `Ok(false)` if no
1518    /// episode with that `id` exists for `agent_id`.
1519    ///
1520    /// `new_importance` is clamped to `[0.0, 1.0]`.
1521    pub fn update_importance(
1522        &self,
1523        agent_id: &AgentId,
1524        id: &MemoryId,
1525        new_importance: f32,
1526    ) -> Result<bool, AgentRuntimeError> {
1527        let importance = new_importance.clamp(0.0, 1.0);
1528        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::update_importance");
1529        if let Some(items) = inner.items.get_mut(agent_id) {
1530            if let Some(item) = items.iter_mut().find(|i| &i.id == id) {
1531                item.importance = importance;
1532                return Ok(true);
1533            }
1534        }
1535        Ok(false)
1536    }
1537
1538    /// Recall episodes for `agent_id` inserted at or after `cutoff`.
1539    ///
1540    /// Returns items ordered by descending importance (same as [`recall`]).
1541    /// Pass `limit = 0` to return all matching items.
1542    ///
1543    /// [`recall`]: EpisodicStore::recall
1544    pub fn recall_since(
1545        &self,
1546        agent_id: &AgentId,
1547        cutoff: chrono::DateTime<chrono::Utc>,
1548        limit: usize,
1549    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1550        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_since");
1551        let mut items: Vec<MemoryItem> = inner
1552            .items
1553            .get(agent_id)
1554            .cloned()
1555            .unwrap_or_default()
1556            .into_iter()
1557            .filter(|i| i.timestamp >= cutoff)
1558            .collect();
1559        drop(inner);
1560        items.sort_unstable_by(|a, b| {
1561            b.importance
1562                .partial_cmp(&a.importance)
1563                .unwrap_or(std::cmp::Ordering::Equal)
1564        });
1565        if limit > 0 {
1566            items.truncate(limit);
1567        }
1568        Ok(items)
1569    }
1570
1571    /// Update the `content` of an episode identified by its `MemoryId`.
1572    ///
1573    /// Returns `Ok(true)` if found and updated, `Ok(false)` if no episode with that
1574    /// `id` exists for `agent_id`.
1575    pub fn update_content(
1576        &self,
1577        agent_id: &AgentId,
1578        id: &MemoryId,
1579        new_content: impl Into<String>,
1580    ) -> Result<bool, AgentRuntimeError> {
1581        let new_content = new_content.into();
1582        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::update_content");
1583        if let Some(items) = inner.items.get_mut(agent_id) {
1584            if let Some(item) = items.iter_mut().find(|i| &i.id == id) {
1585                item.content = new_content;
1586                return Ok(true);
1587            }
1588        }
1589        Ok(false)
1590    }
1591
1592    /// Recall the most recently added episodes for `agent_id` in reverse insertion order.
1593    ///
1594    /// Unlike [`recall`] (which ranks by importance), this returns the `limit` most
1595    /// recently inserted items with no re-ordering.  Useful when recency matters more
1596    /// than importance, e.g. retrieving the latest context window entries.
1597    ///
1598    /// [`recall`]: EpisodicStore::recall
1599    pub fn recall_recent(
1600        &self,
1601        agent_id: &AgentId,
1602        limit: usize,
1603    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1604        let inner = recover_lock(self.inner.lock(), "EpisodicStore::recall_recent");
1605        let items = inner.items.get(agent_id).cloned().unwrap_or_default();
1606        drop(inner);
1607        let start = if limit > 0 && limit < items.len() {
1608            items.len() - limit
1609        } else {
1610            0
1611        };
1612        Ok(items[start..].iter().rev().cloned().collect())
1613    }
1614
1615    /// Retrieve all stored episodes for `agent_id` without any limit.
1616    ///
1617    /// Returns items in descending importance order, consistent with [`recall`].
1618    /// For large stores, prefer [`recall`] with an explicit limit.
1619    ///
1620    /// [`recall`]: EpisodicStore::recall
1621    pub fn recall_all(&self, agent_id: &AgentId) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1622        self.recall(agent_id, usize::MAX)
1623    }
1624
1625    /// Return the top `n` episodes for `agent_id` ordered by descending importance.
1626    ///
1627    /// When `n == 0` all episodes are returned. Does not increment `recall_count`.
1628    pub fn top_n(&self, agent_id: &AgentId, n: usize) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1629        let inner = recover_lock(self.inner.lock(), "EpisodicStore::top_n");
1630        let mut items = inner.items.get(agent_id).cloned().unwrap_or_default();
1631        items.sort_unstable_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal));
1632        if n > 0 {
1633            items.truncate(n);
1634        }
1635        Ok(items)
1636    }
1637
1638    /// Return episodes for `agent_id` whose importance is in `[min, max]`, most
1639    /// important first. Passing `limit == 0` returns all matching episodes.
1640    pub fn search_by_importance_range(
1641        &self,
1642        agent_id: &AgentId,
1643        min: f32,
1644        max: f32,
1645        limit: usize,
1646    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1647        let inner = recover_lock(self.inner.lock(), "EpisodicStore::search_by_importance_range");
1648        let mut items: Vec<MemoryItem> = inner
1649            .items
1650            .get(agent_id)
1651            .map_or_else(Vec::new, |v| {
1652                v.iter().filter(|i| i.importance >= min && i.importance <= max).cloned().collect()
1653            });
1654        items.sort_unstable_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal));
1655        if limit > 0 {
1656            items.truncate(limit);
1657        }
1658        Ok(items)
1659    }
1660
1661    /// Return the sum of `recall_count` across all episodes for `agent_id`.
1662    ///
1663    /// Useful for tracking aggregate access frequency per agent.
1664    pub fn total_recall_count(&self, agent_id: &AgentId) -> Result<u64, AgentRuntimeError> {
1665        let inner = recover_lock(self.inner.lock(), "EpisodicStore::total_recall_count");
1666        Ok(inner
1667            .items
1668            .get(agent_id)
1669            .map_or(0, |items| items.iter().map(|i| i.recall_count).sum()))
1670    }
1671
1672    /// Return summary statistics (count, min, max, mean importance) for `agent_id`.
1673    ///
1674    /// Returns `(0, 0.0, 0.0, 0.0)` if the agent has no stored episodes.
1675    pub fn importance_stats(&self, agent_id: &AgentId) -> Result<(usize, f32, f32, f32), AgentRuntimeError> {
1676        let inner = recover_lock(self.inner.lock(), "EpisodicStore::importance_stats");
1677        let items = inner.items.get(agent_id).map(|v| v.as_slice()).unwrap_or(&[]);
1678        if items.is_empty() {
1679            return Ok((0, 0.0, 0.0, 0.0));
1680        }
1681        let count = items.len();
1682        let min = items.iter().map(|i| i.importance).fold(f32::MAX, f32::min);
1683        let max = items.iter().map(|i| i.importance).fold(f32::MIN, f32::max);
1684        let mean = items.iter().map(|i| i.importance).sum::<f32>() / count as f32;
1685        Ok((count, min, max, mean))
1686    }
1687
1688    /// Return the oldest (first-inserted) episode for `agent_id`, or `None`
1689    /// if the agent has no stored episodes.
1690    pub fn oldest(
1691        &self,
1692        agent_id: &AgentId,
1693    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
1694        let inner = recover_lock(self.inner.lock(), "EpisodicStore::oldest");
1695        let item = inner.items.get(agent_id).and_then(|v| v.first()).cloned();
1696        Ok(item)
1697    }
1698
1699    /// Remove all stored episodes for `agent_id`.
1700    ///
1701    /// Returns the number of episodes that were removed.
1702    pub fn clear_agent(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1703        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::clear_agent");
1704        let count = inner.items.remove(agent_id).map_or(0, |v| v.len());
1705        Ok(count)
1706    }
1707
1708    /// Return the episode with the earliest timestamp for `agent_id`, or `None`
1709    /// if the agent has no stored episodes.
1710    pub fn oldest_episode(
1711        &self,
1712        agent_id: &AgentId,
1713    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
1714        let inner = recover_lock(self.inner.lock(), "EpisodicStore::oldest_episode");
1715        Ok(inner
1716            .items
1717            .get(agent_id)
1718            .and_then(|v| v.iter().min_by_key(|i| i.timestamp))
1719            .cloned())
1720    }
1721
1722    /// Return the episode with the highest importance score for `agent_id`, or `None`
1723    /// if the agent has no stored episodes.  Ties are broken in favour of the
1724    /// later-inserted episode.
1725    pub fn max_importance_episode(
1726        &self,
1727        agent_id: &AgentId,
1728    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
1729        let inner = recover_lock(self.inner.lock(), "EpisodicStore::max_importance_episode");
1730        let item = inner
1731            .items
1732            .get(agent_id)
1733            .and_then(|v| {
1734                v.iter()
1735                    .max_by(|a, b| {
1736                        a.importance
1737                            .partial_cmp(&b.importance)
1738                            .unwrap_or(std::cmp::Ordering::Equal)
1739                    })
1740            })
1741            .cloned();
1742        Ok(item)
1743    }
1744
1745    /// Return the most recently inserted episode for `agent_id`, or `None`
1746    /// if the agent has no stored episodes.
1747    pub fn newest(
1748        &self,
1749        agent_id: &AgentId,
1750    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
1751        let inner = recover_lock(self.inner.lock(), "EpisodicStore::newest");
1752        let item = inner.items.get(agent_id).and_then(|v| v.last()).cloned();
1753        Ok(item)
1754    }
1755
1756    /// Return the total number of stored episodes across all agents.
1757    pub fn len(&self) -> Result<usize, AgentRuntimeError> {
1758        let inner = recover_lock(self.inner.lock(), "EpisodicStore::len");
1759        Ok(inner.items.values().map(|v| v.len()).sum())
1760    }
1761
1762    /// Return `true` if no episodes have been stored.
1763    pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
1764        Ok(self.len()? == 0)
1765    }
1766
1767    /// Return the number of distinct agents that have at least one stored episode.
1768    pub fn agent_count(&self) -> Result<usize, AgentRuntimeError> {
1769        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agent_count");
1770        Ok(inner.items.len())
1771    }
1772
1773    /// Return the number of stored episodes for a specific agent.
1774    ///
1775    /// Returns `0` if the agent has no episodes or has not been seen before.
1776    pub fn agent_memory_count(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1777        let inner = recover_lock(self.inner.lock(), "EpisodicStore::agent_memory_count");
1778        Ok(inner.items.get(agent_id).map_or(0, |v| v.len()))
1779    }
1780
1781    /// Return `true` if `agent_id` has at least one stored episode.
1782    pub fn has_episodes(&self, agent_id: &AgentId) -> Result<bool, AgentRuntimeError> {
1783        let inner = recover_lock(self.inner.lock(), "EpisodicStore::has_episodes");
1784        Ok(inner
1785            .items
1786            .get(agent_id)
1787            .map_or(false, |v| !v.is_empty()))
1788    }
1789
1790    /// Return the most recently inserted episode for `agent_id`, or `None` if
1791    /// the agent has no stored episodes.
1792    ///
1793    /// "Most recent" is determined by `timestamp`.
1794    pub fn latest_episode(
1795        &self,
1796        agent_id: &AgentId,
1797    ) -> Result<Option<MemoryItem>, AgentRuntimeError> {
1798        let inner = recover_lock(self.inner.lock(), "EpisodicStore::latest_episode");
1799        Ok(inner
1800            .items
1801            .get(agent_id)
1802            .and_then(|v| v.iter().max_by_key(|i| i.timestamp))
1803            .cloned())
1804    }
1805
1806    /// Return the maximum single `recall_count` value across all episodes for
1807    /// `agent_id`, or `None` if the agent has no stored episodes.
1808    pub fn max_recall_count_for(&self, agent_id: &AgentId) -> Result<Option<u64>, AgentRuntimeError> {
1809        let inner = recover_lock(self.inner.lock(), "EpisodicStore::max_recall_count_for");
1810        Ok(inner
1811            .items
1812            .get(agent_id)
1813            .and_then(|v| v.iter().map(|i| i.recall_count).max()))
1814    }
1815
1816    /// Return the mean importance score across all episodes for `agent_id`.
1817    ///
1818    /// Returns `0.0` when the agent has no stored episodes.
1819    pub fn avg_importance(&self, agent_id: &AgentId) -> Result<f64, AgentRuntimeError> {
1820        let inner = recover_lock(self.inner.lock(), "EpisodicStore::avg_importance");
1821        let episodes = match inner.items.get(agent_id) {
1822            Some(v) if !v.is_empty() => v,
1823            _ => return Ok(0.0),
1824        };
1825        let sum: f64 = episodes.iter().map(|i| f64::from(i.importance)).sum();
1826        Ok(sum / episodes.len() as f64)
1827    }
1828
1829    /// Return the `(min, max)` importance pair across all episodes for
1830    /// `agent_id`, or `None` when the agent has no stored episodes.
1831    pub fn importance_range(
1832        &self,
1833        agent_id: &AgentId,
1834    ) -> Result<Option<(f32, f32)>, AgentRuntimeError> {
1835        let inner = recover_lock(self.inner.lock(), "EpisodicStore::importance_range");
1836        Ok(inner.items.get(agent_id).and_then(|v| {
1837            if v.is_empty() {
1838                return None;
1839            }
1840            let min = v
1841                .iter()
1842                .map(|i| i.importance)
1843                .fold(f32::INFINITY, f32::min);
1844            let max = v
1845                .iter()
1846                .map(|i| i.importance)
1847                .fold(f32::NEG_INFINITY, f32::max);
1848            Some((min, max))
1849        }))
1850    }
1851
1852    /// Return the total `recall_count` across all episodes for `agent_id`.
1853    ///
1854    /// Returns `0` if the agent has no stored episodes.
1855    pub fn sum_recall_counts(&self, agent_id: &AgentId) -> Result<u64, AgentRuntimeError> {
1856        let inner = recover_lock(self.inner.lock(), "EpisodicStore::sum_recall_counts");
1857        Ok(inner
1858            .items
1859            .get(agent_id)
1860            .map(|v| v.iter().map(|i| i.recall_count).sum())
1861            .unwrap_or(0))
1862    }
1863
1864    /// Return all agent IDs that have at least one stored episode.
1865    ///
1866    /// The order of agents in the returned vector is not guaranteed.
1867    pub fn list_agents(&self) -> Result<Vec<AgentId>, AgentRuntimeError> {
1868        let inner = recover_lock(self.inner.lock(), "EpisodicStore::list_agents");
1869        Ok(inner.items.keys().cloned().collect())
1870    }
1871
1872    /// Remove all stored episodes for `agent_id` and return the number removed.
1873    ///
1874    /// Returns `0` if the agent had no episodes.  Does not affect other agents.
1875    pub fn purge_agent_memories(&self, agent_id: &AgentId) -> Result<usize, AgentRuntimeError> {
1876        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::purge_agent_memories");
1877        let removed = inner.items.remove(agent_id).map_or(0, |v| v.len());
1878        Ok(removed)
1879    }
1880
1881    /// Remove all memories for the given agent.
1882    ///
1883    /// After this call, `recall` for this agent returns an empty list.
1884    pub fn clear_agent_memory(&self, agent_id: &AgentId) -> Result<(), AgentRuntimeError> {
1885        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::clear_agent_memory");
1886        inner.items.remove(agent_id);
1887        Ok(())
1888    }
1889
1890    /// Remove **all** episodes for **all** agents.
1891    ///
1892    /// After this call `len()` returns `0` and `list_agents()` returns an empty slice.
1893    pub fn clear_all(&self) -> Result<(), AgentRuntimeError> {
1894        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::clear_all");
1895        inner.items.clear();
1896        Ok(())
1897    }
1898
1899    /// Export all memories for the given agent as a serializable Vec.
1900    ///
1901    /// Useful for migrating agent state across runtime instances.
1902    pub fn export_agent_memory(&self, agent_id: &AgentId) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1903        let inner = recover_lock(self.inner.lock(), "EpisodicStore::export_agent_memory");
1904        Ok(inner.items.get(agent_id).cloned().unwrap_or_default())
1905    }
1906
1907    /// Import a Vec of MemoryItems for the given agent, replacing any existing memories.
1908    ///
1909    /// The agent's existing memories are completely replaced by the imported items.
1910    pub fn import_agent_memory(&self, agent_id: &AgentId, items: Vec<MemoryItem>) -> Result<(), AgentRuntimeError> {
1911        let mut inner = recover_lock(self.inner.lock(), "EpisodicStore::import_agent_memory");
1912        inner.items.insert(agent_id.clone(), items);
1913        Ok(())
1914    }
1915
1916    /// Bump the `recall_count` of every item whose content equals `content` by `amount`.
1917    ///
1918    /// This method exists to support integration tests that need to simulate prior recall
1919    /// history without accessing private fields. It is not intended for production use.
1920    #[doc(hidden)]
1921    pub fn bump_recall_count_by_content(&self, content: &str, amount: u64) {
1922        let mut inner = recover_lock(
1923            self.inner.lock(),
1924            "EpisodicStore::bump_recall_count_by_content",
1925        );
1926        for agent_items in inner.items.values_mut() {
1927            for item in agent_items.iter_mut() {
1928                if item.content == content {
1929                    item.recall_count = item.recall_count.saturating_add(amount);
1930                }
1931            }
1932        }
1933    }
1934
1935    /// Search episodes for a given `agent_id` whose content contains `query` as a substring.
1936    ///
1937    /// The comparison is case-sensitive.  Returns at most `limit` matching items,
1938    /// ordered by descending importance (same as [`recall`]).
1939    ///
1940    /// [`recall`]: EpisodicStore::recall
1941    pub fn search_by_content(
1942        &self,
1943        agent_id: &AgentId,
1944        query: &str,
1945        limit: usize,
1946    ) -> Result<Vec<MemoryItem>, AgentRuntimeError> {
1947        let inner = recover_lock(self.inner.lock(), "EpisodicStore::search_by_content");
1948        let items = inner.items.get(agent_id).cloned().unwrap_or_default();
1949        drop(inner);
1950        let mut matched: Vec<MemoryItem> = items
1951            .into_iter()
1952            .filter(|item| item.content.contains(query))
1953            .collect();
1954        matched.sort_unstable_by(|a, b| b.importance.partial_cmp(&a.importance).unwrap_or(std::cmp::Ordering::Equal));
1955        if limit > 0 {
1956            matched.truncate(limit);
1957        }
1958        Ok(matched)
1959    }
1960}
1961
1962impl Default for EpisodicStore {
1963    fn default() -> Self {
1964        Self::new()
1965    }
1966}
1967
1968// ── SemanticStore ─────────────────────────────────────────────────────────────
1969
1970/// Stores semantic (fact-based) knowledge as tagged key-value pairs.
1971///
1972/// ## Guarantees
1973/// - Thread-safe via `Arc<Mutex<_>>`
1974/// - Retrieval by tag intersection
1975/// - Optional vector-based similarity search via stored embeddings
1976#[derive(Debug, Clone)]
1977pub struct SemanticStore {
1978    inner: Arc<Mutex<SemanticInner>>,
1979}
1980
1981#[derive(Debug)]
1982struct SemanticInner {
1983    entries: Vec<SemanticEntry>,
1984    expected_dim: Option<usize>,
1985}
1986
1987#[derive(Debug, Clone)]
1988struct SemanticEntry {
1989    key: String,
1990    value: String,
1991    tags: Vec<String>,
1992    embedding: Option<Vec<f32>>,
1993}
1994
1995impl SemanticStore {
1996    /// Create a new empty semantic store.
1997    pub fn new() -> Self {
1998        Self {
1999            inner: Arc::new(Mutex::new(SemanticInner {
2000                entries: Vec::new(),
2001                expected_dim: None,
2002            })),
2003        }
2004    }
2005
2006    /// Store a key-value pair with associated tags.
2007    #[tracing::instrument(skip(self))]
2008    pub fn store(
2009        &self,
2010        key: impl Into<String> + std::fmt::Debug,
2011        value: impl Into<String> + std::fmt::Debug,
2012        tags: Vec<String>,
2013    ) -> Result<(), AgentRuntimeError> {
2014        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::store");
2015        inner.entries.push(SemanticEntry {
2016            key: key.into(),
2017            value: value.into(),
2018            tags,
2019            embedding: None,
2020        });
2021        Ok(())
2022    }
2023
2024    /// Store a key-value pair with an embedding vector for similarity search.
2025    ///
2026    /// # Errors
2027    /// Returns `Err(AgentRuntimeError::Memory)` if `embedding` is empty or dimension mismatches.
2028    #[tracing::instrument(skip(self))]
2029    pub fn store_with_embedding(
2030        &self,
2031        key: impl Into<String> + std::fmt::Debug,
2032        value: impl Into<String> + std::fmt::Debug,
2033        tags: Vec<String>,
2034        embedding: Vec<f32>,
2035    ) -> Result<(), AgentRuntimeError> {
2036        if embedding.is_empty() {
2037            return Err(AgentRuntimeError::Memory(
2038                "embedding vector must not be empty".into(),
2039            ));
2040        }
2041        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::store_with_embedding");
2042        // Validate dimension consistency using expected_dim.
2043        if let Some(expected) = inner.expected_dim {
2044            if expected != embedding.len() {
2045                return Err(AgentRuntimeError::Memory(format!(
2046                    "embedding dimension mismatch: expected {expected}, got {}",
2047                    embedding.len()
2048                )));
2049            }
2050        } else {
2051            inner.expected_dim = Some(embedding.len());
2052        }
2053        // Pre-normalize so retrieve_similar can use dot product instead of
2054        // full cosine similarity for a ~3× speedup on large stores.
2055        let mut embedding = embedding;
2056        normalize_in_place(&mut embedding);
2057        inner.entries.push(SemanticEntry {
2058            key: key.into(),
2059            value: value.into(),
2060            tags,
2061            embedding: Some(embedding),
2062        });
2063        Ok(())
2064    }
2065
2066    /// Retrieve all entries that contain **all** of the given tags.
2067    ///
2068    /// If `tags` is empty, returns all entries.
2069    #[tracing::instrument(skip(self))]
2070    pub fn retrieve(&self, tags: &[&str]) -> Result<Vec<(String, String)>, AgentRuntimeError> {
2071        let inner = recover_lock(self.inner.lock(), "SemanticStore::retrieve");
2072
2073        let results = inner
2074            .entries
2075            .iter()
2076            .filter(|entry| {
2077                tags.iter()
2078                    .all(|t| entry.tags.iter().any(|et| et.as_str() == *t))
2079            })
2080            .map(|e| (e.key.clone(), e.value.clone()))
2081            .collect();
2082
2083        Ok(results)
2084    }
2085
2086    /// Retrieve top-k entries by cosine similarity to `query_embedding`.
2087    ///
2088    /// Only entries that were stored with an embedding (via [`store_with_embedding`])
2089    /// are considered.  Returns `(key, value, similarity)` sorted by descending
2090    /// similarity.
2091    ///
2092    /// Returns `Err(AgentRuntimeError::Memory)` if `query_embedding` dimension mismatches.
2093    ///
2094    /// [`store_with_embedding`]: SemanticStore::store_with_embedding
2095    #[tracing::instrument(skip(self, query_embedding))]
2096    pub fn retrieve_similar(
2097        &self,
2098        query_embedding: &[f32],
2099        top_k: usize,
2100    ) -> Result<Vec<(String, String, f32)>, AgentRuntimeError> {
2101        let inner = recover_lock(self.inner.lock(), "SemanticStore::retrieve_similar");
2102
2103        // Check dimension against expected_dim.
2104        if let Some(expected) = inner.expected_dim {
2105            if expected != query_embedding.len() {
2106                return Err(AgentRuntimeError::Memory(format!(
2107                    "query embedding dimension mismatch: expected {expected}, got {}",
2108                    query_embedding.len()
2109                )));
2110            }
2111        }
2112
2113        // Normalize the query so we can use dot product against pre-normalized
2114        // stored embeddings (equivalent to cosine similarity, but faster).
2115        let query_norm: f32 = query_embedding.iter().map(|x| x * x).sum::<f32>().sqrt();
2116        if query_norm < f32::EPSILON {
2117            return Ok(vec![]);
2118        }
2119        let query_unit: Vec<f32> = query_embedding.iter().map(|x| x / query_norm).collect();
2120
2121        let mut scored: Vec<(String, String, f32)> = inner
2122            .entries
2123            .iter()
2124            .filter_map(|entry| {
2125                entry.embedding.as_ref().map(|emb| {
2126                    let sim = emb
2127                        .iter()
2128                        .zip(query_unit.iter())
2129                        .map(|(a, b)| a * b)
2130                        .sum::<f32>()
2131                        .clamp(-1.0, 1.0);
2132                    (entry.key.clone(), entry.value.clone(), sim)
2133                })
2134            })
2135            .collect();
2136
2137        // Partial sort: when top_k < total candidates, select_nth_unstable_by
2138        // partitions in O(n), then sort only the top top_k in O(top_k log top_k).
2139        let cmp = |a: &(String, String, f32), b: &(String, String, f32)| {
2140            b.2.partial_cmp(&a.2).unwrap_or(std::cmp::Ordering::Equal)
2141        };
2142        if top_k > 0 && top_k < scored.len() {
2143            scored.select_nth_unstable_by(top_k - 1, cmp);
2144            scored[..top_k].sort_unstable_by(cmp);
2145        } else {
2146            scored.sort_unstable_by(cmp);
2147        }
2148        scored.truncate(top_k);
2149        Ok(scored)
2150    }
2151
2152    /// Update the `value` of the first entry whose key matches `key`.
2153    ///
2154    /// Returns `Ok(true)` if found and updated, `Ok(false)` if not found.
2155    pub fn update(&self, key: &str, new_value: impl Into<String>) -> Result<bool, AgentRuntimeError> {
2156        let new_value = new_value.into();
2157        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::update");
2158        if let Some(entry) = inner.entries.iter_mut().find(|e| e.key == key) {
2159            entry.value = new_value;
2160            Ok(true)
2161        } else {
2162            Ok(false)
2163        }
2164    }
2165
2166    /// Look up a single entry by its exact key.
2167    ///
2168    /// Returns `Ok(Some((value, tags)))` if found, `Ok(None)` if not.
2169    pub fn retrieve_by_key(&self, key: &str) -> Result<Option<(String, Vec<String>)>, AgentRuntimeError> {
2170        let inner = recover_lock(self.inner.lock(), "SemanticStore::retrieve_by_key");
2171        Ok(inner.entries.iter().find(|e| e.key == key).map(|e| (e.value.clone(), e.tags.clone())))
2172    }
2173
2174    /// Return `true` if an entry with the given key is stored.
2175    pub fn contains(&self, key: &str) -> Result<bool, AgentRuntimeError> {
2176        let inner = recover_lock(self.inner.lock(), "SemanticStore::contains");
2177        Ok(inner.entries.iter().any(|e| e.key == key))
2178    }
2179
2180    /// Remove all entries.
2181    pub fn clear(&self) -> Result<(), AgentRuntimeError> {
2182        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::clear");
2183        inner.entries.clear();
2184        inner.expected_dim = None;
2185        Ok(())
2186    }
2187
2188    /// Count entries that contain `tag` (case-sensitive).
2189    pub fn count_by_tag(&self, tag: &str) -> Result<usize, AgentRuntimeError> {
2190        let inner = recover_lock(self.inner.lock(), "SemanticStore::count_by_tag");
2191        Ok(inner
2192            .entries
2193            .iter()
2194            .filter(|e| e.tags.iter().any(|t| t.as_str() == tag))
2195            .count())
2196    }
2197
2198    /// Return all unique tags present across all stored entries, in sorted order.
2199    pub fn list_tags(&self) -> Result<Vec<String>, AgentRuntimeError> {
2200        let inner = recover_lock(self.inner.lock(), "SemanticStore::list_tags");
2201        let mut tags: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
2202        for entry in &inner.entries {
2203            for tag in &entry.tags {
2204                tags.insert(tag.clone());
2205            }
2206        }
2207        Ok(tags.into_iter().collect())
2208    }
2209
2210    /// Remove all entries that have the given tag.
2211    ///
2212    /// Returns the number of entries removed.
2213    pub fn remove_entries_with_tag(&self, tag: &str) -> Result<usize, AgentRuntimeError> {
2214        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::remove_entries_with_tag");
2215        let before = inner.entries.len();
2216        inner.entries.retain(|e| !e.tags.iter().any(|t| t == tag));
2217        Ok(before - inner.entries.len())
2218    }
2219
2220    /// Return the tag that appears most often across all entries.
2221    ///
2222    /// Returns `None` if there are no tagged entries.
2223    pub fn most_common_tag(&self) -> Result<Option<String>, AgentRuntimeError> {
2224        let inner = recover_lock(self.inner.lock(), "SemanticStore::most_common_tag");
2225        let mut counts: std::collections::HashMap<&str, usize> = std::collections::HashMap::new();
2226        for entry in &inner.entries {
2227            for tag in &entry.tags {
2228                *counts.entry(tag.as_str()).or_insert(0) += 1;
2229            }
2230        }
2231        Ok(counts.into_iter().max_by_key(|(_, c)| *c).map(|(t, _)| t.to_string()))
2232    }
2233
2234    /// Return the keys of all entries that have the given tag.
2235    pub fn keys_for_tag(&self, tag: &str) -> Result<Vec<String>, AgentRuntimeError> {
2236        let inner = recover_lock(self.inner.lock(), "SemanticStore::keys_for_tag");
2237        let keys = inner
2238            .entries
2239            .iter()
2240            .filter(|e| e.tags.iter().any(|t| t == tag))
2241            .map(|e| e.key.clone())
2242            .collect();
2243        Ok(keys)
2244    }
2245
2246    /// Return a sorted list of every distinct tag appearing across all entries.
2247    pub fn unique_tags(&self) -> Result<Vec<String>, AgentRuntimeError> {
2248        let inner = recover_lock(self.inner.lock(), "SemanticStore::unique_tags");
2249        let mut tags: Vec<String> = inner
2250            .entries
2251            .iter()
2252            .flat_map(|e| e.tags.iter().cloned())
2253            .collect::<std::collections::HashSet<_>>()
2254            .into_iter()
2255            .collect();
2256        tags.sort_unstable();
2257        Ok(tags)
2258    }
2259
2260    /// Return the number of distinct tags across all stored entries.
2261    ///
2262    /// Equivalent to `list_tags()?.len()` but avoids allocating the full tag list.
2263    pub fn tag_count(&self) -> Result<usize, AgentRuntimeError> {
2264        let inner = recover_lock(self.inner.lock(), "SemanticStore::tag_count");
2265        let distinct: std::collections::HashSet<&str> = inner
2266            .entries
2267            .iter()
2268            .flat_map(|e| e.tags.iter().map(|t| t.as_str()))
2269            .collect();
2270        Ok(distinct.len())
2271    }
2272
2273    /// Return the number of entries that have an associated embedding vector.
2274    pub fn entry_count_with_embedding(&self) -> Result<usize, AgentRuntimeError> {
2275        let inner = recover_lock(self.inner.lock(), "SemanticStore::entry_count_with_embedding");
2276        Ok(inner.entries.iter().filter(|e| e.embedding.is_some()).count())
2277    }
2278
2279    /// Return the stored value for the first entry whose key matches `key`,
2280    /// or `None` if no such entry exists.
2281    ///
2282    /// Simpler alternative to [`retrieve_by_key`] when only the value is needed
2283    /// and tags are not required.
2284    ///
2285    /// [`retrieve_by_key`]: SemanticStore::retrieve_by_key
2286    pub fn get_value(&self, key: &str) -> Result<Option<String>, AgentRuntimeError> {
2287        let inner = recover_lock(self.inner.lock(), "SemanticStore::get_value");
2288        Ok(inner
2289            .entries
2290            .iter()
2291            .find(|e| e.key == key)
2292            .map(|e| e.value.clone()))
2293    }
2294
2295    /// Return the tags for the first entry whose key matches `key`.
2296    ///
2297    /// Returns `None` if no entry with that key exists.
2298    pub fn get_tags(&self, key: &str) -> Result<Option<Vec<String>>, AgentRuntimeError> {
2299        let inner = recover_lock(self.inner.lock(), "SemanticStore::get_tags");
2300        Ok(inner.entries.iter().find(|e| e.key == key).map(|e| e.tags.clone()))
2301    }
2302
2303    /// Return all entry keys that contain `tag` (case-sensitive).
2304    pub fn keys_with_tag(&self, tag: &str) -> Result<Vec<String>, AgentRuntimeError> {
2305        let inner = recover_lock(self.inner.lock(), "SemanticStore::keys_with_tag");
2306        Ok(inner
2307            .entries
2308            .iter()
2309            .filter(|e| e.tags.iter().any(|t| t.as_str() == tag))
2310            .map(|e| e.key.clone())
2311            .collect())
2312    }
2313
2314    /// Return the tags associated with the first entry matching `key`, or
2315    /// `None` if no entry with that key exists.
2316    pub fn tags_for(&self, key: &str) -> Result<Option<Vec<String>>, AgentRuntimeError> {
2317        let inner = recover_lock(self.inner.lock(), "SemanticStore::tags_for");
2318        Ok(inner
2319            .entries
2320            .iter()
2321            .find(|e| e.key == key)
2322            .map(|e| e.tags.clone()))
2323    }
2324
2325    /// Return `true` if at least one entry with the given `key` exists.
2326    pub fn has_key(&self, key: &str) -> Result<bool, AgentRuntimeError> {
2327        let inner = recover_lock(self.inner.lock(), "SemanticStore::has_key");
2328        Ok(inner.entries.iter().any(|e| e.key == key))
2329    }
2330
2331    /// Return the value string of the first entry whose key matches `key`,
2332    /// or `None` if no such entry exists.
2333    pub fn value_for(&self, key: &str) -> Result<Option<String>, AgentRuntimeError> {
2334        let inner = recover_lock(self.inner.lock(), "SemanticStore::value_for");
2335        Ok(inner
2336            .entries
2337            .iter()
2338            .find(|e| e.key == key)
2339            .map(|e| e.value.clone()))
2340    }
2341
2342    /// Return the number of entries that have no tags.
2343    pub fn entries_without_tags(&self) -> Result<usize, AgentRuntimeError> {
2344        let inner = recover_lock(self.inner.lock(), "SemanticStore::entries_without_tags");
2345        Ok(inner.entries.iter().filter(|e| e.tags.is_empty()).count())
2346    }
2347
2348    /// Return the keys of all entries that have no tags.
2349    /// Return the key of the entry with the most tags, or `None` if the store
2350    /// is empty.
2351    pub fn most_tagged_key(&self) -> Result<Option<String>, AgentRuntimeError> {
2352        let inner = recover_lock(self.inner.lock(), "SemanticStore::most_tagged_key");
2353        Ok(inner
2354            .entries
2355            .iter()
2356            .max_by_key(|e| e.tags.len())
2357            .map(|e| e.key.clone()))
2358    }
2359
2360    /// Return the count of entries whose value contains `substring`.
2361    /// Rename `old_tag` to `new_tag` across all entries.
2362    ///
2363    /// Returns the number of entries that were modified.
2364    pub fn rename_tag(&self, old_tag: &str, new_tag: &str) -> Result<usize, AgentRuntimeError> {
2365        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::rename_tag");
2366        let mut count = 0;
2367        for entry in &mut inner.entries {
2368            for tag in &mut entry.tags {
2369                if tag == old_tag {
2370                    *tag = new_tag.to_string();
2371                    count += 1;
2372                }
2373            }
2374        }
2375        Ok(count)
2376    }
2377
2378    /// Return the count of entries whose value contains `substring`.
2379    pub fn count_matching_value(&self, substring: &str) -> Result<usize, AgentRuntimeError> {
2380        let inner = recover_lock(self.inner.lock(), "SemanticStore::count_matching_value");
2381        Ok(inner.entries.iter().filter(|e| e.value.contains(substring)).count())
2382    }
2383
2384    /// Return the keys of all entries that have no tags.
2385    pub fn entries_with_no_tags(&self) -> Result<Vec<String>, AgentRuntimeError> {
2386        let inner = recover_lock(self.inner.lock(), "SemanticStore::entries_with_no_tags");
2387        Ok(inner
2388            .entries
2389            .iter()
2390            .filter(|e| e.tags.is_empty())
2391            .map(|e| e.key.clone())
2392            .collect())
2393    }
2394
2395    /// Return the mean number of tags per entry.
2396    ///
2397    /// Returns `0.0` when the store is empty.
2398    pub fn avg_tag_count_per_entry(&self) -> Result<f64, AgentRuntimeError> {
2399        let inner = recover_lock(self.inner.lock(), "SemanticStore::avg_tag_count_per_entry");
2400        let n = inner.entries.len();
2401        if n == 0 {
2402            return Ok(0.0);
2403        }
2404        let total: usize = inner.entries.iter().map(|e| e.tags.len()).sum();
2405        Ok(total as f64 / n as f64)
2406    }
2407
2408    /// Return the key of the most recently inserted entry, or `None` if empty.
2409    pub fn most_recent_key(&self) -> Result<Option<String>, AgentRuntimeError> {
2410        let inner = recover_lock(self.inner.lock(), "SemanticStore::most_recent_key");
2411        Ok(inner.entries.last().map(|e| e.key.clone()))
2412    }
2413
2414    /// Return the key of the earliest inserted entry, or `None` if empty.
2415    pub fn oldest_key(&self) -> Result<Option<String>, AgentRuntimeError> {
2416        let inner = recover_lock(self.inner.lock(), "SemanticStore::oldest_key");
2417        Ok(inner.entries.first().map(|e| e.key.clone()))
2418    }
2419
2420    /// Remove all entries whose key equals `key`.
2421    ///
2422    /// Returns the number of entries removed.
2423    pub fn remove_by_key(&self, key: &str) -> Result<usize, AgentRuntimeError> {
2424        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::remove_by_key");
2425        let before = inner.entries.len();
2426        inner.entries.retain(|e| e.key != key);
2427        Ok(before - inner.entries.len())
2428    }
2429
2430    /// Return the number of entries that include `tag` in their tag list.
2431    pub fn entry_count_with_tag(&self, tag: &str) -> Result<usize, AgentRuntimeError> {
2432        let inner = recover_lock(self.inner.lock(), "SemanticStore::entry_count_with_tag");
2433        Ok(inner
2434            .entries
2435            .iter()
2436            .filter(|e| e.tags.iter().any(|t| t == tag))
2437            .count())
2438    }
2439
2440    /// Return all stored entry keys in insertion order.
2441    ///
2442    /// Duplicate keys (if any) will appear multiple times.
2443    pub fn list_keys(&self) -> Result<Vec<String>, AgentRuntimeError> {
2444        let inner = recover_lock(self.inner.lock(), "SemanticStore::list_keys");
2445        Ok(inner.entries.iter().map(|e| e.key.clone()).collect())
2446    }
2447
2448    /// Return all stored entry keys in insertion order.
2449    ///
2450    /// Alias for [`list_keys`] using more idiomatic naming.
2451    ///
2452    /// [`list_keys`]: SemanticStore::list_keys
2453    pub fn keys(&self) -> Result<Vec<String>, AgentRuntimeError> {
2454        self.list_keys()
2455    }
2456
2457    /// Return the stored value for every entry, in insertion order.
2458    pub fn values(&self) -> Result<Vec<String>, AgentRuntimeError> {
2459        let inner = recover_lock(self.inner.lock(), "SemanticStore::values");
2460        Ok(inner.entries.iter().map(|e| e.value.clone()).collect())
2461    }
2462
2463    /// Return all keys that contain `pattern` as a substring (case-insensitive).
2464    ///
2465    /// Useful for prefix/contains searches without loading values.
2466    pub fn keys_matching(&self, pattern: &str) -> Result<Vec<String>, AgentRuntimeError> {
2467        let inner = recover_lock(self.inner.lock(), "SemanticStore::keys_matching");
2468        let lower = pattern.to_ascii_lowercase();
2469        Ok(inner
2470            .entries
2471            .iter()
2472            .filter(|e| e.key.to_ascii_lowercase().contains(&lower))
2473            .map(|e| e.key.clone())
2474            .collect())
2475    }
2476
2477    /// Update the stored value of the first entry whose key matches `key`.
2478    ///
2479    /// Returns `Ok(true)` if found and updated, `Ok(false)` if not found.
2480    pub fn update_value(
2481        &self,
2482        key: &str,
2483        new_value: impl Into<String>,
2484    ) -> Result<bool, AgentRuntimeError> {
2485        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::update_value");
2486        if let Some(entry) = inner.entries.iter_mut().find(|e| e.key == key) {
2487            entry.value = new_value.into();
2488            Ok(true)
2489        } else {
2490            Ok(false)
2491        }
2492    }
2493
2494    /// Return all stored entries as a `HashMap<key, value>`.
2495    ///
2496    /// Useful for serialization or bulk inspection.  Tags and embeddings are
2497    /// discarded; use `iter()` / `retrieve_by_key` when those are needed.
2498    pub fn to_map(&self) -> Result<std::collections::HashMap<String, String>, AgentRuntimeError> {
2499        let inner = recover_lock(self.inner.lock(), "SemanticStore::to_map");
2500        Ok(inner
2501            .entries
2502            .iter()
2503            .map(|e| (e.key.clone(), e.value.clone()))
2504            .collect())
2505    }
2506
2507    /// Update the tags of the first entry whose key matches `key`.
2508    ///
2509    /// Returns `Ok(true)` if found and updated, `Ok(false)` if not found.
2510    pub fn update_tags(
2511        &self,
2512        key: &str,
2513        new_tags: Vec<String>,
2514    ) -> Result<bool, AgentRuntimeError> {
2515        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::update_tags");
2516        if let Some(entry) = inner.entries.iter_mut().find(|e| e.key == key) {
2517            entry.tags = new_tags;
2518            Ok(true)
2519        } else {
2520            Ok(false)
2521        }
2522    }
2523
2524    /// Return the sum of byte lengths of all stored values.
2525    ///
2526    /// Returns `0` if no entries have been stored.
2527    pub fn total_value_bytes(&self) -> Result<usize, AgentRuntimeError> {
2528        let inner = recover_lock(self.inner.lock(), "SemanticStore::total_value_bytes");
2529        Ok(inner.entries.iter().map(|e| e.value.len()).sum())
2530    }
2531
2532    /// Return the mean byte length of all stored values.
2533    ///
2534    /// Returns `0.0` if no entries have been stored.
2535    pub fn avg_value_bytes(&self) -> Result<f64, AgentRuntimeError> {
2536        let inner = recover_lock(self.inner.lock(), "SemanticStore::avg_value_bytes");
2537        if inner.entries.is_empty() {
2538            return Ok(0.0);
2539        }
2540        let total: usize = inner.entries.iter().map(|e| e.value.len()).sum();
2541        Ok(total as f64 / inner.entries.len() as f64)
2542    }
2543
2544    /// Return the byte length of the longest stored value.
2545    ///
2546    /// Returns `0` if no entries have been stored.
2547    pub fn max_value_bytes(&self) -> Result<usize, AgentRuntimeError> {
2548        let inner = recover_lock(self.inner.lock(), "SemanticStore::max_value_bytes");
2549        Ok(inner.entries.iter().map(|e| e.value.len()).max().unwrap_or(0))
2550    }
2551
2552    /// Return the byte length of the shortest stored value.
2553    ///
2554    /// Returns `0` if no entries have been stored.
2555    pub fn min_value_bytes(&self) -> Result<usize, AgentRuntimeError> {
2556        let inner = recover_lock(self.inner.lock(), "SemanticStore::min_value_bytes");
2557        Ok(inner.entries.iter().map(|e| e.value.len()).min().unwrap_or(0))
2558    }
2559
2560    /// Return all stored keys in ascending sorted order.
2561    pub fn all_keys(&self) -> Result<Vec<String>, AgentRuntimeError> {
2562        let inner = recover_lock(self.inner.lock(), "SemanticStore::all_keys");
2563        let mut keys: Vec<String> = inner.entries.iter().map(|e| e.key.clone()).collect();
2564        keys.sort_unstable();
2565        Ok(keys)
2566    }
2567
2568    /// Return the total number of stored entries.
2569    pub fn len(&self) -> Result<usize, AgentRuntimeError> {
2570        let inner = recover_lock(self.inner.lock(), "SemanticStore::len");
2571        Ok(inner.entries.len())
2572    }
2573
2574    /// Return `true` if no entries have been stored.
2575    pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
2576        Ok(self.len()? == 0)
2577    }
2578
2579    /// Return the number of stored entries.
2580    ///
2581    /// Alias for [`len`] using conventional naming.
2582    ///
2583    /// [`len`]: SemanticStore::len
2584    pub fn count(&self) -> Result<usize, AgentRuntimeError> {
2585        self.len()
2586    }
2587
2588    /// Remove the first entry with key `key`.
2589    ///
2590    /// Returns `Ok(true)` if an entry was found and removed, `Ok(false)` if
2591    /// no entry with that key exists.
2592    pub fn remove(&self, key: &str) -> Result<bool, AgentRuntimeError> {
2593        let mut inner = recover_lock(self.inner.lock(), "SemanticStore::remove");
2594        let before = inner.entries.len();
2595        inner.entries.retain(|e| e.key != key);
2596        Ok(inner.entries.len() < before)
2597    }
2598
2599}
2600
2601impl Default for SemanticStore {
2602    fn default() -> Self {
2603        Self::new()
2604    }
2605}
2606
2607// ── WorkingMemory ─────────────────────────────────────────────────────────────
2608
2609/// A bounded, key-value working memory for transient agent state.
2610///
2611/// When capacity is exceeded, the oldest entry (by insertion order) is evicted.
2612///
2613/// ## Guarantees
2614/// - Thread-safe via `Arc<Mutex<_>>`
2615/// - Bounded: never exceeds `capacity` entries
2616/// - Deterministic eviction: LRU (oldest insertion first)
2617#[derive(Debug, Clone)]
2618pub struct WorkingMemory {
2619    capacity: usize,
2620    inner: Arc<Mutex<WorkingInner>>,
2621}
2622
2623#[derive(Debug)]
2624struct WorkingInner {
2625    map: HashMap<String, String>,
2626    order: VecDeque<String>,
2627}
2628
2629impl WorkingMemory {
2630    /// Create a new `WorkingMemory` with the given capacity.
2631    ///
2632    /// # Returns
2633    /// - `Ok(WorkingMemory)` — on success
2634    /// - `Err(AgentRuntimeError::Memory)` — if `capacity == 0`
2635    pub fn new(capacity: usize) -> Result<Self, AgentRuntimeError> {
2636        if capacity == 0 {
2637            return Err(AgentRuntimeError::Memory(
2638                "WorkingMemory capacity must be > 0".into(),
2639            ));
2640        }
2641        Ok(Self {
2642            capacity,
2643            inner: Arc::new(Mutex::new(WorkingInner {
2644                map: HashMap::new(),
2645                order: VecDeque::new(),
2646            })),
2647        })
2648    }
2649
2650    /// Insert or update a key-value pair, evicting the oldest entry if over capacity.
2651    #[tracing::instrument(skip(self))]
2652    pub fn set(
2653        &self,
2654        key: impl Into<String> + std::fmt::Debug,
2655        value: impl Into<String> + std::fmt::Debug,
2656    ) -> Result<(), AgentRuntimeError> {
2657        let key = key.into();
2658        let value = value.into();
2659        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::set");
2660
2661        // Remove existing key from order tracking if present
2662        if inner.map.contains_key(&key) {
2663            inner.order.retain(|k| k != &key);
2664        } else if inner.map.len() >= self.capacity {
2665            // Evict oldest
2666            if let Some(oldest) = inner.order.pop_front() {
2667                inner.map.remove(&oldest);
2668            }
2669        }
2670
2671        inner.order.push_back(key.clone());
2672        inner.map.insert(key, value);
2673        Ok(())
2674    }
2675
2676    /// Retrieve a value by key.
2677    ///
2678    /// # Returns
2679    /// - `Some(value)` — if the key exists
2680    /// - `None` — if not found
2681    #[tracing::instrument(skip(self))]
2682    pub fn get(&self, key: &str) -> Result<Option<String>, AgentRuntimeError> {
2683        let inner = recover_lock(self.inner.lock(), "WorkingMemory::get");
2684        Ok(inner.map.get(key).cloned())
2685    }
2686
2687    /// Insert multiple key-value pairs with a single lock acquisition.
2688    ///
2689    /// Each entry follows the same eviction semantics as [`set`]: if the key
2690    /// already exists it is updated in-place; if inserting a new key would
2691    /// exceed capacity, the oldest key is evicted first.
2692    ///
2693    /// [`set`]: WorkingMemory::set
2694    pub fn set_many(
2695        &self,
2696        pairs: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
2697    ) -> Result<(), AgentRuntimeError> {
2698        let capacity = self.capacity;
2699        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::set_many");
2700        for (key, value) in pairs {
2701            let key: String = key.into();
2702            let value: String = value.into();
2703            if inner.map.contains_key(&key) {
2704                inner.order.retain(|k| k != &key);
2705            } else if inner.map.len() >= capacity {
2706                if let Some(oldest) = inner.order.pop_front() {
2707                    inner.map.remove(&oldest);
2708                }
2709            }
2710            inner.order.push_back(key.clone());
2711            inner.map.insert(key, value);
2712        }
2713        Ok(())
2714    }
2715
2716    /// Update the value of an existing key without inserting if absent.
2717    ///
2718    /// Insert `key → value` only if `key` is not already present.
2719    ///
2720    /// Returns `Ok(true)` if the entry was inserted, `Ok(false)` if the key
2721    /// was already present (the existing value is left unchanged).  Capacity
2722    /// limits and LRU eviction apply as with [`set`].
2723    ///
2724    /// [`set`]: WorkingMemory::set
2725    pub fn set_if_absent(
2726        &self,
2727        key: impl Into<String> + std::fmt::Debug,
2728        value: impl Into<String> + std::fmt::Debug,
2729    ) -> Result<bool, AgentRuntimeError> {
2730        let key = key.into();
2731        {
2732            let inner = recover_lock(self.inner.lock(), "WorkingMemory::set_if_absent");
2733            if inner.map.contains_key(&key) {
2734                return Ok(false);
2735            }
2736        }
2737        self.set(key, value)?;
2738        Ok(true)
2739    }
2740
2741    /// Returns `Ok(true)` if the key existed and was updated, `Ok(false)` if not present.
2742    pub fn update_if_exists(
2743        &self,
2744        key: &str,
2745        value: impl Into<String>,
2746    ) -> Result<bool, AgentRuntimeError> {
2747        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::update_if_exists");
2748        if let Some(v) = inner.map.get_mut(key) {
2749            *v = value.into();
2750            Ok(true)
2751        } else {
2752            Ok(false)
2753        }
2754    }
2755
2756    /// Update multiple existing keys in a single lock acquisition.
2757    ///
2758    /// Each `(key, value)` pair is applied only if the key is already present
2759    /// (same semantics as [`update_if_exists`]).  Returns the number of keys
2760    /// that were found and updated.
2761    ///
2762    /// [`update_if_exists`]: WorkingMemory::update_if_exists
2763    pub fn update_many(
2764        &self,
2765        pairs: impl IntoIterator<Item = (impl Into<String>, impl Into<String>)>,
2766    ) -> Result<usize, AgentRuntimeError> {
2767        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::update_many");
2768        let mut updated = 0;
2769        for (key, value) in pairs {
2770            let key: String = key.into();
2771            if let Some(v) = inner.map.get_mut(&key) {
2772                *v = value.into();
2773                updated += 1;
2774            }
2775        }
2776        Ok(updated)
2777    }
2778
2779    /// Return all keys whose names start with `prefix`, in insertion order.
2780    ///
2781    /// Useful for namespace-prefixed keys such as `"user:name"`,
2782    /// `"user:email"` where all user-related keys share the `"user:"` prefix.
2783    pub fn keys_starting_with(&self, prefix: &str) -> Result<Vec<String>, AgentRuntimeError> {
2784        let inner = recover_lock(self.inner.lock(), "WorkingMemory::keys_starting_with");
2785        Ok(inner
2786            .map
2787            .keys()
2788            .filter(|k| k.starts_with(prefix))
2789            .cloned()
2790            .collect())
2791    }
2792
2793    /// Return all `(key, value)` pairs whose value contains `pattern` as a
2794    /// substring.  Comparison is case-sensitive.
2795    ///
2796    /// Useful for scanning working memory for values that match a keyword
2797    /// without iterating the full map externally.
2798    pub fn values_matching(&self, pattern: &str) -> Result<Vec<(String, String)>, AgentRuntimeError> {
2799        let inner = recover_lock(self.inner.lock(), "WorkingMemory::values_matching");
2800        Ok(inner
2801            .map
2802            .iter()
2803            .filter(|(_, v)| v.contains(pattern))
2804            .map(|(k, v)| (k.clone(), v.clone()))
2805            .collect())
2806    }
2807
2808    /// Return the byte length of the value stored at `key`, or `None` if the
2809    /// key is not present.
2810    ///
2811    /// Useful for estimating memory usage without cloning the value string.
2812    pub fn value_length(&self, key: &str) -> Result<Option<usize>, AgentRuntimeError> {
2813        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_length");
2814        Ok(inner.map.get(key).map(|v| v.len()))
2815    }
2816
2817    /// Rename a key without changing its value or insertion order.
2818    ///
2819    /// Return `true` if **all** of the given keys are currently stored.
2820    ///
2821    /// An empty iterator returns `true` vacuously.
2822    pub fn contains_all<'a>(&self, keys: impl IntoIterator<Item = &'a str>) -> Result<bool, AgentRuntimeError> {
2823        let inner = recover_lock(self.inner.lock(), "WorkingMemory::contains_all");
2824        Ok(keys.into_iter().all(|k| inner.map.contains_key(k)))
2825    }
2826
2827    /// Return `true` if **any** of the given keys is currently stored.
2828    ///
2829    /// An empty iterator returns `false`.
2830    pub fn has_any_key<'a>(&self, keys: impl IntoIterator<Item = &'a str>) -> Result<bool, AgentRuntimeError> {
2831        let inner = recover_lock(self.inner.lock(), "WorkingMemory::has_any_key");
2832        Ok(keys.into_iter().any(|k| inner.map.contains_key(k)))
2833    }
2834
2835    /// Returns `true` if `old_key` existed and was renamed.  Returns `false` if
2836    /// `old_key` is not present.  If `new_key` already exists it is overwritten
2837    /// and its old value is lost.
2838    pub fn rename(
2839        &self,
2840        old_key: &str,
2841        new_key: impl Into<String>,
2842    ) -> Result<bool, AgentRuntimeError> {
2843        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::rename");
2844        let value = match inner.map.remove(old_key) {
2845            None => return Ok(false),
2846            Some(v) => v,
2847        };
2848        let new_key = new_key.into();
2849        // Update the insertion-order list.
2850        if let Some(pos) = inner.order.iter().position(|k| k == old_key) {
2851            inner.order[pos] = new_key.clone();
2852        }
2853        inner.map.insert(new_key, value);
2854        Ok(true)
2855    }
2856
2857    /// Retrieve multiple values in a single lock acquisition.
2858    ///
2859    /// Returns a `Vec` of the same length as `keys`.  Each entry is `Some(value)`
2860    /// if the key is present, `None` if not.
2861    pub fn get_many(&self, keys: &[&str]) -> Result<Vec<Option<String>>, AgentRuntimeError> {
2862        let inner = recover_lock(self.inner.lock(), "WorkingMemory::get_many");
2863        Ok(keys.iter().map(|k| inner.map.get(*k).cloned()).collect())
2864    }
2865
2866    /// Return `true` if a value is stored under `key`.
2867    pub fn contains(&self, key: &str) -> Result<bool, AgentRuntimeError> {
2868        let inner = recover_lock(self.inner.lock(), "WorkingMemory::contains");
2869        Ok(inner.map.contains_key(key))
2870    }
2871
2872    /// Return the value associated with `key`, or `default` if not set.
2873    pub fn get_or_default(
2874        &self,
2875        key: &str,
2876        default: impl Into<String>,
2877    ) -> Result<String, AgentRuntimeError> {
2878        Ok(self.get(key)?.unwrap_or_else(|| default.into()))
2879    }
2880
2881    /// Remove a single entry by key.  Returns `true` if the key existed.
2882    pub fn remove(&self, key: &str) -> Result<bool, AgentRuntimeError> {
2883        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::remove");
2884        if inner.map.remove(key).is_some() {
2885            inner.order.retain(|k| k != key);
2886            Ok(true)
2887        } else {
2888            Ok(false)
2889        }
2890    }
2891
2892    /// Return all keys in insertion order.
2893    pub fn keys(&self) -> Result<Vec<String>, AgentRuntimeError> {
2894        let inner = recover_lock(self.inner.lock(), "WorkingMemory::keys");
2895        Ok(inner.order.iter().cloned().collect())
2896    }
2897
2898    /// Return all values in insertion order (parallel to [`keys`]).
2899    ///
2900    /// [`keys`]: WorkingMemory::keys
2901    pub fn values(&self) -> Result<Vec<String>, AgentRuntimeError> {
2902        let inner = recover_lock(self.inner.lock(), "WorkingMemory::values");
2903        Ok(inner
2904            .order
2905            .iter()
2906            .filter_map(|k| inner.map.get(k).cloned())
2907            .collect())
2908    }
2909
2910    /// Remove all entries from working memory.
2911    pub fn clear(&self) -> Result<(), AgentRuntimeError> {
2912        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::clear");
2913        inner.map.clear();
2914        inner.order.clear();
2915        Ok(())
2916    }
2917
2918    /// Remove all entries and return them as a `Vec<(key, value)>` in insertion order.
2919    ///
2920    /// Return all entries as `(key, value)` pairs sorted alphabetically by key.
2921    ///
2922    /// Unlike [`iter`]/[`entries`] which return entries in insertion order,
2923    /// this always returns a deterministically ordered snapshot.
2924    ///
2925    /// [`iter`]: WorkingMemory::iter
2926    /// [`entries`]: WorkingMemory::entries
2927    pub fn iter_sorted(&self) -> Result<Vec<(String, String)>, AgentRuntimeError> {
2928        let inner = recover_lock(self.inner.lock(), "WorkingMemory::iter_sorted");
2929        let mut pairs: Vec<(String, String)> = inner
2930            .map
2931            .iter()
2932            .map(|(k, v)| (k.clone(), v.clone()))
2933            .collect();
2934        pairs.sort_unstable_by(|a, b| a.0.cmp(&b.0));
2935        Ok(pairs)
2936    }
2937
2938    /// After this call the memory is empty. Useful for atomically moving the
2939    /// contents to another data structure.
2940    pub fn drain(&self) -> Result<Vec<(String, String)>, AgentRuntimeError> {
2941        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::drain");
2942        let pairs: Vec<(String, String)> = inner
2943            .order
2944            .iter()
2945            .filter_map(|k| inner.map.get(k).map(|v| (k.clone(), v.clone())))
2946            .collect();
2947        inner.map.clear();
2948        inner.order.clear();
2949        Ok(pairs)
2950    }
2951
2952    /// Clone all current entries into a `HashMap<String, String>`.
2953    ///
2954    /// Unlike [`drain`], this leaves the working memory unchanged.
2955    ///
2956    /// [`drain`]: WorkingMemory::drain
2957    pub fn snapshot(&self) -> Result<std::collections::HashMap<String, String>, AgentRuntimeError> {
2958        let inner = recover_lock(self.inner.lock(), "WorkingMemory::snapshot");
2959        Ok(inner.map.clone())
2960    }
2961
2962    /// Return the current number of entries.
2963    pub fn len(&self) -> Result<usize, AgentRuntimeError> {
2964        let inner = recover_lock(self.inner.lock(), "WorkingMemory::len");
2965        Ok(inner.map.len())
2966    }
2967
2968    /// Return `true` if no entries are stored.
2969    pub fn is_empty(&self) -> Result<bool, AgentRuntimeError> {
2970        Ok(self.len()? == 0)
2971    }
2972
2973    /// Iterate over all key-value pairs in insertion order.
2974    ///
2975    /// Equivalent to [`entries`]; provided as a more idiomatic name
2976    /// for `for`-loop patterns.
2977    ///
2978    /// [`entries`]: WorkingMemory::entries
2979    pub fn iter(&self) -> Result<Vec<(String, String)>, AgentRuntimeError> {
2980        self.entries()
2981    }
2982
2983    /// Return all key-value pairs in insertion order.
2984    pub fn entries(&self) -> Result<Vec<(String, String)>, AgentRuntimeError> {
2985        let inner = recover_lock(self.inner.lock(), "WorkingMemory::entries");
2986        let entries = inner
2987            .order
2988            .iter()
2989            .filter_map(|k| inner.map.get(k).map(|v| (k.clone(), v.clone())))
2990            .collect();
2991        Ok(entries)
2992    }
2993
2994    /// Remove and return the oldest entry (first inserted that is still present).
2995    ///
2996    /// Returns `None` if the memory is empty.
2997    pub fn pop_oldest(&self) -> Result<Option<(String, String)>, AgentRuntimeError> {
2998        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::pop_oldest");
2999        if let Some(key) = inner.order.pop_front() {
3000            let value = inner.map.remove(&key).unwrap_or_default();
3001            Ok(Some((key, value)))
3002        } else {
3003            Ok(None)
3004        }
3005    }
3006
3007    /// Peek at the oldest entry without removing it.
3008    ///
3009    /// Returns `None` if the memory is empty.  Unlike [`pop_oldest`] this
3010    /// method does not modify the store.
3011    ///
3012    /// [`pop_oldest`]: WorkingMemory::pop_oldest
3013    pub fn peek_oldest(&self) -> Result<Option<(String, String)>, AgentRuntimeError> {
3014        let inner = recover_lock(self.inner.lock(), "WorkingMemory::peek_oldest");
3015        Ok(inner.order.front().and_then(|key| {
3016            inner.map.get(key).map(|val| (key.clone(), val.clone()))
3017        }))
3018    }
3019
3020    /// Return the maximum number of entries this store can hold.
3021    ///
3022    /// When [`len`] reaches this value, the oldest entry is evicted on the
3023    /// next [`set`] call for a new key.
3024    ///
3025    /// [`len`]: WorkingMemory::len
3026    /// [`set`]: WorkingMemory::set
3027    pub fn capacity(&self) -> usize {
3028        self.capacity
3029    }
3030
3031    /// Return the current fill ratio as `len / capacity`.
3032    ///
3033    /// Returns a value in `[0.0, 1.0]`.  `1.0` means the memory is full and
3034    /// the next insert will evict the oldest entry.
3035    pub fn fill_ratio(&self) -> Result<f64, AgentRuntimeError> {
3036        Ok(self.len()? as f64 / self.capacity as f64)
3037    }
3038
3039    /// Return `true` when the number of stored entries equals the configured capacity.
3040    ///
3041    /// When `true`, the next [`set`] call for a new key will evict the oldest entry.
3042    ///
3043    /// [`set`]: WorkingMemory::set
3044    pub fn is_at_capacity(&self) -> Result<bool, AgentRuntimeError> {
3045        Ok(self.len()? >= self.capacity)
3046    }
3047
3048    /// Remove all entries whose key begins with `prefix`.
3049    ///
3050    /// Returns the number of entries removed.  Removal preserves insertion order
3051    /// for the surviving entries.
3052    pub fn remove_keys_starting_with(&self, prefix: &str) -> Result<usize, AgentRuntimeError> {
3053        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::remove_keys_starting_with");
3054        let before = inner.map.len();
3055        inner.map.retain(|k, _| !k.starts_with(prefix));
3056        Ok(before - inner.map.len())
3057    }
3058
3059    /// Return the total number of bytes used by all stored values.
3060    ///
3061    /// Useful for estimating memory pressure without allocating a full snapshot.
3062    pub fn total_value_bytes(&self) -> Result<usize, AgentRuntimeError> {
3063        let inner = recover_lock(self.inner.lock(), "WorkingMemory::total_value_bytes");
3064        Ok(inner.map.values().map(|v| v.len()).sum())
3065    }
3066
3067    /// Return the byte length of the longest key currently stored.
3068    ///
3069    /// Returns `0` when the memory is empty.
3070    pub fn max_key_length(&self) -> Result<usize, AgentRuntimeError> {
3071        let inner = recover_lock(self.inner.lock(), "WorkingMemory::max_key_length");
3072        Ok(inner.map.keys().map(|k| k.len()).max().unwrap_or(0))
3073    }
3074
3075    /// Return the byte length of the longest stored value.
3076    ///
3077    /// Returns `0` when the memory is empty.
3078    pub fn max_value_length(&self) -> Result<usize, AgentRuntimeError> {
3079        let inner = recover_lock(self.inner.lock(), "WorkingMemory::max_value_length");
3080        Ok(inner.map.values().map(|v| v.len()).max().unwrap_or(0))
3081    }
3082
3083    /// Return the byte length of the shortest stored value.
3084    ///
3085    /// Returns `0` when the memory is empty.
3086    pub fn min_value_length(&self) -> Result<usize, AgentRuntimeError> {
3087        let inner = recover_lock(self.inner.lock(), "WorkingMemory::min_value_length");
3088        Ok(inner.map.values().map(|v| v.len()).min().unwrap_or(0))
3089    }
3090
3091    /// Return the number of keys whose text contains `substring`.
3092    ///
3093    /// The search is case-sensitive.  Returns `0` when the store is empty or
3094    /// no key matches.
3095    pub fn key_count_matching(&self, substring: &str) -> Result<usize, AgentRuntimeError> {
3096        let inner = recover_lock(self.inner.lock(), "WorkingMemory::key_count_matching");
3097        Ok(inner.map.keys().filter(|k| k.contains(substring)).count())
3098    }
3099
3100    /// Return the mean byte length of all stored values.
3101    ///
3102    /// Returns `0.0` when the store is empty.
3103    pub fn avg_value_length(&self) -> Result<f64, AgentRuntimeError> {
3104        let inner = recover_lock(self.inner.lock(), "WorkingMemory::avg_value_length");
3105        let n = inner.map.len();
3106        if n == 0 {
3107            return Ok(0.0);
3108        }
3109        let total: usize = inner.map.values().map(|v| v.len()).sum();
3110        Ok(total as f64 / n as f64)
3111    }
3112
3113    /// Return the number of entries whose value exceeds `min_bytes` bytes in length.
3114    pub fn count_above_value_length(&self, min_bytes: usize) -> Result<usize, AgentRuntimeError> {
3115        let inner = recover_lock(self.inner.lock(), "WorkingMemory::count_above_value_length");
3116        Ok(inner.map.values().filter(|v| v.len() > min_bytes).count())
3117    }
3118
3119    /// Return the key with the most bytes, or `None` if the store is empty.
3120    ///
3121    /// When multiple keys share the maximum byte length, one of them is
3122    /// returned (unspecified which).
3123    pub fn longest_key(&self) -> Result<Option<String>, AgentRuntimeError> {
3124        let inner = recover_lock(self.inner.lock(), "WorkingMemory::longest_key");
3125        Ok(inner
3126            .map
3127            .keys()
3128            .max_by_key(|k| k.len())
3129            .map(|k| k.clone()))
3130    }
3131
3132    /// Return the value with the most bytes, or `None` if the store is empty.
3133    ///
3134    /// When multiple values share the maximum byte length, one of them is
3135    /// returned (unspecified which).
3136    pub fn longest_value(&self) -> Result<Option<String>, AgentRuntimeError> {
3137        let inner = recover_lock(self.inner.lock(), "WorkingMemory::longest_value");
3138        Ok(inner
3139            .map
3140            .values()
3141            .max_by_key(|v| v.len())
3142            .map(|v| v.clone()))
3143    }
3144
3145    /// Return all `(key, value)` pairs whose key starts with `prefix`.
3146    pub fn pairs_starting_with(&self, prefix: &str) -> Result<Vec<(String, String)>, AgentRuntimeError> {
3147        let inner = recover_lock(self.inner.lock(), "WorkingMemory::pairs_starting_with");
3148        let pairs = inner
3149            .map
3150            .iter()
3151            .filter(|(k, _)| k.starts_with(prefix))
3152            .map(|(k, v)| (k.clone(), v.clone()))
3153            .collect();
3154        Ok(pairs)
3155    }
3156
3157    /// Return the sum of byte lengths of all stored keys.
3158    pub fn total_key_bytes(&self) -> Result<usize, AgentRuntimeError> {
3159        let inner = recover_lock(self.inner.lock(), "WorkingMemory::total_key_bytes");
3160        Ok(inner.map.keys().map(|k| k.len()).sum())
3161    }
3162
3163    /// Return the shortest key length, or `0` if the store is empty.
3164    pub fn min_key_length(&self) -> Result<usize, AgentRuntimeError> {
3165        let inner = recover_lock(self.inner.lock(), "WorkingMemory::min_key_length");
3166        Ok(inner.map.keys().map(|k| k.len()).min().unwrap_or(0))
3167    }
3168
3169    /// Return the number of keys that start with the given prefix.
3170    pub fn count_matching_prefix(&self, prefix: &str) -> Result<usize, AgentRuntimeError> {
3171        let inner = recover_lock(self.inner.lock(), "WorkingMemory::count_matching_prefix");
3172        Ok(inner.map.keys().filter(|k| k.starts_with(prefix)).count())
3173    }
3174
3175    /// Return a list of `(key, value_byte_length)` pairs for all entries.
3176    /// Return the number of key-value entries currently stored.
3177    pub fn entry_count(&self) -> Result<usize, AgentRuntimeError> {
3178        let inner = recover_lock(self.inner.lock(), "WorkingMemory::entry_count");
3179        Ok(inner.map.len())
3180    }
3181
3182    /// Return a list of `(key, value_byte_length)` pairs for all entries.
3183    pub fn value_lengths(&self) -> Result<Vec<(String, usize)>, AgentRuntimeError> {
3184        let inner = recover_lock(self.inner.lock(), "WorkingMemory::value_lengths");
3185        Ok(inner.map.iter().map(|(k, v)| (k.clone(), v.len())).collect())
3186    }
3187
3188    /// Return the keys of all entries whose value byte length is strictly
3189    /// greater than `threshold`.
3190    pub fn keys_with_value_longer_than(&self, threshold: usize) -> Result<Vec<String>, AgentRuntimeError> {
3191        let inner = recover_lock(self.inner.lock(), "WorkingMemory::keys_with_value_longer_than");
3192        Ok(inner
3193            .map
3194            .iter()
3195            .filter(|(_, v)| v.len() > threshold)
3196            .map(|(k, _)| k.clone())
3197            .collect())
3198    }
3199
3200    /// Return the key whose associated value has the most bytes, or `None` if empty.
3201    pub fn longest_value_key(&self) -> Result<Option<String>, AgentRuntimeError> {
3202        let inner = recover_lock(self.inner.lock(), "WorkingMemory::longest_value_key");
3203        Ok(inner
3204            .map
3205            .iter()
3206            .max_by_key(|(_, v)| v.len())
3207            .map(|(k, _)| k.clone()))
3208    }
3209
3210    /// Remove all entries for which `predicate(key, value)` returns `false`.
3211    ///
3212    /// Preserves insertion order of the surviving entries.
3213    /// Returns the number of entries removed.
3214    pub fn retain<F>(&self, mut predicate: F) -> Result<usize, AgentRuntimeError>
3215    where
3216        F: FnMut(&str, &str) -> bool,
3217    {
3218        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::retain");
3219        let before = inner.map.len();
3220        inner.map.retain(|k, v| predicate(k.as_str(), v.as_str()));
3221        let surviving: std::collections::HashSet<String> =
3222            inner.map.keys().cloned().collect();
3223        inner.order.retain(|k| surviving.contains(k));
3224        Ok(before - inner.map.len())
3225    }
3226
3227    /// Copy all entries from `other` into `self`.
3228    ///
3229    /// Entries from `other` are inserted in its insertion order.  Capacity
3230    /// limits and eviction apply as if each entry were inserted individually
3231    /// via [`set`].
3232    ///
3233    /// [`set`]: WorkingMemory::set
3234    pub fn merge_from(&self, other: &WorkingMemory) -> Result<usize, AgentRuntimeError> {
3235        let pairs: Vec<(String, String)> = {
3236            let inner = recover_lock(other.inner.lock(), "WorkingMemory::merge_from(read)");
3237            inner.order.iter().filter_map(|k| {
3238                inner.map.get(k).map(|v| (k.clone(), v.clone()))
3239            }).collect()
3240        };
3241        let count = pairs.len();
3242        self.set_many(pairs)?;
3243        Ok(count)
3244    }
3245
3246    /// Return the number of entries for which `predicate(key, value)` returns `true`.
3247    pub fn entry_count_satisfying<F>(&self, mut predicate: F) -> Result<usize, AgentRuntimeError>
3248    where
3249        F: FnMut(&str, &str) -> bool,
3250    {
3251        let inner = recover_lock(self.inner.lock(), "WorkingMemory::entry_count_satisfying");
3252        Ok(inner.map.iter().filter(|(k, v)| predicate(k.as_str(), v.as_str())).count())
3253    }
3254
3255    /// Return all keys in insertion order.
3256    pub fn get_all_keys(&self) -> Result<Vec<String>, AgentRuntimeError> {
3257        let inner = recover_lock(self.inner.lock(), "WorkingMemory::get_all_keys");
3258        Ok(inner.order.iter().cloned().collect())
3259    }
3260
3261    /// Atomically replace **all** entries with `map`.
3262    ///
3263    /// The new entries are stored in iteration order of `map`.  Capacity
3264    /// limits apply: if `map.len() > capacity`, only the last `capacity`
3265    /// entries (in iteration order) are retained.
3266    pub fn replace_all(
3267        &self,
3268        map: std::collections::HashMap<String, String>,
3269    ) -> Result<(), AgentRuntimeError> {
3270        let capacity = self.capacity;
3271        let mut inner = recover_lock(self.inner.lock(), "WorkingMemory::replace_all");
3272        inner.map.clear();
3273        inner.order.clear();
3274        for (k, v) in map {
3275            if inner.map.len() >= capacity {
3276                if let Some(oldest) = inner.order.pop_front() {
3277                    inner.map.remove(&oldest);
3278                }
3279            }
3280            inner.order.push_back(k.clone());
3281            inner.map.insert(k, v);
3282        }
3283        Ok(())
3284    }
3285}
3286
3287// ── Tests ─────────────────────────────────────────────────────────────────────
3288
3289#[cfg(test)]
3290mod tests {
3291    use super::*;
3292
3293    // ── AgentId / MemoryId ────────────────────────────────────────────────────
3294
3295    #[test]
3296    fn test_agent_id_new_stores_string() {
3297        let id = AgentId::new("agent-1");
3298        assert_eq!(id.0, "agent-1");
3299    }
3300
3301    #[test]
3302    fn test_agent_id_random_is_unique() {
3303        let a = AgentId::random();
3304        let b = AgentId::random();
3305        assert_ne!(a, b);
3306    }
3307
3308    #[test]
3309    fn test_memory_id_new_stores_string() {
3310        let id = MemoryId::new("mem-1");
3311        assert_eq!(id.0, "mem-1");
3312    }
3313
3314    #[test]
3315    fn test_memory_id_random_is_unique() {
3316        let a = MemoryId::random();
3317        let b = MemoryId::random();
3318        assert_ne!(a, b);
3319    }
3320
3321    // ── MemoryItem ────────────────────────────────────────────────────────────
3322
3323    #[test]
3324    fn test_memory_item_new_clamps_importance_above_one() {
3325        let item = MemoryItem::new(AgentId::new("a"), "test", 1.5, vec![]);
3326        assert_eq!(item.importance, 1.0);
3327    }
3328
3329    #[test]
3330    fn test_memory_item_new_clamps_importance_below_zero() {
3331        let item = MemoryItem::new(AgentId::new("a"), "test", -0.5, vec![]);
3332        assert_eq!(item.importance, 0.0);
3333    }
3334
3335    #[test]
3336    fn test_memory_item_new_preserves_valid_importance() {
3337        let item = MemoryItem::new(AgentId::new("a"), "test", 0.7, vec![]);
3338        assert!((item.importance - 0.7).abs() < 1e-6);
3339    }
3340
3341    // ── DecayPolicy ───────────────────────────────────────────────────────────
3342
3343    #[test]
3344    fn test_decay_policy_rejects_zero_half_life() {
3345        assert!(DecayPolicy::exponential(0.0).is_err());
3346    }
3347
3348    #[test]
3349    fn test_decay_policy_rejects_negative_half_life() {
3350        assert!(DecayPolicy::exponential(-1.0).is_err());
3351    }
3352
3353    #[test]
3354    fn test_decay_policy_no_decay_at_age_zero() {
3355        let p = DecayPolicy::exponential(24.0).unwrap();
3356        let decayed = p.apply(1.0, 0.0);
3357        assert!((decayed - 1.0).abs() < 1e-5);
3358    }
3359
3360    #[test]
3361    fn test_decay_policy_half_importance_at_half_life() {
3362        let p = DecayPolicy::exponential(24.0).unwrap();
3363        let decayed = p.apply(1.0, 24.0);
3364        assert!((decayed - 0.5).abs() < 1e-5);
3365    }
3366
3367    #[test]
3368    fn test_decay_policy_quarter_importance_at_two_half_lives() {
3369        let p = DecayPolicy::exponential(24.0).unwrap();
3370        let decayed = p.apply(1.0, 48.0);
3371        assert!((decayed - 0.25).abs() < 1e-5);
3372    }
3373
3374    #[test]
3375    fn test_decay_policy_result_is_clamped_to_zero_one() {
3376        let p = DecayPolicy::exponential(1.0).unwrap();
3377        let decayed = p.apply(0.0, 1000.0);
3378        assert!(decayed >= 0.0 && decayed <= 1.0);
3379    }
3380
3381    // ── EpisodicStore ─────────────────────────────────────────────────────────
3382
3383    #[test]
3384    fn test_episodic_store_add_episode_returns_id() {
3385        let store = EpisodicStore::new();
3386        let id = store.add_episode(AgentId::new("a"), "event", 0.8).unwrap();
3387        assert!(!id.0.is_empty());
3388    }
3389
3390    #[test]
3391    fn test_episodic_store_recall_returns_stored_item() {
3392        let store = EpisodicStore::new();
3393        let agent = AgentId::new("agent-1");
3394        store
3395            .add_episode(agent.clone(), "hello world", 0.9)
3396            .unwrap();
3397        let items = store.recall(&agent, 10).unwrap();
3398        assert_eq!(items.len(), 1);
3399        assert_eq!(items[0].content, "hello world");
3400    }
3401
3402    #[test]
3403    fn test_episodic_store_recall_filters_by_agent() {
3404        let store = EpisodicStore::new();
3405        let a = AgentId::new("agent-a");
3406        let b = AgentId::new("agent-b");
3407        store.add_episode(a.clone(), "for a", 0.5).unwrap();
3408        store.add_episode(b.clone(), "for b", 0.5).unwrap();
3409        let items = store.recall(&a, 10).unwrap();
3410        assert_eq!(items.len(), 1);
3411        assert_eq!(items[0].content, "for a");
3412    }
3413
3414    #[test]
3415    fn test_episodic_store_recall_sorted_by_descending_importance() {
3416        let store = EpisodicStore::new();
3417        let agent = AgentId::new("agent-1");
3418        store.add_episode(agent.clone(), "low", 0.1).unwrap();
3419        store.add_episode(agent.clone(), "high", 0.9).unwrap();
3420        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
3421        let items = store.recall(&agent, 10).unwrap();
3422        assert_eq!(items[0].content, "high");
3423        assert_eq!(items[1].content, "mid");
3424        assert_eq!(items[2].content, "low");
3425    }
3426
3427    #[test]
3428    fn test_episodic_store_recall_respects_limit() {
3429        let store = EpisodicStore::new();
3430        let agent = AgentId::new("agent-1");
3431        for i in 0..5 {
3432            store
3433                .add_episode(agent.clone(), format!("item {i}"), 0.5)
3434                .unwrap();
3435        }
3436        let items = store.recall(&agent, 3).unwrap();
3437        assert_eq!(items.len(), 3);
3438    }
3439
3440    #[test]
3441    fn test_episodic_store_len_tracks_insertions() {
3442        let store = EpisodicStore::new();
3443        let agent = AgentId::new("a");
3444        store.add_episode(agent.clone(), "a", 0.5).unwrap();
3445        store.add_episode(agent.clone(), "b", 0.5).unwrap();
3446        assert_eq!(store.len().unwrap(), 2);
3447    }
3448
3449    #[test]
3450    fn test_episodic_store_is_empty_initially() {
3451        let store = EpisodicStore::new();
3452        assert!(store.is_empty().unwrap());
3453    }
3454
3455    #[test]
3456    fn test_episodic_store_with_decay_reduces_importance() {
3457        let policy = DecayPolicy::exponential(0.001).unwrap(); // very fast decay
3458        let store = EpisodicStore::with_decay(policy);
3459        let agent = AgentId::new("a");
3460
3461        // Insert an old item by manipulating via add_episode_at.
3462        let old_ts = Utc::now() - chrono::Duration::hours(1);
3463        store
3464            .add_episode_at(agent.clone(), "old event", 1.0, old_ts)
3465            .unwrap();
3466
3467        let items = store.recall(&agent, 10).unwrap();
3468        // With half_life=0.001h and age=1h, importance should be near 0
3469        assert_eq!(items.len(), 1);
3470        assert!(
3471            items[0].importance < 0.01,
3472            "expected near-zero importance, got {}",
3473            items[0].importance
3474        );
3475    }
3476
3477    // ── max_age eviction ──────────────────────────────────────────────────────
3478
3479    #[test]
3480    fn test_max_age_rejects_zero() {
3481        assert!(EpisodicStore::with_max_age(0.0).is_err());
3482    }
3483
3484    #[test]
3485    fn test_max_age_rejects_negative() {
3486        assert!(EpisodicStore::with_max_age(-1.0).is_err());
3487    }
3488
3489    #[test]
3490    fn test_max_age_evicts_old_items_on_recall() {
3491        // max_age = 0.001 hours (~3.6 seconds) — items 1 hour old should be evicted
3492        let store = EpisodicStore::with_max_age(0.001).unwrap();
3493        let agent = AgentId::new("a");
3494
3495        let old_ts = Utc::now() - chrono::Duration::hours(1);
3496        store
3497            .add_episode_at(agent.clone(), "old", 0.9, old_ts)
3498            .unwrap();
3499        store.add_episode(agent.clone(), "new", 0.5).unwrap();
3500
3501        let items = store.recall(&agent, 10).unwrap();
3502        assert_eq!(items.len(), 1, "old item should be evicted by max_age");
3503        assert_eq!(items[0].content, "new");
3504    }
3505
3506    #[test]
3507    fn test_max_age_evicts_old_items_on_add() {
3508        let store = EpisodicStore::with_max_age(0.001).unwrap();
3509        let agent = AgentId::new("a");
3510
3511        let old_ts = Utc::now() - chrono::Duration::hours(1);
3512        store
3513            .add_episode_at(agent.clone(), "old", 0.9, old_ts)
3514            .unwrap();
3515        // Adding a new item triggers stale purge.
3516        store.add_episode(agent.clone(), "new", 0.5).unwrap();
3517
3518        assert_eq!(store.len().unwrap(), 1);
3519    }
3520
3521    // ── RecallPolicy / per-agent capacity tests ───────────────────────────────
3522
3523    #[test]
3524    fn test_recall_increments_recall_count() {
3525        let store = EpisodicStore::new();
3526        let agent = AgentId::new("agent-rc");
3527        store.add_episode(agent.clone(), "memory", 0.5).unwrap();
3528
3529        // First recall — count becomes 1
3530        let items = store.recall(&agent, 10).unwrap();
3531        assert_eq!(items[0].recall_count, 1);
3532
3533        // Second recall — count becomes 2
3534        let items = store.recall(&agent, 10).unwrap();
3535        assert_eq!(items[0].recall_count, 2);
3536    }
3537
3538    #[test]
3539    fn test_hybrid_recall_policy_prefers_recently_used() {
3540        let store = EpisodicStore::with_recall_policy(RecallPolicy::Hybrid {
3541            recency_weight: 0.1,
3542            frequency_weight: 2.0,
3543        });
3544        let agent = AgentId::new("agent-hybrid");
3545
3546        let old_ts = Utc::now() - chrono::Duration::hours(48);
3547        store
3548            .add_episode_at(agent.clone(), "old_frequent", 0.5, old_ts)
3549            .unwrap();
3550        store.add_episode(agent.clone(), "new_never", 0.5).unwrap();
3551
3552        // Simulate many prior recalls of "old_frequent".
3553        store.bump_recall_count_by_content("old_frequent", 100);
3554
3555        let items = store.recall(&agent, 10).unwrap();
3556        assert_eq!(items.len(), 2);
3557        assert_eq!(
3558            items[0].content, "old_frequent",
3559            "hybrid policy should rank the frequently-recalled item first"
3560        );
3561    }
3562
3563    #[test]
3564    fn test_per_agent_capacity_evicts_lowest_importance() {
3565        let store = EpisodicStore::with_per_agent_capacity(2);
3566        let agent = AgentId::new("agent-cap");
3567
3568        // Add two items; capacity is full.
3569        store.add_episode(agent.clone(), "low", 0.1).unwrap();
3570        store.add_episode(agent.clone(), "high", 0.9).unwrap();
3571        // Adding "new" triggers eviction of the EXISTING lowest-importance item
3572        // ("low", 0.1) — the newly added item is never the one evicted.
3573        store.add_episode(agent.clone(), "new", 0.5).unwrap();
3574
3575        assert_eq!(
3576            store.len().unwrap(),
3577            2,
3578            "store should hold exactly 2 items after eviction"
3579        );
3580
3581        let items = store.recall(&agent, 10).unwrap();
3582        let contents: Vec<&str> = items.iter().map(|i| i.content.as_str()).collect();
3583        assert!(
3584            !contents.contains(&"low"),
3585            "the pre-existing lowest-importance item should have been evicted; remaining: {:?}",
3586            contents
3587        );
3588        assert!(
3589            contents.contains(&"new"),
3590            "the newly added item must never be evicted; remaining: {:?}",
3591            contents
3592        );
3593    }
3594
3595    // ── O(1) agent isolation ──────────────────────────────────────────────────
3596
3597    #[test]
3598    fn test_many_agents_do_not_see_each_others_memories() {
3599        let store = EpisodicStore::new();
3600        let n_agents = 20usize;
3601        for i in 0..n_agents {
3602            let agent = AgentId::new(format!("agent-{i}"));
3603            for j in 0..5 {
3604                store
3605                    .add_episode(agent.clone(), format!("item-{i}-{j}"), 0.5)
3606                    .unwrap();
3607            }
3608        }
3609        // Each agent should only see its own 5 items.
3610        for i in 0..n_agents {
3611            let agent = AgentId::new(format!("agent-{i}"));
3612            let items = store.recall(&agent, 100).unwrap();
3613            assert_eq!(
3614                items.len(),
3615                5,
3616                "agent {i} should see exactly 5 items, got {}",
3617                items.len()
3618            );
3619            for item in &items {
3620                assert!(
3621                    item.content.starts_with(&format!("item-{i}-")),
3622                    "agent {i} saw foreign item: {}",
3623                    item.content
3624                );
3625            }
3626        }
3627    }
3628
3629    // ── agent_memory_count / list_agents / purge_agent_memories ──────────────
3630
3631    #[test]
3632    fn test_agent_memory_count_returns_zero_for_unknown_agent() {
3633        let store = EpisodicStore::new();
3634        let count = store.agent_memory_count(&AgentId::new("ghost")).unwrap();
3635        assert_eq!(count, 0);
3636    }
3637
3638    #[test]
3639    fn test_agent_memory_count_tracks_insertions() {
3640        let store = EpisodicStore::new();
3641        let agent = AgentId::new("a");
3642        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
3643        store.add_episode(agent.clone(), "e2", 0.5).unwrap();
3644        assert_eq!(store.agent_memory_count(&agent).unwrap(), 2);
3645    }
3646
3647    #[test]
3648    fn test_list_agents_returns_all_known_agents() {
3649        let store = EpisodicStore::new();
3650        let a = AgentId::new("agent-a");
3651        let b = AgentId::new("agent-b");
3652        store.add_episode(a.clone(), "x", 0.5).unwrap();
3653        store.add_episode(b.clone(), "y", 0.5).unwrap();
3654        let agents = store.list_agents().unwrap();
3655        assert_eq!(agents.len(), 2);
3656        assert!(agents.contains(&a));
3657        assert!(agents.contains(&b));
3658    }
3659
3660    #[test]
3661    fn test_list_agents_empty_when_no_episodes() {
3662        let store = EpisodicStore::new();
3663        let agents = store.list_agents().unwrap();
3664        assert!(agents.is_empty());
3665    }
3666
3667    #[test]
3668    fn test_purge_agent_memories_removes_all_for_agent() {
3669        let store = EpisodicStore::new();
3670        let a = AgentId::new("a");
3671        let b = AgentId::new("b");
3672        store.add_episode(a.clone(), "ep1", 0.5).unwrap();
3673        store.add_episode(a.clone(), "ep2", 0.5).unwrap();
3674        store.add_episode(b.clone(), "ep-b", 0.5).unwrap();
3675
3676        let removed = store.purge_agent_memories(&a).unwrap();
3677        assert_eq!(removed, 2);
3678        assert_eq!(store.agent_memory_count(&a).unwrap(), 0);
3679        assert_eq!(store.agent_memory_count(&b).unwrap(), 1);
3680        assert_eq!(store.len().unwrap(), 1);
3681    }
3682
3683    #[test]
3684    fn test_purge_agent_memories_returns_zero_for_unknown_agent() {
3685        let store = EpisodicStore::new();
3686        let removed = store.purge_agent_memories(&AgentId::new("ghost")).unwrap();
3687        assert_eq!(removed, 0);
3688    }
3689
3690    // ── All-stale recall ──────────────────────────────────────────────────────
3691
3692    #[test]
3693    fn test_recall_returns_empty_when_all_items_are_stale() {
3694        // max_age = 0.001 hours — all items inserted 1 hour ago will be evicted.
3695        let store = EpisodicStore::with_max_age(0.001).unwrap();
3696        let agent = AgentId::new("stale-agent");
3697
3698        let old_ts = Utc::now() - chrono::Duration::hours(1);
3699        store
3700            .add_episode_at(agent.clone(), "stale-1", 0.9, old_ts)
3701            .unwrap();
3702        store
3703            .add_episode_at(agent.clone(), "stale-2", 0.7, old_ts)
3704            .unwrap();
3705
3706        let items = store.recall(&agent, 100).unwrap();
3707        assert!(
3708            items.is_empty(),
3709            "all stale items should be evicted on recall, got {}",
3710            items.len()
3711        );
3712    }
3713
3714    // ── Concurrency stress tests ──────────────────────────────────────────────
3715
3716    #[test]
3717    fn test_concurrent_add_and_recall_are_consistent() {
3718        use std::sync::Arc;
3719        use std::thread;
3720
3721        let store = Arc::new(EpisodicStore::new());
3722        let agent = AgentId::new("concurrent-agent");
3723        let n_threads = 8;
3724        let items_per_thread = 25;
3725
3726        // Spawn writers.
3727        let mut handles = Vec::new();
3728        for t in 0..n_threads {
3729            let s = Arc::clone(&store);
3730            let a = agent.clone();
3731            handles.push(thread::spawn(move || {
3732                for i in 0..items_per_thread {
3733                    s.add_episode(a.clone(), format!("t{t}-i{i}"), 0.5).unwrap();
3734                }
3735            }));
3736        }
3737        for h in handles {
3738            h.join().unwrap();
3739        }
3740
3741        // Spawn readers.
3742        let mut read_handles = Vec::new();
3743        for _ in 0..n_threads {
3744            let s = Arc::clone(&store);
3745            let a = agent.clone();
3746            read_handles.push(thread::spawn(move || {
3747                let items = s.recall(&a, 1000).unwrap();
3748                assert!(items.len() <= n_threads * items_per_thread);
3749            }));
3750        }
3751        for h in read_handles {
3752            h.join().unwrap();
3753        }
3754    }
3755
3756    // ── Concurrent capacity eviction ──────────────────────────────────────────
3757
3758    #[test]
3759    fn test_concurrent_capacity_eviction_never_exceeds_cap() {
3760        use std::sync::Arc;
3761        use std::thread;
3762
3763        let cap = 5usize;
3764        let store = Arc::new(EpisodicStore::with_per_agent_capacity(cap));
3765        let agent = AgentId::new("cap-agent");
3766        let n_threads = 8;
3767        let items_per_thread = 10;
3768
3769        let mut handles = Vec::new();
3770        for t in 0..n_threads {
3771            let s = Arc::clone(&store);
3772            let a = agent.clone();
3773            handles.push(thread::spawn(move || {
3774                for i in 0..items_per_thread {
3775                    let importance = (t * items_per_thread + i) as f32 / 100.0;
3776                    s.add_episode(a.clone(), format!("t{t}-i{i}"), importance)
3777                        .unwrap();
3778                }
3779            }));
3780        }
3781        for h in handles {
3782            h.join().unwrap();
3783        }
3784
3785        // The store may momentarily exceed cap+1 during concurrent eviction,
3786        // but after all threads complete the final count must be <= cap + n_threads.
3787        // (Each thread's last insert may not have been evicted yet.)
3788        // The strong invariant: agent_memory_count <= cap + n_threads - 1.
3789        let count = store.agent_memory_count(&agent).unwrap();
3790        assert!(
3791            count <= cap + n_threads,
3792            "expected at most {} items, got {}",
3793            cap + n_threads,
3794            count
3795        );
3796    }
3797
3798    // ── SemanticStore ─────────────────────────────────────────────────────────
3799
3800    #[test]
3801    fn test_semantic_store_store_and_retrieve_all() {
3802        let store = SemanticStore::new();
3803        store.store("key1", "value1", vec!["tag-a".into()]).unwrap();
3804        store.store("key2", "value2", vec!["tag-b".into()]).unwrap();
3805        let results = store.retrieve(&[]).unwrap();
3806        assert_eq!(results.len(), 2);
3807    }
3808
3809    #[test]
3810    fn test_semantic_store_retrieve_filters_by_tag() {
3811        let store = SemanticStore::new();
3812        store
3813            .store("k1", "v1", vec!["rust".into(), "async".into()])
3814            .unwrap();
3815        store.store("k2", "v2", vec!["rust".into()]).unwrap();
3816        let results = store.retrieve(&["async"]).unwrap();
3817        assert_eq!(results.len(), 1);
3818        assert_eq!(results[0].0, "k1");
3819    }
3820
3821    #[test]
3822    fn test_semantic_store_retrieve_requires_all_tags() {
3823        let store = SemanticStore::new();
3824        store
3825            .store("k1", "v1", vec!["a".into(), "b".into()])
3826            .unwrap();
3827        store.store("k2", "v2", vec!["a".into()]).unwrap();
3828        let results = store.retrieve(&["a", "b"]).unwrap();
3829        assert_eq!(results.len(), 1);
3830    }
3831
3832    #[test]
3833    fn test_semantic_store_is_empty_initially() {
3834        let store = SemanticStore::new();
3835        assert!(store.is_empty().unwrap());
3836    }
3837
3838    #[test]
3839    fn test_semantic_store_len_tracks_insertions() {
3840        let store = SemanticStore::new();
3841        store.store("k", "v", vec![]).unwrap();
3842        assert_eq!(store.len().unwrap(), 1);
3843    }
3844
3845    #[test]
3846    fn test_semantic_store_empty_embedding_is_rejected() {
3847        let store = SemanticStore::new();
3848        let result = store.store_with_embedding("k", "v", vec![], vec![]);
3849        assert!(result.is_err(), "empty embedding should be rejected");
3850    }
3851
3852    #[test]
3853    fn test_semantic_store_dimension_mismatch_is_rejected() {
3854        let store = SemanticStore::new();
3855        store
3856            .store_with_embedding("k1", "v1", vec![], vec![1.0, 0.0])
3857            .unwrap();
3858        // Different dimension
3859        let result = store.store_with_embedding("k2", "v2", vec![], vec![1.0, 0.0, 0.0]);
3860        assert!(
3861            result.is_err(),
3862            "embedding dimension mismatch should be rejected"
3863        );
3864    }
3865
3866    #[test]
3867    fn test_semantic_store_retrieve_similar_returns_closest() {
3868        let store = SemanticStore::new();
3869        store
3870            .store_with_embedding("close", "close value", vec![], vec![1.0, 0.0, 0.0])
3871            .unwrap();
3872        store
3873            .store_with_embedding("far", "far value", vec![], vec![0.0, 1.0, 0.0])
3874            .unwrap();
3875
3876        let query = vec![1.0, 0.0, 0.0];
3877        let results = store.retrieve_similar(&query, 2).unwrap();
3878        assert_eq!(results.len(), 2);
3879        assert_eq!(results[0].0, "close");
3880        assert!(
3881            (results[0].2 - 1.0).abs() < 1e-5,
3882            "expected similarity ~1.0, got {}",
3883            results[0].2
3884        );
3885        assert!(
3886            (results[1].2).abs() < 1e-5,
3887            "expected similarity ~0.0, got {}",
3888            results[1].2
3889        );
3890    }
3891
3892    #[test]
3893    fn test_semantic_store_retrieve_similar_ignores_unembedded_entries() {
3894        let store = SemanticStore::new();
3895        store.store("no-emb", "no embedding value", vec![]).unwrap();
3896        store
3897            .store_with_embedding("with-emb", "with embedding value", vec![], vec![1.0, 0.0])
3898            .unwrap();
3899
3900        let query = vec![1.0, 0.0];
3901        let results = store.retrieve_similar(&query, 10).unwrap();
3902        assert_eq!(results.len(), 1, "only the embedded entry should appear");
3903        assert_eq!(results[0].0, "with-emb");
3904    }
3905
3906    #[test]
3907    fn test_cosine_similarity_orthogonal_vectors_return_zero() {
3908        let store = SemanticStore::new();
3909        store
3910            .store_with_embedding("a", "va", vec![], vec![1.0, 0.0])
3911            .unwrap();
3912        store
3913            .store_with_embedding("b", "vb", vec![], vec![0.0, 1.0])
3914            .unwrap();
3915
3916        let query = vec![1.0, 0.0];
3917        let results = store.retrieve_similar(&query, 2).unwrap();
3918        assert_eq!(results.len(), 2);
3919        let b_result = results.iter().find(|(k, _, _)| k == "b").unwrap();
3920        assert!(
3921            b_result.2.abs() < 1e-5,
3922            "expected cosine similarity 0.0 for orthogonal vectors, got {}",
3923            b_result.2
3924        );
3925    }
3926
3927    // ── WorkingMemory ─────────────────────────────────────────────────────────
3928
3929    #[test]
3930    fn test_working_memory_new_rejects_zero_capacity() {
3931        assert!(WorkingMemory::new(0).is_err());
3932    }
3933
3934    #[test]
3935    fn test_working_memory_set_and_get() {
3936        let wm = WorkingMemory::new(10).unwrap();
3937        wm.set("foo", "bar").unwrap();
3938        let val = wm.get("foo").unwrap();
3939        assert_eq!(val, Some("bar".into()));
3940    }
3941
3942    #[test]
3943    fn test_working_memory_get_missing_key_returns_none() {
3944        let wm = WorkingMemory::new(10).unwrap();
3945        assert_eq!(wm.get("missing").unwrap(), None);
3946    }
3947
3948    #[test]
3949    fn test_working_memory_bounded_evicts_oldest() {
3950        let wm = WorkingMemory::new(3).unwrap();
3951        wm.set("k1", "v1").unwrap();
3952        wm.set("k2", "v2").unwrap();
3953        wm.set("k3", "v3").unwrap();
3954        wm.set("k4", "v4").unwrap(); // k1 should be evicted
3955        assert_eq!(wm.get("k1").unwrap(), None);
3956        assert_eq!(wm.get("k4").unwrap(), Some("v4".into()));
3957    }
3958
3959    #[test]
3960    fn test_working_memory_update_existing_key_no_eviction() {
3961        let wm = WorkingMemory::new(2).unwrap();
3962        wm.set("k1", "v1").unwrap();
3963        wm.set("k2", "v2").unwrap();
3964        wm.set("k1", "v1-updated").unwrap(); // update, not eviction
3965        assert_eq!(wm.len().unwrap(), 2);
3966        assert_eq!(wm.get("k1").unwrap(), Some("v1-updated".into()));
3967        assert_eq!(wm.get("k2").unwrap(), Some("v2".into()));
3968    }
3969
3970    #[test]
3971    fn test_working_memory_clear_removes_all() {
3972        let wm = WorkingMemory::new(10).unwrap();
3973        wm.set("a", "1").unwrap();
3974        wm.set("b", "2").unwrap();
3975        wm.clear().unwrap();
3976        assert!(wm.is_empty().unwrap());
3977    }
3978
3979    #[test]
3980    fn test_working_memory_is_empty_initially() {
3981        let wm = WorkingMemory::new(5).unwrap();
3982        assert!(wm.is_empty().unwrap());
3983    }
3984
3985    #[test]
3986    fn test_working_memory_len_tracks_entries() {
3987        let wm = WorkingMemory::new(10).unwrap();
3988        wm.set("a", "1").unwrap();
3989        wm.set("b", "2").unwrap();
3990        assert_eq!(wm.len().unwrap(), 2);
3991    }
3992
3993    #[test]
3994    fn test_working_memory_capacity_never_exceeded() {
3995        let cap = 5usize;
3996        let wm = WorkingMemory::new(cap).unwrap();
3997        for i in 0..20 {
3998            wm.set(format!("key-{i}"), format!("val-{i}")).unwrap();
3999            assert!(wm.len().unwrap() <= cap);
4000        }
4001    }
4002
4003    // ── Improvement 6: SemanticStore dimension validation on retrieve ──────────
4004
4005    #[test]
4006    fn test_semantic_dimension_mismatch_on_retrieve_returns_error() {
4007        let store = SemanticStore::new();
4008        store
4009            .store_with_embedding("k1", "v1", vec![], vec![1.0, 0.0, 0.0])
4010            .unwrap();
4011        // Query with wrong dimension
4012        let result = store.retrieve_similar(&[1.0, 0.0], 10);
4013        assert!(result.is_err(), "dimension mismatch on retrieve should error");
4014    }
4015
4016    // ── Improvement 12: EvictionPolicy::Oldest ────────────────────────────────
4017
4018    // ── #3 clear_agent_memory ────────────────────────────────────────────────
4019
4020    #[test]
4021    fn test_clear_agent_memory_removes_all_episodes() {
4022        let store = EpisodicStore::new();
4023        let agent = AgentId::new("a");
4024        store.add_episode(agent.clone(), "ep1", 0.5).unwrap();
4025        store.add_episode(agent.clone(), "ep2", 0.9).unwrap();
4026        store.clear_agent_memory(&agent).unwrap();
4027        let items = store.recall(&agent, 10).unwrap();
4028        assert!(items.is_empty(), "all memories should be cleared");
4029    }
4030
4031    // ── #13 AgentId::as_str / MemoryId::as_str ───────────────────────────────
4032
4033    #[test]
4034    fn test_agent_id_as_str() {
4035        let id = AgentId::new("hello");
4036        assert_eq!(id.as_str(), "hello");
4037    }
4038
4039    // ── #15 export/import round trip ─────────────────────────────────────────
4040
4041    #[test]
4042    fn test_export_import_agent_memory_round_trip() {
4043        let store = EpisodicStore::new();
4044        let agent = AgentId::new("export-agent");
4045        store.add_episode(agent.clone(), "fact1", 0.8).unwrap();
4046        store.add_episode(agent.clone(), "fact2", 0.6).unwrap();
4047
4048        let exported = store.export_agent_memory(&agent).unwrap();
4049        assert_eq!(exported.len(), 2);
4050
4051        let new_store = EpisodicStore::new();
4052        new_store.import_agent_memory(&agent, exported).unwrap();
4053        let recalled = new_store.recall(&agent, 10).unwrap();
4054        assert_eq!(recalled.len(), 2);
4055    }
4056
4057    // ── #19 WorkingMemory::iter ───────────────────────────────────────────────
4058
4059    #[test]
4060    fn test_working_memory_iter_matches_entries() {
4061        let wm = WorkingMemory::new(10).unwrap();
4062        wm.set("a", "1").unwrap();
4063        wm.set("b", "2").unwrap();
4064        let via_iter = wm.iter().unwrap();
4065        let via_entries = wm.entries().unwrap();
4066        assert_eq!(via_iter, via_entries);
4067    }
4068
4069    // ── #37 AsRef<str> for AgentId and MemoryId ──────────────────────────────
4070
4071    #[test]
4072    fn test_agent_id_as_ref_str() {
4073        let id = AgentId::new("ref-test");
4074        let s: &str = id.as_ref();
4075        assert_eq!(s, "ref-test");
4076    }
4077
4078    #[test]
4079    fn test_eviction_policy_oldest_evicts_first_inserted() {
4080        let store = EpisodicStore::with_eviction_policy(EvictionPolicy::Oldest);
4081        // Override capacity by building a combined store.
4082        // We need a store with capacity=2 AND oldest eviction.
4083        // Use `with_per_agent_capacity` approach on a new store then set policy.
4084        // Since we can't combine constructors directly yet, we test via a different path:
4085        // Insert items and check that the oldest is evicted.
4086        let store = {
4087            // Build internal store with per_agent_capacity=2 and Oldest policy
4088            let inner = EpisodicInner {
4089                items: std::collections::HashMap::new(),
4090                decay: None,
4091                recall_policy: RecallPolicy::Importance,
4092                per_agent_capacity: Some(2),
4093                max_age_hours: None,
4094                eviction_policy: EvictionPolicy::Oldest,
4095            };
4096            EpisodicStore {
4097                inner: std::sync::Arc::new(std::sync::Mutex::new(inner)),
4098            }
4099        };
4100
4101        let agent = AgentId::new("agent");
4102        // Add items with distinct timestamps by using add_episode_at
4103        let t1 = chrono::Utc::now() - chrono::Duration::seconds(100);
4104        let t2 = chrono::Utc::now() - chrono::Duration::seconds(50);
4105        store.add_episode_at(agent.clone(), "oldest", 0.9, t1).unwrap();
4106        store.add_episode_at(agent.clone(), "newer", 0.8, t2).unwrap();
4107        // Adding a third item should evict "oldest" (earliest timestamp)
4108        store.add_episode(agent.clone(), "newest", 0.5).unwrap();
4109
4110        let items = store.recall(&agent, 10).unwrap();
4111        assert_eq!(items.len(), 2);
4112        let contents: Vec<&str> = items.iter().map(|i| i.content.as_str()).collect();
4113        assert!(!contents.contains(&"oldest"), "oldest item should have been evicted; got: {contents:?}");
4114    }
4115
4116    // ── New API tests (Rounds 4-8) ────────────────────────────────────────────
4117
4118    #[test]
4119    fn test_search_by_content_finds_matching_episodes() {
4120        let store = EpisodicStore::new();
4121        let agent = AgentId::new("a");
4122        store.add_episode(agent.clone(), "the quick brown fox", 0.9).unwrap();
4123        store.add_episode(agent.clone(), "jumps over the lazy dog", 0.5).unwrap();
4124        store.add_episode(agent.clone(), "hello world", 0.7).unwrap();
4125
4126        let results = store.search_by_content(&agent, "the", 10).unwrap();
4127        assert_eq!(results.len(), 2);
4128        // results are sorted by importance descending
4129        assert_eq!(results[0].content, "the quick brown fox");
4130    }
4131
4132    #[test]
4133    fn test_search_by_content_returns_empty_on_no_match() {
4134        let store = EpisodicStore::new();
4135        let agent = AgentId::new("a");
4136        store.add_episode(agent.clone(), "hello", 0.5).unwrap();
4137        let results = store.search_by_content(&agent, "xyz", 10).unwrap();
4138        assert!(results.is_empty());
4139    }
4140
4141    #[test]
4142    fn test_recall_tagged_filters_by_all_tags() {
4143        let store = EpisodicStore::new();
4144        let agent = AgentId::new("a");
4145        let mut inner = store.inner.lock().unwrap();
4146        let item1 = MemoryItem { id: MemoryId::random(), agent_id: agent.clone(), content: "rust".into(), importance: 0.8, timestamp: Utc::now(), tags: vec!["lang".into(), "sys".into()], recall_count: 0 };
4147        let item2 = MemoryItem { id: MemoryId::random(), agent_id: agent.clone(), content: "python".into(), importance: 0.6, timestamp: Utc::now(), tags: vec!["lang".into()], recall_count: 0 };
4148        inner.items.entry(agent.clone()).or_default().push(item1);
4149        inner.items.entry(agent.clone()).or_default().push(item2);
4150        drop(inner);
4151
4152        let results = store.recall_tagged(&agent, &["lang", "sys"], 10).unwrap();
4153        assert_eq!(results.len(), 1);
4154        assert_eq!(results[0].content, "rust");
4155
4156        let all = store.recall_tagged(&agent, &["lang"], 10).unwrap();
4157        assert_eq!(all.len(), 2);
4158    }
4159
4160    #[test]
4161    fn test_recall_recent_returns_newest_first() {
4162        let store = EpisodicStore::new();
4163        let agent = AgentId::new("a");
4164        store.add_episode(agent.clone(), "first", 0.3).unwrap();
4165        store.add_episode(agent.clone(), "second", 0.5).unwrap();
4166        store.add_episode(agent.clone(), "third", 0.9).unwrap();
4167
4168        let recent = store.recall_recent(&agent, 2).unwrap();
4169        assert_eq!(recent.len(), 2);
4170        assert_eq!(recent[0].content, "third");
4171        assert_eq!(recent[1].content, "second");
4172    }
4173
4174    #[test]
4175    fn test_recall_by_id_finds_specific_episode() {
4176        let store = EpisodicStore::new();
4177        let agent = AgentId::new("a");
4178        let id = store.add_episode(agent.clone(), "specific", 0.7).unwrap();
4179        store.add_episode(agent.clone(), "other", 0.5).unwrap();
4180
4181        let found = store.recall_by_id(&agent, &id).unwrap();
4182        assert!(found.is_some());
4183        assert_eq!(found.unwrap().content, "specific");
4184    }
4185
4186    #[test]
4187    fn test_recall_by_id_returns_none_for_unknown_id() {
4188        let store = EpisodicStore::new();
4189        let agent = AgentId::new("a");
4190        let result = store.recall_by_id(&agent, &MemoryId::random()).unwrap();
4191        assert!(result.is_none());
4192    }
4193
4194    #[test]
4195    fn test_update_importance_changes_score() {
4196        let store = EpisodicStore::new();
4197        let agent = AgentId::new("a");
4198        let id = store.add_episode(agent.clone(), "item", 0.5).unwrap();
4199        let updated = store.update_importance(&agent, &id, 0.9).unwrap();
4200        assert!(updated);
4201        let item = store.recall_by_id(&agent, &id).unwrap().unwrap();
4202        assert!((item.importance - 0.9).abs() < 1e-5);
4203    }
4204
4205    #[test]
4206    fn test_merge_from_imports_episodes() {
4207        let src = EpisodicStore::new();
4208        let agent = AgentId::new("a");
4209        src.add_episode(agent.clone(), "ep1", 0.8).unwrap();
4210        src.add_episode(agent.clone(), "ep2", 0.6).unwrap();
4211
4212        let dst = EpisodicStore::new();
4213        let count = dst.merge_from(&src, &agent).unwrap();
4214        assert_eq!(count, 2);
4215        assert_eq!(dst.agent_memory_count(&agent).unwrap(), 2);
4216    }
4217
4218    #[test]
4219    fn test_memory_item_age_hours_is_non_negative() {
4220        let item = MemoryItem::new(AgentId::new("a"), "test", 0.5, vec![]);
4221        let age = item.age_hours();
4222        assert!(age >= 0.0);
4223    }
4224
4225    #[test]
4226    fn test_working_memory_remove_and_contains() {
4227        let wm = WorkingMemory::new(10).unwrap();
4228        wm.set("k", "v").unwrap();
4229        assert!(wm.contains("k").unwrap());
4230        let removed = wm.remove("k").unwrap();
4231        assert!(removed);
4232        assert!(!wm.contains("k").unwrap());
4233        assert!(!wm.remove("k").unwrap()); // second remove returns false
4234    }
4235
4236    #[test]
4237    fn test_working_memory_keys_in_insertion_order() {
4238        let wm = WorkingMemory::new(10).unwrap();
4239        wm.set("b", "2").unwrap();
4240        wm.set("a", "1").unwrap();
4241        wm.set("c", "3").unwrap();
4242        assert_eq!(wm.keys().unwrap(), vec!["b", "a", "c"]);
4243    }
4244
4245    #[test]
4246    fn test_working_memory_set_many_batch_insert() {
4247        let wm = WorkingMemory::new(10).unwrap();
4248        wm.set_many([("x", "1"), ("y", "2"), ("z", "3")]).unwrap();
4249        assert_eq!(wm.len().unwrap(), 3);
4250        assert_eq!(wm.get("y").unwrap(), Some("2".into()));
4251    }
4252
4253    #[test]
4254    fn test_working_memory_get_or_default() {
4255        let wm = WorkingMemory::new(5).unwrap();
4256        wm.set("a", "val").unwrap();
4257        assert_eq!(wm.get_or_default("a", "fallback").unwrap(), "val");
4258        assert_eq!(wm.get_or_default("missing", "fallback").unwrap(), "fallback");
4259    }
4260
4261    #[test]
4262    fn test_semantic_store_remove_deletes_entry() {
4263        let store = SemanticStore::new();
4264        store.store("k", "v", vec![]).unwrap();
4265        assert_eq!(store.len().unwrap(), 1);
4266        let removed = store.remove("k").unwrap();
4267        assert!(removed);
4268        assert_eq!(store.len().unwrap(), 0);
4269    }
4270
4271    #[test]
4272    fn test_semantic_store_clear_empties_store() {
4273        let store = SemanticStore::new();
4274        store.store("a", "1", vec!["t".into()]).unwrap();
4275        store.store("b", "2", vec!["t".into()]).unwrap();
4276        store.clear().unwrap();
4277        assert!(store.is_empty().unwrap());
4278    }
4279
4280    #[test]
4281    fn test_semantic_store_update_changes_value() {
4282        let store = SemanticStore::new();
4283        store.store("k", "old", vec![]).unwrap();
4284        let updated = store.update("k", "new").unwrap();
4285        assert!(updated);
4286        let (val, _) = store.retrieve_by_key("k").unwrap().unwrap();
4287        assert_eq!(val, "new");
4288    }
4289
4290    #[test]
4291    fn test_semantic_store_retrieve_by_key() {
4292        let store = SemanticStore::new();
4293        store.store("key", "value", vec!["tag1".into()]).unwrap();
4294        let result = store.retrieve_by_key("key").unwrap().unwrap();
4295        assert_eq!(result.0, "value");
4296        assert_eq!(result.1, vec!["tag1".to_string()]);
4297        assert!(store.retrieve_by_key("missing").unwrap().is_none());
4298    }
4299
4300    #[test]
4301    fn test_semantic_store_list_tags() {
4302        let store = SemanticStore::new();
4303        store.store("a", "1", vec!["rust".into(), "sys".into()]).unwrap();
4304        store.store("b", "2", vec!["rust".into(), "ml".into()]).unwrap();
4305        let tags = store.list_tags().unwrap();
4306        assert_eq!(tags, vec!["ml", "rust", "sys"]); // sorted
4307    }
4308
4309    #[test]
4310    fn test_semantic_store_count_by_tag() {
4311        let store = SemanticStore::new();
4312        store.store("a", "1", vec!["rust".into(), "sys".into()]).unwrap();
4313        store.store("b", "2", vec!["rust".into(), "ml".into()]).unwrap();
4314        store.store("c", "3", vec!["ml".into()]).unwrap();
4315        assert_eq!(store.count_by_tag("rust").unwrap(), 2);
4316        assert_eq!(store.count_by_tag("ml").unwrap(), 2);
4317        assert_eq!(store.count_by_tag("sys").unwrap(), 1);
4318        assert_eq!(store.count_by_tag("absent").unwrap(), 0);
4319    }
4320
4321    #[test]
4322    fn test_working_memory_get_many_returns_present_values() {
4323        let wm = WorkingMemory::new(10).unwrap();
4324        wm.set("a", "alpha".to_string()).unwrap();
4325        wm.set("b", "beta".to_string()).unwrap();
4326        // get_many returns a Vec<Option<String>> in the same order as the input keys
4327        let results = wm.get_many(&["a", "b", "c"]).unwrap();
4328        assert_eq!(results[0].as_deref(), Some("alpha"));
4329        assert_eq!(results[1].as_deref(), Some("beta"));
4330        assert_eq!(results[2], None);
4331    }
4332
4333    #[test]
4334    fn test_working_memory_update_if_exists_changes_value() {
4335        let wm = WorkingMemory::new(5).unwrap();
4336        wm.set("x", "old".to_string()).unwrap();
4337        assert!(wm.update_if_exists("x", "new".to_string()).unwrap());
4338        assert_eq!(wm.get("x").unwrap().as_deref(), Some("new"));
4339    }
4340
4341    #[test]
4342    fn test_working_memory_update_if_exists_returns_false_for_missing() {
4343        let wm = WorkingMemory::new(5).unwrap();
4344        assert!(!wm.update_if_exists("nope", "val".to_string()).unwrap());
4345    }
4346
4347    #[test]
4348    fn test_episodic_total_recall_count_sums_all_accesses() {
4349        let store = EpisodicStore::new();
4350        let agent = AgentId::new("agent1");
4351        store.add_episode(agent.clone(), "first", 0.8).unwrap();
4352        store.add_episode(agent.clone(), "second", 0.5).unwrap();
4353        // recall() increments recall_count on each returned item
4354        store.recall(&agent, 10).unwrap();
4355        store.recall(&agent, 10).unwrap();
4356        let total = store.total_recall_count(&agent).unwrap();
4357        // 2 items × 2 recalls = 4
4358        assert_eq!(total, 4);
4359    }
4360
4361    #[test]
4362    fn test_episodic_total_recall_count_zero_before_recall() {
4363        let store = EpisodicStore::new();
4364        let agent = AgentId::new("fresh");
4365        store.add_episode(agent.clone(), "ep", 0.5).unwrap();
4366        assert_eq!(store.total_recall_count(&agent).unwrap(), 0);
4367    }
4368
4369    #[test]
4370    fn test_episodic_recall_all_returns_all_items() {
4371        let store = EpisodicStore::new();
4372        let agent = AgentId::new("agent-all");
4373        store.add_episode(agent.clone(), "one", 0.9).unwrap();
4374        store.add_episode(agent.clone(), "two", 0.5).unwrap();
4375        store.add_episode(agent.clone(), "three", 0.1).unwrap();
4376        let all = store.recall_all(&agent).unwrap();
4377        assert_eq!(all.len(), 3);
4378    }
4379
4380    #[test]
4381    fn test_episodic_recall_all_empty_agent_returns_empty() {
4382        let store = EpisodicStore::new();
4383        let agent = AgentId::new("nobody");
4384        let all = store.recall_all(&agent).unwrap();
4385        assert!(all.is_empty());
4386    }
4387
4388    #[test]
4389    fn test_episodic_clear_all_removes_all_agents() {
4390        let store = EpisodicStore::new();
4391        let a1 = AgentId::new("a1");
4392        let a2 = AgentId::new("a2");
4393        store.add_episode(a1.clone(), "ep", 0.5).unwrap();
4394        store.add_episode(a2.clone(), "ep", 0.5).unwrap();
4395        store.clear_all().unwrap();
4396        assert_eq!(store.len().unwrap(), 0);
4397        assert!(store.list_agents().unwrap().is_empty());
4398    }
4399
4400    #[test]
4401    fn test_working_memory_drain_returns_all_and_empties() {
4402        let wm = WorkingMemory::new(10).unwrap();
4403        wm.set("x", "1".to_string()).unwrap();
4404        wm.set("y", "2".to_string()).unwrap();
4405        let drained = wm.drain().unwrap();
4406        assert_eq!(drained.len(), 2);
4407        assert!(wm.is_empty().unwrap());
4408    }
4409
4410    #[test]
4411    fn test_working_memory_drain_preserves_insertion_order() {
4412        let wm = WorkingMemory::new(10).unwrap();
4413        wm.set("a", "1".to_string()).unwrap();
4414        wm.set("b", "2".to_string()).unwrap();
4415        wm.set("c", "3".to_string()).unwrap();
4416        let drained = wm.drain().unwrap();
4417        let keys: Vec<&str> = drained.iter().map(|(k, _)| k.as_str()).collect();
4418        assert_eq!(keys, vec!["a", "b", "c"]);
4419    }
4420
4421    #[test]
4422    fn test_semantic_store_tag_count() {
4423        let store = SemanticStore::new();
4424        store.store("a", "1", vec!["rust".into(), "sys".into()]).unwrap();
4425        store.store("b", "2", vec!["rust".into(), "ml".into()]).unwrap();
4426        assert_eq!(store.tag_count().unwrap(), 3); // rust, sys, ml
4427    }
4428
4429    #[test]
4430    fn test_semantic_store_tag_count_empty() {
4431        let store = SemanticStore::new();
4432        assert_eq!(store.tag_count().unwrap(), 0);
4433    }
4434
4435    #[test]
4436    fn test_episodic_top_n_returns_highest_importance() {
4437        let store = EpisodicStore::new();
4438        let agent = AgentId::new("ag");
4439        store.add_episode(agent.clone(), "high", 0.9).unwrap();
4440        store.add_episode(agent.clone(), "med", 0.5).unwrap();
4441        store.add_episode(agent.clone(), "low", 0.1).unwrap();
4442        let top = store.top_n(&agent, 2).unwrap();
4443        assert_eq!(top.len(), 2);
4444        assert_eq!(top[0].content, "high");
4445        assert_eq!(top[1].content, "med");
4446    }
4447
4448    #[test]
4449    fn test_episodic_top_n_zero_returns_all() {
4450        let store = EpisodicStore::new();
4451        let agent = AgentId::new("ag");
4452        store.add_episode(agent.clone(), "a", 0.9).unwrap();
4453        store.add_episode(agent.clone(), "b", 0.5).unwrap();
4454        assert_eq!(store.top_n(&agent, 0).unwrap().len(), 2);
4455    }
4456
4457    #[test]
4458    fn test_episodic_search_by_importance_range() {
4459        let store = EpisodicStore::new();
4460        let agent = AgentId::new("ag");
4461        store.add_episode(agent.clone(), "high", 0.9).unwrap();
4462        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
4463        store.add_episode(agent.clone(), "low", 0.1).unwrap();
4464        let results = store.search_by_importance_range(&agent, 0.4, 1.0, 0).unwrap();
4465        assert_eq!(results.len(), 2);
4466        assert_eq!(results[0].content, "high");
4467    }
4468
4469    #[test]
4470    fn test_semantic_store_contains_true_for_stored_key() {
4471        let store = SemanticStore::new();
4472        store.store("k", "v", vec![]).unwrap();
4473        assert!(store.contains("k").unwrap());
4474    }
4475
4476    #[test]
4477    fn test_semantic_store_contains_false_for_missing_key() {
4478        let store = SemanticStore::new();
4479        assert!(!store.contains("missing").unwrap());
4480    }
4481
4482    #[test]
4483    fn test_working_memory_rename_changes_key() {
4484        let wm = WorkingMemory::new(10).unwrap();
4485        wm.set("old", "value".to_string()).unwrap();
4486        assert!(wm.rename("old", "new").unwrap());
4487        assert_eq!(wm.get("new").unwrap().as_deref(), Some("value"));
4488        assert!(wm.get("old").unwrap().is_none());
4489    }
4490
4491    #[test]
4492    fn test_working_memory_rename_preserves_order() {
4493        let wm = WorkingMemory::new(10).unwrap();
4494        wm.set("a", "1".to_string()).unwrap();
4495        wm.set("b", "2".to_string()).unwrap();
4496        wm.rename("a", "x").unwrap();
4497        assert_eq!(wm.keys().unwrap(), vec!["x", "b"]);
4498    }
4499
4500    #[test]
4501    fn test_working_memory_rename_missing_returns_false() {
4502        let wm = WorkingMemory::new(10).unwrap();
4503        assert!(!wm.rename("nope", "other").unwrap());
4504    }
4505
4506    #[test]
4507    fn test_episodic_importance_stats_basic() {
4508        let store = EpisodicStore::new();
4509        let agent = AgentId::new("ag");
4510        store.add_episode(agent.clone(), "a", 0.2).unwrap();
4511        store.add_episode(agent.clone(), "b", 0.6).unwrap();
4512        store.add_episode(agent.clone(), "c", 1.0).unwrap();
4513        let (count, min, max, mean) = store.importance_stats(&agent).unwrap();
4514        assert_eq!(count, 3);
4515        assert!((min - 0.2).abs() < 1e-5);
4516        assert!((max - 1.0).abs() < 1e-5);
4517        assert!((mean - 0.6).abs() < 1e-4);
4518    }
4519
4520    #[test]
4521    fn test_episodic_importance_stats_empty_agent() {
4522        let store = EpisodicStore::new();
4523        let agent = AgentId::new("nobody");
4524        let (count, min, max, mean) = store.importance_stats(&agent).unwrap();
4525        assert_eq!(count, 0);
4526        assert_eq!(min, 0.0);
4527        assert_eq!(max, 0.0);
4528        assert_eq!(mean, 0.0);
4529    }
4530
4531    #[test]
4532    fn test_semantic_store_keys_returns_stored_keys() {
4533        let store = SemanticStore::new();
4534        store.store("alpha", "v1", vec![]).unwrap();
4535        store.store("beta", "v2", vec![]).unwrap();
4536        let keys = store.keys().unwrap();
4537        assert_eq!(keys.len(), 2);
4538        assert!(keys.contains(&"alpha".to_string()));
4539        assert!(keys.contains(&"beta".to_string()));
4540    }
4541
4542    #[test]
4543    fn test_working_memory_snapshot_clones_contents() {
4544        let wm = WorkingMemory::new(10).unwrap();
4545        wm.set("x", "1".to_string()).unwrap();
4546        wm.set("y", "2".to_string()).unwrap();
4547        let snap = wm.snapshot().unwrap();
4548        assert_eq!(snap.get("x").map(|s| s.as_str()), Some("1"));
4549        assert_eq!(snap.get("y").map(|s| s.as_str()), Some("2"));
4550        // Memory still has items
4551        assert_eq!(wm.len().unwrap(), 2);
4552    }
4553
4554    // ── Round 3: new methods ──────────────────────────────────────────────────
4555
4556    #[test]
4557    fn test_memory_item_display_includes_content() {
4558        let agent = AgentId::new("a");
4559        let item = MemoryItem::new(agent, "hello world", 0.75, vec![]);
4560        let s = format!("{item}");
4561        assert!(s.contains("hello world"), "Display should include content");
4562        assert!(s.contains("0.75"), "Display should include importance");
4563    }
4564
4565    #[test]
4566    fn test_decay_policy_half_life_hours_accessor() {
4567        let policy = DecayPolicy::exponential(12.5).unwrap();
4568        assert!((policy.half_life_hours() - 12.5).abs() < f64::EPSILON);
4569    }
4570
4571    #[test]
4572    fn test_semantic_store_list_keys_returns_all_keys() {
4573        let store = SemanticStore::new();
4574        store.store("k1", "v1", vec![]).unwrap();
4575        store.store("k2", "v2", vec![]).unwrap();
4576        let keys = store.list_keys().unwrap();
4577        assert_eq!(keys.len(), 2);
4578        assert!(keys.contains(&"k1".to_string()));
4579        assert!(keys.contains(&"k2".to_string()));
4580    }
4581
4582    #[test]
4583    fn test_semantic_store_update_tags_returns_true_on_found() {
4584        let store = SemanticStore::new();
4585        store.store("k", "v", vec!["old".into()]).unwrap();
4586        let updated = store.update_tags("k", vec!["new".into()]).unwrap();
4587        assert!(updated);
4588        let (_, tags) = store.retrieve_by_key("k").unwrap().unwrap();
4589        assert_eq!(tags, vec!["new".to_string()]);
4590    }
4591
4592    #[test]
4593    fn test_semantic_store_update_tags_returns_false_on_missing() {
4594        let store = SemanticStore::new();
4595        assert!(!store.update_tags("missing", vec![]).unwrap());
4596    }
4597
4598    #[test]
4599    fn test_episodic_store_recall_since_filters_old() {
4600        let store = EpisodicStore::new();
4601        let agent = AgentId::new("a");
4602        let old_ts = Utc::now() - chrono::Duration::hours(2);
4603        store.add_episode_at(agent.clone(), "old", 0.9, old_ts).unwrap();
4604        store.add_episode(agent.clone(), "new", 0.5).unwrap();
4605
4606        let cutoff = Utc::now() - chrono::Duration::minutes(30);
4607        let items = store.recall_since(&agent, cutoff, 0).unwrap();
4608        assert_eq!(items.len(), 1);
4609        assert_eq!(items[0].content, "new");
4610    }
4611
4612    #[test]
4613    fn test_episodic_store_recall_since_limit_respected() {
4614        let store = EpisodicStore::new();
4615        let agent = AgentId::new("a");
4616        for i in 0..5 {
4617            store.add_episode(agent.clone(), format!("item {i}"), 0.5).unwrap();
4618        }
4619        let cutoff = Utc::now() - chrono::Duration::hours(1);
4620        let items = store.recall_since(&agent, cutoff, 2).unwrap();
4621        assert_eq!(items.len(), 2);
4622    }
4623
4624    #[test]
4625    fn test_episodic_store_update_content_returns_true_on_found() {
4626        let store = EpisodicStore::new();
4627        let agent = AgentId::new("a");
4628        let id = store.add_episode(agent.clone(), "original", 0.5).unwrap();
4629        let updated = store.update_content(&agent, &id, "updated").unwrap();
4630        assert!(updated);
4631        let item = store.recall_by_id(&agent, &id).unwrap().unwrap();
4632        assert_eq!(item.content, "updated");
4633    }
4634
4635    #[test]
4636    fn test_episodic_store_update_content_returns_false_on_missing() {
4637        let store = EpisodicStore::new();
4638        let agent = AgentId::new("a");
4639        let fake_id = MemoryId::new("does-not-exist");
4640        assert!(!store.update_content(&agent, &fake_id, "x").unwrap());
4641    }
4642
4643    #[test]
4644    fn test_working_memory_capacity_matches_constructor() {
4645        let wm = WorkingMemory::new(7).unwrap();
4646        assert_eq!(wm.capacity(), 7);
4647    }
4648
4649    // ── Round 4: EpisodicStore new helpers ───────────────────────────────────
4650
4651    #[test]
4652    fn test_add_episode_with_tags_stores_and_recalls() {
4653        let store = EpisodicStore::new();
4654        let agent = AgentId::new("a");
4655        let id = store
4656            .add_episode_with_tags(agent.clone(), "tagged", 0.8, vec!["t1".to_string(), "t2".to_string()])
4657            .unwrap();
4658        let item = store.recall_by_id(&agent, &id).unwrap().unwrap();
4659        assert_eq!(item.content, "tagged");
4660        assert_eq!(item.tags, vec!["t1", "t2"]);
4661    }
4662
4663    #[test]
4664    fn test_remove_by_id_deletes_episode() {
4665        let store = EpisodicStore::new();
4666        let agent = AgentId::new("a");
4667        let id = store.add_episode(agent.clone(), "to-delete", 0.5).unwrap();
4668        assert!(store.remove_by_id(&agent, &id).unwrap());
4669        assert!(store.recall_by_id(&agent, &id).unwrap().is_none());
4670    }
4671
4672    #[test]
4673    fn test_remove_by_id_returns_false_for_missing() {
4674        let store = EpisodicStore::new();
4675        let agent = AgentId::new("a");
4676        let fake = MemoryId::new("does-not-exist");
4677        assert!(!store.remove_by_id(&agent, &fake).unwrap());
4678    }
4679
4680    #[test]
4681    fn test_update_tags_by_id_replaces_tags() {
4682        let store = EpisodicStore::new();
4683        let agent = AgentId::new("a");
4684        let id = store
4685            .add_episode_with_tags(agent.clone(), "x", 0.5, vec!["old".to_string()])
4686            .unwrap();
4687        let updated = store
4688            .update_tags_by_id(&agent, &id, vec!["new1".to_string(), "new2".to_string()])
4689            .unwrap();
4690        assert!(updated);
4691        let item = store.recall_by_id(&agent, &id).unwrap().unwrap();
4692        assert_eq!(item.tags, vec!["new1", "new2"]);
4693    }
4694
4695    #[test]
4696    fn test_update_tags_by_id_returns_false_for_missing() {
4697        let store = EpisodicStore::new();
4698        let agent = AgentId::new("a");
4699        let fake = MemoryId::new("nope");
4700        assert!(!store.update_tags_by_id(&agent, &fake, vec![]).unwrap());
4701    }
4702
4703    #[test]
4704    fn test_max_importance_for_returns_highest() {
4705        let store = EpisodicStore::new();
4706        let agent = AgentId::new("a");
4707        store.add_episode(agent.clone(), "low", 0.2).unwrap();
4708        store.add_episode(agent.clone(), "high", 0.9).unwrap();
4709        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
4710        let max = store.max_importance_for(&agent).unwrap().unwrap();
4711        assert!((max - 0.9).abs() < 1e-6);
4712    }
4713
4714    #[test]
4715    fn test_max_importance_for_returns_none_when_empty() {
4716        let store = EpisodicStore::new();
4717        let agent = AgentId::new("empty");
4718        assert!(store.max_importance_for(&agent).unwrap().is_none());
4719    }
4720
4721    // ── Round 4: WorkingMemory::snapshot / pop_oldest ────────────────────────
4722
4723    #[test]
4724    fn test_working_memory_snapshot_returns_all_entries() {
4725        let wm = WorkingMemory::new(10).unwrap();
4726        wm.set("k1", "v1").unwrap();
4727        wm.set("k2", "v2").unwrap();
4728        let snap = wm.snapshot().unwrap();
4729        assert_eq!(snap.len(), 2);
4730        assert_eq!(snap.get("k1").unwrap(), "v1");
4731        assert_eq!(snap.get("k2").unwrap(), "v2");
4732    }
4733
4734    #[test]
4735    fn test_working_memory_pop_oldest_removes_first_inserted() {
4736        let wm = WorkingMemory::new(10).unwrap();
4737        wm.set("first", "1").unwrap();
4738        wm.set("second", "2").unwrap();
4739        let popped = wm.pop_oldest().unwrap().unwrap();
4740        assert_eq!(popped.0, "first");
4741        assert_eq!(popped.1, "1");
4742        assert_eq!(wm.len().unwrap(), 1);
4743    }
4744
4745    #[test]
4746    fn test_working_memory_pop_oldest_returns_none_when_empty() {
4747        let wm = WorkingMemory::new(5).unwrap();
4748        assert!(wm.pop_oldest().unwrap().is_none());
4749    }
4750
4751    // ── Round 4: SemanticStore::keys_with_tag ────────────────────────────────
4752
4753    #[test]
4754    fn test_semantic_store_keys_with_tag_returns_matching() {
4755        let store = SemanticStore::new();
4756        store
4757            .store("doc1", "content one", vec!["alpha".to_string(), "beta".to_string()])
4758            .unwrap();
4759        store
4760            .store("doc2", "content two", vec!["alpha".to_string()])
4761            .unwrap();
4762        store
4763            .store("doc3", "content three", vec!["gamma".to_string()])
4764            .unwrap();
4765        let mut keys = store.keys_with_tag("alpha").unwrap();
4766        keys.sort();
4767        assert_eq!(keys, vec!["doc1", "doc2"]);
4768    }
4769
4770    #[test]
4771    fn test_semantic_store_keys_with_tag_empty_when_no_match() {
4772        let store = SemanticStore::new();
4773        store
4774            .store("doc1", "content", vec!["x".to_string()])
4775            .unwrap();
4776        let keys = store.keys_with_tag("z").unwrap();
4777        assert!(keys.is_empty());
4778    }
4779
4780    // ── Round 16: oldest, get_or_default ─────────────────────────────────────
4781
4782    #[test]
4783    fn test_episodic_oldest_returns_first_inserted() {
4784        let store = EpisodicStore::new();
4785        let agent = AgentId::new("agent-oldest");
4786        store.add_episode(agent.clone(), "first episode", 0.5).unwrap();
4787        store.add_episode(agent.clone(), "second episode", 0.9).unwrap();
4788        let oldest = store.oldest(&agent).unwrap().unwrap();
4789        assert_eq!(oldest.content, "first episode");
4790    }
4791
4792    #[test]
4793    fn test_episodic_oldest_none_when_no_episodes() {
4794        let store = EpisodicStore::new();
4795        let agent = AgentId::new("no-episodes");
4796        assert!(store.oldest(&agent).unwrap().is_none());
4797    }
4798
4799    #[test]
4800    fn test_working_memory_get_or_default_returns_value_when_present() {
4801        let wm = WorkingMemory::new(10).unwrap();
4802        wm.set("key", "value").unwrap();
4803        assert_eq!(wm.get_or_default("key", "fallback").unwrap(), "value");
4804    }
4805
4806    #[test]
4807    fn test_working_memory_get_or_default_returns_default_when_absent() {
4808        let wm = WorkingMemory::new(10).unwrap();
4809        assert_eq!(wm.get_or_default("missing", "fallback").unwrap(), "fallback");
4810    }
4811
4812    // ── Round 5: EpisodicStore new helpers ───────────────────────────────────
4813
4814    #[test]
4815    fn test_episodic_count_for_returns_correct_count() {
4816        let store = EpisodicStore::new();
4817        let agent = AgentId::new("a");
4818        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
4819        store.add_episode(agent.clone(), "e2", 0.5).unwrap();
4820        assert_eq!(store.count_for(&agent).unwrap(), 2);
4821    }
4822
4823    #[test]
4824    fn test_episodic_count_for_returns_zero_for_unknown_agent() {
4825        let store = EpisodicStore::new();
4826        let agent = AgentId::new("unknown");
4827        assert_eq!(store.count_for(&agent).unwrap(), 0);
4828    }
4829
4830    #[test]
4831    fn test_recall_by_tag_returns_matching_sorted_by_importance() {
4832        let store = EpisodicStore::new();
4833        let agent = AgentId::new("a");
4834        store
4835            .add_episode_with_tags(agent.clone(), "low", 0.1, vec!["news".to_string()])
4836            .unwrap();
4837        store
4838            .add_episode_with_tags(agent.clone(), "high", 0.9, vec!["news".to_string()])
4839            .unwrap();
4840        store
4841            .add_episode_with_tags(agent.clone(), "other", 0.8, vec!["other".to_string()])
4842            .unwrap();
4843        let items = store.recall_by_tag(&agent, "news", 0).unwrap();
4844        assert_eq!(items.len(), 2);
4845        assert_eq!(items[0].content, "high");
4846    }
4847
4848    #[test]
4849    fn test_recall_by_tag_respects_limit() {
4850        let store = EpisodicStore::new();
4851        let agent = AgentId::new("a");
4852        for i in 0..5 {
4853            store
4854                .add_episode_with_tags(agent.clone(), format!("ep{i}"), 0.5, vec!["t".to_string()])
4855                .unwrap();
4856        }
4857        let items = store.recall_by_tag(&agent, "t", 2).unwrap();
4858        assert_eq!(items.len(), 2);
4859    }
4860
4861    #[test]
4862    fn test_merge_from_copies_episodes() {
4863        let src = EpisodicStore::new();
4864        let dst = EpisodicStore::new();
4865        let agent = AgentId::new("a");
4866        src.add_episode(agent.clone(), "ep1", 0.5).unwrap();
4867        src.add_episode(agent.clone(), "ep2", 0.9).unwrap();
4868        let count = dst.merge_from(&src, &agent).unwrap();
4869        assert_eq!(count, 2);
4870        assert_eq!(dst.count_for(&agent).unwrap(), 2);
4871    }
4872
4873    // ── Round 5: WorkingMemory get_all_keys / replace_all ────────────────────
4874
4875    #[test]
4876    fn test_working_memory_get_all_keys_preserves_insertion_order() {
4877        let wm = WorkingMemory::new(10).unwrap();
4878        wm.set("first", "1").unwrap();
4879        wm.set("second", "2").unwrap();
4880        wm.set("third", "3").unwrap();
4881        assert_eq!(wm.get_all_keys().unwrap(), vec!["first", "second", "third"]);
4882    }
4883
4884    #[test]
4885    fn test_working_memory_replace_all_replaces_contents() {
4886        let wm = WorkingMemory::new(10).unwrap();
4887        wm.set("old", "x").unwrap();
4888        let mut new_map = std::collections::HashMap::new();
4889        new_map.insert("a".to_string(), "1".to_string());
4890        new_map.insert("b".to_string(), "2".to_string());
4891        wm.replace_all(new_map).unwrap();
4892        assert_eq!(wm.len().unwrap(), 2);
4893        assert!(wm.get("old").unwrap().is_none());
4894        assert_eq!(wm.get("a").unwrap().unwrap(), "1");
4895    }
4896
4897    // ── Round 5: SemanticStore::remove / count ───────────────────────────────
4898
4899    #[test]
4900    fn test_semantic_store_remove_returns_true_and_removes() {
4901        let store = SemanticStore::new();
4902        store.store("k1", "v1", vec![]).unwrap();
4903        store.store("k2", "v2", vec![]).unwrap();
4904        assert!(store.remove("k1").unwrap());
4905        assert_eq!(store.count().unwrap(), 1);
4906        let keys = store.list_keys().unwrap();
4907        assert_eq!(keys, vec!["k2"]);
4908    }
4909
4910    #[test]
4911    fn test_semantic_store_remove_returns_false_when_missing() {
4912        let store = SemanticStore::new();
4913        assert!(!store.remove("ghost").unwrap());
4914    }
4915
4916    #[test]
4917    fn test_semantic_store_count_matches_len() {
4918        let store = SemanticStore::new();
4919        store.store("a", "1", vec![]).unwrap();
4920        store.store("b", "2", vec![]).unwrap();
4921        assert_eq!(store.count().unwrap(), store.len().unwrap());
4922    }
4923
4924    // ── Round 17: newest, values ─────────────────────────────────────────────
4925
4926    #[test]
4927    fn test_episodic_newest_returns_last_inserted() {
4928        let store = EpisodicStore::new();
4929        let agent = AgentId::new("agent-newest");
4930        store.add_episode(agent.clone(), "first", 0.3).unwrap();
4931        store.add_episode(agent.clone(), "last", 0.8).unwrap();
4932        let newest = store.newest(&agent).unwrap().unwrap();
4933        assert_eq!(newest.content, "last");
4934    }
4935
4936    #[test]
4937    fn test_episodic_newest_none_when_empty() {
4938        let store = EpisodicStore::new();
4939        let agent = AgentId::new("no-ep");
4940        assert!(store.newest(&agent).unwrap().is_none());
4941    }
4942
4943    #[test]
4944    fn test_working_memory_values_returns_values_in_insertion_order() {
4945        let wm = WorkingMemory::new(10).unwrap();
4946        wm.set("a", "apple").unwrap();
4947        wm.set("b", "banana").unwrap();
4948        let vals = wm.values().unwrap();
4949        assert_eq!(vals, vec!["apple", "banana"]);
4950    }
4951
4952    #[test]
4953    fn test_working_memory_values_empty_when_no_entries() {
4954        let wm = WorkingMemory::new(10).unwrap();
4955        assert!(wm.values().unwrap().is_empty());
4956    }
4957
4958    // ── Round 18: clear_agent ────────────────────────────────────────────────
4959
4960    #[test]
4961    fn test_episodic_clear_agent_removes_all_episodes() {
4962        let store = EpisodicStore::new();
4963        let agent = AgentId::new("agent-clear");
4964        store.add_episode(agent.clone(), "ep1", 0.5).unwrap();
4965        store.add_episode(agent.clone(), "ep2", 0.7).unwrap();
4966        let removed = store.clear_agent(&agent).unwrap();
4967        assert_eq!(removed, 2);
4968        assert_eq!(store.agent_memory_count(&agent).unwrap(), 0);
4969    }
4970
4971    #[test]
4972    fn test_episodic_clear_agent_returns_zero_when_none() {
4973        let store = EpisodicStore::new();
4974        let agent = AgentId::new("no-agent");
4975        assert_eq!(store.clear_agent(&agent).unwrap(), 0);
4976    }
4977
4978    // ── Round 19: max_importance_episode, SemanticStore::update ──────────────
4979
4980    #[test]
4981    fn test_episodic_max_importance_episode_returns_highest() {
4982        let store = EpisodicStore::new();
4983        let agent = AgentId::new("agent-max");
4984        store.add_episode(agent.clone(), "low", 0.2).unwrap();
4985        store.add_episode(agent.clone(), "high", 0.9).unwrap();
4986        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
4987        let top = store.max_importance_episode(&agent).unwrap().unwrap();
4988        assert_eq!(top.content, "high");
4989    }
4990
4991    #[test]
4992    fn test_episodic_max_importance_episode_none_when_empty() {
4993        let store = EpisodicStore::new();
4994        let agent = AgentId::new("empty-max");
4995        assert!(store.max_importance_episode(&agent).unwrap().is_none());
4996    }
4997
4998    #[test]
4999    fn test_semantic_store_update_replaces_value() {
5000        let store = SemanticStore::new();
5001        store.store("key1", "original", vec![]).unwrap();
5002        let updated = store.update("key1", "new value").unwrap();
5003        assert!(updated);
5004        let retrieved = store.retrieve_by_key("key1").unwrap().unwrap();
5005        assert_eq!(retrieved.0, "new value");
5006    }
5007
5008    #[test]
5009    fn test_semantic_store_update_returns_false_for_missing_key() {
5010        let store = SemanticStore::new();
5011        assert!(!store.update("missing", "value").unwrap());
5012    }
5013
5014    // ── Round 6: EpisodicStore::clear_for / importance_sum ───────────────────
5015
5016    #[test]
5017    fn test_episodic_clear_for_removes_all_episodes_for_agent() {
5018        let store = EpisodicStore::new();
5019        let agent = AgentId::new("a");
5020        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
5021        store.add_episode(agent.clone(), "e2", 0.9).unwrap();
5022        let removed = store.clear_for(&agent).unwrap();
5023        assert_eq!(removed, 2);
5024        assert_eq!(store.count_for(&agent).unwrap(), 0);
5025    }
5026
5027    #[test]
5028    fn test_episodic_clear_for_returns_zero_for_unknown_agent() {
5029        let store = EpisodicStore::new();
5030        let agent = AgentId::new("ghost");
5031        assert_eq!(store.clear_for(&agent).unwrap(), 0);
5032    }
5033
5034    #[test]
5035    fn test_episodic_importance_sum_correct() {
5036        let store = EpisodicStore::new();
5037        let agent = AgentId::new("a");
5038        store.add_episode(agent.clone(), "e1", 0.3).unwrap();
5039        store.add_episode(agent.clone(), "e2", 0.5).unwrap();
5040        let sum = store.importance_sum(&agent).unwrap();
5041        assert!((sum - 0.8).abs() < 1e-5);
5042    }
5043
5044    #[test]
5045    fn test_episodic_importance_sum_zero_when_empty() {
5046        let store = EpisodicStore::new();
5047        let agent = AgentId::new("empty");
5048        assert_eq!(store.importance_sum(&agent).unwrap(), 0.0);
5049    }
5050
5051    // ── Round 6: WorkingMemory::merge_from / entry_count_satisfying ──────────
5052
5053    #[test]
5054    fn test_working_memory_merge_from_copies_entries() {
5055        let src = WorkingMemory::new(10).unwrap();
5056        src.set("x", "1").unwrap();
5057        src.set("y", "2").unwrap();
5058        let dst = WorkingMemory::new(10).unwrap();
5059        let count = dst.merge_from(&src).unwrap();
5060        assert_eq!(count, 2);
5061        assert_eq!(dst.get("x").unwrap().unwrap(), "1");
5062        assert_eq!(dst.get("y").unwrap().unwrap(), "2");
5063    }
5064
5065    #[test]
5066    fn test_working_memory_entry_count_satisfying_counts_matching() {
5067        let wm = WorkingMemory::new(10).unwrap();
5068        wm.set("a", "hello").unwrap();
5069        wm.set("b", "world").unwrap();
5070        wm.set("c", "hello world").unwrap();
5071        let count = wm
5072            .entry_count_satisfying(|_, v| v.contains("hello"))
5073            .unwrap();
5074        assert_eq!(count, 2);
5075    }
5076
5077    // ── Round 6: SemanticStore::update_value ─────────────────────────────────
5078
5079    #[test]
5080    fn test_semantic_update_value_replaces_stored_value() {
5081        let store = SemanticStore::new();
5082        store.store("k", "old", vec![]).unwrap();
5083        assert!(store.update_value("k", "new").unwrap());
5084        let pairs = store.retrieve(&[]).unwrap();
5085        assert!(pairs.iter().any(|(_, v)| v == "new"));
5086    }
5087
5088    #[test]
5089    fn test_semantic_update_value_returns_false_when_missing() {
5090        let store = SemanticStore::new();
5091        assert!(!store.update_value("nope", "x").unwrap());
5092    }
5093
5094    // ── Round 7: EpisodicStore::agent_ids / find_by_content ──────────────────
5095
5096    #[test]
5097    fn test_episodic_agent_ids_returns_all_agents() {
5098        let store = EpisodicStore::new();
5099        let a1 = AgentId::new("agent-1");
5100        let a2 = AgentId::new("agent-2");
5101        store.add_episode(a1.clone(), "e", 0.5).unwrap();
5102        store.add_episode(a2.clone(), "e", 0.5).unwrap();
5103        let mut ids = store.agent_ids().unwrap();
5104        ids.sort_by_key(|id| id.0.clone());
5105        assert_eq!(ids, vec![a1, a2]);
5106    }
5107
5108    #[test]
5109    fn test_episodic_agent_ids_empty_for_new_store() {
5110        let store = EpisodicStore::new();
5111        assert!(store.agent_ids().unwrap().is_empty());
5112    }
5113
5114    #[test]
5115    fn test_find_by_content_returns_matching_episodes() {
5116        let store = EpisodicStore::new();
5117        let agent = AgentId::new("a");
5118        store.add_episode(agent.clone(), "hello world", 0.5).unwrap();
5119        store.add_episode(agent.clone(), "goodbye world", 0.8).unwrap();
5120        store.add_episode(agent.clone(), "something else", 0.9).unwrap();
5121        let results = store.find_by_content(&agent, "world").unwrap();
5122        assert_eq!(results.len(), 2);
5123        // sorted by descending importance
5124        assert_eq!(results[0].content, "goodbye world");
5125    }
5126
5127    #[test]
5128    fn test_find_by_content_returns_empty_when_no_match() {
5129        let store = EpisodicStore::new();
5130        let agent = AgentId::new("a");
5131        store.add_episode(agent.clone(), "hello", 0.5).unwrap();
5132        let results = store.find_by_content(&agent, "xyz").unwrap();
5133        assert!(results.is_empty());
5134    }
5135
5136    // ── Round 20: add_episode_at / add_episodes_batch / SemanticStore::keys_matching ──
5137
5138    #[test]
5139    fn test_add_episode_at_stores_with_given_timestamp() {
5140        let store = EpisodicStore::new();
5141        let agent = AgentId::new("agent-ts");
5142        let ts = chrono::Utc::now() - chrono::Duration::hours(2);
5143        let id = store.add_episode_at(agent.clone(), "past event", 0.7, ts).unwrap();
5144        let items = store.recall_all(&agent).unwrap();
5145        assert_eq!(items.len(), 1);
5146        assert_eq!(items[0].id, id);
5147        assert_eq!(items[0].content, "past event");
5148        assert!((items[0].timestamp - ts).num_seconds().abs() < 1);
5149    }
5150
5151    #[test]
5152    fn test_add_episodes_batch_returns_all_ids() {
5153        let store = EpisodicStore::new();
5154        let agent = AgentId::new("batch-agent");
5155        let episodes = vec![
5156            ("first", 0.5f32),
5157            ("second", 0.8f32),
5158            ("third", 0.3f32),
5159        ];
5160        let ids = store.add_episodes_batch(agent.clone(), episodes).unwrap();
5161        assert_eq!(ids.len(), 3);
5162        let all = store.recall_all(&agent).unwrap();
5163        assert_eq!(all.len(), 3);
5164    }
5165
5166    #[test]
5167    fn test_add_episodes_batch_empty_iter_returns_empty_ids() {
5168        let store = EpisodicStore::new();
5169        let agent = AgentId::new("empty-batch");
5170        let ids = store
5171            .add_episodes_batch(agent.clone(), Vec::<(String, f32)>::new())
5172            .unwrap();
5173        assert!(ids.is_empty());
5174        assert_eq!(store.count_for(&agent).unwrap(), 0);
5175    }
5176
5177    #[test]
5178    fn test_semantic_keys_matching_returns_substring_matches() {
5179        let store = SemanticStore::new();
5180        store.store("rust_intro", "value1", vec![]).unwrap();
5181        store.store("rust_advanced", "value2", vec![]).unwrap();
5182        store.store("python_basics", "value3", vec![]).unwrap();
5183        let matches = store.keys_matching("rust").unwrap();
5184        assert_eq!(matches.len(), 2);
5185        assert!(matches.contains(&"rust_intro".to_string()));
5186        assert!(matches.contains(&"rust_advanced".to_string()));
5187    }
5188
5189    #[test]
5190    fn test_semantic_keys_matching_is_case_insensitive() {
5191        let store = SemanticStore::new();
5192        store.store("UPPER_KEY", "v", vec![]).unwrap();
5193        let matches = store.keys_matching("upper").unwrap();
5194        assert_eq!(matches.len(), 1);
5195    }
5196
5197    #[test]
5198    fn test_semantic_keys_matching_empty_when_no_match() {
5199        let store = SemanticStore::new();
5200        store.store("abc", "val", vec![]).unwrap();
5201        let matches = store.keys_matching("xyz").unwrap();
5202        assert!(matches.is_empty());
5203    }
5204
5205    // ── Round 8: EpisodicStore::importance_avg / deduplicate_content ─────────
5206
5207    #[test]
5208    fn test_importance_avg_correct() {
5209        let store = EpisodicStore::new();
5210        let agent = AgentId::new("a");
5211        store.add_episode(agent.clone(), "e1", 0.2).unwrap();
5212        store.add_episode(agent.clone(), "e2", 0.8).unwrap();
5213        let avg = store.importance_avg(&agent).unwrap();
5214        assert!((avg - 0.5).abs() < 1e-5);
5215    }
5216
5217    #[test]
5218    fn test_importance_avg_zero_for_empty() {
5219        let store = EpisodicStore::new();
5220        let agent = AgentId::new("empty");
5221        assert_eq!(store.importance_avg(&agent).unwrap(), 0.0);
5222    }
5223
5224    #[test]
5225    fn test_deduplicate_content_removes_lower_importance_duplicate() {
5226        let store = EpisodicStore::new();
5227        let agent = AgentId::new("a");
5228        store.add_episode(agent.clone(), "same", 0.3).unwrap();
5229        store.add_episode(agent.clone(), "same", 0.9).unwrap();
5230        store.add_episode(agent.clone(), "different", 0.5).unwrap();
5231        let removed = store.deduplicate_content(&agent).unwrap();
5232        assert_eq!(removed, 1);
5233        assert_eq!(store.count_for(&agent).unwrap(), 2);
5234    }
5235
5236    #[test]
5237    fn test_deduplicate_content_keeps_highest_importance() {
5238        let store = EpisodicStore::new();
5239        let agent = AgentId::new("a");
5240        store.add_episode(agent.clone(), "dup", 0.1).unwrap();
5241        store.add_episode(agent.clone(), "dup", 0.7).unwrap();
5242        store.deduplicate_content(&agent).unwrap();
5243        let items = store.recall(&agent, 10).unwrap();
5244        assert_eq!(items.len(), 1);
5245        assert!((items[0].importance - 0.7).abs() < 1e-5);
5246    }
5247
5248    // ── Round 8: WorkingMemory::iter_sorted / SemanticStore::get_value ───────
5249
5250    #[test]
5251    fn test_working_memory_iter_sorted_alphabetical_order() {
5252        let wm = WorkingMemory::new(10).unwrap();
5253        wm.set("c", "3").unwrap();
5254        wm.set("a", "1").unwrap();
5255        wm.set("b", "2").unwrap();
5256        let sorted = wm.iter_sorted().unwrap();
5257        let keys: Vec<&str> = sorted.iter().map(|(k, _)| k.as_str()).collect();
5258        assert_eq!(keys, vec!["a", "b", "c"]);
5259    }
5260
5261    #[test]
5262    fn test_semantic_get_value_returns_value_for_existing_key() {
5263        let store = SemanticStore::new();
5264        store.store("mykey", "myvalue", vec![]).unwrap();
5265        assert_eq!(store.get_value("mykey").unwrap(), Some("myvalue".to_string()));
5266    }
5267
5268    #[test]
5269    fn test_semantic_get_value_returns_none_for_missing_key() {
5270        let store = SemanticStore::new();
5271        assert!(store.get_value("ghost").unwrap().is_none());
5272    }
5273
5274    // ── Round 9: recall_top_n / filter_by_importance / update_many / entry_count_with_embedding ──
5275
5276    #[test]
5277    fn test_recall_top_n_returns_highest_importance_items() {
5278        let store = EpisodicStore::new();
5279        let agent = AgentId::new("a");
5280        store.add_episode(agent.clone(), "low", 0.1).unwrap();
5281        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
5282        store.add_episode(agent.clone(), "high", 0.9).unwrap();
5283        let top = store.recall_top_n(&agent, 2).unwrap();
5284        assert_eq!(top.len(), 2);
5285        assert!((top[0].importance - 0.9).abs() < 1e-5);
5286    }
5287
5288    #[test]
5289    fn test_recall_top_n_clamps_to_available_items() {
5290        let store = EpisodicStore::new();
5291        let agent = AgentId::new("a");
5292        store.add_episode(agent.clone(), "only", 0.5).unwrap();
5293        let top = store.recall_top_n(&agent, 100).unwrap();
5294        assert_eq!(top.len(), 1);
5295    }
5296
5297    #[test]
5298    fn test_filter_by_importance_returns_items_in_range() {
5299        let store = EpisodicStore::new();
5300        let agent = AgentId::new("b");
5301        store.add_episode(agent.clone(), "low", 0.1).unwrap();
5302        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
5303        store.add_episode(agent.clone(), "high", 0.9).unwrap();
5304        let mid_range = store.filter_by_importance(&agent, 0.3, 0.7).unwrap();
5305        assert_eq!(mid_range.len(), 1);
5306        assert!((mid_range[0].importance - 0.5).abs() < 1e-5);
5307    }
5308
5309    #[test]
5310    fn test_update_many_sets_multiple_keys() {
5311        let wm = WorkingMemory::new(10).unwrap();
5312        wm.set("x", "old_x").unwrap();
5313        wm.set("y", "old_y").unwrap();
5314        let updated = wm
5315            .update_many(vec![("x".to_string(), "new_x".to_string()), ("y".to_string(), "new_y".to_string())])
5316            .unwrap();
5317        assert_eq!(updated, 2);
5318        assert_eq!(wm.get("x").unwrap(), Some("new_x".to_string()));
5319        assert_eq!(wm.get("y").unwrap(), Some("new_y".to_string()));
5320    }
5321
5322    #[test]
5323    fn test_update_many_returns_zero_for_empty_iter() {
5324        let wm = WorkingMemory::new(5).unwrap();
5325        let updated = wm.update_many(Vec::<(String, String)>::new()).unwrap();
5326        assert_eq!(updated, 0);
5327    }
5328
5329    #[test]
5330    fn test_entry_count_with_embedding_counts_only_embedded_entries() {
5331        let store = SemanticStore::new();
5332        store.store_with_embedding("has_emb", "v1", vec![], vec![0.1_f32, 0.2_f32]).unwrap();
5333        store.store("no_emb", "v2", vec![]).unwrap();
5334        assert_eq!(store.entry_count_with_embedding().unwrap(), 1);
5335    }
5336
5337    // ── Round 10: retain_top_n / keys_starting_with ───────────────────────────
5338
5339    #[test]
5340    fn test_retain_top_n_removes_low_importance_episodes() {
5341        let store = EpisodicStore::new();
5342        let agent = AgentId::new("a");
5343        store.add_episode(agent.clone(), "low", 0.1).unwrap();
5344        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
5345        store.add_episode(agent.clone(), "high", 0.9).unwrap();
5346        let removed = store.retain_top_n(&agent, 2).unwrap();
5347        assert_eq!(removed, 1);
5348        let remaining = store.recall(&agent, 10).unwrap();
5349        assert_eq!(remaining.len(), 2);
5350        assert!(remaining.iter().all(|i| i.importance >= 0.5));
5351    }
5352
5353    #[test]
5354    fn test_retain_top_n_noop_when_fewer_than_n() {
5355        let store = EpisodicStore::new();
5356        let agent = AgentId::new("b");
5357        store.add_episode(agent.clone(), "only", 0.7).unwrap();
5358        let removed = store.retain_top_n(&agent, 5).unwrap();
5359        assert_eq!(removed, 0);
5360        assert_eq!(store.recall(&agent, 10).unwrap().len(), 1);
5361    }
5362
5363    #[test]
5364    fn test_keys_starting_with_returns_matching_keys() {
5365        let wm = WorkingMemory::new(10).unwrap();
5366        wm.set("user:name", "alice").unwrap();
5367        wm.set("user:email", "alice@example.com").unwrap();
5368        wm.set("session:id", "abc").unwrap();
5369        let mut keys = wm.keys_starting_with("user:").unwrap();
5370        keys.sort();
5371        assert_eq!(keys, vec!["user:email", "user:name"]);
5372    }
5373
5374    #[test]
5375    fn test_keys_starting_with_returns_empty_when_no_match() {
5376        let wm = WorkingMemory::new(5).unwrap();
5377        wm.set("foo", "bar").unwrap();
5378        assert!(wm.keys_starting_with("xyz:").unwrap().is_empty());
5379    }
5380
5381    // ── Round 11: most_recent / contains_all / has_any_key / to_map / fill_ratio ──
5382
5383    #[test]
5384    fn test_episodic_most_recent_returns_last_inserted() {
5385        let store = EpisodicStore::new();
5386        let agent = AgentId::new("a");
5387        store.add_episode(agent.clone(), "first", 0.5).unwrap();
5388        store.add_episode(agent.clone(), "second", 0.3).unwrap();
5389        let recent = store.most_recent(&agent).unwrap().unwrap();
5390        assert_eq!(recent.content, "second");
5391    }
5392
5393    #[test]
5394    fn test_episodic_most_recent_none_when_empty() {
5395        let store = EpisodicStore::new();
5396        let agent = AgentId::new("nobody");
5397        assert!(store.most_recent(&agent).unwrap().is_none());
5398    }
5399
5400    #[test]
5401    fn test_working_memory_contains_all_true_when_all_present() {
5402        let wm = WorkingMemory::new(10).unwrap();
5403        wm.set("x", "1").unwrap();
5404        wm.set("y", "2").unwrap();
5405        assert!(wm.contains_all(["x", "y"]).unwrap());
5406    }
5407
5408    #[test]
5409    fn test_working_memory_contains_all_false_when_one_missing() {
5410        let wm = WorkingMemory::new(10).unwrap();
5411        wm.set("x", "1").unwrap();
5412        assert!(!wm.contains_all(["x", "missing"]).unwrap());
5413    }
5414
5415    #[test]
5416    fn test_working_memory_contains_all_vacuously_true_for_empty() {
5417        let wm = WorkingMemory::new(5).unwrap();
5418        assert!(wm.contains_all(std::iter::empty::<&str>()).unwrap());
5419    }
5420
5421    #[test]
5422    fn test_working_memory_has_any_key_true_when_at_least_one_present() {
5423        let wm = WorkingMemory::new(5).unwrap();
5424        wm.set("present", "v").unwrap();
5425        assert!(wm.has_any_key(["missing", "present"]).unwrap());
5426    }
5427
5428    #[test]
5429    fn test_working_memory_has_any_key_false_when_none_present() {
5430        let wm = WorkingMemory::new(5).unwrap();
5431        assert!(!wm.has_any_key(["a", "b"]).unwrap());
5432    }
5433
5434    #[test]
5435    fn test_working_memory_has_any_key_false_for_empty_iter() {
5436        let wm = WorkingMemory::new(5).unwrap();
5437        assert!(!wm.has_any_key(std::iter::empty::<&str>()).unwrap());
5438    }
5439
5440    #[test]
5441    fn test_semantic_to_map_returns_key_value_pairs() {
5442        let store = SemanticStore::new();
5443        store.store("key1", "val1", vec![]).unwrap();
5444        store.store("key2", "val2", vec![]).unwrap();
5445        let map = store.to_map().unwrap();
5446        assert_eq!(map.get("key1").map(String::as_str), Some("val1"));
5447        assert_eq!(map.get("key2").map(String::as_str), Some("val2"));
5448        assert_eq!(map.len(), 2);
5449    }
5450
5451    #[test]
5452    fn test_semantic_to_map_empty_when_no_entries() {
5453        let store = SemanticStore::new();
5454        assert!(store.to_map().unwrap().is_empty());
5455    }
5456
5457    #[test]
5458    fn test_working_memory_fill_ratio_zero_when_empty() {
5459        let wm = WorkingMemory::new(10).unwrap();
5460        assert_eq!(wm.fill_ratio().unwrap(), 0.0);
5461    }
5462
5463    #[test]
5464    fn test_working_memory_fill_ratio_correct_proportion() {
5465        let wm = WorkingMemory::new(4).unwrap();
5466        wm.set("a", "1").unwrap();
5467        wm.set("b", "2").unwrap();
5468        // 2 out of 4 = 0.5
5469        assert!((wm.fill_ratio().unwrap() - 0.5).abs() < 1e-9);
5470    }
5471
5472    // ── Round 11: most_recent / contains_all / has_any_key ───────────────────
5473
5474    #[test]
5475    fn test_most_recent_returns_last_inserted_episode() {
5476        let store = EpisodicStore::new();
5477        let agent = AgentId::new("a");
5478        store.add_episode(agent.clone(), "first", 0.5).unwrap();
5479        store.add_episode(agent.clone(), "second", 0.8).unwrap();
5480        let recent = store.most_recent(&agent).unwrap().unwrap();
5481        assert_eq!(recent.content, "second");
5482    }
5483
5484    #[test]
5485    fn test_most_recent_returns_none_for_new_agent() {
5486        let store = EpisodicStore::new();
5487        let agent = AgentId::new("empty");
5488        assert!(store.most_recent(&agent).unwrap().is_none());
5489    }
5490
5491    #[test]
5492    fn test_contains_all_true_when_all_keys_present() {
5493        let wm = WorkingMemory::new(10).unwrap();
5494        wm.set("a", "1").unwrap();
5495        wm.set("b", "2").unwrap();
5496        assert!(wm.contains_all(["a", "b"]).unwrap());
5497    }
5498
5499    #[test]
5500    fn test_contains_all_false_when_one_key_missing() {
5501        let wm = WorkingMemory::new(10).unwrap();
5502        wm.set("a", "1").unwrap();
5503        assert!(!wm.contains_all(["a", "missing"]).unwrap());
5504    }
5505
5506    #[test]
5507    fn test_contains_all_true_for_empty_iter() {
5508        let wm = WorkingMemory::new(5).unwrap();
5509        assert!(wm.contains_all([]).unwrap());
5510    }
5511
5512    #[test]
5513    fn test_has_any_key_true_when_one_key_present() {
5514        let wm = WorkingMemory::new(10).unwrap();
5515        wm.set("x", "v").unwrap();
5516        assert!(wm.has_any_key(["x", "y"]).unwrap());
5517    }
5518
5519    #[test]
5520    fn test_has_any_key_false_when_none_present() {
5521        let wm = WorkingMemory::new(5).unwrap();
5522        assert!(!wm.has_any_key(["nope", "also_nope"]).unwrap());
5523    }
5524
5525    #[test]
5526    fn test_has_any_key_false_for_empty_iter() {
5527        let wm = WorkingMemory::new(5).unwrap();
5528        wm.set("a", "1").unwrap();
5529        assert!(!wm.has_any_key([]).unwrap());
5530    }
5531
5532    // ── Round 13: EpisodicStore::most_recalled ────────────────────────────────
5533
5534    #[test]
5535    fn test_most_recalled_returns_none_for_new_agent() {
5536        let store = EpisodicStore::new();
5537        let agent = AgentId::new("fresh");
5538        assert!(store.most_recalled(&agent).unwrap().is_none());
5539    }
5540
5541    #[test]
5542    fn test_most_recalled_returns_highest_recall_count() {
5543        use std::sync::Arc;
5544        let store = EpisodicStore::new();
5545        let agent = AgentId::new("a");
5546        store.add_episode(agent.clone(), "low", 0.3).unwrap();
5547        store.add_episode(agent.clone(), "high", 0.9).unwrap();
5548        store.add_episode(agent.clone(), "mid", 0.6).unwrap();
5549        // Increment recall count on "high" episode by recalling it multiple times
5550        let items = store.recall(&agent, 3).unwrap();
5551        // recall_count is incremented on each recall; all items start at 0 after add,
5552        // then each recall increments them. We need to find which has highest recall_count.
5553        // After recall(3), all 3 items get incremented once. Let's recall just "high" more.
5554        // Simulate: call recall again to increment counts further
5555        store.recall(&agent, 1).unwrap();
5556        let top = store.most_recalled(&agent).unwrap();
5557        assert!(top.is_some());
5558        // The most recalled has recall_count >= 1
5559        assert!(top.unwrap().recall_count >= 1);
5560    }
5561
5562    #[test]
5563    fn test_most_recalled_single_episode() {
5564        let store = EpisodicStore::new();
5565        let agent = AgentId::new("solo");
5566        store.add_episode(agent.clone(), "only one", 0.7).unwrap();
5567        store.recall(&agent, 1).unwrap();
5568        let top = store.most_recalled(&agent).unwrap();
5569        assert_eq!(top.unwrap().content, "only one");
5570    }
5571
5572    // ── Round 13: max_importance / min_importance / values_matching ──────────
5573
5574    #[test]
5575    fn test_max_importance_returns_highest_score() {
5576        let store = EpisodicStore::new();
5577        let agent = AgentId::new("a");
5578        store.add_episode(agent.clone(), "low", 0.1).unwrap();
5579        store.add_episode(agent.clone(), "high", 0.9).unwrap();
5580        let max = store.max_importance(&agent).unwrap().unwrap();
5581        assert!((max - 0.9).abs() < 1e-5);
5582    }
5583
5584    #[test]
5585    fn test_min_importance_returns_lowest_score() {
5586        let store = EpisodicStore::new();
5587        let agent = AgentId::new("b");
5588        store.add_episode(agent.clone(), "low", 0.1).unwrap();
5589        store.add_episode(agent.clone(), "high", 0.9).unwrap();
5590        let min = store.min_importance(&agent).unwrap().unwrap();
5591        assert!((min - 0.1).abs() < 1e-5);
5592    }
5593
5594    #[test]
5595    fn test_max_importance_none_for_empty_agent() {
5596        let store = EpisodicStore::new();
5597        let agent = AgentId::new("empty");
5598        assert!(store.max_importance(&agent).unwrap().is_none());
5599    }
5600
5601    #[test]
5602    fn test_values_matching_returns_pairs_with_pattern() {
5603        let wm = WorkingMemory::new(10).unwrap();
5604        wm.set("name", "alice wonder").unwrap();
5605        wm.set("city", "wonderland").unwrap();
5606        wm.set("role", "engineer").unwrap();
5607        let mut matches = wm.values_matching("wonder").unwrap();
5608        matches.sort_by_key(|(k, _)| k.clone());
5609        assert_eq!(
5610            matches,
5611            vec![
5612                ("city".to_string(), "wonderland".to_string()),
5613                ("name".to_string(), "alice wonder".to_string()),
5614            ]
5615        );
5616    }
5617
5618    #[test]
5619    fn test_values_matching_returns_empty_when_no_match() {
5620        let wm = WorkingMemory::new(5).unwrap();
5621        wm.set("a", "foo").unwrap();
5622        assert!(wm.values_matching("xyz").unwrap().is_empty());
5623    }
5624
5625    // ── Round 14: count_above_importance, value_length, tags_for ─────────────
5626
5627    #[test]
5628    fn test_count_above_importance_correct_count() {
5629        let store = EpisodicStore::new();
5630        let agent = AgentId::new("agent");
5631        store.add_episode(agent.clone(), "low", 0.2).unwrap();
5632        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
5633        store.add_episode(agent.clone(), "high", 0.9).unwrap();
5634        assert_eq!(store.count_above_importance(&agent, 0.4).unwrap(), 2);
5635    }
5636
5637    #[test]
5638    fn test_count_above_importance_zero_for_empty_agent() {
5639        let store = EpisodicStore::new();
5640        let agent = AgentId::new("nobody");
5641        assert_eq!(store.count_above_importance(&agent, 0.0).unwrap(), 0);
5642    }
5643
5644    #[test]
5645    fn test_count_above_importance_threshold_is_exclusive() {
5646        let store = EpisodicStore::new();
5647        let agent = AgentId::new("ex");
5648        store.add_episode(agent.clone(), "exact", 0.5).unwrap();
5649        // threshold 0.5 — strictly greater than, so 0.5 should not count
5650        assert_eq!(store.count_above_importance(&agent, 0.5).unwrap(), 0);
5651    }
5652
5653    #[test]
5654    fn test_working_memory_value_length_some_for_existing_key() {
5655        let wm = WorkingMemory::new(10).unwrap();
5656        wm.set("greeting", "hello").unwrap();
5657        assert_eq!(wm.value_length("greeting").unwrap(), Some(5));
5658    }
5659
5660    #[test]
5661    fn test_working_memory_value_length_none_for_absent_key() {
5662        let wm = WorkingMemory::new(10).unwrap();
5663        assert!(wm.value_length("missing").unwrap().is_none());
5664    }
5665
5666    #[test]
5667    fn test_semantic_store_tags_for_returns_tags() {
5668        let store = SemanticStore::new();
5669        store
5670            .store("key1", "value1", vec!["alpha".to_string(), "beta".to_string()])
5671            .unwrap();
5672        let tags = store.tags_for("key1").unwrap().unwrap();
5673        assert_eq!(tags, vec!["alpha".to_string(), "beta".to_string()]);
5674    }
5675
5676    #[test]
5677    fn test_semantic_store_tags_for_none_for_missing_key() {
5678        let store = SemanticStore::new();
5679        assert!(store.tags_for("ghost").unwrap().is_none());
5680    }
5681
5682    #[test]
5683    fn test_semantic_store_tags_for_empty_tags() {
5684        let store = SemanticStore::new();
5685        store.store("k", "v", vec![]).unwrap();
5686        let tags = store.tags_for("k").unwrap().unwrap();
5687        assert!(tags.is_empty());
5688    }
5689
5690    // ── Round 27: peek_oldest, SemanticStore::values, SemanticStore::get_tags ─
5691
5692    #[test]
5693    fn test_peek_oldest_returns_oldest_without_removing() {
5694        let wm = WorkingMemory::new(10).unwrap();
5695        wm.set("first", "alpha").unwrap();
5696        wm.set("second", "beta").unwrap();
5697        let peeked = wm.peek_oldest().unwrap();
5698        assert_eq!(peeked, Some(("first".into(), "alpha".into())));
5699        // Still there after peek
5700        assert_eq!(wm.len().unwrap(), 2);
5701    }
5702
5703    #[test]
5704    fn test_peek_oldest_empty_returns_none() {
5705        let wm = WorkingMemory::new(5).unwrap();
5706        assert_eq!(wm.peek_oldest().unwrap(), None);
5707    }
5708
5709    #[test]
5710    fn test_peek_oldest_does_not_remove_entry() {
5711        let wm = WorkingMemory::new(5).unwrap();
5712        wm.set("k", "v").unwrap();
5713        wm.peek_oldest().unwrap();
5714        assert_eq!(wm.get("k").unwrap(), Some("v".into()));
5715    }
5716
5717    #[test]
5718    fn test_semantic_store_values_returns_all_values() {
5719        let store = SemanticStore::new();
5720        store.store("k1", "hello", vec![]).unwrap();
5721        store.store("k2", "world", vec![]).unwrap();
5722        let vals = store.values().unwrap();
5723        assert_eq!(vals.len(), 2);
5724        assert!(vals.contains(&"hello".to_string()));
5725        assert!(vals.contains(&"world".to_string()));
5726    }
5727
5728    #[test]
5729    fn test_semantic_store_values_empty() {
5730        let store = SemanticStore::new();
5731        assert!(store.values().unwrap().is_empty());
5732    }
5733
5734    #[test]
5735    fn test_semantic_store_get_tags_returns_tags() {
5736        let store = SemanticStore::new();
5737        store.store("k1", "val", vec!["tag-a".to_string(), "tag-b".to_string()]).unwrap();
5738        let tags = store.get_tags("k1").unwrap();
5739        assert!(tags.is_some());
5740        let tags = tags.unwrap();
5741        assert!(tags.contains(&"tag-a".to_string()));
5742        assert!(tags.contains(&"tag-b".to_string()));
5743    }
5744
5745    #[test]
5746    fn test_semantic_store_get_tags_missing_key_returns_none() {
5747        let store = SemanticStore::new();
5748        assert!(store.get_tags("no-such-key").unwrap().is_none());
5749    }
5750
5751    // ── Round 16 (duplicate block): WorkingMemory::value_length, iter_sorted ──
5752
5753    #[test]
5754    fn test_working_memory_value_length_returns_char_count_r27() {
5755        let wm = WorkingMemory::new(10).unwrap();
5756        wm.set("k", "hello").unwrap();
5757        assert_eq!(wm.value_length("k").unwrap(), Some(5));
5758    }
5759
5760    #[test]
5761    fn test_working_memory_value_length_none_for_missing_key_r27() {
5762        let wm = WorkingMemory::new(10).unwrap();
5763        assert!(wm.value_length("nope").unwrap().is_none());
5764    }
5765
5766    #[test]
5767    fn test_working_memory_iter_sorted_returns_sorted_pairs() {
5768        let wm = WorkingMemory::new(10).unwrap();
5769        wm.set("b", "2").unwrap();
5770        wm.set("a", "1").unwrap();
5771        let pairs = wm.iter_sorted().unwrap();
5772        assert_eq!(pairs[0].0, "a");
5773        assert_eq!(pairs[1].0, "b");
5774    }
5775
5776    #[test]
5777    fn test_working_memory_drain_empties_store() {
5778        let wm = WorkingMemory::new(10).unwrap();
5779        wm.set("x", "1").unwrap();
5780        wm.set("y", "2").unwrap();
5781        let drained = wm.drain().unwrap();
5782        assert_eq!(drained.len(), 2);
5783        assert!(wm.is_empty().unwrap());
5784    }
5785
5786    #[test]
5787    fn test_working_memory_snapshot_returns_all_entries_r27() {
5788        let wm = WorkingMemory::new(10).unwrap();
5789        wm.set("a", "alpha").unwrap();
5790        wm.set("b", "beta").unwrap();
5791        let snap = wm.snapshot().unwrap();
5792        assert_eq!(snap.get("a").map(String::as_str), Some("alpha"));
5793        assert_eq!(snap.get("b").map(String::as_str), Some("beta"));
5794    }
5795
5796    #[test]
5797    fn test_working_memory_snapshot_empty_when_no_entries_r27() {
5798        let wm = WorkingMemory::new(5).unwrap();
5799        assert!(wm.snapshot().unwrap().is_empty());
5800    }
5801
5802    // ── Round 15: agent_count, is_at_capacity, remove_keys_starting_with,
5803    //              has_key, entry_count_with_tag ─────────────────────────────
5804
5805    #[test]
5806    fn test_episodic_store_agent_count_zero_when_empty() {
5807        let store = EpisodicStore::new();
5808        assert_eq!(store.agent_count().unwrap(), 0);
5809    }
5810
5811    #[test]
5812    fn test_episodic_store_agent_count_distinct_agents() {
5813        let store = EpisodicStore::new();
5814        store.add_episode(AgentId::new("a"), "ep1", 0.5).unwrap();
5815        store.add_episode(AgentId::new("b"), "ep2", 0.7).unwrap();
5816        store.add_episode(AgentId::new("a"), "ep3", 0.3).unwrap();
5817        assert_eq!(store.agent_count().unwrap(), 2);
5818    }
5819
5820    #[test]
5821    fn test_working_memory_is_at_capacity_true_when_full() {
5822        let wm = WorkingMemory::new(2).unwrap();
5823        wm.set("k1", "v1").unwrap();
5824        wm.set("k2", "v2").unwrap();
5825        assert!(wm.is_at_capacity().unwrap());
5826    }
5827
5828    #[test]
5829    fn test_working_memory_is_at_capacity_false_when_not_full() {
5830        let wm = WorkingMemory::new(5).unwrap();
5831        wm.set("k1", "v1").unwrap();
5832        assert!(!wm.is_at_capacity().unwrap());
5833    }
5834
5835    #[test]
5836    fn test_remove_keys_starting_with_removes_matching_keys() {
5837        let wm = WorkingMemory::new(10).unwrap();
5838        wm.set("prefix_a", "1").unwrap();
5839        wm.set("prefix_b", "2").unwrap();
5840        wm.set("other", "3").unwrap();
5841        let removed = wm.remove_keys_starting_with("prefix_").unwrap();
5842        assert_eq!(removed, 2);
5843        assert!(wm.get("prefix_a").unwrap().is_none());
5844        assert!(wm.get("prefix_b").unwrap().is_none());
5845        assert_eq!(wm.get("other").unwrap(), Some("3".into()));
5846    }
5847
5848    #[test]
5849    fn test_remove_keys_starting_with_returns_zero_when_no_match() {
5850        let wm = WorkingMemory::new(5).unwrap();
5851        wm.set("foo", "bar").unwrap();
5852        assert_eq!(wm.remove_keys_starting_with("xyz_").unwrap(), 0);
5853    }
5854
5855    #[test]
5856    fn test_semantic_store_has_key_true_when_exists() {
5857        let store = SemanticStore::new();
5858        store.store("mykey", "val", vec![]).unwrap();
5859        assert!(store.has_key("mykey").unwrap());
5860    }
5861
5862    #[test]
5863    fn test_semantic_store_has_key_false_when_absent() {
5864        let store = SemanticStore::new();
5865        assert!(!store.has_key("ghost").unwrap());
5866    }
5867
5868    #[test]
5869    fn test_entry_count_with_tag_counts_correctly() {
5870        let store = SemanticStore::new();
5871        store.store("k1", "v1", vec!["rust".into(), "async".into()]).unwrap();
5872        store.store("k2", "v2", vec!["rust".into()]).unwrap();
5873        store.store("k3", "v3", vec!["python".into()]).unwrap();
5874        assert_eq!(store.entry_count_with_tag("rust").unwrap(), 2);
5875        assert_eq!(store.entry_count_with_tag("async").unwrap(), 1);
5876        assert_eq!(store.entry_count_with_tag("absent").unwrap(), 0);
5877    }
5878
5879    // ── Round 18: importance_sum, update_content, recall_all, top_n, search_by_importance_range ──
5880
5881    #[test]
5882    fn test_importance_sum_returns_sum_of_all_importances() {
5883        let store = EpisodicStore::new();
5884        let agent = AgentId::new("a");
5885        store.add_episode(agent.clone(), "e1", 0.2).unwrap();
5886        store.add_episode(agent.clone(), "e2", 0.3).unwrap();
5887        store.add_episode(agent.clone(), "e3", 0.5).unwrap();
5888        let sum = store.importance_sum(&agent).unwrap();
5889        assert!((sum - 1.0).abs() < 1e-5);
5890    }
5891
5892    #[test]
5893    fn test_importance_sum_zero_for_unknown_agent() {
5894        let store = EpisodicStore::new();
5895        let agent = AgentId::new("nobody");
5896        assert!((store.importance_sum(&agent).unwrap() - 0.0).abs() < 1e-9);
5897    }
5898
5899    #[test]
5900    fn test_update_content_changes_stored_content() {
5901        let store = EpisodicStore::new();
5902        let agent = AgentId::new("a");
5903        let id = store.add_episode(agent.clone(), "old content", 0.5).unwrap();
5904        let updated = store.update_content(&agent, &id, "new content").unwrap();
5905        assert!(updated);
5906        let items = store.recall_all(&agent).unwrap();
5907        assert_eq!(items[0].content, "new content");
5908    }
5909
5910    #[test]
5911    fn test_update_content_false_for_missing_id() {
5912        let store = EpisodicStore::new();
5913        let agent = AgentId::new("a");
5914        let fake_id = MemoryId::new("nonexistent");
5915        assert!(!store.update_content(&agent, &fake_id, "x").unwrap());
5916    }
5917
5918    #[test]
5919    fn test_recall_all_returns_all_episodes() {
5920        let store = EpisodicStore::new();
5921        let agent = AgentId::new("a");
5922        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
5923        store.add_episode(agent.clone(), "e2", 0.3).unwrap();
5924        store.add_episode(agent.clone(), "e3", 0.9).unwrap();
5925        let all = store.recall_all(&agent).unwrap();
5926        assert_eq!(all.len(), 3);
5927    }
5928
5929    #[test]
5930    fn test_top_n_returns_top_by_importance() {
5931        let store = EpisodicStore::new();
5932        let agent = AgentId::new("a");
5933        store.add_episode(agent.clone(), "low", 0.1).unwrap();
5934        store.add_episode(agent.clone(), "high", 0.9).unwrap();
5935        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
5936        let top = store.top_n(&agent, 2).unwrap();
5937        assert_eq!(top.len(), 2);
5938        assert_eq!(top[0].content, "high");
5939        assert_eq!(top[1].content, "mid");
5940    }
5941
5942    #[test]
5943    fn test_search_by_importance_range_filters_correctly() {
5944        let store = EpisodicStore::new();
5945        let agent = AgentId::new("a");
5946        store.add_episode(agent.clone(), "low", 0.1).unwrap();
5947        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
5948        store.add_episode(agent.clone(), "high", 0.9).unwrap();
5949        let results = store.search_by_importance_range(&agent, 0.4, 0.8, 0).unwrap();
5950        assert_eq!(results.len(), 1);
5951        assert_eq!(results[0].content, "mid");
5952    }
5953
5954    // ── Round 16: oldest_episode, remove_by_key, total_value_bytes ───────────
5955
5956    #[test]
5957    fn test_oldest_episode_returns_none_for_new_agent() {
5958        let store = EpisodicStore::new();
5959        let agent = AgentId::new("fresh");
5960        assert!(store.oldest_episode(&agent).unwrap().is_none());
5961    }
5962
5963    #[test]
5964    fn test_oldest_episode_returns_earliest_timestamp() {
5965        let store = EpisodicStore::new();
5966        let agent = AgentId::new("a");
5967        store.add_episode(agent.clone(), "first", 0.5).unwrap();
5968        store.add_episode(agent.clone(), "second", 0.7).unwrap();
5969        store.add_episode(agent.clone(), "third", 0.3).unwrap();
5970        let oldest = store.oldest_episode(&agent).unwrap().unwrap();
5971        // Insertion order determines timestamp; "first" should be oldest
5972        assert_eq!(oldest.content, "first");
5973    }
5974
5975    #[test]
5976    fn test_semantic_store_remove_by_key_removes_all_matching() {
5977        let store = SemanticStore::new();
5978        store.store("target", "v1", vec![]).unwrap();
5979        store.store("target", "v2", vec![]).unwrap();
5980        store.store("keep", "vk", vec![]).unwrap();
5981        let removed = store.remove_by_key("target").unwrap();
5982        assert_eq!(removed, 2);
5983        assert!(!store.has_key("target").unwrap());
5984        assert!(store.has_key("keep").unwrap());
5985    }
5986
5987    #[test]
5988    fn test_semantic_store_remove_by_key_zero_for_absent_key() {
5989        let store = SemanticStore::new();
5990        assert_eq!(store.remove_by_key("ghost").unwrap(), 0);
5991    }
5992
5993    #[test]
5994    fn test_working_memory_total_value_bytes_sums_lengths() {
5995        let wm = WorkingMemory::new(10).unwrap();
5996        wm.set("a", "hello").unwrap();    // 5 bytes
5997        wm.set("b", "world!").unwrap();   // 6 bytes
5998        assert_eq!(wm.total_value_bytes().unwrap(), 11);
5999    }
6000
6001    #[test]
6002    fn test_working_memory_total_value_bytes_zero_when_empty() {
6003        let wm = WorkingMemory::new(5).unwrap();
6004        assert_eq!(wm.total_value_bytes().unwrap(), 0);
6005    }
6006
6007    // ── Round 19: agent_ids, clear_for, recall_since ──────────────────────────
6008
6009    #[test]
6010    fn test_agent_ids_returns_all_tracked_agents() {
6011        let store = EpisodicStore::new();
6012        let a1 = AgentId::new("alice");
6013        let a2 = AgentId::new("bob");
6014        store.add_episode(a1.clone(), "x", 0.5).unwrap();
6015        store.add_episode(a2.clone(), "y", 0.5).unwrap();
6016        let mut ids = store.agent_ids().unwrap();
6017        ids.sort_by_key(|id| id.as_str().to_string());
6018        assert_eq!(ids.len(), 2);
6019        assert_eq!(ids[0].as_str(), "alice");
6020        assert_eq!(ids[1].as_str(), "bob");
6021    }
6022
6023    #[test]
6024    fn test_agent_ids_empty_for_new_store() {
6025        let store = EpisodicStore::new();
6026        assert!(store.agent_ids().unwrap().is_empty());
6027    }
6028
6029    #[test]
6030    fn test_clear_for_removes_all_episodes_for_agent() {
6031        let store = EpisodicStore::new();
6032        let agent = AgentId::new("a");
6033        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
6034        store.add_episode(agent.clone(), "e2", 0.3).unwrap();
6035        let removed = store.clear_for(&agent).unwrap();
6036        assert_eq!(removed, 2);
6037        assert_eq!(store.count_for(&agent).unwrap(), 0);
6038    }
6039
6040    #[test]
6041    fn test_clear_for_returns_zero_for_unknown_agent() {
6042        let store = EpisodicStore::new();
6043        let agent = AgentId::new("ghost");
6044        assert_eq!(store.clear_for(&agent).unwrap(), 0);
6045    }
6046
6047    #[test]
6048    fn test_recall_since_returns_episodes_after_cutoff() {
6049        let store = EpisodicStore::new();
6050        let agent = AgentId::new("a");
6051        let past = chrono::Utc::now() - chrono::Duration::hours(2);
6052        let future_cutoff = chrono::Utc::now() + chrono::Duration::seconds(1);
6053        // Add one in the past
6054        store.add_episode_at(agent.clone(), "old", 0.5, past).unwrap();
6055        // Add one now
6056        store.add_episode(agent.clone(), "new", 0.5).unwrap();
6057        // Recall only future episodes (should be 0)
6058        let future = store.recall_since(&agent, future_cutoff, 0).unwrap();
6059        assert!(future.is_empty());
6060        // Recall from far past (should include both)
6061        let all = store.recall_since(&agent, past - chrono::Duration::seconds(1), 0).unwrap();
6062        assert_eq!(all.len(), 2);
6063    }
6064
6065    // ── Round 17: EpisodicStore::sum_recall_counts ────────────────────────────
6066
6067    #[test]
6068    fn test_sum_recall_counts_zero_for_new_agent() {
6069        let store = EpisodicStore::new();
6070        let agent = AgentId::new("new");
6071        assert_eq!(store.sum_recall_counts(&agent).unwrap(), 0);
6072    }
6073
6074    #[test]
6075    fn test_sum_recall_counts_increases_with_recalls() {
6076        let store = EpisodicStore::new();
6077        let agent = AgentId::new("a");
6078        store.add_episode(agent.clone(), "ep1", 0.5).unwrap();
6079        store.add_episode(agent.clone(), "ep2", 0.8).unwrap();
6080        // Recall both episodes once
6081        store.recall(&agent, 2).unwrap();
6082        let total = store.sum_recall_counts(&agent).unwrap();
6083        assert!(total >= 2);
6084    }
6085
6086    // ── Round 22: max_recall_count_for, most_recent_key, max_key_length ───────
6087
6088    #[test]
6089    fn test_max_recall_count_for_none_for_new_agent() {
6090        let store = EpisodicStore::new();
6091        let agent = AgentId::new("ghost");
6092        assert_eq!(store.max_recall_count_for(&agent).unwrap(), None);
6093    }
6094
6095    #[test]
6096    fn test_max_recall_count_for_returns_highest_after_recalls() {
6097        let store = EpisodicStore::new();
6098        let agent = AgentId::new("a");
6099        store.add_episode(agent.clone(), "ep1", 0.9).unwrap();
6100        store.add_episode(agent.clone(), "ep2", 0.1).unwrap();
6101        // Recall several times to accumulate counts
6102        store.recall(&agent, 2).unwrap();
6103        store.recall(&agent, 1).unwrap();
6104        let max = store.max_recall_count_for(&agent).unwrap().unwrap();
6105        assert!(max >= 1);
6106    }
6107
6108    #[test]
6109    fn test_semantic_most_recent_key_none_when_empty() {
6110        let store = SemanticStore::new();
6111        assert_eq!(store.most_recent_key().unwrap(), None);
6112    }
6113
6114    #[test]
6115    fn test_semantic_most_recent_key_returns_last_inserted() {
6116        let store = SemanticStore::new();
6117        store.store("first", "v1", vec![]).unwrap();
6118        store.store("second", "v2", vec![]).unwrap();
6119        assert_eq!(store.most_recent_key().unwrap(), Some("second".to_string()));
6120    }
6121
6122    #[test]
6123    fn test_working_memory_max_key_length_zero_when_empty() {
6124        let mem = WorkingMemory::new(10).unwrap();
6125        assert_eq!(mem.max_key_length().unwrap(), 0);
6126    }
6127
6128    #[test]
6129    fn test_working_memory_max_key_length_returns_longest() {
6130        let mem = WorkingMemory::new(10).unwrap();
6131        mem.set("ab", "v1").unwrap();
6132        mem.set("abcde", "v2").unwrap();
6133        mem.set("abc", "v3").unwrap();
6134        assert_eq!(mem.max_key_length().unwrap(), 5);
6135    }
6136
6137    // ── Round 29: WorkingMemory::set_if_absent ────────────────────────────────
6138
6139    #[test]
6140    fn test_working_memory_set_if_absent_inserts_new_key() {
6141        let mem = WorkingMemory::new(10).unwrap();
6142        let inserted = mem.set_if_absent("fresh", "value").unwrap();
6143        assert!(inserted);
6144        assert_eq!(mem.get("fresh").unwrap(), Some("value".to_string()));
6145    }
6146
6147    #[test]
6148    fn test_working_memory_set_if_absent_does_not_overwrite_existing() {
6149        let mem = WorkingMemory::new(10).unwrap();
6150        mem.set("key", "original").unwrap();
6151        let inserted = mem.set_if_absent("key", "replacement").unwrap();
6152        assert!(!inserted);
6153        assert_eq!(mem.get("key").unwrap(), Some("original".to_string()));
6154    }
6155
6156    #[test]
6157    fn test_working_memory_set_if_absent_second_call_returns_false() {
6158        let mem = WorkingMemory::new(10).unwrap();
6159        assert!(mem.set_if_absent("k", "v1").unwrap());
6160        assert!(!mem.set_if_absent("k", "v2").unwrap());
6161    }
6162
6163    // ── Round 24: MemoryItem helpers, DecayPolicy, export/bump ───────────────
6164
6165    #[test]
6166    fn test_memory_item_has_tag_true_when_tag_present() {
6167        let item = MemoryItem::new(
6168            AgentId::new("a"),
6169            "content",
6170            0.5,
6171            vec!["important".to_string(), "work".to_string()],
6172        );
6173        assert!(item.has_tag("important"));
6174        assert!(item.has_tag("work"));
6175    }
6176
6177    #[test]
6178    fn test_memory_item_has_tag_false_when_tag_absent() {
6179        let item = MemoryItem::new(AgentId::new("a"), "content", 0.5, vec![]);
6180        assert!(!item.has_tag("missing"));
6181    }
6182
6183    #[test]
6184    fn test_memory_item_word_count_counts_words() {
6185        let item = MemoryItem::new(AgentId::new("a"), "one two three", 0.5, vec![]);
6186        assert_eq!(item.word_count(), 3);
6187    }
6188
6189    #[test]
6190    fn test_memory_item_word_count_zero_for_empty_content() {
6191        let item = MemoryItem::new(AgentId::new("a"), "", 0.5, vec![]);
6192        assert_eq!(item.word_count(), 0);
6193    }
6194
6195    #[test]
6196    fn test_decay_policy_apply_reduces_importance_after_one_half_life() {
6197        let policy = DecayPolicy::exponential(1.0).unwrap(); // half-life 1 hour
6198        let decayed = policy.apply(1.0, 1.0); // after 1 hour, should be ~0.5
6199        assert!((decayed - 0.5).abs() < 1e-5);
6200    }
6201
6202    #[test]
6203    fn test_decay_policy_apply_no_change_for_zero_age() {
6204        let policy = DecayPolicy::exponential(10.0).unwrap();
6205        let decayed = policy.apply(0.8, 0.0);
6206        assert!((decayed - 0.8).abs() < 1e-5);
6207    }
6208
6209    #[test]
6210    fn test_decay_policy_decay_item_reduces_importance_for_old_item() {
6211        let policy = DecayPolicy::exponential(0.0001).unwrap(); // very short half-life
6212        let mut item = MemoryItem::new(AgentId::new("a"), "old memory", 1.0, vec![]);
6213        item.timestamp = Utc::now() - chrono::Duration::hours(1);
6214        policy.decay_item(&mut item);
6215        assert!(item.importance < 1.0);
6216    }
6217
6218    #[test]
6219    fn test_episodic_export_returns_empty_for_unknown_agent() {
6220        let store = EpisodicStore::new();
6221        let agent = AgentId::new("ghost");
6222        let exported = store.export_agent_memory(&agent).unwrap();
6223        assert!(exported.is_empty());
6224    }
6225
6226    #[test]
6227    fn test_episodic_export_returns_all_stored_items() {
6228        let store = EpisodicStore::new();
6229        let agent = AgentId::new("a");
6230        store.add_episode(agent.clone(), "memory1", 0.9).unwrap();
6231        store.add_episode(agent.clone(), "memory2", 0.5).unwrap();
6232        let exported = store.export_agent_memory(&agent).unwrap();
6233        assert_eq!(exported.len(), 2);
6234    }
6235
6236    #[test]
6237    fn test_bump_recall_count_increases_by_amount() {
6238        let store = EpisodicStore::new();
6239        let agent = AgentId::new("a");
6240        store.add_episode(agent.clone(), "the content", 0.7).unwrap();
6241        store.bump_recall_count_by_content("the content", 5);
6242        let max = store.max_recall_count_for(&agent).unwrap().unwrap();
6243        assert_eq!(max, 5);
6244    }
6245
6246    #[test]
6247    fn test_bump_recall_count_no_effect_for_absent_content() {
6248        let store = EpisodicStore::new();
6249        let agent = AgentId::new("a");
6250        store.add_episode(agent.clone(), "existing", 0.5).unwrap();
6251        store.bump_recall_count_by_content("not here", 10);
6252        let max = store.max_recall_count_for(&agent).unwrap().unwrap();
6253        assert_eq!(max, 0);
6254    }
6255
6256    // ── Round 23: latest_episode / oldest_key / key_count_matching / avg_value_length
6257
6258    #[test]
6259    fn test_latest_episode_none_for_new_agent() {
6260        let store = EpisodicStore::new();
6261        let agent = AgentId::new("ghost");
6262        assert!(store.latest_episode(&agent).unwrap().is_none());
6263    }
6264
6265    #[test]
6266    fn test_latest_episode_returns_most_recent_by_timestamp() {
6267        let store = EpisodicStore::new();
6268        let agent = AgentId::new("a");
6269        let old = chrono::Utc::now() - chrono::Duration::hours(1);
6270        store.add_episode_at(agent.clone(), "old ep", 0.5, old).unwrap();
6271        store.add_episode(agent.clone(), "new ep", 0.8).unwrap();
6272        let latest = store.latest_episode(&agent).unwrap().unwrap();
6273        assert_eq!(latest.content, "new ep");
6274    }
6275
6276    #[test]
6277    fn test_semantic_oldest_key_none_when_empty() {
6278        let store = SemanticStore::new();
6279        assert!(store.oldest_key().unwrap().is_none());
6280    }
6281
6282    #[test]
6283    fn test_semantic_oldest_key_returns_first_inserted() {
6284        let store = SemanticStore::new();
6285        store.store("first", "value1", vec![]).unwrap();
6286        store.store("second", "value2", vec![]).unwrap();
6287        assert_eq!(store.oldest_key().unwrap().as_deref(), Some("first"));
6288    }
6289
6290    #[test]
6291    fn test_working_memory_key_count_matching_zero_when_empty() {
6292        let mem = WorkingMemory::new(10).unwrap();
6293        assert_eq!(mem.key_count_matching("foo").unwrap(), 0);
6294    }
6295
6296    #[test]
6297    fn test_working_memory_key_count_matching_counts_correctly() {
6298        let mem = WorkingMemory::new(10).unwrap();
6299        mem.set("foo_bar", "v1").unwrap();
6300        mem.set("foo_baz", "v2").unwrap();
6301        mem.set("other", "v3").unwrap();
6302        assert_eq!(mem.key_count_matching("foo").unwrap(), 2);
6303    }
6304
6305    #[test]
6306    fn test_working_memory_avg_value_length_zero_when_empty() {
6307        let mem = WorkingMemory::new(10).unwrap();
6308        assert!((mem.avg_value_length().unwrap() - 0.0).abs() < 1e-9);
6309    }
6310
6311    #[test]
6312    fn test_working_memory_avg_value_length_correct_mean() {
6313        let mem = WorkingMemory::new(10).unwrap();
6314        mem.set("k1", "ab").unwrap();    // 2 bytes
6315        mem.set("k2", "abcd").unwrap();  // 4 bytes
6316        // mean = 3.0
6317        assert!((mem.avg_value_length().unwrap() - 3.0).abs() < 1e-9);
6318    }
6319
6320    // ── Round 30: EpisodicStore::has_agent, export_agent_memory ──────────────
6321
6322    #[test]
6323    fn test_episodic_store_has_agent_false_when_empty() {
6324        let store = EpisodicStore::new();
6325        assert!(!store.has_agent(&AgentId::new("nobody")).unwrap());
6326    }
6327
6328    #[test]
6329    fn test_episodic_store_has_agent_true_after_episode() {
6330        let store = EpisodicStore::new();
6331        let agent = AgentId::new("agent-ha");
6332        store.add_episode(agent.clone(), "event", 0.5).unwrap();
6333        assert!(store.has_agent(&agent).unwrap());
6334    }
6335
6336    #[test]
6337    fn test_episodic_store_export_agent_memory_empty_for_unknown() {
6338        let store = EpisodicStore::new();
6339        let exported = store.export_agent_memory(&AgentId::new("ghost")).unwrap();
6340        assert!(exported.is_empty());
6341    }
6342
6343    #[test]
6344    fn test_episodic_store_export_agent_memory_returns_episodes() {
6345        let store = EpisodicStore::new();
6346        let agent = AgentId::new("agent-exp");
6347        store.add_episode(agent.clone(), "ep1", 0.9).unwrap();
6348        store.add_episode(agent.clone(), "ep2", 0.7).unwrap();
6349        let exported = store.export_agent_memory(&agent).unwrap();
6350        assert_eq!(exported.len(), 2);
6351    }
6352
6353    // ── Round 30: SemanticStore::store_with_embedding ────────────────────────
6354
6355    #[test]
6356    fn test_semantic_store_store_with_embedding_rejects_empty_vec() {
6357        let store = SemanticStore::new();
6358        let result = store.store_with_embedding("k", "v", vec![], vec![]);
6359        assert!(result.is_err());
6360    }
6361
6362    #[test]
6363    fn test_semantic_store_store_with_embedding_stores_entry() {
6364        let store = SemanticStore::new();
6365        let emb = vec![1.0f32, 0.0, 0.0];
6366        store.store_with_embedding("k1", "v1", vec![], emb).unwrap();
6367        assert_eq!(store.len().unwrap(), 1);
6368    }
6369
6370    #[test]
6371    fn test_semantic_store_store_with_embedding_rejects_dimension_mismatch() {
6372        let store = SemanticStore::new();
6373        store.store_with_embedding("k1", "v1", vec![], vec![1.0f32, 0.0]).unwrap();
6374        // Second call with different dimension should fail
6375        let result = store.store_with_embedding("k2", "v2", vec![], vec![1.0f32, 0.0, 0.0]);
6376        assert!(result.is_err());
6377    }
6378
6379    // ── Round 24: avg_importance / importance_range / entries_without_tags /
6380    //             avg_tag_count_per_entry / longest_key / longest_value ────────
6381
6382    #[test]
6383    fn test_avg_importance_zero_for_new_agent() {
6384        let store = EpisodicStore::new();
6385        let agent = AgentId::new("ghost");
6386        assert!((store.avg_importance(&agent).unwrap() - 0.0).abs() < 1e-9);
6387    }
6388
6389    #[test]
6390    fn test_avg_importance_correct_mean() {
6391        let store = EpisodicStore::new();
6392        let agent = AgentId::new("a");
6393        store.add_episode(agent.clone(), "ep1", 0.2).unwrap();
6394        store.add_episode(agent.clone(), "ep2", 0.8).unwrap();
6395        // mean = 0.5
6396        assert!((store.avg_importance(&agent).unwrap() - 0.5).abs() < 1e-6);
6397    }
6398
6399    #[test]
6400    fn test_importance_range_none_for_new_agent() {
6401        let store = EpisodicStore::new();
6402        let agent = AgentId::new("ghost");
6403        assert!(store.importance_range(&agent).unwrap().is_none());
6404    }
6405
6406    #[test]
6407    fn test_importance_range_returns_min_max() {
6408        let store = EpisodicStore::new();
6409        let agent = AgentId::new("a");
6410        store.add_episode(agent.clone(), "ep1", 0.1).unwrap();
6411        store.add_episode(agent.clone(), "ep2", 0.9).unwrap();
6412        let (min, max) = store.importance_range(&agent).unwrap().unwrap();
6413        assert!((min - 0.1_f32).abs() < 1e-6);
6414        assert!((max - 0.9_f32).abs() < 1e-6);
6415    }
6416
6417    #[test]
6418    fn test_semantic_entries_without_tags_all_untagged() {
6419        let store = SemanticStore::new();
6420        store.store("k1", "v1", vec![]).unwrap();
6421        store.store("k2", "v2", vec![]).unwrap();
6422        assert_eq!(store.entries_without_tags().unwrap(), 2);
6423    }
6424
6425    #[test]
6426    fn test_semantic_entries_without_tags_mixed() {
6427        let store = SemanticStore::new();
6428        store.store("k1", "v1", vec!["tag".to_string()]).unwrap();
6429        store.store("k2", "v2", vec![]).unwrap();
6430        assert_eq!(store.entries_without_tags().unwrap(), 1);
6431    }
6432
6433    #[test]
6434    fn test_semantic_avg_tag_count_zero_when_empty() {
6435        let store = SemanticStore::new();
6436        assert!((store.avg_tag_count_per_entry().unwrap() - 0.0).abs() < 1e-9);
6437    }
6438
6439    #[test]
6440    fn test_semantic_avg_tag_count_correct_mean() {
6441        let store = SemanticStore::new();
6442        store.store("k1", "v1", vec!["a".to_string(), "b".to_string()]).unwrap(); // 2
6443        store.store("k2", "v2", vec![]).unwrap(); // 0
6444        // mean = 1.0
6445        assert!((store.avg_tag_count_per_entry().unwrap() - 1.0).abs() < 1e-9);
6446    }
6447
6448    #[test]
6449    fn test_working_memory_longest_key_none_when_empty() {
6450        let mem = WorkingMemory::new(10).unwrap();
6451        assert!(mem.longest_key().unwrap().is_none());
6452    }
6453
6454    #[test]
6455    fn test_working_memory_longest_key_returns_longest() {
6456        let mem = WorkingMemory::new(10).unwrap();
6457        mem.set("ab", "v1").unwrap();
6458        mem.set("abcde", "v2").unwrap();
6459        assert_eq!(mem.longest_key().unwrap().as_deref(), Some("abcde"));
6460    }
6461
6462    #[test]
6463    fn test_working_memory_longest_value_none_when_empty() {
6464        let mem = WorkingMemory::new(10).unwrap();
6465        assert!(mem.longest_value().unwrap().is_none());
6466    }
6467
6468    #[test]
6469    fn test_working_memory_longest_value_returns_longest() {
6470        let mem = WorkingMemory::new(10).unwrap();
6471        mem.set("k1", "short").unwrap();
6472        mem.set("k2", "much longer value").unwrap();
6473        assert_eq!(mem.longest_value().unwrap().as_deref(), Some("much longer value"));
6474    }
6475
6476    // ── Round 26: SemanticStore::store_with_embedding ─────────────────────────
6477
6478    #[test]
6479    fn test_semantic_store_with_embedding_rejects_empty_vector() {
6480        let store = SemanticStore::new();
6481        let result = store.store_with_embedding("k", "v", vec![], vec![]);
6482        assert!(result.is_err());
6483    }
6484
6485    #[test]
6486    fn test_semantic_store_with_embedding_stores_and_retrievable() {
6487        let store = SemanticStore::new();
6488        store.store_with_embedding("k", "v", vec![], vec![1.0, 0.0]).unwrap();
6489        let entry = store.retrieve_by_key("k").unwrap();
6490        assert_eq!(entry.map(|(val, _)| val), Some("v".to_string()));
6491    }
6492
6493    #[test]
6494    fn test_semantic_store_with_embedding_dimension_mismatch_errors() {
6495        let store = SemanticStore::new();
6496        store.store_with_embedding("k1", "v1", vec![], vec![1.0, 0.0]).unwrap();
6497        let result = store.store_with_embedding("k2", "v2", vec![], vec![1.0, 0.0, 0.0]);
6498        assert!(result.is_err());
6499    }
6500
6501    // ── Round 26: has_episodes / value_for / count_above_value_length ─────────
6502
6503    #[test]
6504    fn test_episodic_store_has_episodes_false_when_empty() {
6505        let store = EpisodicStore::new();
6506        let id = AgentId::new("agent-x");
6507        assert!(!store.has_episodes(&id).unwrap());
6508    }
6509
6510    #[test]
6511    fn test_episodic_store_has_episodes_true_after_recording() {
6512        let store = EpisodicStore::new();
6513        let id = AgentId::new("agent-y");
6514        store.add_episode(id.clone(), "e1", 0.8).unwrap();
6515        assert!(store.has_episodes(&id).unwrap());
6516    }
6517
6518    #[test]
6519    fn test_semantic_store_value_for_none_when_missing() {
6520        let store = SemanticStore::new();
6521        assert!(store.value_for("missing-key").unwrap().is_none());
6522    }
6523
6524    #[test]
6525    fn test_semantic_store_value_for_returns_stored_value() {
6526        let store = SemanticStore::new();
6527        store.store("mykey", "myvalue", vec![]).unwrap();
6528        assert_eq!(store.value_for("mykey").unwrap(), Some("myvalue".to_string()));
6529    }
6530
6531    #[test]
6532    fn test_working_memory_count_above_value_length_zero_when_empty() {
6533        let wm = WorkingMemory::new(10).unwrap();
6534        assert_eq!(wm.count_above_value_length(5).unwrap(), 0);
6535    }
6536
6537    #[test]
6538    fn test_working_memory_count_above_value_length_counts_correctly() {
6539        let wm = WorkingMemory::new(10).unwrap();
6540        wm.set("k1", "short").unwrap();        // 5 bytes
6541        wm.set("k2", "a longer value").unwrap(); // 13 bytes
6542        wm.set("k3", "hi").unwrap();             // 2 bytes
6543        // min_bytes=5: values strictly > 5 bytes: "a longer value" (13)
6544        assert_eq!(wm.count_above_value_length(5).unwrap(), 1);
6545    }
6546
6547    // ── Round 27: total_episode_count ─────────────────────────────────────────
6548
6549    #[test]
6550    fn test_total_episode_count_zero_when_empty() {
6551        let store = EpisodicStore::new();
6552        assert_eq!(store.total_episode_count().unwrap(), 0);
6553    }
6554
6555    #[test]
6556    fn test_total_episode_count_sums_across_agents() {
6557        let store = EpisodicStore::new();
6558        let a1 = AgentId::new("a1");
6559        let a2 = AgentId::new("a2");
6560        store.add_episode(a1.clone(), "e1", 0.5).unwrap();
6561        store.add_episode(a1.clone(), "e2", 0.6).unwrap();
6562        store.add_episode(a2.clone(), "e3", 0.7).unwrap();
6563        assert_eq!(store.total_episode_count().unwrap(), 3);
6564    }
6565
6566    // ── Round 28: agents_with_min_episodes / entries_with_no_tags / longest_value_key
6567
6568    #[test]
6569    fn test_agents_with_min_episodes_empty_when_below_min() {
6570        let store = EpisodicStore::new();
6571        let a = AgentId::new("a1");
6572        store.add_episode(a.clone(), "e1", 0.5).unwrap();
6573        // min=2 → no agents qualify
6574        assert!(store.agents_with_min_episodes(2).unwrap().is_empty());
6575    }
6576
6577    #[test]
6578    fn test_agents_with_min_episodes_includes_qualifying_agents() {
6579        let store = EpisodicStore::new();
6580        let a1 = AgentId::new("a1");
6581        let a2 = AgentId::new("a2");
6582        store.add_episode(a1.clone(), "e1", 0.5).unwrap();
6583        store.add_episode(a1.clone(), "e2", 0.6).unwrap();
6584        store.add_episode(a2.clone(), "only-one", 0.7).unwrap();
6585        let result = store.agents_with_min_episodes(2).unwrap();
6586        assert_eq!(result, vec![a1]);
6587    }
6588
6589    #[test]
6590    fn test_entries_with_no_tags_returns_empty_list_when_all_have_tags() {
6591        let store = SemanticStore::new();
6592        store.store("k1", "v1", vec!["tag".to_string()]).unwrap();
6593        assert!(store.entries_with_no_tags().unwrap().is_empty());
6594    }
6595
6596    #[test]
6597    fn test_entries_with_no_tags_returns_untagged_keys() {
6598        let store = SemanticStore::new();
6599        store.store("k1", "v1", vec![]).unwrap();
6600        store.store("k2", "v2", vec!["tag".to_string()]).unwrap();
6601        let result = store.entries_with_no_tags().unwrap();
6602        assert_eq!(result, vec!["k1".to_string()]);
6603    }
6604
6605    #[test]
6606    fn test_working_memory_longest_value_key_none_when_empty() {
6607        let wm = WorkingMemory::new(10).unwrap();
6608        assert!(wm.longest_value_key().unwrap().is_none());
6609    }
6610
6611    #[test]
6612    fn test_working_memory_longest_value_key_returns_key_with_longest_value() {
6613        let wm = WorkingMemory::new(10).unwrap();
6614        wm.set("short_key", "hi").unwrap();
6615        wm.set("long_key", "a much longer value string").unwrap();
6616        assert_eq!(wm.longest_value_key().unwrap(), Some("long_key".to_string()));
6617    }
6618
6619    // ── Round 29: agent_with_most_episodes / most_tagged_key / value_lengths ──
6620
6621    #[test]
6622    fn test_agent_with_most_episodes_none_when_empty() {
6623        let store = EpisodicStore::new();
6624        assert!(store.agent_with_most_episodes().unwrap().is_none());
6625    }
6626
6627    #[test]
6628    fn test_agent_with_most_episodes_returns_agent_with_most() {
6629        let store = EpisodicStore::new();
6630        let a1 = AgentId::new("a1");
6631        let a2 = AgentId::new("a2");
6632        store.add_episode(a1.clone(), "e1", 0.5).unwrap();
6633        store.add_episode(a2.clone(), "e1", 0.5).unwrap();
6634        store.add_episode(a2.clone(), "e2", 0.6).unwrap();
6635        assert_eq!(store.agent_with_most_episodes().unwrap(), Some(a2));
6636    }
6637
6638    #[test]
6639    fn test_most_tagged_key_none_when_empty() {
6640        let store = SemanticStore::new();
6641        assert!(store.most_tagged_key().unwrap().is_none());
6642    }
6643
6644    #[test]
6645    fn test_most_tagged_key_returns_key_with_most_tags() {
6646        let store = SemanticStore::new();
6647        store.store("k1", "v1", vec!["a".to_string()]).unwrap();
6648        store.store("k2", "v2", vec!["a".to_string(), "b".to_string(), "c".to_string()]).unwrap();
6649        store.store("k3", "v3", vec![]).unwrap();
6650        assert_eq!(store.most_tagged_key().unwrap(), Some("k2".to_string()));
6651    }
6652
6653    #[test]
6654    fn test_value_lengths_empty_when_empty() {
6655        let wm = WorkingMemory::new(10).unwrap();
6656        assert!(wm.value_lengths().unwrap().is_empty());
6657    }
6658
6659    #[test]
6660    fn test_value_lengths_returns_all_pairs() {
6661        let wm = WorkingMemory::new(10).unwrap();
6662        wm.set("k", "hello").unwrap();
6663        let lengths = wm.value_lengths().unwrap();
6664        assert_eq!(lengths.len(), 1);
6665        assert_eq!(lengths[0], ("k".to_string(), 5));
6666    }
6667
6668    // ── Round 30: importance_variance_for / count_matching_value / keys_with_value_longer_than
6669
6670    #[test]
6671    fn test_importance_variance_for_zero_when_fewer_than_two() {
6672        let store = EpisodicStore::new();
6673        let id = AgentId::new("a");
6674        store.add_episode(id.clone(), "e", 0.5).unwrap();
6675        assert!((store.importance_variance_for(&id).unwrap() - 0.0).abs() < 1e-6);
6676    }
6677
6678    #[test]
6679    fn test_importance_variance_for_nonzero_with_spread() {
6680        let store = EpisodicStore::new();
6681        let id = AgentId::new("a");
6682        store.add_episode(id.clone(), "e1", 0.0).unwrap();
6683        store.add_episode(id.clone(), "e2", 1.0).unwrap();
6684        // mean=0.5, variance = 0.25
6685        let v = store.importance_variance_for(&id).unwrap();
6686        assert!((v - 0.25).abs() < 1e-5);
6687    }
6688
6689    #[test]
6690    fn test_count_matching_value_zero_when_no_match() {
6691        let store = SemanticStore::new();
6692        store.store("k", "hello world", vec![]).unwrap();
6693        assert_eq!(store.count_matching_value("xyz").unwrap(), 0);
6694    }
6695
6696    #[test]
6697    fn test_count_matching_value_counts_containing_entries() {
6698        let store = SemanticStore::new();
6699        store.store("k1", "hello world", vec![]).unwrap();
6700        store.store("k2", "world peace", vec![]).unwrap();
6701        store.store("k3", "goodbye", vec![]).unwrap();
6702        assert_eq!(store.count_matching_value("world").unwrap(), 2);
6703    }
6704
6705    #[test]
6706    fn test_keys_with_value_longer_than_empty_when_all_short() {
6707        let wm = WorkingMemory::new(10).unwrap();
6708        wm.set("k", "hi").unwrap();
6709        assert!(wm.keys_with_value_longer_than(10).unwrap().is_empty());
6710    }
6711
6712    #[test]
6713    fn test_keys_with_value_longer_than_returns_qualifying_keys() {
6714        let wm = WorkingMemory::new(10).unwrap();
6715        wm.set("short", "hi").unwrap();
6716        wm.set("long", "this is a longer value").unwrap();
6717        let keys = wm.keys_with_value_longer_than(5).unwrap();
6718        assert_eq!(keys, vec!["long".to_string()]);
6719    }
6720
6721    #[test]
6722    fn test_episodic_store_max_importance_overall_returns_highest() {
6723        let store = EpisodicStore::new();
6724        let agent = AgentId::new("a");
6725        store.add_episode(agent.clone(), "low", 0.2).unwrap();
6726        store.add_episode(agent.clone(), "high", 0.9).unwrap();
6727        store.add_episode(agent.clone(), "mid", 0.5).unwrap();
6728        let max = store.max_importance_overall().unwrap();
6729        assert!((max.unwrap() - 0.9).abs() < 1e-6);
6730    }
6731
6732    #[test]
6733    fn test_episodic_store_max_importance_overall_empty_returns_none() {
6734        let store = EpisodicStore::new();
6735        assert!(store.max_importance_overall().unwrap().is_none());
6736    }
6737
6738    #[test]
6739    fn test_semantic_store_rename_tag_updates_all_occurrences() {
6740        let store = SemanticStore::new();
6741        store.store("k1", "v1", vec!["old".to_string(), "x".to_string()]).unwrap();
6742        store.store("k2", "v2", vec!["old".to_string()]).unwrap();
6743        let count = store.rename_tag("old", "new").unwrap();
6744        assert_eq!(count, 2);
6745    }
6746
6747    #[test]
6748    fn test_semantic_store_rename_tag_nonexistent_returns_zero() {
6749        let store = SemanticStore::new();
6750        store.store("k", "v", vec!["alpha".to_string()]).unwrap();
6751        assert_eq!(store.rename_tag("missing", "new").unwrap(), 0);
6752    }
6753
6754    #[test]
6755    fn test_working_memory_entry_count_reflects_stored_entries() {
6756        let wm = WorkingMemory::new(10).unwrap();
6757        assert_eq!(wm.entry_count().unwrap(), 0);
6758        wm.set("a", "1").unwrap();
6759        wm.set("b", "2").unwrap();
6760        assert_eq!(wm.entry_count().unwrap(), 2);
6761    }
6762
6763    #[test]
6764    fn test_episode_count_for_returns_correct_count() {
6765        let store = EpisodicStore::new();
6766        let agent = AgentId::new("a");
6767        store.add_episode(agent.clone(), "e1", 0.5).unwrap();
6768        store.add_episode(agent.clone(), "e2", 0.5).unwrap();
6769        assert_eq!(store.episode_count_for(&agent).unwrap(), 2);
6770    }
6771
6772    #[test]
6773    fn test_episode_count_for_unknown_agent_returns_zero() {
6774        let store = EpisodicStore::new();
6775        assert_eq!(store.episode_count_for(&AgentId::new("x")).unwrap(), 0);
6776    }
6777
6778    #[test]
6779    fn test_semantic_store_unique_tags_returns_sorted_distinct_tags() {
6780        let store = SemanticStore::new();
6781        store.store("k1", "v1", vec!["b".to_string(), "a".to_string()]).unwrap();
6782        store.store("k2", "v2", vec!["a".to_string(), "c".to_string()]).unwrap();
6783        assert_eq!(store.unique_tags().unwrap(), vec!["a", "b", "c"]);
6784    }
6785
6786    #[test]
6787    fn test_semantic_store_unique_tags_empty_returns_empty() {
6788        let store = SemanticStore::new();
6789        assert!(store.unique_tags().unwrap().is_empty());
6790    }
6791
6792    #[test]
6793    fn test_working_memory_count_matching_prefix_counts_correctly() {
6794        let wm = WorkingMemory::new(10).unwrap();
6795        wm.set("user:a", "1").unwrap();
6796        wm.set("user:b", "2").unwrap();
6797        wm.set("other", "3").unwrap();
6798        assert_eq!(wm.count_matching_prefix("user:").unwrap(), 2);
6799        assert_eq!(wm.count_matching_prefix("other").unwrap(), 1);
6800        assert_eq!(wm.count_matching_prefix("none").unwrap(), 0);
6801    }
6802
6803    #[test]
6804    fn test_episodic_store_total_content_bytes_sums_lengths() {
6805        let store = EpisodicStore::new();
6806        let agent = AgentId::new("a");
6807        store.add_episode(agent.clone(), "hi", 0.5).unwrap();   // 2 bytes
6808        store.add_episode(agent.clone(), "hello", 0.5).unwrap(); // 5 bytes
6809        assert_eq!(store.total_content_bytes(&agent).unwrap(), 7);
6810    }
6811
6812    #[test]
6813    fn test_episodic_store_total_content_bytes_unknown_agent_returns_zero() {
6814        let store = EpisodicStore::new();
6815        assert_eq!(store.total_content_bytes(&AgentId::new("x")).unwrap(), 0);
6816    }
6817
6818    #[test]
6819    fn test_episodic_store_avg_content_length_correct_mean() {
6820        let store = EpisodicStore::new();
6821        let agent = AgentId::new("a");
6822        store.add_episode(agent.clone(), "hi", 0.5).unwrap();    // 2
6823        store.add_episode(agent.clone(), "hello", 0.5).unwrap(); // 5
6824        assert!((store.avg_content_length(&agent).unwrap() - 3.5).abs() < 1e-9);
6825    }
6826
6827    #[test]
6828    fn test_episodic_store_avg_content_length_empty_returns_zero() {
6829        let store = EpisodicStore::new();
6830        assert_eq!(store.avg_content_length(&AgentId::new("x")).unwrap(), 0.0);
6831    }
6832
6833    #[test]
6834    fn test_semantic_store_keys_for_tag_returns_matching_keys() {
6835        let store = SemanticStore::new();
6836        store.store("k1", "v1", vec!["rust".to_string()]).unwrap();
6837        store.store("k2", "v2", vec!["python".to_string()]).unwrap();
6838        store.store("k3", "v3", vec!["rust".to_string(), "async".to_string()]).unwrap();
6839        let mut keys = store.keys_for_tag("rust").unwrap();
6840        keys.sort_unstable();
6841        assert_eq!(keys, vec!["k1", "k3"]);
6842    }
6843
6844    #[test]
6845    fn test_semantic_store_keys_for_tag_nonexistent_tag_returns_empty() {
6846        let store = SemanticStore::new();
6847        store.store("k", "v", vec!["rust".to_string()]).unwrap();
6848        assert!(store.keys_for_tag("missing").unwrap().is_empty());
6849    }
6850
6851    #[test]
6852    fn test_count_episodes_with_tag_returns_correct_count() {
6853        let store = EpisodicStore::new();
6854        let agent = AgentId::new("a");
6855        store.add_episode_with_tags(agent.clone(), "e1", 0.5, vec!["ai".to_string()]).unwrap();
6856        store.add_episode_with_tags(agent.clone(), "e2", 0.5, vec!["ai".to_string(), "ml".to_string()]).unwrap();
6857        store.add_episode_with_tags(agent.clone(), "e3", 0.5, vec!["ml".to_string()]).unwrap();
6858        assert_eq!(store.count_episodes_with_tag(&agent, "ai").unwrap(), 2);
6859        assert_eq!(store.count_episodes_with_tag(&agent, "ml").unwrap(), 2);
6860    }
6861
6862    #[test]
6863    fn test_episodes_with_content_returns_matching_content() {
6864        let store = EpisodicStore::new();
6865        let agent = AgentId::new("a");
6866        store.add_episode(agent.clone(), "rust is great", 0.5).unwrap();
6867        store.add_episode(agent.clone(), "python is fun", 0.5).unwrap();
6868        store.add_episode(agent.clone(), "rust and python", 0.5).unwrap();
6869        let matches = store.episodes_with_content(&agent, "rust").unwrap();
6870        assert_eq!(matches.len(), 2);
6871    }
6872
6873    #[test]
6874    fn test_semantic_store_most_common_tag_returns_most_frequent() {
6875        let store = SemanticStore::new();
6876        store.store("k1", "v1", vec!["a".to_string(), "b".to_string()]).unwrap();
6877        store.store("k2", "v2", vec!["a".to_string()]).unwrap();
6878        store.store("k3", "v3", vec!["b".to_string()]).unwrap();
6879        // "a" appears 2 times, "b" appears 2 times - either is valid
6880        let tag = store.most_common_tag().unwrap();
6881        assert!(tag.is_some());
6882    }
6883
6884    #[test]
6885    fn test_semantic_store_most_common_tag_empty_returns_none() {
6886        let store = SemanticStore::new();
6887        assert!(store.most_common_tag().unwrap().is_none());
6888    }
6889
6890    #[test]
6891    fn test_working_memory_pairs_starting_with_returns_matching_pairs() {
6892        let wm = WorkingMemory::new(10).unwrap();
6893        wm.set("user:name", "alice").unwrap();
6894        wm.set("user:age", "30").unwrap();
6895        wm.set("sys:mode", "prod").unwrap();
6896        let pairs = wm.pairs_starting_with("user:").unwrap();
6897        assert_eq!(pairs.len(), 2);
6898        assert!(pairs.iter().all(|(k, _)| k.starts_with("user:")));
6899    }
6900
6901    #[test]
6902    fn test_working_memory_total_key_bytes_sums_key_lengths() {
6903        let wm = WorkingMemory::new(10).unwrap();
6904        wm.set("ab", "x").unwrap();   // 2 bytes
6905        wm.set("cde", "y").unwrap();  // 3 bytes
6906        assert_eq!(wm.total_key_bytes().unwrap(), 5);
6907    }
6908
6909    #[test]
6910    fn test_working_memory_min_key_length_returns_shortest() {
6911        let wm = WorkingMemory::new(10).unwrap();
6912        wm.set("ab", "x").unwrap();
6913        wm.set("abcd", "y").unwrap();
6914        assert_eq!(wm.min_key_length().unwrap(), 2);
6915    }
6916
6917    #[test]
6918    fn test_working_memory_min_key_length_empty_returns_zero() {
6919        let wm = WorkingMemory::new(10).unwrap();
6920        assert_eq!(wm.min_key_length().unwrap(), 0);
6921    }
6922
6923    #[test]
6924    fn test_episodic_store_content_lengths_returns_lengths_in_order() {
6925        let store = EpisodicStore::new();
6926        let agent = AgentId::new("a");
6927        store.add_episode(agent.clone(), "hi", 0.5).unwrap();    // 2
6928        store.add_episode(agent.clone(), "hello", 0.5).unwrap(); // 5
6929        assert_eq!(store.content_lengths(&agent).unwrap(), vec![2, 5]);
6930    }
6931
6932    #[test]
6933    fn test_semantic_store_remove_entries_with_tag_removes_matching() {
6934        let store = SemanticStore::new();
6935        store.store("k1", "v1", vec!["old".to_string()]).unwrap();
6936        store.store("k2", "v2", vec!["keep".to_string()]).unwrap();
6937        store.store("k3", "v3", vec!["old".to_string(), "keep".to_string()]).unwrap();
6938        let removed = store.remove_entries_with_tag("old").unwrap();
6939        assert_eq!(removed, 2);
6940        assert_eq!(store.len().unwrap(), 1);
6941    }
6942
6943    // ── Round 36 ──────────────────────────────────────────────────────────────
6944
6945    #[test]
6946    fn test_episodic_store_max_content_length_returns_longest() {
6947        let store = EpisodicStore::new();
6948        let agent = AgentId::new("a");
6949        store.add_episode(agent.clone(), "hi", 0.5).unwrap();       // 2
6950        store.add_episode(agent.clone(), "hello!", 0.5).unwrap();   // 6
6951        store.add_episode(agent.clone(), "yo", 0.5).unwrap();       // 2
6952        assert_eq!(store.max_content_length(&agent).unwrap(), 6);
6953    }
6954
6955    #[test]
6956    fn test_episodic_store_max_content_length_unknown_agent_returns_zero() {
6957        let store = EpisodicStore::new();
6958        assert_eq!(store.max_content_length(&AgentId::new("x")).unwrap(), 0);
6959    }
6960
6961    #[test]
6962    fn test_episodic_store_min_content_length_returns_shortest() {
6963        let store = EpisodicStore::new();
6964        let agent = AgentId::new("a");
6965        store.add_episode(agent.clone(), "hi", 0.5).unwrap();      // 2
6966        store.add_episode(agent.clone(), "hello!", 0.5).unwrap();  // 6
6967        assert_eq!(store.min_content_length(&agent).unwrap(), 2);
6968    }
6969
6970    #[test]
6971    fn test_episodic_store_min_content_length_unknown_agent_returns_zero() {
6972        let store = EpisodicStore::new();
6973        assert_eq!(store.min_content_length(&AgentId::new("x")).unwrap(), 0);
6974    }
6975
6976    #[test]
6977    fn test_semantic_store_total_value_bytes_sums_value_lengths() {
6978        let store = SemanticStore::new();
6979        store.store("k1", "ab", vec![]).unwrap();    // 2
6980        store.store("k2", "cde", vec![]).unwrap();   // 3
6981        store.store("k3", "fghij", vec![]).unwrap(); // 5
6982        assert_eq!(store.total_value_bytes().unwrap(), 10);
6983    }
6984
6985    #[test]
6986    fn test_semantic_store_total_value_bytes_empty_returns_zero() {
6987        let store = SemanticStore::new();
6988        assert_eq!(store.total_value_bytes().unwrap(), 0);
6989    }
6990
6991    // ── Round 37 ──────────────────────────────────────────────────────────────
6992
6993    #[test]
6994    fn test_episodic_store_agents_with_episodes_returns_sorted_ids() {
6995        let store = EpisodicStore::new();
6996        let b = AgentId::new("b");
6997        let a = AgentId::new("a");
6998        store.add_episode(b.clone(), "episode b", 0.5).unwrap();
6999        store.add_episode(a.clone(), "episode a", 0.5).unwrap();
7000        let agents = store.agents_with_episodes().unwrap();
7001        assert_eq!(agents, vec![a, b]);
7002    }
7003
7004    #[test]
7005    fn test_episodic_store_agents_with_episodes_empty_returns_empty() {
7006        let store = EpisodicStore::new();
7007        assert!(store.agents_with_episodes().unwrap().is_empty());
7008    }
7009
7010    #[test]
7011    fn test_episodic_store_high_importance_count_counts_above_threshold() {
7012        let store = EpisodicStore::new();
7013        let agent = AgentId::new("a");
7014        store.add_episode(agent.clone(), "low", 0.3).unwrap();
7015        store.add_episode(agent.clone(), "high", 0.8).unwrap();
7016        store.add_episode(agent.clone(), "med", 0.6).unwrap();
7017        assert_eq!(store.high_importance_count(&agent, 0.5).unwrap(), 2);
7018    }
7019
7020    #[test]
7021    fn test_episodic_store_high_importance_count_unknown_agent_returns_zero() {
7022        let store = EpisodicStore::new();
7023        assert_eq!(store.high_importance_count(&AgentId::new("x"), 0.5).unwrap(), 0);
7024    }
7025
7026    #[test]
7027    fn test_semantic_store_avg_value_bytes_returns_mean() {
7028        let store = SemanticStore::new();
7029        store.store("k1", "ab", vec![]).unwrap();   // 2
7030        store.store("k2", "abcd", vec![]).unwrap(); // 4
7031        let avg = store.avg_value_bytes().unwrap();
7032        assert!((avg - 3.0).abs() < 1e-9);
7033    }
7034
7035    #[test]
7036    fn test_semantic_store_avg_value_bytes_empty_returns_zero() {
7037        let store = SemanticStore::new();
7038        assert_eq!(store.avg_value_bytes().unwrap(), 0.0);
7039    }
7040
7041    #[test]
7042    fn test_semantic_store_max_value_bytes_returns_longest() {
7043        let store = SemanticStore::new();
7044        store.store("k1", "hi", vec![]).unwrap();      // 2
7045        store.store("k2", "hello!", vec![]).unwrap();  // 6
7046        assert_eq!(store.max_value_bytes().unwrap(), 6);
7047    }
7048
7049    #[test]
7050    fn test_semantic_store_max_value_bytes_empty_returns_zero() {
7051        let store = SemanticStore::new();
7052        assert_eq!(store.max_value_bytes().unwrap(), 0);
7053    }
7054
7055    // ── Round 38 ──────────────────────────────────────────────────────────────
7056
7057    #[test]
7058    fn test_episodic_store_content_contains_count_counts_matches() {
7059        let store = EpisodicStore::new();
7060        let agent = AgentId::new("a");
7061        store.add_episode(agent.clone(), "rust is great", 0.5).unwrap();
7062        store.add_episode(agent.clone(), "python is ok", 0.5).unwrap();
7063        store.add_episode(agent.clone(), "rust rocks", 0.5).unwrap();
7064        assert_eq!(store.content_contains_count(&agent, "rust").unwrap(), 2);
7065        assert_eq!(store.content_contains_count(&agent, "java").unwrap(), 0);
7066    }
7067
7068    #[test]
7069    fn test_episodic_store_content_contains_count_unknown_agent_returns_zero() {
7070        let store = EpisodicStore::new();
7071        assert_eq!(store.content_contains_count(&AgentId::new("x"), "anything").unwrap(), 0);
7072    }
7073
7074    #[test]
7075    fn test_semantic_store_min_value_bytes_returns_shortest() {
7076        let store = SemanticStore::new();
7077        store.store("k1", "hello world", vec![]).unwrap(); // 11
7078        store.store("k2", "hi", vec![]).unwrap();          // 2
7079        assert_eq!(store.min_value_bytes().unwrap(), 2);
7080    }
7081
7082    #[test]
7083    fn test_semantic_store_min_value_bytes_empty_returns_zero() {
7084        let store = SemanticStore::new();
7085        assert_eq!(store.min_value_bytes().unwrap(), 0);
7086    }
7087
7088    #[test]
7089    fn test_working_memory_max_value_length_returns_longest() {
7090        let wm = WorkingMemory::new(10).unwrap();
7091        wm.set("k1", "ab").unwrap();     // 2
7092        wm.set("k2", "abcde").unwrap(); // 5
7093        assert_eq!(wm.max_value_length().unwrap(), 5);
7094    }
7095
7096    #[test]
7097    fn test_working_memory_max_value_length_empty_returns_zero() {
7098        let wm = WorkingMemory::new(10).unwrap();
7099        assert_eq!(wm.max_value_length().unwrap(), 0);
7100    }
7101
7102    // ── Round 39 ──────────────────────────────────────────────────────────────
7103
7104    #[test]
7105    fn test_episodic_store_episodes_by_importance_returns_desc_order() {
7106        let store = EpisodicStore::new();
7107        let agent = AgentId::new("a");
7108        store.add_episode(agent.clone(), "low", 0.2).unwrap();
7109        store.add_episode(agent.clone(), "high", 0.9).unwrap();
7110        store.add_episode(agent.clone(), "med", 0.5).unwrap();
7111        let contents = store.episodes_by_importance(&agent).unwrap();
7112        assert_eq!(contents[0], "high");
7113        assert_eq!(contents[2], "low");
7114    }
7115
7116    #[test]
7117    fn test_episodic_store_episodes_by_importance_empty_agent_returns_empty() {
7118        let store = EpisodicStore::new();
7119        assert!(store.episodes_by_importance(&AgentId::new("x")).unwrap().is_empty());
7120    }
7121
7122    #[test]
7123    fn test_semantic_store_all_keys_returns_sorted_keys() {
7124        let store = SemanticStore::new();
7125        store.store("banana", "v1", vec![]).unwrap();
7126        store.store("apple", "v2", vec![]).unwrap();
7127        store.store("cherry", "v3", vec![]).unwrap();
7128        assert_eq!(store.all_keys().unwrap(), vec!["apple", "banana", "cherry"]);
7129    }
7130
7131    #[test]
7132    fn test_semantic_store_all_keys_empty_returns_empty() {
7133        let store = SemanticStore::new();
7134        assert!(store.all_keys().unwrap().is_empty());
7135    }
7136
7137    #[test]
7138    fn test_working_memory_min_value_length_returns_shortest() {
7139        let wm = WorkingMemory::new(10).unwrap();
7140        wm.set("k1", "ab").unwrap();    // 2
7141        wm.set("k2", "abcde").unwrap(); // 5
7142        assert_eq!(wm.min_value_length().unwrap(), 2);
7143    }
7144
7145    #[test]
7146    fn test_working_memory_min_value_length_empty_returns_zero() {
7147        let wm = WorkingMemory::new(10).unwrap();
7148        assert_eq!(wm.min_value_length().unwrap(), 0);
7149    }
7150}