Amy
Concepts

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

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 messages array 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):

kindWhat it isExample
goalSomething 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."
preferenceA constraint or preference the user has stated. Coaching respects these as hard filters."Vegetarian. No fish. Mornings only for workouts."
barrierSomething getting in the user's way."Travels two weeks a month; gym access is unreliable."
insightA validated finding about the user. Sourced from the validator's fact sheet."Recovery score correlates with sleep consistency (validated, ρ=0.41 over 90 days)."
hypothesisA candidate pattern the agent is still investigating."Late caffeine may be suppressing deep sleep — not yet tested."
decisionA choice the user made or that was agreed in conversation."Switched long runs from evening to morning, 2026-06."
valueSomething the user cares about / how they want to be coached."Optimizing for healthspan, not short-term performance."
tested_hypothesisInternal 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
}
FieldTypeNotes
idstring?Optional, typed prefix mem_…. Treat as opaque — it's a random id, not time-sortable; order by ts.
tsISO-8601When the memory was written. Memory is append-only, there's no updated_at.
agentenumWho wrote it: user · ds · de · hc · investigator · validator · orchestrator. user means you/the person wrote it directly.
kindenumThe category — goal · preference · barrier · insight · hypothesis · decision · value · tested_hypothesis.
textstringThe memory itself. One sentence, plain English.
confidencenumber?0-1, from the extractor. Higher = more certain the user explicitly stated it.
metaobject | nullFor 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:

  1. The LLM extractor produces goal / preference / barrier / insight / decision / value entries (with agent: "user") based on what was said.
  2. The validator writes one tested_hypothesis entry 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:

ReasonExample
Running evals on isolated turnsFrozen-input regression suite
Memory-extraction debuggingWant to see what Amy would remember without re-using prior memory
Cost-sensitive batch jobsMemory 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:

ParamTypeDefault
kindenumall (excluding tested_hypothesis)
afterISO-8601unbounded; useful for ?after=<turn.completed_at>
beforeISO-8601unbounded
includetested_hypothesisexcluded by default; pass this to include
limit1–10020

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"
}
FieldTypeRequiredNotes
textstringyesPlain text.
kindenumyesgoal · preference · barrier · insight · hypothesis · decision · value · tested_hypothesis.
confidencenumbernoDefaults 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 preference from 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

QuestionAnswer
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

On this page