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: datetimeCheckpoints 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() parityWhen the runtime supports checkpoints and one exists for
session_id:
session.messages,turnsandcumulative_usageare restored verbatim from the snapshot.- Memory-rehydration seeding is skipped — the restored transcript IS the state, so no duplicate system/memory blocks are injected.
promptis appended as a fresh USER turn.prompt=Noneinjects an internal continuation nudge ("Continue the interrupted task.") so the model picks up without new user input.- The loop continues. The returned
RunResultrolls 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 forkPassing 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:
| Journal | Checkpoint | |
|---|---|---|
| What it stores | Each step’s result (model call, tool dispatch), keyed by name + input fingerprint | The whole transcript — messages + usage + cursor — at a pass boundary |
| What it’s for | Memoisation: a re-run skips already-completed steps | Restoration: a resumed run starts from the snapshot, no replay walk |
| Granularity | Per step, within a pass | Per architecture pass |
| Wire format | pickle (trust the store) | JSON (model_dump_json, inspectable) |
| Enabled by | Using 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.