Agents and Tool UseRunning the loop

What sticks around after the run ends

Memory across sessions is a database you let a language model write to, with no review step.

WHY THIS, FOR YOU

A user opened your agent and it greeted them with a summary in bullet points, because it remembered they prefer bullets. They never said that. Three weeks earlier they had asked one question to be answered briefly, and somewhere in that run the agent wrote `prefers short, bulleted answers` into its store as a standing fact about this person. Nothing was corrupted and nobody attacked you. A guess got written down, and from then on every run read it as truth. This lesson is about the writes.

1.Two memories, two problems

What the loop remembers within one run was about one task, one user, one window. The worst thing that happens there is that the run forgets and redoes work, and when the run ends the mess goes with it.

Everything changes when a write outlives the run that made it.

Within one runAcross runs
LifetimeMinutesUntil somebody deletes it
Blast radius of a bad entryThis runEvery future run
Who can check itThe run itself, against the world it is looking atNobody, unless you build the check
Cost of forgettingRepeated workLost context, and a colder start
Cost of remembering wronglyOne bad stepA wrong default, permanently
How much to trust itThe same as the transcriptStrictly less

That last row is the one people get backwards. A persistent store feels authoritative because it survived - it looks like a settled fact rather than a passing remark. It is the opposite. The transcript at least came from the conversation you are having. The store came from a conversation you cannot see, written by a model that was not asked to justify itself, read now by a run with no idea where any of it came from.

The mechanics are simple enough to describe in a paragraph. There is a store of files or rows. The agent reads it at the start of a run and writes to it during one. Anthropic ships this as the memory tool - one entry in your tools array, {"type": "memory_20250818", "name": "memory"} - and the model then issues view, create, str_replace, insert, delete and rename commands against paths under /memories. Every other implementation is a variant of that shape.

The part that matters is the half people skim: the memory tool is client-side. The model only asks. /memories is a prefix your handler maps onto real storage, and your code performs every operation. Nothing about it is managed for you, which means no check exists unless you write it.

2.Deciding what gets written

The default failure is writing too much. An agent that records something after every turn builds a store that takes longer to read than the task takes to do, full of things that were true once.

Worth persistingNot worth persisting
A stable preference, in the user's own wordsA fact about the world the agent derived - derive it again, it may have moved
A decision and the reason for itA blow-by-blow of what happened this run - that is a log
A correction the user made to the agent's behaviourAnything read out of a tool result
A pointer to where something livesCredentials, tokens, personal data
A constraint that applies to all future workAnything the agent inferred but nobody confirmed
The same sort applied five times: the left column survives being wrong later and still being the user's own words; the right column is a log line, a re-derivable fact, or a guess nobody confirmed.

Four rules that hold up:

Write on a decision, not on a turn. The test is whether it will still be true next month. "The user asked for this in bullets" is an event. "The user prefers bullets" is a claim, and the gap between them is the opener's entire bug.

A single request, answered once
The user asks one question and asks for it briefly. Nothing about future requests is stated.
  1. store entrynone
  2. what was actually saidbe brief, this once
The same store entry, before, during and after the run that created it. Nothing needed to go wrong for the wrong fact to end up written down.

Store provenance with the fact. Who said it, when, in which run. A fact with no source cannot be audited, cannot be explained to the user who asks why the agent thinks that, and cannot be safely believed.

Prefer editing to appending. The memory tool has str_replace, rename and delete for a reason. A store that only grows becomes a store nobody reads, including the agent - it will start paging through it and skipping.

Expire, and scope. Give entries a last-read timestamp and delete the cold ones. And map /memories per user in your handler, or one person's preference quietly becomes everybody's.

3.When a memory is wrong, or was planted

Two shapes, and they are indistinguishable once written.

Drift. The agent turned a one-off into a standing rule. Nobody lied, nothing was attacked, and the store is simply wrong in a way that steers every future run a few degrees off. This is the common case by a wide margin.

Poisoning. Content from a tool result - a web page, an inbound email, a document, a file in a repo - ends up in the store as a fact or an instruction. OWASP tracks this as ASI06: Memory & Context Poisoning in its Top 10 for Agentic Applications, and has a reference defence project, Agent Memory Guard, aimed squarely at it.

DriftNobody attacked you

The agent generalised a one-off into a standing rule. This is the common case, and ordinary use is enough to produce it - nothing needs to go wrong on purpose.

PoisoningContent from outside became a fact

A web page, an email or a file the agent read got stored as if the user had said it. The page that carried it can be long gone by the time the write finally fires.

The same wrong entry in the store, reached two different ways. Only one of them is an attack, and by the time either shows up the store looks identical.

Where the instruction comes from and how it gets into a model's context is the Prompt Engineering course's lesson Prompt injection, and this course's later lesson Letting an agent touch the real world covers limiting the damage once it lands. Neither is re-taught here. The part that belongs to memory is narrow and specific: a persistent store turns a one-off injection into a standing instruction. The page that carried it may be gone by the time it fires. The attack and its effect are separated in time, which is also why it does not look like an attack when you finally investigate - it looks like the model behaving oddly.

Which leads to the practical framing. Do not think of this as a security feature bolted onto memory. Think of it as hygiene, and the checks fall out of that:

  • Classify by source at write time. A fact the user typed and a fact scraped from a page are not the same class. Refuse the second outright, or store it labelled, and make sure the label is still attached when the agent reads it back.
  • Validate in the handler, not in the prompt. Your write path is the choke point every operation passes through. Path validation first - Letting an agent touch the real world's resolve-then-check, unchanged, because /memories/../../secrets.env is a real request your handler will receive. Then a size cap, then a schema, then the source rule.
  • Keep the store human-readable. Plain files, plain sentences. If a store is a pile of embeddings, nobody can look at it and notice a line that should not be there.
  • Ship a delete path. The user must be able to see what the agent believes about them and remove it. This is a product requirement before it is a security one.
  • Log and diff every write. Attach the run id. When behaviour changes for no apparent reason, the diff of the store is the first place to look, and without the log you will be reading model outputs and guessing.

Within a run, forgetting is the bug. Across runs, remembering the wrong thing is the bug - and it never announces itself.

import logging
from datetime import datetime, timezone
from pathlib import Path

MEMORY_ROOT = Path("/srv/agent-memory")
MAX_BYTES = 32_000
audit = logging.getLogger("memory.audit")

def resolve(user_id: str, path: str) -> Path:
    # /memories is only a prefix the model sees. Map it to storage you own,
    # scoped per user.
    if not path.startswith("/memories"):
        raise ValueError("path must start with /memories")
    root = (MEMORY_ROOT / user_id).resolve()
    target = (root / path.removeprefix("/memories").lstrip("/")).resolve()
    target.relative_to(root)          # same guard as the real-world tool lesson: resolve, then check
    return target

def write(user_id: str, path: str, text: str, source: str, run_id: str) -> str:
    # Anything that arrived in a tool result is untrusted. It does not get to
    # become a standing fact about this user.
    if source not in {"user_stated", "user_confirmed"}:
        audit.warning("refused memory write source=%s path=%s run=%s", source, path, run_id)
        raise PermissionError(f"will not persist memory from source={source!r}")
    if len(text.encode()) > MAX_BYTES:
        raise ValueError("memory entry too large")

    target = resolve(user_id, path)
    target.parent.mkdir(parents=True, exist_ok=True)
    target.write_text(
        f"# source: {source}\n"
        f"# run: {run_id}\n"
        f"# written: {datetime.now(timezone.utc).isoformat()}\n"
        f"{text}\n"
    )
    audit.info("memory write path=%s run=%s source=%s", path, run_id, source)
    return f"File created successfully at: {path}"

Three headers and a path check. That is the whole difference between a store you can reason about a year from now and one you can only apologise for.

WHAT YOU TAKE AWAY

  1. Treat a persistent store as lower-trust than the transcript, not higher.
  2. Write on a decision, not on a turn - and only things that will still be true next month.
  3. Store the source and the run id next to every fact, or you cannot audit it.
  4. Refuse to persist anything that came out of a tool result.
  5. Give the store a human-readable form, a delete path, and an expiry.

RECALL NO SCROLLING BACK

  1. Why is a bad entry in a persistent store worse than a bad turn inside one run?

  2. The agent read a preference off a web page it had fetched and wrote it to memory. Where do you stop that?

  3. What should sit alongside every stored fact?

  4. Anthropic's memory tool is client-side. What follows from that?

  5. An agent 'remembers' a preference the user never set. The likeliest cause is...

GO DEEPER