use std::collections::{BTreeMap, VecDeque};
use std::fs;
use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
const MAX_SUMMARIES: usize = 10;
const SUMMARY_MAX_CHARS: usize = 280;
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
struct MemoryData {
#[serde(default)]
preferences: BTreeMap<String, String>,
#[serde(default)]
summaries: VecDeque<String>,
}
pub struct MemoryStore {
path: PathBuf,
data: MemoryData,
}
impl MemoryStore {
pub fn open(path: impl Into<PathBuf>) -> Self {
let path = path.into();
let data = match fs::read(&path) {
Ok(bytes) => serde_json::from_slice::<MemoryData>(&bytes).unwrap_or_else(|err| {
tracing::warn!(
path = %path.display(),
error = %err,
"memory file unreadable; starting with empty memory"
);
MemoryData::default()
}),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => MemoryData::default(),
Err(err) => {
tracing::warn!(
path = %path.display(),
error = %err,
"memory file unreadable; starting with empty memory"
);
MemoryData::default()
}
};
Self { path, data }
}
pub fn remember(&mut self, key: &str, value: &str) {
self.data
.preferences
.insert(key.to_string(), value.to_string());
self.save();
}
pub fn recall(&self, key: &str) -> Option<String> {
self.data.preferences.get(key).cloned()
}
pub fn summarize_turn(&mut self, user_text: &str, agent_reply: &str) {
let summary = format_turn_summary(user_text, agent_reply);
self.data.summaries.push_back(summary);
while self.data.summaries.len() > MAX_SUMMARIES {
self.data.summaries.pop_front();
}
self.save();
}
pub fn preferences_summary(&self) -> Option<String> {
if self.data.preferences.is_empty() {
return None;
}
Some(
self.data
.preferences
.iter()
.map(|(k, v)| format!("{k}: {v}"))
.collect::<Vec<_>>()
.join("; "),
)
}
pub fn summaries(&self) -> Vec<String> {
self.data.summaries.iter().cloned().collect()
}
pub fn path(&self) -> &Path {
&self.path
}
fn save(&self) {
if let Some(parent) = self.path.parent() {
if !parent.as_os_str().is_empty() {
if let Err(err) = fs::create_dir_all(parent) {
tracing::warn!(
path = %self.path.display(),
error = %err,
"memory: could not create directory"
);
return;
}
}
}
match serde_json::to_vec_pretty(&self.data) {
Ok(bytes) => {
if let Err(err) = fs::write(&self.path, bytes) {
tracing::warn!(
path = %self.path.display(),
error = %err,
"memory: save failed"
);
}
}
Err(err) => tracing::warn!(error = %err, "memory: serialize failed"),
}
}
}
fn format_turn_summary(user_text: &str, agent_reply: &str) -> String {
let half = SUMMARY_MAX_CHARS / 2;
let user = truncate(user_text.trim(), half);
let agent = truncate(agent_reply.trim(), half);
format!("User: {user} | Assistant: {agent}")
}
fn truncate(s: &str, max: usize) -> &str {
if s.len() <= max {
return s;
}
let mut idx = max;
while idx > 0 && !s.is_char_boundary(idx) {
idx -= 1;
}
&s[..idx]
}