Skip to content

Toolnexus.Agents.Home.memory_tool

Elixir · package toolnexus · SPEC §7E · elixir/lib/toolnexus/agents/home.ex

@spec memory_tool(String.t()) :: Toolnexus.Tool.t()
def memory_tool(dir)
# input_schema: {action: "add" | "replace" | "remove", target?: "self" | "user", text, with?}
# action=add → appends "- <text>" to the target file
# action=replace → swaps an existing `text` substring for `with` (isError if absent)
# action=remove → deletes an existing `text` substring (isError if absent)
# target=self (default) → MEMORY.md; target=user → USER.md

One tool, three actions, over two files under dir: MEMORY.md (the agent’s own durable notes) and USER.md (its model of the user). Every write lands on disk immediately; the current session’s prompt is not touched — the frozen soul snapshot picks it up at the start of the next session. A replace/remove whose target substring is not found in the file is a loud is_error: true, never a silent no-op.

  • You’re building a persona that should remember things — this is the builtin Toolnexus.Agents.Home.from_dir/2 wires in by default; call memory_tool/1 directly only when you want it standalone (a custom agent definition, a test, a tool list you’re assembling by hand).
  • You want the model’s mental model of the user tracked separately from its own notestarget: "user" writes USER.md instead of MEMORY.md.

1. The smallest useful call — add, executed directly

Section titled “1. The smallest useful call — add, executed directly”
alias Toolnexus.{Context, ToolResult}
alias Toolnexus.Agents.Home
dir = Path.join(System.tmp_dir!(), "toolnexus_doc_memory_tool_1_#{System.unique_integer([:positive])}")
File.rm_rf!(dir)
File.mkdir_p!(dir)
tool = Home.memory_tool(dir)
true = tool.name == "memory"
result = tool.execute.(%{"action" => "add", "text" => "prefers dark mode"}, %Context{})
%ToolResult{is_error: false} = result
true = String.contains?(result.output, "MEMORY.md")
true = File.read!(Path.join(dir, "MEMORY.md")) == "- prefers dark mode\n"
IO.puts("ok: #{result.output}")

2. The realistic case — replace and remove, and the loud miss

Section titled “2. The realistic case — replace and remove, and the loud miss”
alias Toolnexus.{Context, ToolResult}
alias Toolnexus.Agents.Home
dir = Path.join(System.tmp_dir!(), "toolnexus_doc_memory_tool_2_#{System.unique_integer([:positive])}")
File.rm_rf!(dir)
File.mkdir_p!(dir)
File.write!(Path.join(dir, "MEMORY.md"), "- prefers dark mode\n- likes verbose logs\n")
tool = Home.memory_tool(dir)
replaced = tool.execute.(%{"action" => "replace", "text" => "verbose logs", "with" => "terse logs"}, %Context{})
false = replaced.is_error
true = File.read!(Path.join(dir, "MEMORY.md")) =~ "terse logs"
removed = tool.execute.(%{"action" => "remove", "text" => "- prefers dark mode\n"}, %Context{})
false = removed.is_error
false = File.read!(Path.join(dir, "MEMORY.md")) =~ "dark mode"
# a replace/remove target that isn't present is a loud error, not a silent no-op
miss = tool.execute.(%{"action" => "replace", "text" => "nonexistent", "with" => "x"}, %Context{})
true = miss.is_error
true = String.contains?(miss.output, "not found")
IO.puts("ok: replace + remove applied; miss reported as isError")

3. The full surface — target: "user" writes USER.md instead

Section titled “3. The full surface — target: "user" writes USER.md instead”
alias Toolnexus.{Context, ToolResult}
alias Toolnexus.Agents.Home
dir = Path.join(System.tmp_dir!(), "toolnexus_doc_memory_tool_3_#{System.unique_integer([:positive])}")
File.rm_rf!(dir)
File.mkdir_p!(dir)
tool = Home.memory_tool(dir)
self_note = tool.execute.(%{"action" => "add", "text" => "checked the invoice twice"}, %Context{})
user_note = tool.execute.(%{"action" => "add", "target" => "user", "text" => "works in UTC+5:30"}, %Context{})
false = self_note.is_error
false = user_note.is_error
true = String.contains?(self_note.output, "MEMORY.md")
true = String.contains?(user_note.output, "USER.md")
true = File.read!(Path.join(dir, "MEMORY.md")) =~ "checked the invoice twice"
true = File.read!(Path.join(dir, "USER.md")) =~ "works in UTC+5:30"
# writes are on disk only — they say so, so a caller never assumes the live prompt changed
true = String.contains?(user_note.output, "loads next session")
IO.puts("ok: self note → MEMORY.md, user note → USER.md")
Field Type Required What it is
action "add" | "replace" | "remove" yes The operation.
target "self" | "user" no (default "self") selfMEMORY.md, userUSER.md.
text String.t() yes For add: the entry. For replace/remove: the existing substring.
with String.t() for replace The replacement text.