# Memory Contracts
Jidoka memory is a small set of data contracts and a single `Jidoka.Memory.Store`
behaviour. The agent spec declares a memory policy; the runtime turns that
policy into recall and write requests; a pluggable store answers those
requests. This guide documents each struct and the store interface so that
custom stores (Postgres, Redis, vector DB, etc.) can interoperate without
guesswork.
## When To Use This
- Use this guide when you need the exact shape of a memory entry, recall, or
write to build a store, replay tool, or audit query.
- Use this guide when wiring `Jidoka.Memory.Store.InMemory` into tests.
- Do not use this guide as a memory tutorial. The high-level workflow lives in
[Runtime And Harness](runtime-and-harness.md).
## Prerequisites
- You have read [Agent Spec Contract](agent-spec-contract.md), in particular
`Jidoka.Agent.Spec.Memory`.
- You can build and run a Jidoka turn.
## Quick Example
A complete memory round-trip uses the in-memory store and the three core
contracts: `WriteRequest`, `RecallRequest`, `RecallResult`.
```elixir
alias Jidoka.Memory.{Entry, RecallRequest, WriteRequest, Store}
{:ok, pid} = Jidoka.Memory.Store.InMemory.start_link([])
store = {Jidoka.Memory.Store.InMemory, pid: pid}
entry = Entry.new!(agent_id: "time_agent", content: "User prefers Chicago time")
{:ok, _write} = Store.write(store, WriteRequest.new!(entry: entry))
request =
RecallRequest.new!(
agent_id: "time_agent",
scope: :agent,
query: "preferred timezone",
limit: 3
)
{:ok, recall} = Store.recall(store, request)
length(recall.entries)
#=> 1
```
## Concepts
```diagram
╭──────────────────╮ ╭───────────────────╮ ╭──────────────────╮
│ Spec.Memory │────▶│ Runtime │────▶│ Memory.Store │
│ (policy) │ │ assembles recall │ │ (behaviour) │
╰──────────────────╯ ╰─────────┬─────────╯ ╰────────┬─────────╯
▼ ▼
╭──────────────────╮ ╭──────────────────╮
│ RecallRequest │ │ RecallResult │
│ WriteRequest │ │ WriteResult │
╰──────────────────╯ ╰──────────────────╯
```
`Spec.Memory` is policy (definition data). Stores are supplied per run through
harness options. The runtime negotiates the conversation by emitting
`RecallRequest` and `WriteRequest` structs and reading `RecallResult` /
`WriteResult` back.
## Fields
### `Jidoka.Memory.Entry`
Durable memory entry available to prompt assembly.
| Field | Type | Default | Purpose |
| --- | --- | --- | --- |
| `id` | non-empty string | generated `"mem_…"` | Stable id for upsert/dedupe. |
| `agent_id` | non-empty string | required | Owning agent (matches `Spec.id`). |
| `session_id` | non-empty string or `nil` | `nil` | Session scope when applicable. |
| `content` | non-empty string | required | Content injected into the prompt or context. |
| `metadata` | map | `%{}` | Arbitrary caller metadata. |
### `Jidoka.Memory.RecallRequest`
Request sent to a store before prompt assembly.
| Field | Type | Default | Purpose |
| --- | --- | --- | --- |
| `agent_id` | non-empty string | required | Whose memory to read. |
| `session_id` | non-empty string or `nil` | `nil` | Session scope for `:session` policies. |
| `scope` | `:agent \| :session` | `:agent` | Mirrors `Spec.Memory.scope`. |
| `query` | non-empty string | required | Query used by the store (free text, embedding key, etc.). |
| `limit` | positive integer | `5` | Maximum entries to return. |
| `metadata` | map | `%{}` | Caller metadata for tracing or routing. |
### `Jidoka.Memory.RecallResult`
Result returned by `Memory.Store.recall/2`.
| Field | Type | Default | Purpose |
| --- | --- | --- | --- |
| `request` | `RecallRequest.t()` | required | The original recall request (echoed for trace clarity). |
| `entries` | `[Entry.t()]` | `[]` | Recalled entries in store-defined order. |
| `metadata` | map | `%{}` | Store metadata (similarity scores, latency, etc.). |
### `Jidoka.Memory.WriteRequest`
Request to upsert one entry.
| Field | Type | Default | Purpose |
| --- | --- | --- | --- |
| `entry` | `Entry.t()` | required | Entry to persist. |
| `metadata` | map | `%{}` | Write-specific metadata. |
### `Jidoka.Memory.WriteResult`
Acknowledgement returned by `Memory.Store.write/2`.
| Field | Type | Default | Purpose |
| --- | --- | --- | --- |
| `request` | `WriteRequest.t()` | required | The original write request. |
| `entry` | `Entry.t()` | required | The (possibly normalized) entry as stored. |
| `status` | `:ok` | `:ok` | Reserved for future statuses; today always `:ok`. |
| `metadata` | map | `%{}` | Store metadata. |
### `Jidoka.Memory.Store` Behaviour
Three callbacks define the store contract. A store is a module or
`{module, opts}` tuple.
| Callback | Purpose |
| --- | --- |
| `recall(RecallRequest.t(), opts) :: {:ok, RecallResult.t()} \| {:error, term()}` | Read entries matching the request. |
| `write(WriteRequest.t(), opts) :: {:ok, WriteResult.t()} \| {:error, term()}` | Upsert one entry. |
| `list_entries(opts) :: {:ok, [Entry.t()]} \| {:error, term()}` | Diagnostic listing for tests and inspectors. |
Top-level helpers `Memory.Store.recall/2`, `Memory.Store.write/2`, and
`Memory.Store.list_entries/1` normalize the `{module, opts}` shape before
dispatching.
## Common Patterns
- **Reuse `Entry.new!/1` to generate ids.** The default `"mem_…"` id is enough
for most stores; supply your own only when integrating with an external
primary key.
- **Carry routing data in `metadata`.** Stores should ignore unknown keys, so
feel free to thread tenant ids, embedding model names, or trace ids through
any of the request/result maps.
- **Use scope to gate session-only memory.** `RecallRequest.scope: :session`
with a `session_id` lets a store filter cross-session data without app code.
- **Keep stores small.** Implementing the three callbacks is enough; the
runtime handles policy, capture, and injection.
## Testing
The in-memory store is the canonical test fixture. It exercises the full
contract without touching disk or network.
```elixir
setup do
{:ok, pid} = Jidoka.Memory.Store.InMemory.start_link([])
{:ok, store: {Jidoka.Memory.Store.InMemory, pid: pid}}
end
test "writes and recalls a single entry", %{store: store} do
entry = Jidoka.Memory.Entry.new!(agent_id: "demo", content: "hello")
assert {:ok, _} =
Jidoka.Memory.Store.write(store,
Jidoka.Memory.WriteRequest.new!(entry: entry)
)
request =
Jidoka.Memory.RecallRequest.new!(
agent_id: "demo",
query: "hello",
limit: 5
)
assert {:ok, recall} = Jidoka.Memory.Store.recall(store, request)
assert [%{content: "hello"}] = recall.entries
end
```
## Troubleshooting
| Symptom | Likely Cause | Fix |
| --- | --- | --- |
| `ArgumentError: invalid memory entry: ...` | `agent_id` or `content` was empty. | Both are required non-empty strings. |
| `Recall returns no entries` despite writes | Scope/session_id mismatch. | Use `scope: :agent` for shared memory; provide a matching `session_id` for session scope. |
| `in-memory memory store requires :pid` | Used `Memory.Store.InMemory` without the `pid:` opt. | Pass `{Jidoka.Memory.Store.InMemory, pid: pid}` as the store value. |
| `Recall ignores limit` | A custom store did not honor `RecallRequest.limit`. | Truncate inside the store; the runtime trusts the result. |
## Reference
- [`Jidoka.Memory`](`Jidoka.Memory`) - public type aliases.
- [`Jidoka.Memory.Entry`](`Jidoka.Memory.Entry`)
- [`Jidoka.Memory.RecallRequest`](`Jidoka.Memory.RecallRequest`)
- [`Jidoka.Memory.RecallResult`](`Jidoka.Memory.RecallResult`)
- [`Jidoka.Memory.WriteRequest`](`Jidoka.Memory.WriteRequest`)
- [`Jidoka.Memory.WriteResult`](`Jidoka.Memory.WriteResult`)
- [`Jidoka.Memory.Store`](`Jidoka.Memory.Store`) - behaviour.
- [`Jidoka.Memory.Store.InMemory`](`Jidoka.Memory.Store.InMemory`) -
reference store.
- [`Jidoka.Agent.Spec.Memory`](`Jidoka.Agent.Spec.Memory`) - policy that
drives recall.
## Related Guides
- [Agent Spec Contract](agent-spec-contract.md) - memory policy lives on the
spec.
- [Runtime And Harness](runtime-and-harness.md) - how memory is wired into a
turn.
- [Turn And Effect Contracts](turn-and-effect-contracts.md) - where recall
results land on `Turn.State`.