Normandy.Tools.Memory (normandy v2.2.0)

View Source

Persistent agent memory: the memory tool. The model reads and writes text files under /memories with Anthropic's memory-tool commands (view, create, str_replace, insert, delete, rename); this tool executes them against a Normandy.Behaviours.MemoryStore. Normandy.LLM.ClaudioAdapter sends it as Anthropic's native memory tool (memory_20250818, native_definition/1), so the model gets the API's own instructions for it; description/1 is what other consumers see (an A2A agent card). A view of /memories names the mounts and their access.

The namespace comes from the session's tool_context (namespace_key, default :memory_namespace), never from the tool struct, so it survives Turn.Server rehydration and the model cannot change it. A missing namespace raises: there is no default namespace.

BaseAgent.init(%{client: c, model: m, tool_context: %{memory_namespace: "acme/user-42"}})
|> BaseAgent.register_tool(Memory.new(store: {MemoryStore.Postgres, MyApp.Repo}))

Options (new/1): :store (required, {module, handle}), :namespace_key, :mounts (%{"/memories/org" => {"acme/org-kb", :read_only | :read_write}}), :max_document_bytes (100_000), :max_namespace_bytes (1_000_000), :max_namespace_documents (1000), :max_view_chars (16_000), :max_cas_retries (3), :telemetry_include_paths (false).

:max_namespace_bytes bounds the total content bytes of a namespace's documents and :max_namespace_documents their number; each is a positive integer, or nil for no cap, and they apply independently. A create, str_replace or insert that would push the namespace past either is refused with error type :namespace_full and writes nothing. The store checks the caps in the same atomic step as the write (Normandy.Behaviours.MemoryStore put/6), so they hold under concurrent writers and count every document. A write that does not grow the namespace is always allowed, so a namespace over a lowered cap can still be shortened. A mount's caps apply to the mount's namespace.

Memory carries content across sessions, so it is a prompt-injection channel that persists: keep a namespace no wider than the trust boundary of the people whose input reaches the agent. Never store secrets in memory.

A mount's access holds only while no session can write its namespace directly, so a session namespace equal to a mount's namespace raises: when the agent is built (check_context!/2), and on a call whose context was replaced (the call fails; the model sees the mount path, not the namespace), and two mounts of one namespace with different access raise in new/1. Never build a namespace from a user-controlled string (a username, an org slug) without a fixed prefix that mount namespaces do not use, such as "users/" <> user_id beside "shared/org-kb".

Summary

Functions

Refuses a tool_context whose session namespace equals a mount's namespace, when the agent is built. A context without the namespace passes: one can be supplied per call (Normandy.A2A.Server's :tool_context), and run/2 checks it there.

Builds the tool, validating every option; raises ArgumentError naming a bad one. Takes a keyword list or a map.

Types

t()

@type t() :: %Normandy.Tools.Memory{
  max_cas_retries: non_neg_integer(),
  max_document_bytes: pos_integer(),
  max_namespace_bytes: pos_integer() | nil,
  max_namespace_documents: pos_integer() | nil,
  max_view_chars: pos_integer(),
  mounts: %{required(String.t()) => {String.t(), :read_only | :read_write}},
  namespace_key: atom(),
  store: {module(), term()},
  telemetry_include_paths: boolean()
}

Functions

check_context!(tool, values)

@spec check_context!(t(), map()) :: :ok

Refuses a tool_context whose session namespace equals a mount's namespace, when the agent is built. A context without the namespace passes: one can be supplied per call (Normandy.A2A.Server's :tool_context), and run/2 checks it there.

new(opts)

@spec new(keyword() | map()) :: t()

Builds the tool, validating every option; raises ArgumentError naming a bad one. Takes a keyword list or a map.

Options

  • :store ({module, handle}) - Required. The Normandy.Behaviours.MemoryStore module and its handle.

  • :namespace_key (atom/0) - The tool_context key holding the session namespace. The default value is :memory_namespace.

  • :mounts (%{path => {namespace, :read_only | :read_write}}) - Shared directories mounted under /memories. The default value is %{}.

  • :max_document_bytes (pos_integer/0) - Largest document. The default value is 100000.

  • :max_namespace_bytes (pos_integer/0 or nil) - Total content bytes of a namespace; nil = no cap. The default value is 1000000.

  • :max_namespace_documents (pos_integer/0 or nil) - Documents in a namespace; nil = no cap. The default value is 1000.

  • :max_view_chars (pos_integer/0) - Longest view result. The default value is 16000.

  • :max_cas_retries (non_neg_integer/0) - Compare-and-set retries of a write. The default value is 3.

  • :telemetry_include_paths (boolean/0) - Put the namespace and path in telemetry metadata. The default value is false.