use super::core_memory::CoreMemory;
use super::decay::{should_prune, sort_by_decayed_score};
use super::store::{Store, StoreItem};
use std::sync::Arc;
pub struct TieredMemory {
pub core: CoreMemory,
pub short_term: Vec<String>,
pub max_short_term: usize,
pub long_term: Option<Arc<dyn Store>>,
pub overflow_queue: Vec<String>,
}
impl TieredMemory {
pub fn new(max_short_term: usize, max_core_chars: usize) -> Self {
Self {
core: CoreMemory::new(max_core_chars),
short_term: Vec::new(),
max_short_term,
long_term: None,
overflow_queue: Vec::new(),
}
}
pub fn with_store(mut self, store: Arc<dyn Store>) -> Self {
self.long_term = Some(store);
self
}
pub fn add_short_term(&mut self, summary: String) {
self.short_term.push(summary);
if self.short_term.len() > self.max_short_term {
let oldest = self.short_term.remove(0);
self.overflow_queue.push(oldest);
}
}
pub async fn flush_overflow(&mut self) -> usize {
let store = match &self.long_term {
Some(s) => s,
None => {
let drained = self.overflow_queue.len();
self.overflow_queue.clear();
return drained;
}
};
let mut flushed = 0;
for entry in self.overflow_queue.drain(..) {
let key = format!("short_term_{}", uuid::Uuid::new_v4());
let value = serde_json::json!({
"content": entry,
"source": "short_term_overflow",
});
if store
.put(&["memories", "short_term"], &key, value)
.await
.is_ok()
{
flushed += 1;
}
}
flushed
}
pub fn build_context_injection(&self) -> Option<String> {
let mut parts = Vec::new();
if let Some(core) = self.core.to_system_prompt_fragment() {
parts.push(core);
}
if !self.short_term.is_empty() {
parts.push("## Recent Context".to_string());
for (i, entry) in self.short_term.iter().enumerate() {
parts.push(format!("{}. {}", i + 1, entry));
}
}
if parts.is_empty() {
None
} else {
Some(parts.join("\n\n"))
}
}
pub fn prune_candidates(&self, items: &[StoreItem]) -> Vec<String> {
items
.iter()
.filter(|i| should_prune(i))
.map(|i| i.key.clone())
.collect()
}
pub fn rank_by_importance(items: &mut Vec<StoreItem>, limit: usize) {
sort_by_decayed_score(items, limit);
}
}
impl Default for TieredMemory {
fn default() -> Self {
Self::new(5, 2000)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_tiered_memory() {
let tm = TieredMemory::new(5, 2000);
assert!(tm.long_term.is_none());
assert_eq!(tm.short_term.len(), 0);
}
#[test]
fn test_short_term_eviction() {
let mut tm = TieredMemory::new(2, 2000);
tm.add_short_term("summary 1".into());
tm.add_short_term("summary 2".into());
assert_eq!(tm.short_term.len(), 2);
tm.add_short_term("summary 3".into());
assert_eq!(tm.short_term.len(), 2);
assert_eq!(tm.short_term[0], "summary 2");
assert_eq!(tm.overflow_queue.len(), 1);
assert_eq!(tm.overflow_queue[0], "summary 1");
}
#[test]
fn test_overflow_queue_clears_without_store() {
let mut tm = TieredMemory::new(1, 2000);
tm.add_short_term("entry 1".into());
tm.add_short_term("entry 2".into()); assert_eq!(tm.short_term, vec!["entry 2"]);
assert_eq!(tm.overflow_queue, vec!["entry 1"]);
}
#[test]
fn test_context_injection() {
let mut tm = TieredMemory::new(2, 2000);
tm.core.upsert(
super::super::core_memory::CoreMemoryBlock::new("1", "name", "Alice")
.with_importance(8.0),
);
tm.add_short_term("Previous conversation about Rust".into());
let ctx = tm.build_context_injection().unwrap();
assert!(ctx.contains("Alice"));
assert!(ctx.contains("Rust"));
}
}