# Memory

> Give agents memory that outlives the conversation: facts recalled by meaning, JSON documents recalled by key, scoped to the project, a contact or a thread.

Memory is what an agent learns while it runs and needs again later: that this
customer prefers WhatsApp, the order it looked up an hour ago, the address
someone gave on Tuesday. It lives in Zavu, so an agent, a Function, your
backend and the dashboard all read the same store.

It holds two shapes:

| You recall it by | Use | How it works |
|---|---|---|
| **Meaning**: "how do they like to be contacted?" | **Facts** | Text is embedded when written; search ranks by similarity |
| **Key**: "order ORD-12345" | **Collections** | JSON documents, exact lookup by key, optional indexed fields |

> **Note:**
> Memory is not the [knowledge base](https://www.zavu.dev/docs/guides/ai-agents/knowledge-base) and not the
> conversation history. A knowledge base holds documents you write up front. The
> last messages of a thread are already in the agent's context. Memory is for
> what should outlive both.

## Scope: the one thing to get right

Every read and every write happens in exactly one scope, and scopes are
isolated: a search in one never returns a fact from another.

| Scope | Holds | Fact cap |
|---|---|---|
| `project` | What the agent knows in general, shared by every conversation | 10,000 (5,000 on Free) |
| `contact` | What it knows about one person, across every thread with them | 2,000 per contact |
| `conversation` | What it knows about one inbox thread | 1,000 per thread |

Everything defaults to `project`. That is the mistake to avoid: a fact about one
customer written at project scope can be recalled in every other customer's
conversation. File it against the person it is about.

## Let the agent remember on its own

The fastest way to give an agent memory needs no code. On the dashboard's
**Memory** screen, **Add memory to an agent** gives it `remember` and `recall`
together: one without the other half-works, writing facts nothing reads or
reading facts nothing writes. Each of these library skills can also be added on
its own from the agent's **Tools** tab, under **Library**:

| Skill | What it does |
|---|---|
| `remember` | Saves one fact about the person the agent is talking to |
| `recall` | Searches what was remembered before answering |
| `save_record` | Stores a JSON record under a key, such as an order or a reservation |
| `lookup_record` | Reads a record back by its key |

How they behave, so you can predict what the agent will do:

- `remember` writes to the **contact** scope only. If Zavu has not identified
  who is writing, it refuses instead of falling back to the shared project
  scope.
- `recall` searches the contact first and then the project. In the project
  scope it skips facts that an agent wrote, so what one customer said is never
  read back to another. Facts you seed yourself at project scope (your business
  rules, opening hours) are recalled for everyone. Hits below a similarity of
  0.3 are not returned, so "nothing on record" is a real answer.
- `save_record` and `lookup_record` use the **contact** scope by default. Set
  their scope to `project` only for records that are not personal: anyone who
  talks to the agent can then ask for any record by its key.

The **Memory** screen in the dashboard shows what was stored, per scope, and
lets you search and delete it.

## In a Function: `ctx.memory`

In code, `ctx.memory` is available to every tool handler and event handler.
It needs `@zavudev/functions` 0.3.0 or later.

```ts
import { defineTool } from "@zavudev/functions"

defineTool({
  name: "remember_preference",
  description: "Save something the customer told us about how they want to be served.",
  parameters: {
    type: "object",
    properties: { fact: { type: "string", description: "One fact, in plain language." } },
    required: ["fact"],
  },
  handler: async ({ fact }, ctx) => {
    // ctx.memory.contact is bound to whoever the agent is talking to.
    if (!ctx.memory.contact) return { ok: false, reason: "no contact on this call" }
    await ctx.memory.contact.add(fact, { metadata: { source: "customer" } })
    return { ok: true }
  },
})
```

Recall before answering:

```ts
const hits = await ctx.memory.contact!.search("how they prefer to be contacted", {
  limit: 3,
  minScore: 0.3,
})
// [{ id, text, score, metadata?, createdAt }]
```

The scope handles:

| Handle | Scope | Present |
|---|---|---|
| `ctx.memory` | project | Always |
| `ctx.memory.contact` | the contact being talked to | Only inside a conversation. Undefined in an event or cron handler |
| `ctx.memory.conversation` | the current thread | Same as `contact` |
| `ctx.memory.forContact(id)` | a contact you name | Always |
| `ctx.memory.forConversation(id)` | a thread you name | Always |

`contact` and `conversation` are optional, so TypeScript makes you handle a
missing one. `ctx.memory.add(text, { scope: "contact" })` throws
`MemoryScopeError` when there is no contact; that error means the scope could
not be resolved, never that it is empty. An empty scope returns an empty array.

### Collections

```ts
const orders = ctx.memory.contact!.collection("orders")

await orders.set("ORD-12345", { status: "shipped", total: 42.5 })
const order = await orders.get<{ status: string }>("ORD-12345") // null if absent
await orders.delete("ORD-12345") // resolves true even if absent

const page = await orders.list({ prefix: "ORD-2026", limit: 50 })
// { items: [{ key, value, rev, createdAt, updatedAt }], nextCursor }
```

A collection is defined once per project, and its items are per scope: the same
`orders` collection holds a separate set for the project, for each contact and
for each thread.

**Keys** are 1 to 255 characters from `A-Z a-z 0-9 . _ : @ -`. A `+` is not
allowed, so an E.164 phone number is not a valid key: use its digits
(`56940560201`) or a prefix (`phone:56940560201`), and keep the E.164 inside the
value.

**Querying by a field needs an index, and indexes are declared when the
collection is created.** Writing to a collection that does not exist creates it
with no indexes, and indexes cannot be added later. If you will ever look items
up by a field, create the collection first:

```bash
npx zavudev memory collections create orders --index status
```

```ts
const shipped = await orders.query({ field: "status", op: "eq", value: "shipped" })
```

A collection takes up to 2 indexes. Querying a field without one fails with
`field_not_indexed`.

### Testing locally

`createLocalContext()` gives you a `ctx.memory` backed by an in-process stub, so
a tool runs without network:

```ts
import { createLocalContext } from "@zavudev/functions"

const { memory } = createLocalContext({ contactId: "test-contact" })
```

The stub is not a simulator. Its `search` matches substrings, not meaning, and
scores every hit 1, so `minScore` does nothing locally. Its `query` answers any
field, because it has no indexes. Use it to check that a tool calls the right
scope and handles a missing contact; check retrieval against the real API with a
test-mode key.

## From your backend

The [TypeScript SDK](https://www.zavu.dev/docs/sdks/typescript/overview) has `client.memory`, and every
operation is also plain REST.

```ts
import Zavudev from "@zavudev/sdk"

const zavu = new Zavudev({ apiKey: process.env.ZAVUDEV_API_KEY })

await zavu.memory.add({
  text: "Prefers WhatsApp over email.",
  scope: "contact",
  contactId: "jd7x2k3m4n5p6q7r8s9t0abc",
})

const { items } = await zavu.memory.search({
  query: "how they prefer to be contacted",
  scope: "contact",
  contactId: "jd7x2k3m4n5p6q7r8s9t0abc",
  minScore: 0.3,
})

await zavu.memory.collections.items.put("ORD-12345", {
  collection: "orders",
  scope: "contact:jd7x2k3m4n5p6q7r8s9t0abc",
  value: { status: "shipped", total: 42.5 },
})
```

The scope is passed differently on two endpoints. `POST /v1/memory` and
`POST /v1/memory/search` take it in the **body**, as `scope` plus `contactId` or
`conversationId`. Every other endpoint takes it as a **`scope` query
parameter**: `contact:<id>`, `conversation:<id>`, or omitted for the project.
Reading an item with a different scope than it was written with returns `404`.

```bash
curl -X POST https://api.zavu.dev/v1/memory/search \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"query": "contact preference", "scope": "contact", "contactId": "jd7x2k3m4n5p6q7r8s9t0abc", "minScore": 0.3}'

curl "https://api.zavu.dev/v1/memory/collections/orders/items/ORD-12345?scope=contact:jd7x2k3m4n5p6q7r8s9t0abc" \
  -H "Authorization: Bearer $ZAVUDEV_API_KEY"
```

To make a write conditional, send the item's `rev` from a previous read as
`If-Match`: a write that lost a race answers `409 revision_conflict`. Every
endpoint is in the [API reference](https://www.zavu.dev/docs/api-reference#memory).

## From the CLI

Every command takes `--scope project` (the default), `--scope contact:<id>` or
`--scope conversation:<id>`.

```bash
npx zavudev memory add "Orders before 2pm ship the same day."
npx zavudev memory search "shipping cutoff" --limit 5 --min-score 0.3
npx zavudev memory list --scope contact:jd7x2k3m4n5p6q7r8s9t0abc
npx zavudev memory wipe --scope conversation:<id>

npx zavudev memory collections create orders --index status --default-ttl 2592000
npx zavudev memory collection set orders ORD-1 '{"status":"shipped"}'
npx zavudev memory collection query orders --field status --eq shipped
npx zavudev memory collection export orders > backup.ndjson

npx zavudev memory usage
```

## Writing facts an agent can find

- **One fact per write.** A paragraph with five unrelated facts is embedded as an
  average of all five and found well by none of them.
- **Write it the way it will be asked for.** "Prefers to be called in the
  morning" is found by "when should I call them". "pref: AM" is found by
  nothing.
- **Set `minScore` when your code branches on the result.** Without it, a search
  always returns its closest facts however distant, so "nothing relevant" looks
  the same as "the right fact". `0.3` is a reasonable floor.
- **`metadata` is not searchable.** Search matches the text; metadata comes back
  with each hit.
- **Give temporary facts a TTL.** `ttlSeconds` takes 60 seconds to 2 years. A
  fact without one is kept until deleted and counts against its scope's cap.
- **Do not remember what you can look up.** An order status belongs in a
  collection or in your own system, not in a fact.

## Limits

| Limit | Value |
|---|---|
| Fact text | 8 KB |
| Fact metadata | 2 KB (keys up to 64 characters, values up to 512) |
| Search results | 5 by default, 20 at most |
| Collection document | 200 KB serialized, 32 levels deep |
| Indexes per collection | 2, fixed at creation |
| Batch operations | 25 per call, each reported on its own |
| TTL | 60 seconds to 2 years |

| Error | Means |
|---|---|
| `400 field_not_indexed` | The field has no index, or the collection was created by a write and has none |
| `404` | Nothing under that id or key **in that scope**. Check you passed the scope it was written with |
| `409 revision_conflict` | An `If-Match` write lost a race. Read again and retry |
| `413 document_too_large` | Over 200 KB. Keep media in file storage and store its URL |
| `429 memory_scope_full` | The scope reached its fact cap. Delete facts or use a narrower scope |
| `429 memory_limit_exceeded` | The month's Memory units on Free are used up |

Reads and deletes are never refused for quota reasons, only writes: what you
stored stays readable and removable.

## Test mode and billing

A **test-mode API key** writes to a separate partition that is invisible to live
keys, deletes itself after 30 days, and is never counted or billed.

Live usage is measured in **units**, from the capacity each request consumes; a
write costs 5 times a read. `npx zavudev memory usage` and `GET /v1/memory/usage`
report the month's units, the bytes stored and whether a cap is reached.

- **Free** includes 250,000 units a month, 0.5 GB, 10 collections and 5,000
  project facts. Past the units, writes are refused until the next month.
- **Paid plans** include more (Pro: 2,000,000 units, 5 GB, 100 collections).
  Units past the included amount are billed from your balance at $0.50 per
  100,000. See [pricing](https://www.zavu.dev/en/pricing).

## Next steps

  - [Defining tools](https://www.zavu.dev/docs/guides/functions/defining-tools): The `defineTool` surface `ctx.memory` lives on
  - [Knowledge base](https://www.zavu.dev/docs/guides/ai-agents/knowledge-base): Documents you write up front, versus what the agent learns
  - [Tool library](https://www.zavu.dev/docs/guides/ai-agents/tool-library): The memory and record skills, with no code
  - [API reference](https://www.zavu.dev/docs/api-reference#memory): Every Memory endpoint
