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.
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 run | Across runs | |
|---|---|---|
| Lifetime | Minutes | Until somebody deletes it |
| Blast radius of a bad entry | This run | Every future run |
| Who can check it | The run itself, against the world it is looking at | Nobody, unless you build the check |
| Cost of forgetting | Repeated work | Lost context, and a colder start |
| Cost of remembering wrongly | One bad step | A wrong default, permanently |
| How much to trust it | The same as the transcript | Strictly 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.
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 persisting | Not worth persisting |
|---|---|
| A stable preference, in the user's own words | A fact about the world the agent derived - derive it again, it may have moved |
| A decision and the reason for it | A blow-by-blow of what happened this run - that is a log |
| A correction the user made to the agent's behaviour | Anything read out of a tool result |
| A pointer to where something lives | Credentials, tokens, personal data |
| A constraint that applies to all future work | Anything the agent inferred but 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.
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.
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.
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.envis 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
- Treat a persistent store as lower-trust than the transcript, not higher.
- Write on a decision, not on a turn - and only things that will still be true next month.
- Store the source and the run id next to every fact, or you cannot audit it.
- Refuse to persist anything that came out of a tool result.
- Give the store a human-readable form, a delete path, and an expiry.
RECALL NO SCROLLING BACK
00 / 05 answered
QUESTION 01
Why is a bad entry in a persistent store worse than a bad turn inside one run?
QUESTION 02
The agent read a preference off a web page it had fetched and wrote it to memory. Where do you stop that?
QUESTION 03
What should sit alongside every stored fact?
QUESTION 04
Anthropic's memory tool is client-side. What follows from that?
QUESTION 05
An agent 'remembers' a preference the user never set. The likeliest cause is...