# Memory

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](getting-started.md)).
- Familiarity with the operation contract from
  [Tools And Operations](tools-and-operations.md).
- A provider key in scope for live examples.

```bash
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.

```elixir
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.content
```

The 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.

1. **`Jidoka.Agent.Spec.Memory`** is the policy declared on the spec. It is
   the only memory data that lives on the spec itself: `enabled`, `scope`
   (`:agent` or `:session`), `namespace`, `capture`
   (`:manual | :conversation | :off`), `inject` (`:instructions | :context`),
   `max_entries`, and free-form `metadata`.
2. **`Jidoka.Memory.Store`** is 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 the `memory_store:`
   option.
3. **Request/result data** is a small set of structs:
   `Jidoka.Memory.RecallRequest`, `Jidoka.Memory.RecallResult`,
   `Jidoka.Memory.WriteRequest`, `Jidoka.Memory.WriteResult`, and
   `Jidoka.Memory.Entry`.

```diagram
╭──────────────╮     ╭───────────────────╮     ╭──────────────────╮
│  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: :agent` returns any entry tagged with the agent id, ignoring
  session.
- `scope: :session` returns only entries whose `session_id` matches the
  current `session_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 explicit
  `Jidoka.Memory.write/3` calls.
- `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`:

```elixir
agent :support_agent do
  model "openai:gpt-4o-mini"
  instructions "Answer support questions tersely."
  memory scope: :session, capture: :conversation, max_entries: 8
end
```

`memory 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.

```elixir
{: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.

```elixir
{: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`:

```elixir
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:

```elixir
{:ok, preflight} =
  Jidoka.preflight(MyApp.MemoryAgent, "hello",
    memory_store: store,
    session_id: "conv-1"
  )

preflight.prompt.messages
```

With `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`:

```elixir
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
end
```

Pass 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: :session` for chat experiences.** It keeps unrelated
  conversations from leaking facts into each other.
- **Set `max_entries` deliberately.** The recall limit caps tokens; the
  default of `5` is small.
- **Pair `capture: :conversation` with `Jidoka.Session`.** Manual writes are
  fine for direct `turn/3` callers, 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 supply `context: %{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.

```elixir
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
end
```

To 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`](`Jidoka.Agent.Spec.Memory`) - memory policy
  fields, scopes, captures, injects.
- [`Jidoka.Memory`](`Jidoka.Memory`) - type aliases for the request/result
  structs.
- [`Jidoka.Memory.Store`](`Jidoka.Memory.Store`) - behaviour and
  `recall/2 | write/2 | list_entries/1` delegators.
- [`Jidoka.Memory.Store.InMemory`](`Jidoka.Memory.Store.InMemory`) -
  deterministic test store.
- [`Jidoka.Memory.RecallRequest`](`Jidoka.Memory.RecallRequest`) and
  [`Jidoka.Memory.RecallResult`](`Jidoka.Memory.RecallResult`) - recall data
  contract.
- [`Jidoka.Memory.WriteRequest`](`Jidoka.Memory.WriteRequest`) and
  [`Jidoka.Memory.WriteResult`](`Jidoka.Memory.WriteResult`) - write data
  contract.
- [`Jidoka.Memory.Entry`](`Jidoka.Memory.Entry`) - the entry struct stored
  per memory.
- [`Jidoka.Memory`](`Jidoka.Memory`) - public recall,
  write, and `capture_turn/4` helpers used by the runtime.

## Related Guides

- [Agent DSL](agent-dsl.md) - the `memory` block syntax and validation.
- [Tools And Operations](tools-and-operations.md) - operation contract used
  by tools that also write memory.
- [Inspection And Preflight](inspection-and-preflight.md) - how to see
  exactly what memory contributes to the assembled prompt.
- [Runtime And Harness](runtime-and-harness.md) - sessions, snapshots, and
  how capture interacts with hibernation.
