Save an agent conversation to a vendor-neutral checkpoint. Resume any agent from it — later, on any machine.
- The problem it solves
- The methodology
- How it works, end to end
- Cost — why resume beats re-running
- How this compares
- Install
- Quick start
- Diff two checkpoints
- Merge two checkpoints
- The canonical checkpoint format
- The adapter contract
- Writing your own adapter
- Relationship to agent-vcr
- Scope
- Self-check
- License
A long-running agent session is a conversation with state that lives only in one process's memory — until that process dies. Reboot the machine, get disconnected overnight, or want to hand a run off to a different box, and that state is gone unless the tool you're using happens to have its own save/resume feature. Most don't, and the ones that do all disagree with each other:
flowchart TB
subgraph before["✗ Without a shared checkpoint format"]
direction LR
b1["Claude Code session"] -.->|"its own transcript shape"| bx1(("🔒 locked to<br/>that one tool"))
b2["Custom OpenAI script"] -.->|"whatever ad-hoc state<br/>you hacked together"| bx2(("🔒 locked to<br/>that one tool"))
b3["Some other agent SDK"] -.->|"yet another<br/>internal format"| bx3(("🔒 locked to<br/>that one tool"))
end
subgraph after["✓ With agent-checkpoint"]
direction LR
a1["Claude Code session"] -->|"save"| ck[("checkpoint.jsonl<br/>canonical JSONL")]
a2["Custom OpenAI script"] -->|"save"| ck
a3["Some other agent SDK"] -->|"save"| ck
ck -->|"resume"| out(["any AGENT_CHECKPOINT_INIT-aware agent,<br/>on any machine"])
end
Every framework re-solves "how do I pick this conversation back up" its own way, in its own undocumented shape, portable to nothing else. agent-checkpoint doesn't try to replace any of those internal formats — it sits next to them as a neutral interchange: normalize whatever transcript shape you have into one canonical JSONL file, and any agent that knows the (tiny) resume convention can pick it up from there.
The design rests on three deliberate choices, each one there to keep the project small and the format actually portable rather than accidentally tied to one framework:
-
Capture the conversation, not the process. agent-checkpoint does not try to snapshot memory, open file handles, or any other internal agent state — that's what makes framework-specific checkpoint formats fragile and non-portable in the first place. It captures exactly the part that's actually shared across every agent: the message history. Anything that can decode its own transcript into
{role, content, tool_calls, tool_results}turns can be checkpointed. -
The contract is one environment variable, not an SDK. Plenty of record/replay and checkpoint tools require you to import a library, wrap your client, or restructure your code around their runtime. agent-checkpoint asks for exactly one thing: read
AGENT_CHECKPOINT_INITif it's set, and seed your own message list from the file it points to. That's small enough to add to any agent in a few lines — see the reference adapter, which does exactly this in under 50 lines. -
Never resume from a broken state. If the source transcript ends mid-turn — the process was killed while a tool call was in flight, with no result ever recorded —
savetruncates the checkpoint at the last fully-completed turn. Resume always starts from a known-good place; no adapter ever has to guess whether to re-issue a tool call whose outcome was lost.
What this deliberately does not do: undo or redo side effects. A checkpoint captures conversation state, not the external world — files the original run wrote, API calls it made, rows it changed in some database. Resuming replays the conversation; it does not replay consequences. That's a documented boundary, not a missing feature — see Scope.
flowchart LR
subgraph savepath["Save path"]
T["Provider transcript<br/>(Claude Code JSONL, or an<br/>OpenAI messages array)"] --> S{"sniff<br/>format"}
S -->|"OpenAI shape"| P1["openai_messages<br/>parser"]
S -->|"Claude Code shape"| P2["claude_code<br/>parser"]
P1 --> ENV["canonical envelopes<br/>role · content · tool_calls ·<br/>tool_results · provider · model · ts"]
P2 --> ENV
ENV --> TR["truncate_incomplete<br/>(drop a dangling tool call)"]
TR --> CKPT[("checkpoint.jsonl")]
end
subgraph resumepath["Resume path"]
CKPT --> R["agent-checkpoint resume"]
R -->|"sets AGENT_CHECKPOINT_INIT,<br/>runs the wrapped command"| PROC["your agent process"]
PROC --> AD["adapter reads the env var<br/>+ read_jsonl(...)"]
AD --> SEED["message history seeded,<br/>new turn appended,<br/>conversation continues"]
end
And the same flow as a timeline — this is literally what selfcheck.py
exercises:
sequenceDiagram
participant You
participant CLI as agent-checkpoint
participant FS as checkpoint.jsonl
participant Agent as wrapped agent process
You->>CLI: save transcript.json -o checkpoint.jsonl
CLI->>CLI: sniff format, parse into canonical turns
CLI->>CLI: truncate a dangling tool call, if any
CLI->>FS: write canonical JSONL
Note over You,Agent: …reboot, overnight suspend, new machine…
You->>CLI: resume checkpoint.jsonl -- <agent-cmd...>
CLI->>Agent: subprocess.run(agent-cmd, env + AGENT_CHECKPOINT_INIT)
Agent->>FS: read_jsonl(AGENT_CHECKPOINT_INIT)
Agent->>Agent: seed message history, append new turn
Agent-->>CLI: exit code
CLI-->>You: exit code propagated
A checkpoint is a literal serialization of the conversation; resuming is a file read plus seeding a message list. Re-running is paying for every prior turn again — N model round-trips, full token cost, plus whatever non-determinism the second pass introduces.
| Re-run from scratch | Resume from checkpoint | |
|---|---|---|
| work | replay N prior turns back through the model | read the tape + replant state |
| time | N× model round-trips (seconds to minutes) | O(file size), ~ms |
| tokens | full prior-turn token cost, billed again | zero prior-turn tokens |
| cost | proportional to N and context length | a single file read |
Measured on the synthetic 6-turn transcript from selfcheck.py (run
python benchmarks/resume_cost.py to reproduce):
checkpoint: 6 turns, 1.1 KB on disk
resume read_jsonl: 0.21 ms
Reading the tape is ~4 orders of magnitude cheaper than a single model round-trip, and the gap widens with every turn added.
Checkpointing for agents is mostly framework-locked — LangGraph and Microsoft Agent Framework both persist state, but only for graphs built inside them. This tool is vendor-neutral: it snapshots a conversation you can resume anywhere.
One neighbour is worth naming:
- agentcheckpoint by Ernesto Maldonado — an MCP checkpoint server for atomic state coordination across agents, cron workers and multi-agent systems. Different emphasis (coordinating concurrent workers vs pausing and resuming one run), adjacent territory, and it holds the similar name.
Because PyPI treats agent-checkpoint as too close to agentcheckpoint, this
project installs as localab-checkpoint while the repository, module and
agent-checkpoint CLI command keep their original names.
# From PyPI / GitHub (recommended)
pipx install git+https://github.com/Victorchatter/agent-checkpoint
# Or from a local clone
pipx install .(No pipx? pip install -e . works the same way for local development.)
# 1. Normalize an OpenAI-format transcript into a checkpoint
agent-checkpoint save transcript.json -o checkpoint.jsonl
# Saved 4 turns to checkpoint.jsonl
# 2. ...time passes. Reboot, migrate machines, whatever...
# 3. Resume ANY AGENT_CHECKPOINT_INIT-aware agent from it.
# --print-request runs fully offline — no `openai` package or API key needed —
# and is exactly what the demo above and selfcheck.py both use.
agent-checkpoint resume checkpoint.jsonl -- \
python -m agent_checkpoint.adapters.openai_minimal "What about Italy?" --print-request
# {"model": "gpt-4o-mini", "messages": [
# {"role": "user", "content": "What's the capital of France?"},
# {"role": "assistant", "content": "Paris."},
# {"role": "user", "content": "And Germany?"},
# {"role": "assistant", "content": "Berlin."},
# {"role": "user", "content": "What about Italy?"}
# ]}
# Drop --print-request to actually call the OpenAI API (requires `pip install openai`)Normalizing a Claude Code session log works the same way — save
auto-detects the input shape:
agent-checkpoint save ~/.claude/projects/my-project/session.jsonl -o checkpoint.jsonldiff compares two checkpoints while ignoring save-time metadata (ts, provider/model strings are preserved in the file but not part of the comparison). It returns an exit code so it can be used in scripts and CI:
| Exit code | Meaning |
|---|---|
0 |
checkpoints are identical in role, content, tool_calls, and tool_results |
1 |
checkpoints differ |
2 |
usage error (missing file, bad arguments, malformed JSONL) |
# Text output (default)
agent-checkpoint diff a.jsonl b.jsonl
# identical
# Machine-readable JSON output
agent-checkpoint diff a.jsonl b.jsonl -f json
# {"identical": true, "a_turns": 6, "b_turns": 6, "differences": []}merge appends turns from checkpoint B into A when they are not already present. A turn is considered a duplicate when its role, content, tool_calls, and tool_results all match. The output carries _meta.source provenance tags showing which file each turn originated from, and conflicting tool result hashes for otherwise-matching turns are reported as warnings:
agent-checkpoint merge base.jsonl extra.jsonl -o combined.jsonl
# Merged 6 turns from base.jsonl + 2 new turns from extra.jsonl into combined.jsonlThe merge always runs truncate_incomplete before writing, so a dangling tool-call turn is never persisted.
One JSON object per line, one line per conversation turn:
{"role": "user|assistant|tool", "content": ..., "tool_calls": [...]|null, "tool_results": [...]|null, "provider": "anthropic|openai", "model": "...", "ts": "2026-07-22T01:40:00Z"}| Field | Meaning |
|---|---|
role |
user, assistant, or tool. |
content |
The turn's text/structured content, provider-shape preserved as-is. |
tool_calls |
List of tool calls made in this turn, or null. |
tool_results |
List of tool results for this turn (including tool_call_id), or null. |
provider / model |
Which provider/model produced this turn, for adapters that branch on it. |
ts |
ISO 8601 UTC timestamp, best-effort — synthesized at save-time if the source lacks one. |
A trailing turn that made tool calls with no recorded result is dropped by
save — see The methodology above.
agent-checkpoint save <transcript> -o <ckpt.jsonl>normalizes a provider-specific transcript into the canonical format above.agent-checkpoint resume <ckpt.jsonl> -- <agent-cmd...>setsAGENT_CHECKPOINT_INIT=<abspath to ckpt.jsonl>, runs the wrapped command, and propagates its exit code.- Any agent that reads
AGENT_CHECKPOINT_INITand seeds its own message history from the checkpoint file can resume. That's the entire contract — this project ships one reference adapter to prove it works, not an adapter for every framework.
Read agent_checkpoint/adapters/openai_minimal.py
— it's under 50 lines. The whole adapter contract is:
checkpoint_path = os.environ.get("AGENT_CHECKPOINT_INIT")
if checkpoint_path:
messages = [envelope_to_message(e) for e in read_jsonl(checkpoint_path)]
# ...append your own new turn and continue as normalConvert each envelope into your framework's message shape (for a role: "tool" turn, remember to carry tool_call_id from tool_results[0] — see
the reference adapter for the exact round-trip), and your agent can resume
from any checkpoint this tool produces.
agent-vcr's tape is
wire-level: one JSON line per HTTP event ({kind, seq, provider, body}),
capturing raw request/response bytes for record/replay of model and MCP
traffic. agent-checkpoint's format is turn-level: one JSON line per
decoded conversation turn, meant to directly seed an agent's message
history. These are different granularities by necessity — a checkpoint has
to hand an agent something it can turn straight into messages, not raw
wire bytes it would need to re-parse.
The two formats share a convention — JSONL, one event per line, explicit
provider tagging — and no code. There's no shared envelope and no
tape-import parser in v1.
| Ships | Explicitly out of scope |
|---|---|
| Canonical turn-level JSONL format | Adapters for every framework |
| Claude Code session JSONL parser | Live attach to a running process |
| OpenAI messages array parser | Automatic periodic checkpointing |
save / resume CLI |
Encryption |
diff / merge CLI |
agent-vcr tape import |
| One reference adapter (OpenAI) | Undoing/redoing side effects |
AGENT_CHECKPOINT_INIT resume convention |
Multi-agent real-time coordination |
python selfcheck.pySaves a synthetic transcript, resumes it through the reference adapter
(including a tool-call turn), and asserts the resumed agent's first request
contains every prior turn plus the new one, in order — plain assert
statements, no test framework, matching the project's zero-dependency stance.
MIT