use crate::api::types::Message;
use crate::config::Config;
use crate::token_count::estimate_tokens_with_overhead;
use crate::tools::codemap::update_memory_tokens;
use anyhow::Result;
use chrono::Utc;
use std::collections::VecDeque;
const MAX_MEMORY_ENTRIES: usize = 10_000;
pub struct AgentMemory {
context_window: usize,
max_memory_tokens: usize,
entries: VecDeque<MemoryEntry>,
total_tokens: usize,
}
pub struct MemoryEntry {
pub timestamp: String,
pub role: String,
pub content: String,
pub token_estimate: usize,
pub tool_calls: Option<Vec<crate::api::types::ToolCall>>,
pub image_count: usize,
}
impl MemoryEntry {
pub fn from_message(msg: &Message) -> Self {
let token_estimate = estimate_tokens(msg.content.text());
Self {
timestamp: Utc::now().to_rfc3339(),
role: msg.role.clone(),
content: msg.content.text().to_string(),
token_estimate,
tool_calls: msg.tool_calls.clone(),
image_count: msg.content.image_count(),
}
}
}
fn estimate_tokens(content: &str) -> usize {
estimate_tokens_with_overhead(content, 10)
}
impl AgentMemory {
pub fn new(config: &Config) -> Result<Self> {
let memory_token_cap = config
.context_length
.min(config.resources.quotas.max_context_tokens);
let max_memory_tokens = memory_token_cap.saturating_mul(95) / 100;
Ok(Self {
context_window: config.context_length,
max_memory_tokens,
entries: VecDeque::new(),
total_tokens: 0,
})
}
pub fn add_message(&mut self, msg: &Message) {
if self.entries.len() >= MAX_MEMORY_ENTRIES {
let remove_count = MAX_MEMORY_ENTRIES / 4;
for removed in self.entries.drain(..remove_count) {
self.total_tokens = self.total_tokens.saturating_sub(removed.token_estimate);
}
}
let new_entry = MemoryEntry::from_message(msg);
let new_tokens = new_entry.token_estimate;
while self.total_tokens + new_tokens > self.max_memory_tokens && !self.entries.is_empty() {
if let Some(removed) = self.entries.pop_front() {
self.total_tokens = self.total_tokens.saturating_sub(removed.token_estimate);
}
}
self.total_tokens = self.total_tokens.saturating_add(new_tokens);
self.entries.push_back(new_entry);
update_memory_tokens(self.total_tokens);
}
pub fn total_estimated_tokens(&self) -> usize {
self.total_tokens
}
pub fn total_tokens(&self) -> usize {
self.total_estimated_tokens()
}
pub fn is_near_limit(&self) -> bool {
self.total_tokens().saturating_mul(100) > self.max_memory_tokens.saturating_mul(85)
}
pub fn context_window(&self) -> usize {
self.context_window
}
pub fn max_memory_tokens(&self) -> usize {
self.max_memory_tokens
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn clear(&mut self) {
self.entries.clear();
self.total_tokens = 0;
update_memory_tokens(0);
}
pub fn add_raw_entry(
&mut self,
timestamp: String,
role: String,
content: String,
token_estimate: usize,
) {
if self.entries.len() >= MAX_MEMORY_ENTRIES {
let remove_count = MAX_MEMORY_ENTRIES / 4;
for removed in self.entries.drain(..remove_count) {
self.total_tokens = self.total_tokens.saturating_sub(removed.token_estimate);
}
}
while self.total_tokens + token_estimate > self.max_memory_tokens
&& !self.entries.is_empty()
{
if let Some(removed) = self.entries.pop_front() {
self.total_tokens = self.total_tokens.saturating_sub(removed.token_estimate);
}
}
let entry = MemoryEntry {
timestamp,
role,
content,
token_estimate,
tool_calls: None,
image_count: 0,
};
self.total_tokens = self.total_tokens.saturating_add(token_estimate);
self.entries.push_back(entry);
update_memory_tokens(self.total_tokens);
}
pub fn set_total_tokens(&mut self, tokens: usize) {
self.total_tokens = tokens;
update_memory_tokens(self.total_tokens);
}
pub fn recent(&self, n: usize) -> Vec<&MemoryEntry> {
self.entries.iter().rev().take(n).collect()
}
pub fn summary(&self, n: usize) -> String {
let recent = self.recent(n);
recent
.iter()
.map(|e| {
let preview: String = e.content.chars().take(50).collect();
format!("[{}] {}: {}...", e.timestamp, e.role, preview)
})
.collect::<Vec<_>>()
.join("\n")
}
}
#[cfg(test)]
#[allow(clippy::field_reassign_with_default)] #[path = "../../tests/unit/memory/mod_test.rs"]
mod tests;