WeaveScope

Long-Term Memory

Long-term memory lets an agent store and recall information across different conversations, sessions, and threads. Unlike Short-Term Memory , which is conversation state persisted by a checkpointer, long-term memory is an application data store available through runtime.store and tool injection.

Use long-term memory for user preferences, profile facts, durable extracted insights, feature flags, account metadata, and other data that should survive beyond one thread.

For Deep Agents-style AGENTS.md memory files, route virtual paths through BeamWeaver.Filesystem.Store and configure memory: [...] on the agent. The overview in Memory shows agent-scoped, user-scoped, and organization-scoped memory files. This guide focuses on the direct BeamWeaver.Memory store API.

Usage

Configure a store on a module-defined agent:

defmodule MyApp.MemoryAgent do
  use BeamWeaver.Agent

  model BeamWeaver.Models.init_chat_model!("openai:gpt-5.4")
  store BeamWeaver.Memory.ETS.new()
end

Runtime-built agents use the same concept:

alias BeamWeaver.Agent

store = BeamWeaver.Memory.ETS.new()

{:ok, agent} =
  Agent.build(
    name: "memory_agent",
    model: BeamWeaver.Models.init_chat_model!("openai:gpt-5.4"),
    tools: [],
    store: store,
    context_schema: %{
      user_id: %{type: :string, required: true}
    }
  )

For durable Postgres-backed storage, use the Ecto adapter:

store = BeamWeaver.Memory.Ecto.new(repo: MyApp.Repo)

{:ok, agent} =
  BeamWeaver.Agent.build(
    name: "memory_agent",
    model: BeamWeaver.Models.init_chat_model!("anthropic:claude-sonnet-4-6"),
    tools: [],
    store: store,
    context_schema: %{user_id: %{type: :string, required: true}}
  )

Create the database table in your application migration:

defmodule MyApp.Repo.Migrations.CreateBeamWeaverMemory do
  use Ecto.Migration

  def up do
    BeamWeaver.Migrations.up(adapters: [:memory])
  end

  def down do
    BeamWeaver.Migrations.down(adapters: [:memory], version: 1)
  end
end

Memory Storage

BeamWeaver.Memory stores items by namespace and key. A namespace is a list, atom, or string normalized to string parts. A key is a string or atom. Each item contains:

Field Meaning
namespace Hierarchical path such as ["users", "user_123", "chitchat"].
key Distinct item ID within the namespace.
value Stored data. Prefer JSON-compatible maps and lists for portable stores.
metadata Extra searchable/filterable metadata.
created_at, updated_at, expires_at Adapter-managed timestamps.

Write, read, and search memory directly:

alias BeamWeaver.Memory

store = Memory.ETS.new()
user_id = "user_123"
namespace = ["users", user_id, "chitchat"]

{:ok, _item} =
  Memory.put(
    store,
    namespace,
    "a-memory",
    %{
      "rules" => [
        "User likes short, direct language",
        "User only speaks English and Elixir"
      ],
      "my-key" => "my-value"
    },
    metadata: %{"kind" => "preference"}
  )

{:ok, item} = Memory.get(store, namespace, "a-memory")

items =
  Memory.search(store, ["users", user_id],
    filter: %{"my-key" => "my-value"},
    query: "language preferences"
  )

Namespaces can be searched by prefix:

Memory.search(store, ["users", user_id], limit: 10)
Memory.list_namespaces(store, prefix: ["users"])
Memory.yield_keys(store, namespace, prefix: "a-")

BeamWeaver.Memory.ETS can maintain a simple embedding index for semantic-ish search. Provide an embedding model and the fields to index:

embedding = BeamWeaver.Models.init_embeddings!("openai:text-embedding-3-small")

store =
  BeamWeaver.Memory.ETS.new(
    index: %{
      embed: embedding,
      dims: 1_536,
      fields: ["profile.summary", "rules[*]"]
    }
  )

{:ok, _item} =
  BeamWeaver.Memory.put(
    store,
    ["users", "user_123"],
    "preferences",
    %{
      "profile" => %{"summary" => "Prefers concise technical answers"},
      "rules" => ["Use direct language", "Prefer Elixir examples"]
    }
  )

BeamWeaver.Memory.search(store, ["users", "user_123"], query: "short Elixir answer")

You can disable indexing per write:

BeamWeaver.Memory.put(store, ["users", "user_123"], "raw", %{"text" => "draft"}, index: false)

Read Long-Term Memory In Tools

Tools read long-term memory by injecting :store and, usually, :context. The model does not see injected fields.

alias BeamWeaver.Agent
alias BeamWeaver.Core.{Message, Tool}
alias BeamWeaver.Memory

store = Memory.ETS.new()

{:ok, _item} =
  Memory.put(store, ["users"], "user_123", %{
    "name" => "John Smith",
    "language" => "English"
  })

get_user_info =
  Tool.from_function!(
    name: "get_user_info",
    description: "Look up user information from long-term memory.",
    input_schema: %{
      "type" => "object",
      "properties" => %{
        "context" => %{"type" => "object"},
        "store" => %{"type" => "object"}
      },
      "required" => ["context", "store"]
    },
    injected: [context: :context, store: :store],
    handler: fn input, _opts ->
      context = input[:context] || input["context"] || %{}
      store = input[:store] || input["store"]
      user_id = context[:user_id] || context["user_id"]

      case Memory.get(store, ["users"], user_id) do
        {:ok, item} -> inspect(item.value)
        :error -> "Unknown user"
        {:error, error} -> {:error, error}
      end
    end
  )

{:ok, agent} =
  Agent.build(
    name: "reader",
    model: BeamWeaver.Models.init_chat_model!("openai:gpt-5.4"),
    tools: [get_user_info],
    store: store,
    context_schema: %{user_id: %{type: :string, required: true}}
  )

Agent.invoke(
  agent,
  %{messages: [Message.user("Look up user information.")]},
  context: %{user_id: "user_123"}
)

Write Long-Term Memory From Tools

Tools can also write memories. Keep write tools narrow: expose only the fields the model is allowed to update, and derive ownership from trusted runtime context.

alias BeamWeaver.Agent
alias BeamWeaver.Core.{Message, Tool}
alias BeamWeaver.Memory

store = Memory.ETS.new()

save_user_info =
  Tool.from_function!(
    name: "save_user_info",
    description: "Save user profile information.",
    input_schema: %{
      "type" => "object",
      "properties" => %{
        "name" => %{"type" => "string"},
        "context" => %{"type" => "object"},
        "store" => %{"type" => "object"}
      },
      "required" => ["name", "context", "store"]
    },
    injected: [context: :context, store: :store],
    handler: fn input, _opts ->
      context = input[:context] || input["context"] || %{}
      store = input[:store] || input["store"]
      user_id = context[:user_id] || context["user_id"]
      name = input[:name] || input["name"]

      {:ok, _item} =
        Memory.put(store, ["users"], user_id, %{"name" => name},
          metadata: %{"kind" => "profile"}
        )

      "Successfully saved user info."
    end
  )

{:ok, agent} =
  Agent.build(
    name: "writer",
    model: BeamWeaver.Models.init_chat_model!("openai:gpt-5.4"),
    tools: [save_user_info],
    store: store,
    context_schema: %{user_id: %{type: :string, required: true}}
  )

Agent.invoke(
  agent,
  %{messages: [Message.user("My name is John Smith.")]},
  context: %{user_id: "user_123"}
)

{:ok, item} = Memory.get(store, ["users"], "user_123")

Batch And Maintenance

Use batch operations when you need to group reads and writes:

alias BeamWeaver.Memory
alias BeamWeaver.Memory.{GetOp, ListNamespacesOp, MatchCondition, PutOp, SearchOp}

results =
  Memory.batch(store, [
    %PutOp{namespace: ["users", "user_123"], key: "prefs", value: %{"style" => "brief"}},
    %GetOp{namespace: ["users", "user_123"], key: "prefs"},
    %SearchOp{namespace: ["users"], filter: %{"style" => "brief"}},
    %ListNamespacesOp{match_conditions: [%MatchCondition{type: :prefix, path: ["users"]}]}
  ])

Stores also support TTL and retention where the adapter implements it:

{:ok, _item} =
  Memory.put(store, ["users", "user_123"], "temporary-note", %{"text" => "expires"}, ttl: 60)

{:ok, _expired_count} = Memory.sweep_expired(store)
{:ok, _pruned_count} = Memory.prune(store, namespace: ["users"], max_entries: 1_000)

Use Memory.async_put/5, Memory.async_get/4, Memory.async_search/3, and other async helpers when memory work should run through BeamWeaver's task-backed async boundary.