memory_tool
Python · package toolnexus · SPEC §7E · python/src/toolnexus/agents/surface.py
def memory_tool(dir: str) -> ToolBuilds the memory builtin (§7E): one Tool named "memory", three actions —
add (append an entry), replace (swap an existing substring), remove (delete an existing
substring) — over target="self" (MEMORY.md, default) or target="user" (USER.md) inside
dir. Not one of the default §4A builtins — it only exists when you wire it in, either
directly (extra_tools=[memory_tool(dir)]) or via agent_from_dir
(memory=True, the default there).
When to use it
Section titled “When to use it”- You’re composing a toolkit by hand (
create_toolkit(extra_tools=[...])) and want a persona to be able to persist notes about itself or the user, without pulling in the wholeAgent/agent_from_dirsurface. - You want file-backed memory scoped to a specific directory that isn’t necessarily the
persona’s full home —
memory_toolonly needs a path, not acompose_soul-shaped folder. - You’re building a custom persona surface and want the exact §7E write semantics
(loud error on a missing
replace/removetarget,target=uservstarget=self) without reimplementing them.
Why this and not the alternative
Section titled “Why this and not the alternative”The tool writes to disk immediately on every action, but per SPEC §7E it deliberately does
not mutate the current session’s prompt — the tool’s own description tells the model this,
so it doesn’t expect its next turn to already reflect the write. A replace/remove whose
text isn’t found in the target file is a loud is_error=True, never a silent no-op, so the
model (or your code) can tell a real edit from a missed one.
Examples
Section titled “Examples”1. The smallest useful call — add, called directly
Section titled “1. The smallest useful call — add, called directly”import asyncioimport osimport tempfile
from toolnexus.agents import memory_tool
async def main(): with tempfile.TemporaryDirectory() as home: tool = memory_tool(home) assert tool.name == "memory"
res = await tool.execute({"action": "add", "text": "prefers dark mode"})
assert res.is_error is False assert "MEMORY.md" in res.output
memory_path = os.path.join(home, "MEMORY.md") with open(memory_path, encoding="utf-8") as f: saved = f.read() assert "prefers dark mode" in saved
print("ok:", res.output, "|", saved.strip())
asyncio.run(main())2. The realistic case — replace, remove, and target="user"
Section titled “2. The realistic case — replace, remove, and target="user"”import asyncioimport osimport tempfile
from toolnexus.agents import memory_tool
async def main(): with tempfile.TemporaryDirectory() as home: tool = memory_tool(home)
# target="self" (default) -> MEMORY.md await tool.execute({"action": "add", "text": "timezone: UTC"}) replaced = await tool.execute({"action": "replace", "text": "timezone: UTC", "with": "timezone: IST"}) assert replaced.is_error is False
with open(os.path.join(home, "MEMORY.md"), encoding="utf-8") as f: memory = f.read() assert "timezone: IST" in memory assert "timezone: UTC" not in memory
# target="user" -> USER.md, a separate file await tool.execute({"action": "add", "target": "user", "text": "name: Muthu"}) with open(os.path.join(home, "USER.md"), encoding="utf-8") as f: user = f.read() assert "name: Muthu" in user
removed = await tool.execute({"action": "remove", "target": "user", "text": "name: Muthu"}) assert removed.is_error is False with open(os.path.join(home, "USER.md"), encoding="utf-8") as f: user_after = f.read() assert "name: Muthu" not in user_after
print("ok:", memory.strip(), "|", user_after.strip() or "(empty)")
asyncio.run(main())3. The full surface — a missing substring is a loud error, and it works through a toolkit
Section titled “3. The full surface — a missing substring is a loud error, and it works through a toolkit”import asyncioimport osimport tempfile
from toolnexus import create_toolkitfrom toolnexus.agents import memory_tool
async def main(): with tempfile.TemporaryDirectory() as home: # A replace/remove against text that was never written is a loud is_error, never silent. tool = memory_tool(home) missing = await tool.execute({"action": "replace", "text": "nope", "with": "x"}) assert missing.is_error is True assert missing.output == "not found: nope"
unknown = await tool.execute({"action": "bogus", "text": "x"}) assert unknown.is_error is True
# The common path: wired into a toolkit, invoked by its exposed name like any tool. tk = await create_toolkit(builtins=False, extra_tools=[memory_tool(home)]) try: res = await tk.execute("memory", {"action": "add", "text": "likes concise answers"}) assert res.is_error is False finally: await tk.close()
with open(os.path.join(home, "MEMORY.md"), encoding="utf-8") as f: saved = f.read() assert "likes concise answers" in saved
print("ok:", missing.output, "|", saved.strip())
asyncio.run(main())Tool fields this builds
Section titled “Tool fields this builds”| Field | Value |
|---|---|
name |
"memory" |
source |
"native" |
input_schema |
{action: add|replace|remove, target: self|user, text, with} — action/text required. |
execute |
Async; writes to MEMORY.md (target="self") or USER.md (target="user") under dir. |
See also
Section titled “See also”compose_soul— Build a persona’s system prompt from its home directory: identity, memory, skills.agent_from_dir— Point at an agent home directory and get a configured agent back.