Skip to Content
DocsRuntimeCheckpoints

Checkpoints

With Tuning(checkpoint=True), the agent loop persists a transcript snapshot — messages, turn count, cumulative usage — after every architecture pass. A crashed run resumes from the latest snapshot with agent.resume(): prior turns are restored verbatim, never re-executed, never re-billed.

from loomflow import Agent, Tuning from loomflow.runtime import SqliteRuntime agent = Agent( "...", model="claude-opus-4-7", runtime=SqliteRuntime("./journal.db"), tuning=Tuning(checkpoint=True), ) # First run — killed by OOM / deploy / Ctrl-C partway through: await agent.run("long research task", session_id="research-2026-07-04") # After restart — restore the latest checkpoint and keep going. # The restored turns are accounted in the result's usage but the # model is never re-invoked for them. result = await agent.resume( "pick it up where you left off", session_id="research-2026-07-04", )

Checkpointing is opt-in (default False — zero behaviour change), and checkpoint-write failures are logged as warnings and never kill the run: it’s a durability optimisation, not a correctness dependency.


What a checkpoint contains

class Checkpoint(BaseModel): session_id: str checkpoint_id: str # auto: new_id("ckpt"), time-sortable turn: int messages: list[Message] # the full transcript cumulative_usage: Usage # tokens + cost up to this point cursor: str | None = None # architecture-local re-entry marker created_at: datetime

Checkpoints are serialised as JSON via model_dump_json — never pickle — so payloads are inspectable with jq and safe to load even from a store an attacker could write to. (Journal step entries, by contrast, are pickled — see the trust note on Runtime backends.)

One checkpoint is written per architecture pass: the first pass, and each stop-hook (Ralph) iteration. Snapshots are taken after auto-compaction runs, so a compacted transcript is what gets stored.


agent.resume(...)

async def resume( self, prompt: str | None = None, *, session_id: str, from_checkpoint: str = "latest", user_id: str | None = None, **kwargs, # metadata, context, extra_tools, emit, ) -> RunResult: # output_schema, effort, ... — full run() parity

When the runtime supports checkpoints and one exists for session_id:

  1. session.messages, turns and cumulative_usage are restored verbatim from the snapshot.
  2. Memory-rehydration seeding is skipped — the restored transcript IS the state, so no duplicate system/memory blocks are injected.
  3. prompt is appended as a fresh USER turn. prompt=None injects an internal continuation nudge ("Continue the interrupted task.") so the model picks up without new user input.
  4. The loop continues. The returned RunResult rolls up restored + new usage — prior turns are accounted, not re-billed: no model call is repeated for them.

Fallback. When the runtime has no checkpoint support, or no checkpoint exists for the session (e.g. the run predates Tuning(checkpoint=True)), resume() degrades to the legacy path: run(prompt, session_id=session_id) — memory rehydration rebuilds the conversation and, on a durable runtime, already-journaled steps replay instead of re-executing. See Replay and resume.


Fork from an older checkpoint

from_checkpoint selects the snapshot. "latest" (the default) continues the session in place. An explicit checkpoint id that is not the latest forks: a fresh session_id is derived (new_id("sess")), the checkpoint’s messages are copied in, and the run proceeds under the fork id — the original session’s history and checkpoints are untouched.

metas = await agent.list_checkpoints("research-2026-07-04") older = metas[-1] # newest-first ⇒ last = earliest result = await agent.resume( "try a different approach from here", session_id="research-2026-07-04", from_checkpoint=older.checkpoint_id, ) result.session_id # "sess..." — a NEW id: the fork

Passing the id of the latest checkpoint continues in place, same as "latest" — no fork.


agent.list_checkpoints(...)

metas = await agent.list_checkpoints(session_id, limit=50) for m in metas: # newest first print(m.checkpoint_id, m.turn, m.created_at)

Returns list[CheckpointMeta] — id / turn / timestamp only, no message payload, so listing is cheap. Returns [] when the runtime has no checkpoint support.


Retention

Every checkpoint-capable runtime takes max_checkpoints_per_session (default 20): on each write, the session’s oldest checkpoints beyond the limit are dropped. Values < 1 disable pruning (unbounded).

SqliteRuntime("./journal.db", max_checkpoints_per_session=5) runtime = await PostgresRuntime.connect(dsn, max_checkpoints_per_session=50) InProcRuntime(max_checkpoints_per_session=20)

On the journaled runtimes, runtime.store.prune(before=..., session_id=...) deletes journal entries and checkpoints matching the filters — use it to purge completed sessions. Nothing else prunes automatically.

InProcRuntime checkpoints are process-local. The default runtime implements the full checkpoint API, so Tuning(checkpoint=True) + resume() works within one process (useful for tests and long-running services that survive logical failures) — but the snapshots live in memory and are lost on exit. For crash durability, use SqliteRuntime or PostgresRuntime.


Journal vs checkpoint

Both live in the same store, and they solve different problems:

JournalCheckpoint
What it storesEach step’s result (model call, tool dispatch), keyed by name + input fingerprintThe whole transcript — messages + usage + cursor — at a pass boundary
What it’s forMemoisation: a re-run skips already-completed stepsRestoration: a resumed run starts from the snapshot, no replay walk
GranularityPer step, within a passPer architecture pass
Wire formatpickle (trust the store)JSON (model_dump_json, inspectable)
Enabled byUsing a JournaledRuntime (Sqlite / Postgres)Tuning(checkpoint=True) on any checkpoint-capable runtime

They compose: on a durable runtime with checkpointing on, resume() restores the latest snapshot directly, and the journal still dedupes any step that re-fires with identical inputs after the restore point.

Checkpoints must have been written to be restored. resume() on a session whose runs never had Tuning(checkpoint=True) silently takes the legacy path — that’s the designed fallback, not an error. Check agent.list_checkpoints(session_id) if you need to know which path you’ll get.

For crash-recovery semantics of the journal itself — step keys, the determinism contract, at-least-once delivery — see Replay and resume. For pausing a live run and resuming it on human input, see Signals.

Last updated on