Metadata-Version: 2.4
Name: localab-checkpoint
Version: 0.3.1
Summary: Vendor-neutral save/resume checkpoints for long-running agent conversations.
Author: agent-checkpoint
License: MIT
Project-URL: Homepage, https://github.com/Victorchatter/agent-checkpoint
Project-URL: Repository, https://github.com/Victorchatter/agent-checkpoint
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

<div align="center">

# agent-checkpoint

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

[![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](./LICENSE)
[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org)
[![Dependencies: 0](https://img.shields.io/badge/dependencies-0-success.svg)](#install)
[![Self-check](https://img.shields.io/badge/self--check-passing-brightgreen.svg)](#self-check)

[Repository](https://github.com/Victorchatter/agent-checkpoint)

</div>

<p align="center">
  <img src="./assets/demo.svg" alt="agent-checkpoint demo: save a transcript into a checkpoint, then resume any agent from it with the prior turns seeded" width="820"/>
</p>

## Table of contents

- [The problem it solves](#the-problem-it-solves)
- [The methodology](#the-methodology)
- [How it works, end to end](#how-it-works-end-to-end)
- [Cost — why resume beats re-running](#cost--why-resume-beats-re-running)
- [How this compares](#how-this-compares)
- [Install](#install)
- [Quick start](#quick-start)
- [Diff two checkpoints](#diff-two-checkpoints)
- [Merge two checkpoints](#merge-two-checkpoints)
- [The canonical checkpoint format](#the-canonical-checkpoint-format)
- [The adapter contract](#the-adapter-contract)
- [Writing your own adapter](#writing-your-own-adapter)
- [Relationship to agent-vcr](#relationship-to-agent-vcr)
- [Scope](#scope)
- [Self-check](#self-check)
- [License](#license)

---

## 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:

```mermaid
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 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](agent_checkpoint/adapters/openai_minimal.py), 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](#scope-v1).

## How it works, end to end

```mermaid
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:

```mermaid
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
```

## 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](https://pypi.org/project/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

```bash
# 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

```bash
# 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:

```bash
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) |

```bash
# 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:

```bash
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:

```json
{"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](#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`](agent_checkpoint/adapters/openai_minimal.py)
— it's under 50 lines. The whole adapter contract is:

```python
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](https://github.com/Victorchatter/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

```bash
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
