most “agent” demos still ship one artifact: a chat log.
you scroll. you see what the model said. you see what the user said. you see the tool calls. you nod. you screenshot. you tweet.
then you try to actually run an agent that acts — something that talks to other agents, takes irreversible steps, lives longer than one tab — and the chat log stops being enough. you can’t query it. you can’t roll it back. you can’t tell another agent “pick up where the last one left off.” you can’t show your operator what the agent is allowed to do without making them read three thousand lines of dialogue.
chat is the surface humans see. it isn’t the artifact agents act on.
agents act on state. and right now, almost nobody ships state.
what an agent actually needs
walk through the things you need the moment a single agent stops being a single chat:
- somewhere to keep what it knows that survives the tab.
- a way to find other agents without you wiring sockets by hand.
- a way for those agents to publish what they are without leaking private context.
- a way for you to see who is on the network and what just happened.
- a way to refuse incoming work that doesn’t match policy.
- a way to look up “what did this thing do, when, and why” months later.
none of those are messages. all of them are state.
| operator question | chat log answers it? | state answers it? |
|---|---|---|
| what did the agent do? | yes | yes |
| why did it act? | sometimes | yes |
| what is it allowed to do? | no | yes (policy) |
| who else is in the room? | no | yes (discovery) |
| can the next agent take over? | painful replay | clean read |
| can i roll this back safely? | no | yes |
| can i audit this in 6 months? | grep your luck | indexed table |
if your stack only ships the left column, you’re not running an agent. you’re running a chatbot in a trench coat.
the state object
once you accept the artifact is state, the question becomes: what shape?
if you’re designing a personal agent today, here’s the minimum surface the rest of this article assumes. seven rows. each row is a different table or scope, not a different paragraph in a chat.
| facet | what it holds | what dies without it |
|---|---|---|
| identity | who this agent is, who it trusts, which fingerprints it accepts | impostor inputs, no rotation story |
| policy | what’s allowed in, who can ask, what the defaults are | no fail-closed; “allow” by neglect |
| memory | what the agent already knows that survives restarts | repeats itself, asks twice, forgets context |
| membership | which groups, networks, or rooms the agent is in; public vs private | broadcasts to the wrong audience |
| audit | metadata-only record of who, when, what kind, what verdict | no answer to “what happened” months later |
| queue | what’s pending, what’s being retried, what was already acked | duplicate actions, lost work |
| public profile | the self.md / card / description peers see when they discover this agent | no consented public surface; either you leak everything or nothing |
these aren’t labels you pick because they sound good. they’re the rows your operator will be asking about within a week of running anything serious. you either build them or you do triage in a chat scroll.
a field report
i’ve been working on a P2P harness called network.self.md inside Hermes. it’s the lived version of this argument. the public-safe shape lines up with the surfaces above, so it’s a reasonable demo of what “shipping state” looks like in practice.
every node is an agent process. agents join a fixed global discovery topic at start. when two agents meet they exchange identity, then announce their public groups — group id, member count, and a short self.md describing what the group is for. nothing else leaks. private groups stay private.
each agent stores network state in durable local storage. groups carry an explicit public/private flag and a short profile description. peer agents stick everything they hear about into a discovery inbox — “things in the network you could join, but have not joined yet.” the dashboard reads from the same store and shows: peers online, peers seen, groups joined, public groups discovered, recent activity. the activity feed is metadata only. no message bodies, no sender keys, no plaintext.
joining a public group doesn’t require an invitation. you saw it announced, you can join. it’s the equivalent of “this discord has an invite link in their bio” — except the bio is self.md, not a marketing page.
on the inbound side, every received message goes through a policy gate before anything else touches it: validate shape → check group membership → run the policy decision → write a metadata-only audit row → enqueue, ignore, or ask the operator. the audit row stores who, when, what kind, and the verdict. it doesn’t store the body. it persists before the message is acknowledged for dedup, so a crash between policy and execution doesn’t poison the queue.
operators control the gate from one place: a trust list of fingerprints, a list of interests the agent should respond to, an optional “must @-mention me” flag, and a configurable mention prefix length. that’s the whole knob set. it lives in versioned local storage, not an ad-hoc dotfile, because the agent reads it on every startup and re-validates it on every entry point.
this is mundane. it’s also the part nobody on twitter is showing you.
what disappears when you skip state
most “agent fails” you see are state failures cosplaying as model failures.
→ “the agent forgot what i told it.” → no persisted memory.
→ “the agent took an action twice.” → no audit + no dedup queue.
→ “two agents stepped on each other.” → no shared state.
→ “i can’t tell what the agent did last week.” → no audit table.
→ “the agent did something it should not have been allowed to.” → no policy gate.
→ “i don’t know which agents are running.” → no discovery.
→ “i can’t hand off between agents.” → only chat, no state.
every one of those needs a different artifact. none is solved by a smarter model. all are solved by a worse model with the right state plumbing.
the chronic ones are catalogued at /concepts/agent-failure-modes/ and /concepts/agentic-design-patterns/ . they read like a list of missing tables.
design principles
if you’re building agent infrastructure right now, the cheap version is six rules.
- the database is the source of truth. the transcript is rendered from it, not the other way around.
- default-private wins. public state is opt-in, with an explicit description (a self.md, a card, whatever your equivalent is). you should be able to tell from a single row what the agent is willing to say in public.
- discovery is infrastructure, not config. agents should find each other on a known topic without you editing a yaml file. the moment two operators have to coordinate a port number, the network dies.
- policy gate, fail-closed. every inbound event passes through one chokepoint. unknown shape → reject. unknown sender → reject. unknown policy → reject. defaults are not “allow.”
- audit is metadata only. never write plaintext, message bodies, tool arguments, or private keys to the audit table. write who, when, what kind, what the policy decided. that table is the only thing you’ll trust six months from now.
- the dashboard is for the operator. it isn’t analytics. it’s the trust surface. if it shows things the operator can’t act on, it’s theatre. if it hides things the operator should be acting on, it’s a liability. (control surface is the product is the longer version.)
migrate a chat-only agent to stateful in one afternoon
if you already have a chatbot that works and you want to make it operable, this is the path. one local store, one config file, one gate. nothing exotic.
- pick a local store. sqlite in a single file is fine. one schema, one backup story.
- write identity to a row: name, public key fingerprint, version. stop reading identity from the system prompt.
- persist memory: every fact the agent should remember next session gets a row, keyed by source + topic. the chat history stops being the cache.
- add a policy file in versioned storage: allowed senders, allowed actions, default-deny. the agent reads it on every entry point, not on first boot.
- route every inbound event through one function before the model sees it. reject unknown shape, unknown sender, unknown action.
- write a metadata-only audit row before any side effect runs. who, when, kind, verdict. never after.
- add a discovery surface, even a flat file listing peers. the chat scroll stops being your address book.
- kill the process. restart it. if it can’t pick up where it left off without rereading the chat, you haven’t migrated yet.
eight items. an afternoon if you stay disciplined. you don’t need a new model. you don’t need a new framework. you need rows.
the MCP question
people will ask: “isn’t this what MCP is for?”
no, but you need both. the Model Context Protocol defines the action boundary: how agents reach external tools, with structured calls and structured responses. that’s the right place to put the read/write contract for any external system — fetching a file, calling an api, reading a calendar.
it isn’t a place to put your agent’s memory, your agent’s discovery, your agent’s policy, or your agent’s audit. those are agent-local concerns. MCP is the verb layer; state is the noun layer.
skills (see skills are the new apps ) and config files (see agents.md vs claude.md vs instructions.md ) sit alongside as instruction surfaces. neither solves state either. they tell the agent what to consider. state is what already happened, what is true, what is pending, what is allowed.
four different layers, four different files. mash them together at your peril.
the bottom line
we’ve spent two years optimizing chat. we’re about to spend the next two operating agents. the gap between those two jobs is mostly state.
if you want a useful test, point at any agent demo and ask:
→ where does its memory live?
→ how does it find the others?
→ what’s the chokepoint that decides what it acts on?
→ where’s the audit row that proves what it did?
if the answer is “the conversation,” you’re looking at a chat. if the answer is a row in a table, a topic on a network, a function in a gate, an indexed audit log — now you’re looking at an agent.
agents need states. not chats.