use std::path::PathBuf;
use std::sync::Mutex;
use async_trait::async_trait;
use car_engine::ToolExecutor;
use car_memgine::MemgineEngine;
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Clone, Serialize, Deserialize)]
struct Note {
subject: String,
body: String,
}
pub struct MemoryTools {
inner: Mutex<Inner>,
path: PathBuf,
}
struct Inner {
engine: MemgineEngine,
notes: Vec<Note>,
}
impl MemoryTools {
pub fn open(path: PathBuf) -> Self {
let notes: Vec<Note> = std::fs::read_to_string(&path)
.ok()
.and_then(|s| serde_json::from_str(&s).ok())
.unwrap_or_default();
let mut engine = MemgineEngine::new(None);
for (i, n) in notes.iter().enumerate() {
ingest(&mut engine, i, n);
}
Self {
inner: Mutex::new(Inner { engine, notes }),
path,
}
}
pub fn tool_defs() -> Vec<Value> {
vec![
json!({
"name": "remember",
"description": "Save a durable fact about the user or task so you can recall it in \
future sessions (e.g. a preference, a project name, a decision). \
Use for things worth persisting, not transient details.",
"parameters": {
"type": "object",
"properties": {
"subject": { "type": "string", "description": "Short label for the fact (e.g. 'project name')." },
"body": { "type": "string", "description": "The fact to remember." }
},
"required": ["subject", "body"]
}
}),
json!({
"name": "recall",
"description": "Retrieve previously remembered facts relevant to a query. Use at the \
start of a task to recall what you know about the user or project.",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "What to recall." }
},
"required": ["query"]
}
}),
]
}
fn remember(&self, params: &Value) -> Result<Value, String> {
let subject = params
.get("subject")
.and_then(Value::as_str)
.ok_or("remember requires a 'subject' string")?;
let body = params
.get("body")
.and_then(Value::as_str)
.ok_or("remember requires a 'body' string")?;
let mut g = self.inner.lock().map_err(|_| "memory lock poisoned")?;
let note = Note {
subject: subject.to_string(),
body: body.to_string(),
};
match g
.notes
.iter_mut()
.find(|n| n.subject.eq_ignore_ascii_case(subject))
{
Some(existing) => existing.body = body.to_string(),
None => g.notes.push(note),
}
let mut engine = MemgineEngine::new(None);
for (i, n) in g.notes.iter().enumerate() {
ingest(&mut engine, i, n);
}
g.engine = engine;
if let Some(parent) = self.path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let serialized =
serde_json::to_string_pretty(&g.notes).map_err(|e| format!("serialize: {e}"))?;
std::fs::write(&self.path, serialized).map_err(|e| format!("persist memory: {e}"))?;
Ok(json!({ "remembered": subject, "total_facts": g.notes.len() }))
}
fn recall(&self, params: &Value) -> Result<Value, String> {
let query = params
.get("query")
.and_then(Value::as_str)
.ok_or("recall requires a 'query' string")?;
let mut g = self.inner.lock().map_err(|_| "memory lock poisoned")?;
if g.notes.is_empty() {
return Ok(json!({ "query": query, "context": "", "note": "no facts remembered yet" }));
}
let context = g.engine.build_context_fast(query);
Ok(json!({ "query": query, "context": context }))
}
}
fn ingest(engine: &mut MemgineEngine, idx: usize, n: &Note) {
engine.ingest_fact(
&format!("assistant-note-{idx}"),
&n.subject,
&n.body,
"user",
"user",
chrono::Utc::now(),
"assistant",
None,
Vec::new(),
false,
);
}
#[async_trait]
impl ToolExecutor for MemoryTools {
async fn execute(&self, tool: &str, params: &Value) -> Result<Value, String> {
match tool {
"remember" => self.remember(params),
"recall" => self.recall(params),
other => Err(format!("unknown tool: '{other}'")),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn remembers_across_reopen() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("assistant.json");
{
let mem = MemoryTools::open(path.clone());
mem.execute(
"remember",
&json!({ "subject": "project name", "body": "The project is called Zephyr." }),
)
.await
.unwrap();
}
let mem = MemoryTools::open(path.clone());
let out = mem
.execute("recall", &json!({ "query": "what is my project called?" }))
.await
.unwrap();
let ctx = out["context"].as_str().unwrap();
assert!(ctx.contains("Zephyr"), "recall should surface the fact: {ctx}");
}
#[tokio::test]
async fn remember_supersedes_same_subject() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("m.json");
let mem = MemoryTools::open(path.clone());
mem.execute("remember", &json!({ "subject": "editor", "body": "vim" }))
.await
.unwrap();
let out = mem
.execute("remember", &json!({ "subject": "Editor", "body": "emacs" }))
.await
.unwrap();
assert_eq!(out["total_facts"], 1);
let notes: Vec<Note> =
serde_json::from_str(&std::fs::read_to_string(&path).unwrap()).unwrap();
assert_eq!(notes.len(), 1);
assert_eq!(notes[0].body, "emacs");
}
#[tokio::test]
async fn recall_on_empty_is_graceful() {
let dir = tempfile::tempdir().unwrap();
let mem = MemoryTools::open(dir.path().join("m.json"));
let out = mem
.execute("recall", &json!({ "query": "anything" }))
.await
.unwrap();
assert_eq!(out["context"], "");
}
}