Durable execution
Checkpoint per iteration; resume from the exact step after a kill.
In short: if your agent is interrupted — a crash, a deploy, a server restart — it picks up exactly where it stopped. It doesn't lose the work, and it doesn't redo the parts it already finished. This is the property everything else here depends on.
An agent run is a long-lived, side-effecting process. It calls tools that charge cards, open pull requests and send email. Treating it as an in-memory function call means a deploy, an OOM kill, or a spot-instance reclaim loses the run — or worse, re-runs half of it.
The guarantee
A run is checkpointed after every iteration, not at phase boundaries. After a process death,
resume rebuilds from the last checkpoint and continues from the exact step it was on.
Resume replays state, never effects. A tool that already ran does not run again — its result was persisted with the checkpoint and is stitched back into the transcript.
| llm | claude-sonnet-4-5 | 1 tool call | $0.0031 |
| tool | kb_search | ok · 3 hits — result persisted | — |
| llm | claude-sonnet-4-5 | 1 tool call | $0.0028 |
| tool | order_lookup | ok · ord_1001 — result persisted | — |
| hitl | approval_requested | ◀ SIGKILL here. Everything above survives. | — |
What a checkpoint holds
class StateSnapshot(BaseModel):
run_id: str
context: RunContext # counters, budget, usage, session
messages: list[Message] # authoritative for loop-owning adapters
sdk_state: dict[str, Any] # opaque — the runtime never interprets it
pending: PendingToolCall | None # the call a paused run is waiting on
saved_at: int
sdk_state is the interesting field. Graph-native SDKs like LangGraph persist their own channel
state through their own checkpointer contract; agentFast stores and returns that blob without ever
looking inside it. The house rule is that L3 never touches an SDK's persistence primitives directly
— which is what stops the production layer from acquiring five special cases.
The subtle part: sibling tool calls
When a model emits several tool calls in one batch and one of them needs approval, the non-gated
siblings execute first, and their results are persisted in
snapshot.pending.executed_sibling_results.
Without that, resuming would either re-execute the siblings (double side effects) or lose their results (the model reasons from an incomplete transcript). Both are wrong in ways that are hard to notice until they cost money.
Budgets
Durability isn't a licence to run forever. Every run carries a budget checked at the top of each iteration:
budget:
max_iterations: 50
max_tokens: 150000
max_duration_ms: 300000
Exceeding one ends the run as error with a human-readable reason rather than silently looping. The
complexity classifier adjusts these per-run before the first model call,
so a simple question doesn't carry a research budget.
The proof
The flagship test kills a real OS process mid-approval, against real Postgres, and asserts the run resumes from the exact step with exactly one refund issued:
pytest tests/test_flagship_kill_resume.py -q # LangGraph
pytest tests/test_flagship_vanilla.py -q # the vanilla loop
The quickstart walks through doing this by hand with
docker compose kill api. It takes about a minute, and it's more convincing than reading about
it.