Skip to content

Repository files navigation

agent-checkpoint

Save an agent conversation to a vendor-neutral checkpoint. Resume any agent from it — later, on any machine.

License: MIT Python 3.10+ Dependencies: 0 Self-check

Repository

agent-checkpoint demo: save a transcript into a checkpoint, then resume any agent from it with the prior turns seeded

Table of contents


The problem it solves

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
Loading

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 methodology

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:

  1. 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.

  2. 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_INIT if 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.

  3. 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 — save truncates 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.

How it works, end to end

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
Loading

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
Loading

Cost — why resume beats re-running

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.

How this compares

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.


Install

# 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.)

Quick start

# 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.jsonl

Diff two checkpoints

diff 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 two checkpoints

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.jsonl

The merge always runs truncate_incomplete before writing, so a dangling tool-call turn is never persisted.

The canonical checkpoint format

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.

The adapter contract

  1. agent-checkpoint save <transcript> -o <ckpt.jsonl> normalizes a provider-specific transcript into the canonical format above.
  2. agent-checkpoint resume <ckpt.jsonl> -- <agent-cmd...> sets AGENT_CHECKPOINT_INIT=<abspath to ckpt.jsonl>, runs the wrapped command, and propagates its exit code.
  3. Any agent that reads AGENT_CHECKPOINT_INIT and 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.

Writing your own adapter

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 normal

Convert 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.

Relationship to agent-vcr

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.

Scope

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

Self-check

python selfcheck.py

Saves 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.

License

MIT

About

Pause an agent run. Resume it anywhere, later.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages