Memory
What Amy remembers between turns — goals, preferences, validated insights — read into every turn's context and written to at the end of each one. Public read/write API is Planned; today memory is internal.
Status — partly shipped. Memory is extracted, persisted, and injected into context on every turn today. The public read/write API (
amy.memory.list / .create / .delete,GET/POST/DELETE /v1/memory) is Planned — the examples below show the contract once it ships. Until then, treat memory as an internal optimization the agent manages for you.
Amy is built to be a continuous companion, not a stateless oracle. Without memory, every conversation would start from zero, "remind me again, what's your morning routine?" Memory is what turns Amy from a question-answering API into something that watches your trends and nudges you toward the goals you said mattered.
This page covers what memory is, what categories exist, how it's extracted, how it's injected, and how you read, write, and delete it through the API.
Quick navigation
- What Amy remembers, and what it doesn't
- The categories
- The Memory object
- How extraction works
- How injection works
- Reading memory
- Writing memory
- Deleting memory
- Retention and ownership
- Privacy notes
- Common mistakes
What Amy remembers, and what it doesn't
Remembered:
- Goals you've stated ("I want to lift deep sleep by 15 minutes").
- Preferences you've revealed ("vegetarian, no fish").
- Insights the validator confirmed about your data ("HRV drops 8.2% on days you drink >2 espressos after 2pm, validated, ρ=-0.34, N=87").
- A bookkeeping record of every quantitative hypothesis Amy has tested on your data, so it doesn't re-test the same hypothesis four times in four different conversations.
Not remembered:
- The full text of past conversations. Amy doesn't keep transcripts,
your client does, by passing the
messagesarray on each turn. - Raw biomarker or wearable data. That lives in the data tables
(
/v1/data/*) and is queried as needed, not memorised. - The contents of failed validation gates. Rejected findings are visible in the turn trace but never enter durable memory.
The dividing line: memory is for things that are durably true about the user ("you sleep poorly after late workouts") or things the user told us to act on ("the goal is to fix that"). Everything ephemeral lives in the data tables.
The categories
A memory's category is its kind. The full set (the
MemoryCategory enum in @amy/contracts):
kind | What it is | Example |
|---|---|---|
goal | Something the user is trying to achieve. Tracked across turns; coaching is anchored to it. | "Lift deep sleep average by 15 minutes over the next 6 weeks." |
preference | A constraint or preference the user has stated. Coaching respects these as hard filters. | "Vegetarian. No fish. Mornings only for workouts." |
barrier | Something getting in the user's way. | "Travels two weeks a month; gym access is unreliable." |
insight | A validated finding about the user. Sourced from the validator's fact sheet. | "Recovery score correlates with sleep consistency (validated, ρ=0.41 over 90 days)." |
hypothesis | A candidate pattern the agent is still investigating. | "Late caffeine may be suppressing deep sleep — not yet tested." |
decision | A choice the user made or that was agreed in conversation. | "Switched long runs from evening to morning, 2026-06." |
value | Something the user cares about / how they want to be coached. | "Optimizing for healthspan, not short-term performance." |
tested_hypothesis | Internal bookkeeping. | "[rejected] Caffeine after 2pm hurts deep sleep (ρ=0.04)." |
The last one, tested_hypothesis, is an internal bookkeeping category:
the validator writes one for every finding it processed (validated,
conditional, or rejected) so the Hypothesis Investigator doesn't
re-propose it. The Planned GET /v1/memory excludes these by default;
pass ?include=tested_hypothesis to see them.
The Memory object
{
"id": "mem_01HX2K3M4N5P6Q7R8S9T0V1W2X",
"ts": "2026-05-20T14:33:12Z",
"agent": "user",
"kind": "goal",
"text": "Goal: lift deep sleep by 15 minutes over the next 6 weeks.",
"confidence": 0.8,
"meta": null
}| Field | Type | Notes |
|---|---|---|
id | string? | Optional, typed prefix mem_…. Treat as opaque — it's a random id, not time-sortable; order by ts. |
ts | ISO-8601 | When the memory was written. Memory is append-only, there's no updated_at. |
agent | enum | Who wrote it: user · ds · de · hc · investigator · validator · orchestrator. user means you/the person wrote it directly. |
kind | enum | The category — goal · preference · barrier · insight · hypothesis · decision · value · tested_hypothesis. |
text | string | The memory itself. One sentence, plain English. |
confidence | number? | 0-1, from the extractor. Higher = more certain the user explicitly stated it. |
meta | object | null | For insight memories sourced from validated findings: { finding_id, feature, target, verdict, effect }. For others: null. |
A typical user accumulates 50-200 memory entries over a few months of active use. There's no hard cap; the summary that's injected into turn context is capped at the most recent ~80 entries.
How extraction works
Memory is extracted at the end of every turn (step 9 of the pipeline; see Turns: The pipeline). The extractor runs a small Sonnet call with the user's message, the assistant's answer, and a prompt that says, in effect:
Read the exchange. Emit any new durable facts about the user as JSON. Skip things that are already obvious from context.
Two sources flow in:
- The LLM extractor produces
goal/preference/barrier/insight/decision/valueentries (withagent: "user") based on what was said. - The validator writes one
tested_hypothesisentry per processed finding, with the verdict and effect attached. This is deterministic, no LLM call.
Memories are append-only. The extractor never deletes, if your preferences change, you write a new entry ("Switched to pescatarian 2026-05") that takes precedence by recency in the prompt summary.
If the extractor fails (rare, Sonnet timeout, malformed JSON), the turn still completes successfully. Memory extraction is best-effort; the turn doesn't fail because step 9 hiccuped.
What you'll see in the trace
After the synthesis event, the SSE stream includes a memory frame
listing the new entries extracted on this turn:
event: memory
id: 87
data: {"type":"memory","entries":[{"ts":"2026-05-25T10:01:40Z","agent":"user","kind":"preference","text":"Plays cricket; season runs Mar–Sep."}]}Then turn.completed fires. The final Turn.result doesn't carry
the new memory entries directly — read them out of the memory event
above, or fetch the full set with GET /v1/memory?after=<turn.completed_at>
once that endpoint ships (it's listed as Planned in the API
reference).
How injection works
Every turn (unless you explicitly opt out) injects a compact memory summary at the top of every agent's system prompt. Not the full JSONL, a compressed view that fits the model's attention budget:
## Goals
- (2026-05-20) Lift deep sleep average by 15 minutes over the next 6 weeks.
## Preferences
- (2026-04-12) Vegetarian. No fish.
- (2026-04-12) Mornings only for workouts.
## Insights
- (2026-05-15) Recovery score correlates with sleep consistency (validated, ρ=0.41).
## Tested hypotheses (already validated/rejected)
- (2026-05-18) [rejected] Caffeine after 2pm hurts deep sleep (ρ=0.04, no signal).
- (2026-05-22) [validated] Late workouts (>8pm) drop next-morning recovery (ρ=-0.31).Constraints:
- Most recent first, capped at ~80 entries total.
- Tested hypotheses are summarised with their verdict and effect size so the Investigator can de-prioritise them.
- The summary is read-only context; agents cannot mutate memory mid-turn. Mutations happen only at extraction (step 9).
To skip injection entirely:
const turn = await amy.turns.create({
messages: [...],
context: { include_memory: false }
});When to skip:
| Reason | Example |
|---|---|
| Running evals on isolated turns | Frozen-input regression suite |
| Memory-extraction debugging | Want to see what Amy would remember without re-using prior memory |
| Cost-sensitive batch jobs | Memory inflates the prompt by 2-5kB per agent; for a 100-turn batch that's a measurable saving |
Default is true. Most clients should leave it on.
Reading memory
GET /v1/memory
Authorization: Bearer <clerk-jwt-or-amy-cli-jwt>Response:
{
"data": [
{
"id": "mem_01HX...",
"ts": "2026-05-20T14:33:12Z",
"agent": "user",
"kind": "goal",
"text": "Goal: lift deep sleep by 15 minutes over the next 6 weeks.",
"confidence": 0.8,
"meta": null
}
],
"next_cursor": null,
"has_more": false
}Standard cursor pagination (API conventions). Filters:
| Param | Type | Default |
|---|---|---|
kind | enum | all (excluding tested_hypothesis) |
after | ISO-8601 | unbounded; useful for ?after=<turn.completed_at> |
before | ISO-8601 | unbounded |
include | tested_hypothesis | excluded by default; pass this to include |
limit | 1–100 | 20 |
TypeScript SDK:
const { data: facts } = await amy.memory.list({ kind: "goal" });Writing memory
You can write memory directly, useful for onboarding, settings screens, or when the user explicitly states a fact:
POST /v1/memory
Authorization: Bearer <clerk-jwt-or-amy-cli-jwt>
Content-Type: application/json
{
"text": "Vegetarian. No fish.",
"kind": "preference"
}| Field | Type | Required | Notes |
|---|---|---|---|
text | string | yes | Plain text. |
kind | enum | yes | goal · preference · barrier · insight · hypothesis · decision · value · tested_hypothesis. |
confidence | number | no | Defaults to 1.0 for user-written entries. |
Response (201 Created):
{
"id": "mem_01HX...",
"ts": "2026-05-25T10:14:33Z",
"agent": "user",
"kind": "preference",
"text": "Vegetarian. No fish.",
"confidence": 1.0,
"meta": null
}agent is user for entries you write directly. Use it to distinguish
what Amy inferred (ds / de / hc / investigator / validator /
orchestrator) from what the user told her (user).
TypeScript SDK:
const fact = await amy.memory.create({
text: "Vegetarian. No fish.",
kind: "preference",
});Why write directly?
- Onboarding: ask the user about goals and preferences during setup; write them as memory entries before the first turn runs.
- Settings UI: let the user toggle dietary preferences, contraindications, etc., as durable memory.
- Correcting the extractor: if Amy inferred something wrong, write
the correction explicitly. The summary is biased toward recent
entries, so a new
preferencefrom today will override an old inference.
Deleting memory
DELETE /v1/memory/mem_01HX...
Authorization: Bearer <clerk-jwt-or-amy-cli-jwt>Response: 204 No Content.
Deletes are immediate and hard, the entry is removed from the
JSONL store. No tombstone, no undo. The Investigator's
tested_hypothesis records remain unaffected unless you delete those
specifically.
TypeScript SDK:
await amy.memory.delete(fact.id);To clear all memory at once, list and delete, there's no
DELETE /v1/memory bulk endpoint in v1. The CLI's
amy reset command does the equivalent client-side as part of a
factory reset.
Retention and ownership
| Question | Answer |
|---|---|
| How long is memory kept? | Forever, until you delete it. No automatic expiry. |
| Who can read it? | Only the user it belongs to, via their bearer token. There's no cross-user sharing in v1. |
| What happens on account deletion? | All memory rows are dropped within 30 days, irreversibly. |
| Can I export it? | GET /v1/memory?limit=100 with cursor pagination gives you the full JSON dump. No CSV export endpoint in v1. |
| Does memory inform the model's training? | No. Memory is per-user, sent only to the LLM as context for that user's turns. Anthropic's API zero-data-retention terms apply to all model traffic. |
Privacy notes
- Memory text is stored in D1 (SQLite) at rest, encrypted at the Cloudflare layer. It is not encrypted at the application layer in v1, anyone with access to the database (you, in self-hosted; the Amy team, in managed deployments) can read it.
- Memory is sent to the LLM provider (Anthropic, OpenRouter, or whichever backend you configured) as part of every turn's prompt. Provider data-retention policies apply.
- Memory is never sent to third parties besides the LLM backend. Terra (wearable normalization) does not see memory; PubMed lookups do not include memory in queries.
- Don't write secrets to memory. It's designed for personal health context, not credentials. If the user pastes a token into chat, the extractor is biased against capturing it, but assume nothing is filtered.
Common mistakes
Sending memory yourself in the messages array
You don't need to. Memory is injected automatically when
include_memory: true (the default). Stuffing memory into the
user message wastes tokens and confuses the extractor.
Deleting memory to "reset context"
Memory and conversation are separate. To start a fresh conversation,
just send a fresh messages array, don't delete memory. Deletion is
for things the user no longer wants Amy to know.
Treating insight memories as ground truth indefinitely
An insight is true as of when it was extracted. If the user's
behaviour changes, the old insight is stale. The validator's
tested_hypothesis records carry verdicts, but no one
auto-invalidates an old insight. Write a new decision entry
("Switched from late workouts to mornings 2026-06") so the
summary's recency bias surfaces it.
Writing memory without a kind
kind is required. Sending null or omitting it returns
400 invalid_field.
Expecting mem_… IDs to be ordered
mem_… IDs are random and not time-sortable — don't sort by them.
For chronological ordering, use ts.
Asking Amy "what do you remember about me?"
This works, the model has the memory summary in context, but it's
expensive (a full turn for what could be a GET /v1/memory call). For
"show me what's stored," use the API directly. The CLI's amy memory
command does exactly this without burning an LLM round-trip.
Bulk-importing memories without a category mapping
If you're migrating from another system, map source categories onto
Amy's kind set explicitly. Never import anything as
tested_hypothesis — that's the validator's internal bookkeeping
channel and the Investigator treats those entries as "already tested."
Use value or decision for context that doesn't fit cleanly.
Re-writing a memory instead of deleting + writing
There's no PATCH /v1/memory/:id. To "edit" a memory, write a new
entry with the updated text and delete the old one. The summary
respects recency, so the new entry wins in agent context immediately.
Where to next
- Turns, how memory injection fits into the per-turn pipeline.
- API reference: Memory, endpoint signatures.
- SDK: TypeScript, memory,
typed
amy.memorymethods. - Errors, error codes for memory operations.
- Internals: Storage, D1 schema for the memory table.
Streaming
Amy streams every step of a turn, routing, agent thoughts, validator verdicts, the synthesised answer, over Server-Sent Events. One HTTP connection, one durable replay buffer, no websockets require…
Webhooks
Terra is Amy's single source of wearable and lab data. This page covers the inbound webhook contract: how Terra delivers events, how Amy verifies them, what happens after ingest, and how to replay th…