StepStore & History¶
The PydanticAI family is built on a deliberately tiny contract: an async key-value interface called AsyncKV. Both persistence layers — history and step persistence — are implemented once against that interface, so a backend only has to provide four methods.
The AsyncKV interface¶
A backend implements four async methods:
class AsyncKV(Protocol):
async def put(self, pk: str, sk: str, data: str) -> None: ...
async def get(self, pk: str, sk: str) -> str | None: ...
async def query(self, pk: str, sk_prefix: str | None = None) -> list[tuple[str, str]]: ...
async def delete(self, pk: str, sk: str) -> None: ...
pk/skare a partition key and a sort key — the same two-key model DynamoDB, Cosmos, and Firestore all map onto cleanly.queryreturns(sk, data)pairs ordered ascending by sort key, optionally filtered by anskprefix. Ordering is the one guarantee the stores rely on.datais always an opaque JSON string; serialization lives in the core, not the backend.
InMemoryAsyncKV is a built-in implementation for tests and local dev.
History — KVHistoryStore¶
The simplest layer. PydanticAI hands you a full message list via result.all_messages(); you store it and load it back to resume a conversation.
from pydantic_ai import Agent
from pydantic_ai_persistence import KVHistoryStore, InMemoryAsyncKV
agent = Agent("openai:gpt-4o")
store = KVHistoryStore(InMemoryAsyncKV())
result = agent.run_sync("Hi, I'm Kamal")
await store.save("conv-1", result.all_messages())
prior = await store.load("conv-1") # -> list[ModelMessage] | None
result = agent.run_sync("What's my name?", message_history=prior)
Messages are serialized with PydanticAI's own ModelMessagesTypeAdapter, so tool calls, structured parts, and multi-modal content round-trip exactly.
| Method | Description |
|---|---|
save(conversation_id, messages) |
Persist the full message list for a conversation (overwrites) |
load(conversation_id) |
Return the stored list[ModelMessage], or None if absent |
delete(conversation_id) |
Remove a conversation's stored history |
Step persistence — KVStepStore¶
KVStepStore implements PydanticAI's async StepStore protocol: the durable-execution layer that lets a run be paused, inspected, and resumed. Hand it to an agent as a capability:
from pydantic_ai import Agent
from pydantic_ai_harness.step_persistence import StepPersistence
from pydantic_ai_persistence import KVStepStore, InMemoryAsyncKV
step_store = KVStepStore(InMemoryAsyncKV(), max_snapshots_per_run=10)
agent = Agent("openai:gpt-4o", capabilities=[StepPersistence(store=step_store)])
It records four kinds of durable state:
| Concern | What's stored |
|---|---|
| Runs | A RunRecord per run (conversation id, parent, agent name, start time), plus indexes for list_runs by conversation/parent. |
| Events | An append-only log of StepEvents (one sort-key slot per sequence number). |
| Snapshots | ContinuableSnapshots the run can resume from; latest_snapshot can include or exclude interrupted ones. |
| Tool-effect ledger | A ToolEffectRecord per tool_call_id so side-effecting tools aren't double-applied on resume. |
Snapshot pruning¶
Snapshots are the heaviest records (they carry full message history). Pass max_snapshots_per_run=N to keep only the newest N per run — older snapshots are deleted as new ones are saved, while events and the tool ledger are untouched.
Key layout¶
For the curious, KVStepStore maps everything onto AsyncKV like this (per run):
| Record | pk |
sk |
|---|---|---|
| Run metadata | run#<run_id> |
meta |
| Event | run#<run_id> |
event#<zero-padded seq> |
| Snapshot | run#<run_id> |
snap#<zero-padded seq> |
| Tool effect | run#<run_id> |
tool#<tool_call_id> |
| Run indexes | runs#all / runs#conv#<cid> / runs#parent#<pid> |
<started_at>#<run_id> |
Zero-padding keeps the ascending query order numerically correct, which is why the AsyncKV ordering guarantee is all the stores need.
Build your own backend¶
Implement the four AsyncKV methods for your datastore and hand the instance to KVStepStore / KVHistoryStore — no other code required. The DynamoDB, CosmosDB, and Firestore providers are each a thin AsyncKV wrapper plus convenience subclasses; use any of them as a template.
Beta harness feature
StepStore lives in pydantic-ai-harness and is beta/experimental — its API may still change. KVHistoryStore does not depend on it and is stable.