Memory lets an agent recall facts across turns. Declare memory on the agent, pass a memory store at runtime, and Jidoka injects matching entries into the prompt.
Use This When
- adding short-term conversation memory to an agent;
- integrating a custom memory store (Postgres, Redis, Jido.Memory, etc.).
- debugging "the agent does not remember what I told it".
Prerequisites
- A working Jidoka agent module (see Getting Started).
- Familiarity with the operation contract from Tools And Operations.
- A provider key in scope for live examples.
mix deps.get
mix test
Add Session Memory
Start the bundled in-memory store, declare memory on the agent, and pass
memory_store: when running the turn.
defmodule MyApp.MemoryAgent do
use Jidoka.Agent
agent :memory_agent do
model "openai:gpt-4o-mini"
instructions "Greet the user and use any remembered facts."
memory scope: :session, capture: :conversation, max_entries: 5
end
end
{:ok, store_pid} = Jidoka.Memory.Store.InMemory.start_link([])
store = {Jidoka.Memory.Store.InMemory, pid: store_pid}
{:ok, _write} =
Jidoka.Memory.write(
MyApp.MemoryAgent.spec(),
"User prefers the name Alex.",
memory_store: store,
session_id: "conv-1"
)
{:ok, result} =
Jidoka.turn(MyApp.MemoryAgent, "hello",
memory_store: store,
session_id: "conv-1"
)
result.contentThe recall happened before the prompt was assembled. The entry was injected
into the agent's instructions section (the default for
inject: :instructions).
Concepts
Memory in Jidoka is three pieces of data and one boundary.
Jidoka.Agent.Spec.Memoryis the policy declared on the spec. It is the only memory data that lives on the spec itself:enabled,scope(:agentor:session),namespace,capture(:manual | :conversation | :off),inject(:instructions | :context),max_entries, and free-formmetadata.Jidoka.Memory.Storeis the behaviour each store implements:recall/2,write/2,list_entries/1. Stores are runtime data, not spec data; callers supply them per turn through thememory_store:option.- Request/result data is a small set of structs:
Jidoka.Memory.RecallRequest,Jidoka.Memory.RecallResult,Jidoka.Memory.WriteRequest,Jidoka.Memory.WriteResult, andJidoka.Memory.Entry.
╭──────────────╮ ╭───────────────────╮ ╭──────────────────╮
│ Turn input │────▶│ Memory.Runtime │────▶│ Memory.Store │
│ (request) │ │ .recall(spec, req)│ │ .recall(req) │
╰──────────────╯ ╰─────────┬─────────╯ ╰────────┬─────────╯
│ │
▼ ▼
╭──────────────────╮ ╭──────────────────╮
│ RecallResult │◀────│ matching entries │
╰────────┬─────────╯ ╰──────────────────╯
│
▼
╭───────────────────╮ ╭──────────────────╮
│ Steps.assemble │────▶│ Prompt with │
│ _prompt/1 │ │ memory injected │
╰────────┬──────────╯ ╰──────────────────╯
│
▼
╭───────────────────╮ ╭──────────────────╮
│ LLM + tools loop │────▶│ Memory.Runtime │
╰───────────────────╯ │ .capture_turn/4 │
╰─────────┬────────╯
│
▼
╭──────────────────╮
│ WriteResult │
╰──────────────────╯Jidoka.Memory is the public helper that knows how to translate
between the spec policy, the per-turn options, and the store. Applications
talk to the store through Jidoka.Memory.Store directly (or through the
runtime helper used in tests).
Scope And Session
scope: :agentreturns any entry tagged with the agent id, ignoring session.scope: :sessionreturns only entries whosesession_idmatches the currentsession_id:option. Pass the option through both write and recall.
The InMemory store enforces these rules in matches_request?/2. Session
filtering is exact-match; there is no fuzzy fallback.
Capture Modes
capture: :manual(default) - memory only changes through explicitJidoka.Memory.write/3calls.capture: :conversation- after every successful turn, Jidoka writes"User: ...\nAssistant: ..."to the store.capture: :off- the runtime never writes; useful when memory is populated by another system.
How To
Step 1: Declare The Memory Policy
The DSL accepts a keyword/map equivalent of Jidoka.Agent.Spec.Memory:
agent :support_agent do
model "openai:gpt-4o-mini"
instructions "Answer support questions tersely."
memory scope: :session, capture: :conversation, max_entries: 8
endmemory true enables defaults. memory false disables memory. Anything
else is parsed as a memory policy map.
Step 2: Start An InMemory Store For Tests
Jidoka.Memory.Store.InMemory is an Agent process keyed by :pid. The
test process keeps its lifetime bounded.
{:ok, pid} = Jidoka.Memory.Store.InMemory.start_link([])
store = {Jidoka.Memory.Store.InMemory, pid: pid}The two-tuple form {module, opts} is the standard store input; pass it
wherever a Jidoka.Memory.Store.store() is required.
Step 3: Write An Entry Manually
Jidoka.Memory.write/3 builds the Memory.Entry, applies the spec
policy (scope, namespace), and forwards to the store.
{:ok, %Jidoka.Memory.WriteResult{entry: entry}} =
Jidoka.Memory.write(
MyApp.MemoryAgent.spec(),
"User prefers the name Alex.",
memory_store: store,
session_id: "conv-1",
metadata: %{"kind" => :preference}
)
entry.agent_id
#=> "memory_agent"
entry.session_id
#=> "conv-1"The store you supplied receives a Jidoka.Memory.WriteRequest whose
:entry is the validated Memory.Entry struct.
Step 4: Recall Through The Store Directly
You can also bypass the runtime helper and talk to Jidoka.Memory.Store:
request =
Jidoka.Memory.RecallRequest.new!(
agent_id: "memory_agent",
session_id: "conv-1",
scope: :session,
query: "hello",
limit: 5
)
{:ok, %Jidoka.Memory.RecallResult{entries: entries}} =
Jidoka.Memory.Store.recall(store, request)
Enum.map(entries, & &1.content)
#=> ["User prefers the name Alex."]This is what Jidoka.Memory.recall/3 does when Jidoka assembles the prompt
for a turn.
Step 5: Inspect Memory Injection With Preflight
Before running a live turn, confirm the recalled entries are landing in the prompt:
{:ok, preflight} =
Jidoka.preflight(MyApp.MemoryAgent, "hello",
memory_store: store,
session_id: "conv-1"
)
preflight.prompt.messagesWith inject: :instructions (the default) the recalled content is appended
to the system message. With inject: :context it is added as structured
context the prompt assembler renders separately.
Step 6: Implement A Custom Store
A custom store is one module that implements Jidoka.Memory.Store:
defmodule MyApp.MapStore do
@behaviour Jidoka.Memory.Store
alias Jidoka.Memory.Entry
alias Jidoka.Memory.RecallRequest
alias Jidoka.Memory.RecallResult
alias Jidoka.Memory.WriteRequest
alias Jidoka.Memory.WriteResult
def start_link(_opts), do: Agent.start_link(fn -> [] end)
@impl true
def recall(%RecallRequest{} = request, opts) do
pid = Keyword.fetch!(opts, :pid)
entries = pid |> Agent.get(& &1) |> Enum.take(request.limit)
RecallResult.new(request: request, entries: entries)
end
@impl true
def write(%WriteRequest{entry: %Entry{} = entry} = request, opts) do
pid = Keyword.fetch!(opts, :pid)
Agent.update(pid, &[entry | &1])
WriteResult.new(request: request, entry: entry)
end
@impl true
def list_entries(opts) do
pid = Keyword.fetch!(opts, :pid)
{:ok, pid |> Agent.get(& &1) |> Enum.reverse()}
end
endPass it as {MyApp.MapStore, pid: pid} to memory_store:. Stores must
return {:ok, result} or {:error, reason} and must never raise on missing
entries; an empty RecallResult is the normal "nothing matched" answer.
Common Patterns
- Default to
scope: :sessionfor chat experiences. It keeps unrelated conversations from leaking facts into each other. - Set
max_entriesdeliberately. The recall limit caps tokens; the default of5is small. - Pair
capture: :conversationwithJidoka.Session. Manual writes are fine for directturn/3callers, but multi-turn applications benefit from automatic capture once a session id is in scope. - Namespace memory per tenant. Use
namespace: {:context, :tenant_id}and supplycontext: %{tenant_id: "..."}per turn; the runtime resolves the namespace before talking to the store. - Treat the store as data. A
{module, opts}tuple is the same kind of capability as the LLM function or operations function. Inject it; do not hard-code it in the agent.
Testing
A complete memory test exercises write -> recall -> prompt assembly without calling a provider. The InMemory store and a fake LLM are all that is needed.
defmodule MyApp.MemoryAgentTest do
use ExUnit.Case, async: true
setup do
{:ok, pid} = Jidoka.Memory.Store.InMemory.start_link([])
{:ok, store: {Jidoka.Memory.Store.InMemory, pid: pid}}
end
test "recalls a remembered preference", %{store: store} do
{:ok, _write} =
Jidoka.Memory.write(
MyApp.MemoryAgent.spec(),
"User prefers the name Alex.",
memory_store: store,
session_id: "conv-1"
)
{:ok, preflight} =
Jidoka.preflight(MyApp.MemoryAgent, "hello",
memory_store: store,
session_id: "conv-1"
)
system_message = Enum.find(preflight.prompt.messages, &(&1.role == :system))
assert system_message.content =~ "prefers the name Alex"
llm = fn _intent, _journal ->
{:ok, %{type: :final, content: "Welcome back, Alex."}}
end
assert {:ok, result} =
Jidoka.turn(MyApp.MemoryAgent, "hello",
llm: llm,
memory_store: store,
session_id: "conv-1"
)
assert result.content =~ "Alex"
end
endTo assert the conversation capture path, run the turn with
capture: :conversation and read Jidoka.Memory.Store.list_entries/1
afterwards. The latest entry should contain both the user input and the
assistant content.
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
Jidoka.Memory.recall/3 returns {:ok, nil} | The spec has no memory policy or it is disabled. | Add memory ... to the DSL, or pass a non-false memory policy when building the spec from data. |
| Memory entries are written but never returned | Mismatched scope or session_id. | Use the same session_id on both write and recall; for scope: :agent, pass no session id. |
in-memory memory store requires :pid | The InMemory store was started but the :pid opt was not threaded through. | Pass {Jidoka.Memory.Store.InMemory, pid: pid} to memory_store:. |
{:error, :missing_memory_store} from manual writes | Jidoka.Memory.write/3 was called without memory_store:. | Supply the store explicitly; the runtime does not pull from application config. |
| Entries land in the prompt twice | A manual write/3 plus capture: :conversation recorded the same content. | Pick one capture mode per agent or filter on metadata["source"]. |
Reference
Jidoka.Agent.Spec.Memory- memory policy fields, scopes, captures, injects.Jidoka.Memory- type aliases for the request/result structs.Jidoka.Memory.Store- behaviour andrecall/2 | write/2 | list_entries/1delegators.Jidoka.Memory.Store.InMemory- deterministic test store.Jidoka.Memory.RecallRequestandJidoka.Memory.RecallResult- recall data contract.Jidoka.Memory.WriteRequestandJidoka.Memory.WriteResult- write data contract.Jidoka.Memory.Entry- the entry struct stored per memory.Jidoka.Memory- public recall, write, andcapture_turn/4helpers used by the runtime.
Related Guides
- Agent DSL - the
memoryblock syntax and validation. - Tools And Operations - operation contract used by tools that also write memory.
- Inspection And Preflight - how to see exactly what memory contributes to the assembled prompt.
- Runtime And Harness - sessions, snapshots, and how capture interacts with hibernation.