Sessions & checkpoints¶
So far every agent.run() has started from scratch. Real assistants remember: you ask a follow-up and the agent already knows what "it" refers to. And sometimes the agent goes down a wrong path and you want an undo button.
Two ideas cover both. Sessions continue a conversation across runs by feeding the previous messages back in. Checkpoints snapshot that conversation so you (or the agent) can rewind to a known-good state.
Continue a conversation¶
Let's make the agent remember. The trick is one argument: message_history.
import asyncio
from pydantic_deep import create_deep_agent, DeepAgentDeps
async def main():
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
instructions="You are a helpful assistant.",
)
deps = DeepAgentDeps()
# First turn — no history yet.
first = await agent.run("My favorite language is Python.", deps=deps)
print(first.output)
# Second turn — pass the messages from the first run back in.
second = await agent.run(
"What did I just tell you?",
deps=deps,
message_history=first.all_messages(),
)
print(second.output)
asyncio.run(main())
Run it¶
Save it to main.py and run:
The first reply acknowledges your favorite language. The second reply says Python — because the agent saw the whole first exchange. Without message_history it would have no idea.
Dissect it¶
Two lines do the work:
first.all_messages()returns the complete message history of a run — your prompt, the model's reply, every tool call in between.message_history=...on the nextagent.run()prepends those messages, so the model continues the same conversation.
That's a session: keep the latest all_messages() around and pass it to the next run. To persist a session across process restarts, save those messages — pydantic-ai's ModelMessagesTypeAdapter serializes them to JSON, and the FileCheckpointStore below uses exactly that.
The files come along
The history also carries the ref of the workspace the run worked in, so
the next run works in the same one — the files it wrote in turn one are
still there in turn two, even in the default in-memory workspace. Reuse the
same DeepAgentDeps across turns to keep its todos and uploads too.
Snapshot with checkpoints¶
A session grows in one direction. Checkpoints let you go back. Turn them on with include_checkpoints=True:
That gives the agent three new tools — and an in-memory store to hold the snapshots:
| Tool | What it does |
|---|---|
save_checkpoint |
Label the most recent snapshot (e.g. "before-refactor") |
list_checkpoints |
Show every saved checkpoint with its label and turn |
rewind_to |
Rewind the conversation to a checkpoint |
By default a checkpoint is auto-saved after every tool call, so there's always something to rewind to. Set checkpoint_frequency="every_turn" or "manual_only" to change that.
Rewind to a checkpoint¶
Rewinding is different from the other tools: it can't just return a value, because the conversation it needs to change is the one currently running. Instead, rewind_to raises RewindRequested, which propagates out of agent.run(). You catch it and restore the history.
import asyncio
from pydantic_deep import (
create_deep_agent,
DeepAgentDeps,
RewindRequested,
)
async def main():
agent = create_deep_agent(
model="anthropic:claude-sonnet-4-6",
include_checkpoints=True,
)
deps = DeepAgentDeps()
history = []
while True:
try:
prompt = input("> ")
except EOFError:
break
try:
result = await agent.run(prompt, deps=deps, message_history=history)
history = result.all_messages()
print(result.output)
except RewindRequested as exc:
print(f"Rewound to '{exc.label}'.")
history = exc.messages
asyncio.run(main())
Run it¶
Ask the agent to save a checkpoint, do some work, then tell it to rewind. The rewind_to tool fires, RewindRequested breaks out of the run, and your loop swaps history for exc.messages — the conversation is back where it was. The agent forgets everything after the checkpoint.
Dissect it¶
- The agent decides when to checkpoint and rewind; you just gave it the tools.
RewindRequestedcarries.label(which checkpoint) and.messages(the restored history). Catching it and reassigninghistoryis the whole integration.- Because the loop owns
history, the rewind sticks: the next turn starts from the restored snapshot.
Why an exception, not a return value
pydantic-ai only intercepts ModelRetry and UnexpectedModelBehavior;
every other exception flows straight to your caller. RewindRequested
rides that path out so your run loop — the only code that owns the
history — can act on it.
Persist to disk¶
InMemoryCheckpointStore is the default and vanishes when the process exits. For checkpoints that survive restarts, pass a FileCheckpointStore:
from pydantic_deep import create_deep_agent, FileCheckpointStore
agent = create_deep_agent(
include_checkpoints=True,
checkpoint_store=FileCheckpointStore("/path/to/checkpoints"),
)
Each checkpoint becomes one JSON file. Need a different store — Redis, a database — implement the CheckpointStore protocol and pass it the same way.
One store per user
In a multi-user app, give each session its own store
(checkpoint_store= on DeepAgentDeps works at runtime). A shared
InMemoryCheckpointStore means every user sees everyone's checkpoints.
Fork instead of rewind¶
Rewinding rewrites the current session. To branch off a checkpoint without touching the original — try a different approach in parallel — use fork_from_checkpoint:
from pydantic_deep import fork_from_checkpoint
messages = await fork_from_checkpoint(store, checkpoint_id="abc123")
branch = await agent.run("Try a different approach", deps=deps, message_history=messages)
It returns the checkpoint's messages; feed them to a fresh run as message_history and you have a new session that shares a past with the old one. For more on checkpoint internals and stores, see the Checkpointing reference.
Recap¶
- A session is just
message_history=result.all_messages()carried into the nextagent.run()— that's how the agent remembers across runs. include_checkpoints=Truegives the agentsave_checkpoint,list_checkpoints, andrewind_to, auto-saving after each tool call by default.rewind_toraisesRewindRequested; catch it in your run loop and restorehistory = exc.messages.- Swap
InMemoryCheckpointStoreforFileCheckpointStoreto persist;fork_from_checkpointbranches a new session without disturbing the old one.
Next, let's connect the agent to the outside world.