Where each piece of an agent's state belongs
Most agent bugs that look like model problems are storage problems. The agent asks a question it already has the answer to, or confidently states an order status from three weeks ago, or forgets a commitment it made on Tuesday. In each case the information existed; it was just in a place the agent did not read, or in a place that had gone stale.
5 min read
There are six places state can live, and each has a job.
The six, and what belongs in each
| Store | Holds | Lifetime |
|---|---|---|
| The thread | What was said, in order | Forever, and it is the record |
| Contact | What is true about this person | Until it stops being true |
| Notes | What is true about the workspace | Until policy changes |
| Vault | Credentials the agent may use | Until rotated |
| Your systems | Current state of the business | Now, always |
| Agent storage | Work in progress | The task |
The rule that resolves most questions: the thread is what happened, contacts are who you are dealing with, your systems are what is true right now, and everything else is scaffolding.
Never copy the thread
The most common design mistake is storing your own copy of the conversation, usually because it felt easier to keep a summary alongside the workflow state.
Two things go wrong. Your copy diverges the first time someone replies from their phone to an older message, and now you have two records of one conversation with no way to say which is right. And the copy is personal data with a retention obligation you did not intend to take on, per the compliance floor.
get_thread returns the conversation in order, with quoted history stripped, which is both cheaper in tokens and less confusing to the model than the raw chain. Read it every time. It is one call, and it is authoritative.
Never mirror your own database
The mirror image of the same mistake: recording "order 4012 shipped" as a fact on the contact. It was true when written, it goes stale silently, and an agent that reads it will tell a customer something contradicted by the order system.
Facts on a contact should be things your systems do not hold and that stay true: a preference, a role, a constraint, a commitment you made. Everything about current state comes from your systems at answer time. That division is worked through in giving an email agent memory, and it is why the order status agent fetches live rather than remembering.
What actually needs your own storage
Not much, which surprises people. Three things:
Handled message ids, so a retried webhook does not produce a second reply. This is the one nobody can skip, per webhooks or polling.
Workflow position for multi-step tasks that outlive a run: which supplier has answered, which documents are still missing, whether a follow-up has been sent. Keyed on the thread id so a resume is cheap.
Idempotency and locking state for concurrency, per what happens when forty messages arrive at once.
Everything else you were about to store is already in one of the other five places.
Where that storage should sit
If you are on Durable Objects, one object per thread is a natural fit: the lock, the handled set, and the workflow position all live in the object that owns the conversation, and there is no cross-instance coordination to get wrong. The shape is in build an email agent on the Cloudflare Agents SDK.
If you are on ordinary infrastructure, the same three things live in your database keyed on thread id, with a lock or an advisory lock for the serialisation. The design is identical; only the mechanism differs.
Either way, keep it small. Storage that holds a copy of something authoritative elsewhere is the thing that will be wrong in six months.
The read path, in order
For any inbound message, the sequence that produces a good reply:
- Handled? Check your own set. If yes, stop.
- The thread.
get_thread, not the single message. - The person.
lookup_contactfor what previous conversations established. - The world. Your systems, for whatever the question is actually about.
- Policy, if needed. A note, for rules that apply regardless of who is asking.
- Decide, act, record. Reply in thread, write one durable fact if you learned one, archive when done.
Steps two and three are the ones skipped most often, and skipping them accounts for most complaints about agents that "feel robotic". Step six is the one that makes the next conversation better and breaks nothing when omitted, which is exactly why it gets dropped.
Secrets are not state
Worth stating separately because it gets muddled: credentials are not part of the conversation and do not belong in any of the above. They live in the vault, granted per agent, opened at the moment of use. An agent that was not granted a secret cannot see it exists, which is a property worth having when a message is trying to talk it into something, per permissions and grants.
Questions
- Should I store a copy of the email thread?
- No. The thread is the record, and a copy diverges the first time someone replies to an older message. Call
get_thread, which is cheap, authoritative, and already stripped of quoted history. - What belongs on a contact record?
- Durable facts your own systems do not hold: preferences, role, constraints, commitments made. Not order status or anything your database owns, which goes stale silently.
- What does my agent actually need to store?
- Handled message ids for idempotency, workflow position for multi-step tasks, and whatever locking your concurrency model needs. Everything else lives in the thread, contacts, notes, or your systems.
- Where should that storage live?
- One Durable Object per thread if you are on Cloudflare, since the lock and the state belong to the same conversation. Otherwise your database keyed on thread id. Same design, different mechanism.
- Why does my agent keep asking for information it already has?
- Because it is answering the message rather than reading the thread and the contact record first. Those two calls before writing fix most of it.
- Where do credentials go?
- The vault, granted per agent and opened at the point of use. They are not conversation state and should never sit in a prompt, a note, or your own storage.
Give your agent an address it can answer from.
Create an inbox