# Agent memory

> What an agent remembers across conversations: how memories are written, ranked, deduplicated and labelled by provenance, and how to read, seed and delete them.

A conversation has a history ([Sessions](https://dev.subsidia.protypa.fr/docs/sessions.md)); an agent has a **memory**. Memories are short notes (a decision, a preference, a fact worth keeping) that survive from one session to the next and are recalled into the prompt of later turns. They are what lets an agent say "as you told me last week, the client closes on 30 June" in a brand-new session.

Memories are private to the agent that wrote them. They are not a knowledge base: documents belong in [Knowledge sources](https://dev.subsidia.protypa.fr/docs/knowledge-sources.md), where they are retrieved, cited and signed into a proof. Memory is for what the agent learned *from the conversation*.

## How memories are written

There are two ways in.

- **The agent writes them itself.** At the end of a text answer the agent may emit up to three memory blocks (a short key, a short value, an optional type). They are stripped from what you receive, then stored. The ones saved during a turn come back in `memoriesSaved` in the chat response ([Agent chat](https://dev.subsidia.protypa.fr/docs/agent-chat.md)). This happens on every chat call unless memory is switched off. Structured JSON answers (`responseFormat.type: "json"`) do not carry memory instructions.
- **You write them through the API.** `POST /v1/agents/:id/memory` stores a fact you want the agent to know, without waiting for a conversation to produce it. See the endpoints below for how this path differs.

Every agent-written memory is checked before it is stored. Nothing is refused, but the memory is labelled with where its content came from.

### Provenance

A model that invents "revenue grew 50%" in turn three must not be able to recall it in turn nine as if it had read it in a document. So at write time Subsidia looks for the memory's checkable content (every figure, or enough of its content words) in the evidence that turn actually had, strongest source first:

| Provenance | Found in | Starting salience | How it is recalled |
| --- | --- | --- | --- |
| `knowledge` | Passages retrieved from the knowledge base this turn | 0.65 | Labelled "sourced: knowledge base". |
| `data` | Structured data injected through the `context` array | 0.65 | Labelled "sourced: workspace data". |
| `tool` | A tool result or a colleague consultation of the turn | 0.65 | Labelled "sourced: tool result". |
| `stated` | What the user said in the conversation | 0.50 | Labelled "said in conversation". |
| `unverified` | Nowhere: the agent's own claim | 0.35 | Labelled UNVERIFIED, with an instruction never to present it as established. |

A figure is the part of a memory that can be wrong in a costly way, so when a memory contains numbers, **every** number must appear in the source for it to be credited to that source. Unverified memories rank lower and fade sooner, but they are kept: an agent's own judgment is worth remembering as long as it is not mistaken for a fact. The provenance is stored in the memory's `metadata.provenance`.

### Deduplication

Models re-label the same fact on every turn (`client_year_end`, then `fiscal_closing_date`). Two protections keep the memory from filling with copies:

- A memory with the **same key** replaces the previous value.
- A memory with a **different key but the same content** is folded into the existing record: among the agent's 60 most recent memories, one whose embedding is more than 0.92 cosine-similar to the new value, or whose content words overlap by at least 80%, is treated as a twin. The longer value is kept, the new key is recorded as an alias, and the salience goes up slightly (repetition is evidence of importance, but a claim asserted without a source stays unverified).

These protections apply to memories the agent writes. Memories you create with `POST /v1/agents/:id/memory` are inserted as they are.

## How memories are recalled

Before each turn, the agent's memories are ranked against the new message and the best twelve are placed in its prompt. The score is a weighted sum: 50% semantic similarity to the message, 20% recency (a 14-day half-life, counted from the last time the memory was recalled), 20% salience, and 10% congruence with the agent's current mood. Memories without an embedding, such as those created through the API, get a neutral semantic score: they can still be recalled, but they win less often against memories that match the question closely. If the embedding service is unavailable, the agent falls back to its most recent memories.

Global memories (a fact worth sharing with every agent of the workspace, flagged by the agent when it writes it) are added on top, up to eight. They only exist for agents with sharing enabled in the console; for other agents the flag is ignored and the memory stays private.

## Turning memory off

Memory is on by default for chat. To call an agent **without** it being read or written, use the Gateway address flag `+nomemory`, for example `agent:claire+nomemory` as the model name of a chat completion ([Model addressing](https://dev.subsidia.protypa.fr/docs/model-addressing.md)). The agent then neither recalls nor stores anything for that call. This is the right setting for tests, evaluations and any caller that keeps its own conversation state.

The `/v1/agents/:id/chat` endpoints have no per-call memory switch: they always recall and may always write. Postes never read or write memory ([Postes](https://dev.subsidia.protypa.fr/docs/postes.md)).

> **INFO: Deleting an agent deletes its memory**
> `DELETE /v1/agents/:id` removes every memory of that agent with it. To forget one thing, delete that memory; to forget everything and keep the persona, delete the memories one by one.

## Endpoints

All three routes need the `agents` scope on restricted keys, and the two write routes are refused for `readonly` keys. See [Agents](https://dev.subsidia.protypa.fr/docs/agents.md#authentication-and-scopes).

### GET /v1/agents/:id/memory

List the memories of an agent.

Returns the agent's own memories (those not flagged global), as stored rows. Rows include internal fields such as the embedding vector, a list of floating-point numbers that you can ignore; drop it before logging. Useful to audit what an agent has learned, to find an UNVERIFIED claim before it spreads, or to export memory.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The agent id. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/agents/$AGENT_ID/memory \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_TypeScript_

```typescript
const res = await fetch('https://api.subsidia.protypa.fr/v1/agents/' + agentId + '/memory', {
  headers: { Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY },
})
const { memory } = await res.json()
const doubtful = memory.filter((m: any) => m.metadata?.provenance === 'unverified')
console.log(doubtful.map((m: any) => m.key + ': ' + m.value))
```

_Python_

```python
import os, requests

res = requests.get(
    "https://api.subsidia.protypa.fr/v1/agents/" + agent_id + "/memory",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
)
for m in res.json()["memory"]:
    provenance = (m.get("metadata") or {}).get("provenance", "none")
    print(m["id"], m["key"], provenance)
```

#### Responses

**200**: An object with a `memory` array, in no guaranteed order. The list is not paginated.

```json
{ "memory": [
{
  "id": "cm2kb7q4w0007qz0fm3n8c1xa",
  "agentId": "cm2k8x1ab0001qz0f7h3d9t4e",
  "workspaceId": null,
  "key": "client_fiscal_year_end",
  "value": "The client closes its books on 30 June.",
  "metadata": { "type": "fact", "provenance": "knowledge" },
  "isGlobal": false,
  "type": "fact",
  "connectedTo": [],
  "valence": null,
  "salience": 0.65,
  "embedding": [0.0121, -0.0443, "... 768 numbers"],
  "lastRecalledAt": "2026-10-09T09:02:11.402Z",
  "recallCount": 3,
  "createdAt": "2026-10-02T14:20:05.771Z",
  "updatedAt": "2026-10-09T09:02:11.402Z"
}
] }
```

**404**: No agent with this id in the workspace.

```json
{ "error": "Agent not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | Unknown agent id, or the agent belongs to another workspace. |
| 403 | `scope_denied` | A restricted key without the `agents` scope. |

### POST /v1/agents/:id/memory

Add a memory to an agent.

Stores a fact the agent should know from its next turn on. The memory is created exactly as sent: no provenance check, no deduplication against existing memories, and no embedding at creation time (so it is ranked with a neutral semantic score). It starts with the default salience of 0.5. If you call it twice with the same key you get two records.

Keep values short and factual, one idea per memory, like a note on a card. For documents, use [Knowledge sources](https://dev.subsidia.protypa.fr/docs/knowledge-sources.md) instead.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The agent id. |

#### Request body

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `key` | `string` | yes |  | Short label in snake_case, for example `client_fiscal_year_end`. |
| `value` | `string` | yes |  | The thing to remember, one or two sentences. |

#### Request examples

_curl_

```bash
curl https://api.subsidia.protypa.fr/v1/agents/$AGENT_ID/memory \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"key": "client_fiscal_year_end", "value": "The client closes its books on 30 June."}'
```

_TypeScript_

```typescript
const res = await fetch('https://api.subsidia.protypa.fr/v1/agents/' + agentId + '/memory', {
  method: 'POST',
  headers: {
    Authorization: 'Bearer ' + process.env.SUBSIDIA_API_KEY,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    key: 'client_fiscal_year_end',
    value: 'The client closes its books on 30 June.',
  }),
})
const { memory } = await res.json()
console.log(memory.id)
```

_Python_

```python
import os, requests

res = requests.post(
    "https://api.subsidia.protypa.fr/v1/agents/" + agent_id + "/memory",
    headers={"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]},
    json={"key": "client_fiscal_year_end", "value": "The client closes its books on 30 June."},
)
res.raise_for_status()
memory_id = res.json()["memory"]["id"]
```

#### Responses

**201**: The stored memory.

```json
{ "memory": {
  "id": "cm2kb7q4w0007qz0fm3n8c1xa",
  "agentId": "cm2k8x1ab0001qz0f7h3d9t4e",
  "workspaceId": null,
  "key": "client_fiscal_year_end",
  "value": "The client closes its books on 30 June.",
  "metadata": null,
  "isGlobal": false,
  "type": null,
  "connectedTo": [],
  "valence": null,
  "salience": 0.5,
  "embedding": [],
  "lastRecalledAt": null,
  "recallCount": 0,
  "createdAt": "2026-10-02T14:20:05.771Z",
  "updatedAt": "2026-10-09T09:02:11.402Z"
} }
```

**400**: `key` or `value` is missing or empty.

```json
{ "error": "key and value are required" }
```

**404**: No agent with this id in the workspace.

```json
{ "error": "Agent not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 400 |  | `key` or `value` missing, empty or not a string. |
| 404 |  | Unknown agent id. |
| 403 | `read_only_key` | The key carries the `readonly` scope. |

### DELETE /v1/agents/:id/memory/:memoryId

Delete one memory.

Removes a single memory. Use it to retract a wrong or UNVERIFIED claim. The memory must belong to the agent in the path.

- **Authentication:** API key (Bearer or x-api-key)
- **Scopes:** `agents`

#### Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `id` | `string` | yes |  | The agent id. |
| `memoryId` | `string` | yes |  | The `id` of the memory row, from the list endpoint. |

#### Request examples

_curl_

```bash
curl -X DELETE https://api.subsidia.protypa.fr/v1/agents/$AGENT_ID/memory/$MEMORY_ID \
  -H "Authorization: Bearer $SUBSIDIA_API_KEY"
```

_Python_

```python
import os, requests

headers = {"Authorization": "Bearer " + os.environ["SUBSIDIA_API_KEY"]}
base = "https://api.subsidia.protypa.fr/v1/agents/" + agent_id + "/memory"

# Retract every claim the agent could not source.
for m in requests.get(base, headers=headers).json()["memory"]:
    if (m.get("metadata") or {}).get("provenance") == "unverified":
        requests.delete(base + "/" + m["id"], headers=headers).raise_for_status()
```

#### Responses

**204**: Deleted. No body.

**404**: Unknown agent (`Agent not found`) or unknown memory for this agent (`Memory not found`).

```json
{ "error": "Memory not found" }
```

#### Errors

| Status | Code | When |
| --- | --- | --- |
| 404 |  | The agent or the memory does not exist, or the memory belongs to another agent. |
| 403 | `read_only_key` | The key carries the `readonly` scope. |

## Frequently asked

**Can an agent remember something a user told it in confidence?**

Memory is scoped to the agent and workspace and is never shown to other workspaces. But anything an agent stores is recalled into later prompts, for any user of that agent. Do not use a shared agent as a private notebook, and delete a memory that should not be kept.

**Why did my memory not show up in answers?**

Only the twelve best-ranked memories enter each prompt. A memory created through the API has no embedding, so it competes on recency and salience alone. Phrase it the way a user will ask about it, or put the document in a knowledge source if it must be found reliably.

**Does memory count as a question?**

No. Reading and writing memory is part of the chat turn that triggers it; the CRUD routes above call no model and consume nothing.
