use std::collections::HashMap;
use std::fmt::Write as _;
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, Mutex, PoisonError};
use futures::future::BoxFuture;
use serde::{Deserialize, Serialize};
use crate::error::{AgentError, ErrorKind};
use crate::tools::{Tool, ToolContext, ToolEffect};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(transparent)]
pub struct MemoryScope(String);
impl MemoryScope {
#[must_use]
pub fn new(scope: impl Into<String>) -> Self {
Self(scope.into())
}
#[must_use]
pub fn agent() -> Self {
Self::new("agent")
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct MemoryBlock {
pub label: String,
pub description: String,
pub entries: Vec<String>,
pub limit_chars: usize,
}
impl MemoryBlock {
#[must_use]
pub fn new(
label: impl Into<String>,
description: impl Into<String>,
limit_chars: usize,
) -> Self {
Self {
label: label.into(),
description: description.into(),
entries: Vec::new(),
limit_chars,
}
}
#[must_use]
pub fn used_chars(&self) -> usize {
self.entries.iter().map(|entry| entry.chars().count()).sum()
}
#[must_use]
pub fn defaults() -> Vec<Self> {
vec![
Self::new(
"memory",
"Facts, decisions, and lessons you want to remember across runs.",
2_200,
),
Self::new(
"user",
"Who you work for: preferences, style, standing instructions.",
1_375,
),
]
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "action", rename_all = "snake_case")]
pub enum MemoryOp {
Add {
block: String,
text: String,
},
Replace {
block: String,
old: String,
text: String,
},
Remove {
block: String,
old: String,
},
}
impl MemoryOp {
#[must_use]
pub fn block(&self) -> &str {
match self {
Self::Add { block, .. } | Self::Replace { block, .. } | Self::Remove { block, .. } => {
block
}
}
}
}
pub fn apply_op(block: &mut MemoryBlock, op: &MemoryOp) -> Result<(), AgentError> {
match op {
MemoryOp::Add { text, .. } => {
let text = non_empty(text)?;
check_fits(block, None, text)?;
block.entries.push(text.to_owned());
}
MemoryOp::Replace { old, text, .. } => {
let text = non_empty(text)?;
let index = find_unique(block, old)?;
check_fits(block, Some(index), text)?;
if let Some(entry) = block.entries.get_mut(index) {
text.clone_into(entry);
}
}
MemoryOp::Remove { old, .. } => {
let index = find_unique(block, old)?;
block.entries.remove(index);
}
}
Ok(())
}
fn non_empty(text: &str) -> Result<&str, AgentError> {
let text = text.trim();
if text.is_empty() {
return Err(AgentError::new(
ErrorKind::Tool,
"memory entries must not be empty",
));
}
Ok(text)
}
fn check_fits(block: &MemoryBlock, replacing: Option<usize>, text: &str) -> Result<(), AgentError> {
let freed = replacing
.and_then(|index| block.entries.get(index))
.map_or(0, |entry| entry.chars().count());
let after = block
.used_chars()
.saturating_sub(freed)
.saturating_add(text.chars().count());
if after > block.limit_chars {
return Err(AgentError::new(
ErrorKind::Tool,
format!(
"memory block {:?} is full: this edit needs {after} of {} characters. Remove or merge entries first, then retry.",
block.label, block.limit_chars
),
));
}
Ok(())
}
fn find_unique(block: &MemoryBlock, old: &str) -> Result<usize, AgentError> {
let old = old.trim();
if old.is_empty() {
return Err(AgentError::new(
ErrorKind::Tool,
"give `old`: a substring of the entry to change",
));
}
let matches: Vec<usize> = block
.entries
.iter()
.enumerate()
.filter(|(_, entry)| entry.contains(old))
.map(|(index, _)| index)
.collect();
match matches.as_slice() {
[index] => Ok(*index),
[] => Err(AgentError::new(
ErrorKind::Tool,
format!("no entry in {:?} contains {old:?}", block.label),
)),
_ => Err(AgentError::new(
ErrorKind::Tool,
format!(
"{} entries in {:?} contain {old:?}: give a longer substring",
matches.len(),
block.label
),
)),
}
}
#[must_use]
pub fn render_snapshot(blocks: &[MemoryBlock]) -> String {
let mut out = String::from(
"## Memory\nYour persistent memory, as it was when this run started. Edit it with the `memory` tool; edits show here from the next run.\n",
);
for block in blocks {
let _ = write!(
out,
"\n<memory block=\"{}\" used=\"{}/{}\">\n{}\n",
block.label,
block.used_chars(),
block.limit_chars,
block.description
);
for entry in &block.entries {
out.push_str("- ");
out.push_str(entry);
out.push('\n');
}
out.push_str("</memory>\n");
}
out
}
pub trait MemoryStore: Send + Sync + std::fmt::Debug {
fn load<'a>(
&'a self,
scope: &'a MemoryScope,
) -> BoxFuture<'a, Result<Vec<MemoryBlock>, AgentError>>;
fn apply<'a>(
&'a self,
scope: &'a MemoryScope,
op: MemoryOp,
) -> BoxFuture<'a, Result<MemoryBlock, AgentError>>;
}
#[derive(Debug)]
pub struct InMemoryMemoryStore {
template: Vec<MemoryBlock>,
scopes: Mutex<HashMap<MemoryScope, Vec<MemoryBlock>>>,
}
impl Default for InMemoryMemoryStore {
fn default() -> Self {
Self::new(MemoryBlock::defaults())
}
}
impl InMemoryMemoryStore {
#[must_use]
pub fn new(template: Vec<MemoryBlock>) -> Self {
Self {
template,
scopes: Mutex::new(HashMap::new()),
}
}
#[must_use]
pub fn shared(self) -> Arc<dyn MemoryStore> {
Arc::new(self)
}
}
impl MemoryStore for InMemoryMemoryStore {
fn load<'a>(
&'a self,
scope: &'a MemoryScope,
) -> BoxFuture<'a, Result<Vec<MemoryBlock>, AgentError>> {
let blocks = self
.scopes
.lock()
.unwrap_or_else(PoisonError::into_inner)
.get(scope)
.cloned()
.unwrap_or_else(|| self.template.clone());
Box::pin(std::future::ready(Ok(blocks)))
}
fn apply<'a>(
&'a self,
scope: &'a MemoryScope,
op: MemoryOp,
) -> BoxFuture<'a, Result<MemoryBlock, AgentError>> {
let mut scopes = self.scopes.lock().unwrap_or_else(PoisonError::into_inner);
let blocks = scopes
.entry(scope.clone())
.or_insert_with(|| self.template.clone());
let result = match blocks.iter().position(|block| block.label == op.block()) {
Some(index) => blocks
.get_mut(index)
.ok_or_else(|| unknown_block(op.block(), &[]))
.and_then(|block| apply_op(block, &op).map(|()| block.clone())),
None => Err(unknown_block(op.block(), blocks)),
};
drop(scopes);
Box::pin(std::future::ready(result))
}
}
fn unknown_block(label: &str, blocks: &[MemoryBlock]) -> AgentError {
let known: Vec<&str> = blocks.iter().map(|block| block.label.as_str()).collect();
AgentError::new(
ErrorKind::Tool,
format!("no memory block {label:?}; blocks: {known:?}"),
)
}
#[derive(Debug, Clone)]
pub struct MemoryTool {
store: Arc<dyn MemoryStore>,
scope: MemoryScope,
}
impl MemoryTool {
#[must_use]
pub fn new(store: Arc<dyn MemoryStore>, scope: MemoryScope) -> Self {
Self { store, scope }
}
}
impl Tool for MemoryTool {
fn name(&self) -> &'static str {
"memory"
}
fn description(&self) -> &'static str {
"Edit your persistent memory. action=add appends `text` to `block`; action=replace swaps the entry containing `old` for `text`; action=remove deletes the entry containing `old`. Save durable facts and lessons, not logs."
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"action": {"type": "string", "enum": ["add", "replace", "remove"]},
"block": {"type": "string", "description": "Block label, e.g. memory or user."},
"text": {"type": "string", "description": "New entry text (add, replace)."},
"old": {"type": "string", "description": "Substring of the entry to change (replace, remove)."}
},
"required": ["action", "block"]
})
}
fn effect(&self) -> ToolEffect {
ToolEffect::Internal
}
fn execute<'a>(
&'a self,
input: serde_json::Value,
_ctx: &'a ToolContext,
) -> Pin<Box<dyn Future<Output = Result<serde_json::Value, AgentError>> + Send + 'a>> {
Box::pin(async move {
let op: MemoryOp = serde_json::from_value(input).map_err(|err| {
AgentError::with_source(ErrorKind::Tool, format!("invalid memory edit: {err}"), err)
})?;
let block = self.store.apply(&self.scope, op).await?;
Ok(serde_json::json!({
"ok": true,
"block": block.label,
"used": block.used_chars(),
"limit": block.limit_chars,
}))
})
}
}
#[cfg(test)]
mod tests;