# Agent State You Own: Memory That Survives a Framework Change

*Published 2026-01-28 · Dmitry Shirokov · shirokoff.ca/blog/agent-state-portability*

The estimate was a week: extract the agent from one framework, drop it in another. Tools were clean functions, prompts were strings, orchestration was ~300 lines. Then someone asked about the conversations — 80,000 stored threads, message histories, accumulated user preferences, and a few hundred runs paused mid-flight awaiting human approval, all inside the framework's checkpoint tables in the framework's format. The week became a quarter, and most of it was a data migration negotiating which history we could afford to lose. **Your agent's state is data, and data outlives code.**

## Four kinds of state, three owners

| Kind | Lifetime | Owner | If you lose it |
|---|---|---|---|
| **Conversation history** | As long as the user expects it | **You** — product data | A ticket, or a regulator you can't answer |
| **Durable facts** (preferences, entities, decisions) | Indefinite | **You** — the accumulated asset | The agent gets amnesia; users notice |
| **Run position** (node, step, awaiting what) | Minutes to days | Framework + your shadow record | In-flight work abandoned — worse if it had side effects |
| **Scratch** (reasoning, retries, tool dumps) | The run | Framework — genuinely disposable | Nothing |

Rule: the top two go in **your** database in **your** schema, and the framework gets a copy, not the original. Checkpointers make it so convenient to persist all four in one opaque blob that teams do — and it works right up until you need any of it outside that framework, for an audit query, an export, a migration, or a second consumer.

## A schema worth owning

`conversations` + `messages` (append-only, `seq` ordering not clock ordering, trimmed `tool_result`, pinned `model`, token counts); `agent_facts` (typed `subject`/`key`, jsonb value, `confidence`, **`source_message_id` for provenance**, `valid_from`/`valid_to` for **soft expiry**); `agent_runs` (framework name, `thread_key`, status, `awaiting`, step count, cost).

Three details earn their keep: **provenance on every fact** answers "why does the agent think that?" — you will be asked, sometimes by someone with authority; **soft expiry** because agent memory is often wrong and correcting it is normal, not exceptional; and **recording the framework** per run, which feels redundant until you're running two side by side during a migration.

## What a checkpointer is actually for

Not persistence — that's your database. A checkpointer lets a **framework resume its own execution** after a crash or an approval wait. Valuable and hard; not a system of record. So: **the checkpoint store should be droppable and rebuildable.** If you lost it, you should be able to replay a conversation's messages from your tables into a fresh run. Then migration = stop new work, drain in-flight runs, point the new framework at the same conversation tables. Otherwise it's bespoke ETL between two undocumented serialization formats.

Two operational notes: checkpoint tables **grow** (one row per step per run, carrying accumulated state — the same quadratic pattern that makes agents expensive, now in bytes), so set retention on day one; and keep large, sensitive, or reconstructable state in your store with a *reference* in the checkpoint. A checkpoint blob full of unmasked personal data is a compliance problem where your controls aren't looking.

## The migration trap

Frameworks disagree about what state *is* — message list vs graph state with reducers vs session with typed slots. There's no mechanical translation, because they encode different assumptions. Migrating *from* a framework's format is therefore bespoke, one-way and semantically lossy, and you find the losses in production as an agent that forgot something the user remembers. Migrating from **your** canonical tables is a projection you control the meaning of. First way: a quarter, plus a hard conversation about acceptable history loss. Second way: a sprint, and the interesting part was the agent.

## Three seams

**History port** (`load(conversation_id)`, `append(message)`), **memory port** (`recall(subject, keys)`, `remember(fact, provenance)`), **run port** (`open_run`, `mark_awaiting`, `close_run`). Tools stay outside as plain functions, exposed over [MCP](https://shirokoff.ca/blog/model-context-protocol) where useful. What's left framework-specific is the orchestration graph — a few hundred lines, rewritable in a week, exactly as the original estimate assumed.

## Carry-away

Split the four kinds of state; own history and facts with provenance and soft expiry; keep a small run shadow so "what's in flight?" is SQL. Treat the checkpointer as resumption machinery with a retention policy and references instead of inlined sensitive data. Keep three thin ports. Then ask the design-review question while it's cheap: *if we changed frameworks next quarter, what would we lose?* If the answer is "the orchestration code," you're fine. If it involves the word "conversations," you have an unscheduled data project.

*Related: [AI agent memory](https://shirokoff.ca/blog/ai-agent-memory) · [Agent frameworks compared](https://shirokoff.ca/blog/agent-frameworks-compared) · [What MCP standardizes](https://shirokoff.ca/blog/model-context-protocol) · [RAG fundamentals](https://shirokoff.ca/blog/rag-fundamentals)*
