# Memory Patterns

Course: [Context Engineering](https://www.devtutorials.ai/learn/context-engineering) · Section: Design Patterns
Source: https://www.devtutorials.ai/learn/context-engineering/design-patterns/memory-patterns

> How to plan tool use, track where facts came from, and compress a long conversation into a state snapshot that keeps goals, decisions, constraints and open tasks, without losing the ability to check it.

Imagine a doctor who sees the same patient every month. The doctor does not read every word of every past visit before each appointment. Instead, the patient's file has a short summary at the top: allergies, current medicines, past decisions, and what to check next. The full notes are still in the file, if the doctor ever needs proof.

An AI assistant in a long conversation needs the same thing. Sending every old message on every turn is slow, costly, and buries the facts that matter. A short, well-kept summary works better.

**The main idea:** keep a small, trusted memory of what matters, and keep the full record somewhere it can be checked.

## A Real-World Example

Think about a group project at school.

- After each meeting, someone writes three lines: what we decided, what we must not forget, and who does what next.
- At the next meeting, the group reads those three lines, not a recording of the whole last meeting.
- If someone says "we never agreed to that", the group can check the recording.

The short notes keep everyone on track. The recording settles arguments.

**In simple words:** memory is notes, not a recording. But keep the recording too.

## What You Will Learn

In this lesson, you will learn:

1. How to decide which tools a task needs, and hide the rest.
2. How to record where each fact came from.
3. How to compress a long conversation into a state snapshot.
4. Which things a summary must never lose: decisions, constraints, and open tasks.
5. The trade-off between small memory and being able to check it.
6. Two common memory mistakes.

The Context sandbox has a new **Compact** tab. It asks the model to shrink a long support chat into a short memory, then tests that memory with three questions about early facts. You can edit the memory and test again.

## Tool-Aware Planning

Long conversations often use tools: look up an order, check an export, search a document. Each tool result becomes part of what the assistant remembers. So memory starts with good tool planning.

**Tool-aware planning** means the assistant thinks about which tools a task needs **before** it starts, and keeps track of what each tool returned.

### Tool Evaluation

**Tool evaluation** is the decision "does this question need a tool, and which one?"

A tool helps when the answer depends on **fresh** or **private** data, like the status of today's export. A tool is not needed when the answer is already in the context, or is general knowledge.

```js runnable title="Pick a tool, or none"
const tools = [
  { name: "export_status", useWhen: ["export", "failed", "status"] },
  { name: "invoice_lookup", useWhen: ["invoice", "bill", "payment"] },
  { name: "upgrade_plan", useWhen: ["upgrade", "change plan"] },
];

function pickTool(question) {
  const q = question.toLowerCase();
  const tool = tools.find((t) => t.useWhen.some((w) => q.includes(w)));
  return tool ? tool.name : "no tool: answer from the context";
}

console.log(pickTool("Why did my export fail?"));
console.log(pickTool("Can I see my last invoice?"));
console.log(pickTool("How do I change the grid theme?"));
```

Expected output:

```text
export_status
invoice_lookup
no tool: answer from the context
```

The theme question needs no tool. The help article already answers it. Calling a tool anyway would cost time and add noise to the memory.

### Tool Suppression

**Tool suppression** means hiding tools the current task does not need. If a tool's definition is not in the context, the model can't call it.

This helps in three ways:

- **Fewer tokens.** Every tool definition takes space.
- **Fewer mistakes.** The model can't pick a wrong tool it can't see.
- **More safety.** A risky tool, like deleting an account, is simply not on offer.

```js runnable title="Show only the tools this task needs"
const tokens = (text) => Math.ceil(text.length / 4);

const tools = [
  { name: "export_status", text: "export_status(customer_id): returns the status of the last export, rows requested, and time.", forTask: true },
  { name: "invoice_lookup", text: "invoice_lookup(customer_id, month): returns the invoice amount, due date, and payment status.", forTask: false },
  { name: "upgrade_plan", text: "upgrade_plan(customer_id, plan): moves a customer to a new plan. Needs the customer's approval.", forTask: false },
  { name: "delete_account", text: "delete_account(customer_id): permanently deletes the account and all data.", forTask: false },
];

const shown = tools.filter((t) => t.forTask);
const sum = (list) => list.reduce((n, t) => n + tokens(t.text), 0);

console.log("All tools:", tools.length, "tools,", sum(tools), "tokens");
console.log("Shown for this task:", shown.map((t) => t.name).join(", "), "-", sum(shown), "tokens");
```

Expected output:

```text
All tools: 4 tools, 90 tokens
Shown for this task: export_status - 23 tokens
```

The export task needs one tool. Hiding the other three cuts the tool text by about three quarters. It also removes `delete_account` from view, which matters more than the tokens.

### Provenance Tracking

**Provenance** means where something came from. **Provenance tracking** means every fact in memory keeps a note of its source and time.

Why? Because facts from different places deserve different trust. "The customer said they are on the Pro plan" is not the same as "the database says Pro". And a tool result from this morning may be out of date by evening.

```js runnable title="Every fact keeps its source and time"
const facts = [
  { fact: "Plan is Free", from: "database: customers", at: "2026-10-09 10:02" },
  { fact: "Last export failed at 80,000 rows", from: "tool: export_status", at: "2026-10-09 10:03" },
  { fact: "Customer prefers Excel", from: "chat: message 5", at: "2026-10-09 10:01" },
];

for (const f of facts) {
  console.log("- " + f.fact + "  [" + f.from + " · " + f.at + "]");
}
```

Expected output:

```text
- Plan is Free  [database: customers · 2026-10-09 10:02]
- Last export failed at 80,000 rows  [tool: export_status · 2026-10-09 10:03]
- Customer prefers Excel  [chat: message 5 · 2026-10-09 10:01]
```

When two facts disagree later, the labels show which one to trust, and which one to check again.

## Compact Memory

**Compact memory** is a short, structured note that replaces a long history. The model reads the note instead of every old message.

### Conversation Compression

**Conversation compression** turns many messages into a few lines. Usually, you ask a model to do it, with clear instructions about what to keep.

Try it with the real model. The card sends an 18-message support chat to the Compact tab. The model writes a memory of at most 60 words. Then press **Test the memory**. Three questions about early facts run twice: once with the full chat, and once with the memory only.

Look at the results:

- If the memory answers all three, it kept what matters.
- If it misses one, find which fact it dropped. Edit the memory by hand and test again.
- Try a word limit of 20. What gets lost first?

### State Snapshots

A **state snapshot** is a compact memory with fixed sections. Instead of a free paragraph, it always has the same headings, so nothing important is forgotten and code can read it.

A good snapshot for a support chat has four sections: the goal, the decisions, the constraints, and the open tasks.

```js runnable title="Build a state snapshot from a conversation"
const events = [
  { type: "goal", text: "Export the weekly report (80,000 rows)" },
  { type: "chat", text: "The grid looks great in dark mode." },
  { type: "decision", text: "Keep Excel as the format" },
  { type: "constraint", text: "No upgrade until 1 November" },
  { type: "decision", text: "Split into 16 files of 5,000 rows" },
  { type: "chat", text: "Ctrl+F searches the grid." },
  { type: "constraint", text: "Ready before Friday's meeting" },
  { type: "task", text: "Send Asha the split export steps" },
];

const pick = (type) => events.filter((e) => e.type === type).map((e) => e.text);

const snapshot = {
  goal: pick("goal")[0],
  decisions: pick("decision"),
  constraints: pick("constraint"),
  openTasks: pick("task"),
};

console.log(JSON.stringify(snapshot, null, 2));
```

Expected output:

```text
{
  "goal": "Export the weekly report (80,000 rows)",
  "decisions": [
    "Keep Excel as the format",
    "Split into 16 files of 5,000 rows"
  ],
  "constraints": [
    "No upgrade until 1 November",
    "Ready before Friday's meeting"
  ],
  "openTasks": [
    "Send Asha the split export steps"
  ]
}
```

The two small-talk lines are gone. Everything that changes what the assistant should do next is kept, in a fixed place.

### Decision Preservation

**Decisions** are things the user and assistant agreed on. "Keep Excel." "Split into 16 files."

A summary that drops a decision makes the assistant ask again, or worse, suggest the opposite. Asha already said CSV doesn't work for her finance team. An assistant that forgot this would suggest CSV again.

### Constraint Preservation

**Constraints** are limits the answer must respect. "No upgrade until 1 November." "Ready before Friday."

Constraints are easy to lose, because they often appear once, in passing. But an answer that breaks one is wrong, even if every fact in it is right. Suggesting "just upgrade to Pro" breaks Asha's budget constraint.

### Task Retention

**Open tasks** are things still to be done. **Task retention** means they stay in memory until they are finished, then they are removed.

Without task retention, a long conversation drifts. The assistant answers each new message well, but never finishes the job it promised at the start.

**Easy Way to Remember:** a good memory keeps **G-D-C-T**: the **Goal**, the **Decisions**, the **Constraints**, and the open **Tasks**.

## Pattern Trade-Offs

### Compression

Compression saves space and money, and it often improves answers, because the key facts are no longer buried. The next box compares the tokens sent over a 20-turn chat.

```js runnable title="Raw history grows every turn. A snapshot does not."
const tokensPerTurn = 60;
const snapshotTokens = 120;

let rawSent = 0;
let compactSent = 0;
for (let turn = 1; turn <= 20; turn++) {
  rawSent += turn * tokensPerTurn;
  compactSent += snapshotTokens + tokensPerTurn;
}

console.log("Raw history, 20 turns:", rawSent, "tokens sent in total");
console.log("Snapshot + last turn, 20 turns:", compactSent, "tokens sent in total");
console.log("Turn 20 alone, raw:", 20 * tokensPerTurn, "tokens");
```

Expected output:

```text
Raw history, 20 turns: 12600 tokens sent in total
Snapshot + last turn, 20 turns: 3600 tokens sent in total
Turn 20 alone, raw: 1200 tokens
```

With raw history, each turn sends every earlier turn again, so the cost keeps climbing. With a snapshot, each turn costs about the same. Over 20 turns, that is about 3.5 times fewer tokens.

But compression has a cost. Every summary **loses** something. If it loses the wrong thing, the assistant makes a mistake, and nobody can see why.

### Auditability

**Auditability** means being able to check, later, why the assistant did something. A summary on its own is hard to check. Did the customer really agree to that? Or did the summary get it wrong?

The fix is to keep both:

- The **snapshot**, for the model to read.
- The **full log**, stored outside the context, for people to check.

Each snapshot line can point back to the message it came from.

```js runnable title="Each note points to its proof"
const snapshot = [
  { note: "Keep Excel as the format", fromMessage: 6 },
  { note: "No upgrade until 1 November", fromMessage: 11 },
  { note: "Split into 16 files of 5,000 rows", fromMessage: 14 },
];

const log = {
  6: "Assistant: Understood. We'll keep Excel as the format.",
  11: "User: We can't upgrade this month. Budget is frozen until 1 November.",
  14: "Assistant: Yes. We agreed to split it into 16 files of 5,000 rows each, one per region.",
};

const check = snapshot[1];
console.log("Note:", check.note);
console.log("Proof:", log[check.fromMessage]);
```

Expected output:

```text
Note: No upgrade until 1 November
Proof: User: We can't upgrade this month. Budget is frozen until 1 November.
```

The model reads the short note. A person can follow the number back to the exact message. You get small memory **and** proof.

| | Raw history | Snapshot only | Snapshot + stored log |
|---|---|---|---|
| Tokens per turn | Grows every turn | Small and steady | Small and steady |
| Key facts easy to find | No | Yes | Yes |
| Can be checked later | Yes | No | Yes |

## Memory Pitfalls

### Raw History Usage

The most common mistake is sending the whole raw history on every turn. It works for short chats, so it is easy to keep doing. Then the chat grows, and:

- Costs and wait times climb with every turn.
- Old, wrong assumptions stay in the context and get repeated.
- Key facts drift into the weak middle of the window.
- One day, the history no longer fits, and the oldest messages, often the most important ones, are cut.

### Missing Compaction

The opposite mistake is having a memory, but never updating it. The snapshot was written at turn 5. Now it is turn 40. Three new decisions are missing, and one old constraint no longer applies.

Compact on a schedule, such as every 10 turns, or whenever the history passes a token limit. When you compact, start from the **old snapshot plus the new messages**, so earlier facts are carried forward.

A summary can quietly drop a constraint or keep a fact that is no longer true. Test your compaction with probe questions, like the Compact tab does, before you trust it.

## Common mistakes

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Raw history forever | "Sending everything means nothing is lost." | Compact into a snapshot, keep the log outside. | Costs climb, and key facts sink into the middle. |
| Every tool, every time | "More tools make the assistant more capable." | Show only the tools this task needs. | Wrong tool calls, wasted tokens, and risky actions on offer. |
| Facts with no source | "A fact is a fact." | Keep where and when each fact came from. | A user's guess is trusted like database data. |
| Summary drops a constraint | "A short summary is a good summary." | Always keep goal, decisions, constraints, and tasks. | The assistant suggests something the user ruled out. |
| Snapshot never updated | "We already have a memory." | Compact again on a schedule. | New decisions are missing and old ones stay. |
| Log deleted | "The summary is enough." | Keep the full log for checking. | Nobody can prove what was agreed. |

## Exercises

Each exercise ends with a value on the last line. Press **Check** to compare it with the expected answer.

## What to Remember

- **Tool-aware planning**: decide which tools a task needs, and **suppress** the rest.
- **Provenance tracking**: every remembered fact keeps its source and time.
- **Compact memory** replaces long history with a short **state snapshot**.
- A snapshot keeps **G-D-C-T**: the goal, the decisions, the constraints, and the open tasks.
- **Compression** saves tokens and keeps facts easy to find. **Auditability** needs the full log kept outside the context.
- Avoid **raw history** on every turn, and **compact again** on a schedule.

Next: **Schema Design**, how to make the model's reply follow a fixed shape that code can check.
