memoryTool — file-backed memory
Rust · crate toolnexus · SPEC §7E · rust/src/agents/home.rs
pub fn memory_tool(dir: impl AsRef<Path>) -> Tool // toolnexus::agents// input: { action: "add" | "replace" | "remove", target?: "self" | "user", text: string, with?: string }memory_tool returns a single memory tool with three actions — add, replace, remove —
over two files in dir: MEMORY.md (target: "self", the default) and USER.md
(target: "user"). Every action writes to disk immediately. A replace/remove whose
text substring is not found in the target file is a loud is_error: true. Crucially, writing
does not change the current session’s soul — it takes effect at the start of the next
session, when compose_soul re-reads the directory. This is
the frozen-snapshot rule that keeps a long-lived persona cache-stable within a run.
When to use it
Section titled “When to use it”Reach for memory_tool directly when you’re assembling a persona’s tool list by hand (via
agents::new rather than from_dir)
and want the same file-backed memory builtin from_dir wires automatically. It’s also the tool
to point at when writing a HEARTBEAT.md that asks the agent to persist facts — “fold notes into
MEMORY.md via the memory tool” is the entire recipe behind dream/consolidation patterns.
Why this and not the alternative
Section titled “Why this and not the alternative”Examples
Section titled “Examples”1. The smallest useful call — add an entry, read it back from disk
Section titled “1. The smallest useful call — add an entry, read it back from disk”use serde_json::json;use toolnexus::agents::memory_tool;
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let dir = std::env::temp_dir().join(format!("memtool-mt1-{}", std::process::id())); std::fs::create_dir_all(&dir)?;
let mem = memory_tool(&dir); assert_eq!(mem.name, "memory");
let res = mem.call(json!({"action": "add", "text": "prefers metric units"}), Default::default()).await; assert!(!res.is_error, "{}", res.output); assert_eq!(std::fs::read_to_string(dir.join("MEMORY.md"))?, "- prefers metric units\n");
std::fs::remove_dir_all(&dir)?; println!("ok: {}", res.output); Ok(())}2. The realistic case — replace, remove, and the loud miss
Section titled “2. The realistic case — replace, remove, and the loud miss”replace/remove on text that isn’t present is a real is_error, not a silent no-op — the
model sees the miss and can retry with the right substring.
use serde_json::json;use toolnexus::agents::memory_tool;
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let dir = std::env::temp_dir().join(format!("memtool-mt2-{}", std::process::id())); std::fs::create_dir_all(&dir)?; let mem = memory_tool(&dir);
mem.call(json!({"action": "add", "text": "timezone: UTC"}), Default::default()).await; let replaced = mem .call(json!({"action": "replace", "text": "UTC", "with": "IST"}), Default::default()) .await; assert!(!replaced.is_error, "{}", replaced.output); let on_disk = std::fs::read_to_string(dir.join("MEMORY.md"))?; assert_eq!(on_disk, "- timezone: IST\n");
let miss = mem.call(json!({"action": "remove", "text": "nonexistent phrase"}), Default::default()).await; assert!(miss.is_error, "a remove miss must be a loud is_error"); assert_eq!(std::fs::read_to_string(dir.join("MEMORY.md"))?, on_disk, "file unchanged on a miss");
std::fs::remove_dir_all(&dir)?; println!("ok: replace -> {} | miss -> {}", on_disk.trim(), miss.output); Ok(())}3. The full surface — target: "user" writes USER.md, and next-session-only semantics
Section titled “3. The full surface — target: "user" writes USER.md, and next-session-only semantics”Writing memory during a run does not retroactively change that run’s soul — a fresh
compose_soul call (the next session) is what picks it up.
use serde_json::json;use toolnexus::agents::{compose_soul, memory_tool};
#[tokio::main]async fn main() -> Result<(), Box<dyn std::error::Error>> { let dir = std::env::temp_dir().join(format!("memtool-mt3-{}", std::process::id())); std::fs::create_dir_all(&dir)?; std::fs::write(dir.join("SOUL.md"), "You are Kip.")?;
// Session 1: compose the soul BEFORE any memory write — MEMORY.md does not exist yet. let (soul_before, found_before) = compose_soul(&dir); assert!(!found_before.iter().any(|f| f == "MEMORY.md"));
let mem = memory_tool(&dir); mem.call(json!({"action": "add", "target": "user", "text": "name is Muthu"}), Default::default()).await; mem.call(json!({"action": "add", "text": "always reply tersely"}), Default::default()).await;
// The write does NOT retroactively change soul_before — it's a frozen snapshot. assert!(!soul_before.contains("MEMORY.md")); assert_eq!(std::fs::read_to_string(dir.join("USER.md"))?, "- name is Muthu\n");
// Session 2: composing again picks up both new files. let (soul_after, found_after) = compose_soul(&dir); assert_eq!(found_after, vec!["SOUL.md", "USER.md", "MEMORY.md"]); assert!(soul_after.contains("always reply tersely") && soul_after.contains("name is Muthu"));
std::fs::remove_dir_all(&dir)?; println!("ok: memory writes land on disk immediately, load next session: {found_after:?}"); Ok(())}Actions
Section titled “Actions”action |
Effect | Target file (target) |
|---|---|---|
add |
Appends "- <text>\n" to the file. |
self ⇒ MEMORY.md (default); user ⇒ USER.md |
replace |
Swaps the first occurrence of text with with. is_error if text is absent. |
same |
remove |
Deletes the first occurrence of text. is_error if text is absent. |
same |
See also
Section titled “See also”compose_soul— Build a persona’s system prompt from its home directory: identity, memory, skills.from_dir— Point at an agent home directory and get a configured agent back — wires this tool for you.