Workflow durability: LangGraph checkpointer support - #8
Merged
Conversation
This was referenced Jul 26, 2026
Collaborator
Author
|
Review follow-up pushed to this branch: the postgres arm constructed the sync |
BaseWorkflow.durable (default False) compiles with a checkpointer resolved from a new CheckpointSettings group (AGENTDECK_CHECKPOINT_*, backend: sqlite|postgres|memory). App.run_workflow / BaseWorkflow.run take thread_id, threaded into LangGraph's configurable.thread_id so a run can resume; durable=True with no thread_id raises. sqlite/postgres ship in a new optional [durability] extra, lazy-imported with a clear error if missing. durable=False is unchanged behavior. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- AsyncPostgresSaver (sync PostgresSaver raises NotImplementedError on async methods — same trap the sqlite arm dodged), entered via _run_sync - _run_sync re-raises bootstrap-thread exceptions instead of IndexError - stub-based wiring test that fails on the sync/async mixup, no server needed - ty: unused-ignore-comment off — extra-dependent ignores are env-relative Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
sagi5060
force-pushed
the
feat/3-workflow-durability
branch
from
July 26, 2026 20:16
ba7ed5c to
ebc3b27
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3
What
BaseWorkflowgains a declarativedurable: ClassVar[bool] = Falseopt-in. WhenTrue:build()compiles the graph with a checkpointer resolved from a newCheckpointSettingsgroup (AGENTDECK_CHECKPOINT_*env /checkpoint:YAML section, following the existingLayeredSettingspattern exactly):backend(sqlite|postgres|memory, defaultsqlite) andurl(sqlite file path or postgres DSN).App.run_workflow(...)andBaseWorkflow.run(...)takethread_id: str | None = None, threaded into LangGraph'sconfig={"configurable": {"thread_id": ...}}so a run can resume state from a checkpoint.durable=Truewith nothread_idraises a clearValueErrorinstead of silently running unscoped.durable=False(the default) compiles and runs byte-for-byte as before — verified by the existingtests/test_app.pysuite passing unchanged.agentdeck.runtime.checkpointer.resolve_checkpointer()is the one place that turns settings into a saver:memoryuseslanggraph.checkpoint.memory.MemorySaver, which ships with corelanggraph— no extra needed.sqlite/postgreslive in a new optional[durability]extra (langgraph-checkpoint-sqlite,langgraph-checkpoint-postgres), imported lazily. Ifdurable=Truepicks one of these backends and the extra isn't installed, it raises a clearImportErrorwith an install hint instead of a bareModuleNotFoundError— mirroring howruntime/observability.pydegrades for the optional Langfuse extra.Why
Durable, resumable workflow instances are the core requirement of the Middle PRD (§11: durable waits, resume on restart, optimistic concurrency). Non-durable workflows (the common case today) are completely unaffected.
Notable decisions / deviations
langgraph-checkpoint-sqlitepulls inaiosqliteandsqlite-vec(a LangGraph vector-store dependency unrelated to checkpointing) as transitive requirements. Given "new dependencies need a reason the stdlib or an existing dep can't cover" and the unrelatedsqlite-vecpull-in, both sqlite and postgres landed in the optional[durability]extra rather than core deps.SqliteSaverraisesNotImplementedErroron every async method (aget_tuple, etc.), and the workflow runner always callsgraph.ainvoke. The sqlite backend therefore usesAsyncSqliteSaver(aiosqlite-backed), whose one-shot connection handshake is async;checkpointer._run_syncbridges that fromBaseWorkflow.build()(sync), handling both the "no loop running yet" and "already inside the caller's event loop" cases.AsyncSqliteSaverholds anasyncio.Lockthat binds to whichever event loop first acquires it, andBaseWorkflow._compiledcaches the compiled graph (checkpointer included) for the class's lifetime. A script that callsasyncio.run()more than once against the same durable workflow class in one process will hit "Lock ... bound to a different event loop" on the second call. This is a non-issue for the intended shape (one long-lived loop per process — a server, or a single top-levelasyncio.run), and is called out as future work incheckpointer._sqlite_saver's docstring rather than solved here, to keep this change minimal.App's lifecycle,aclose(), orserve.py. The checkpointer connection is a module-level cache inruntime/checkpointer.pyfor the process lifetime — wiring it intoApp.aclose()for graceful shutdown is explicit follow-up work once App lifecycle: async open/close + DI seam #1 lands.Test plan
New
tests/test_workflow_durability.py:durable=Falseignoresthread_idand never persists (existing behavior unchanged)durable=Truewith nothread_idraisesValueErrormemorybackend: samethread_idaccumulates state across invokes in one event loop; a differentthread_idstarts freshsqlitebackend (skipped if[durability]isn't installed): same-loop sequential invokes accumulate, mirroring one long-lived server processsqlitebackend, genuine cross-process restart: two separatesubprocessinvocations against the same sqlite file andthread_id— the second resumes from the first's checkpoint. This is the issue's actual acceptance test ("interrupted mid-graph resumes ... after process restart").backendvalue raises a clearValueError.[dev,serve]installed (no[durability]extra) —make checkpasses, sqlite/postgres tests skip gracefully viapytest.importorskip🤖 Generated with Claude Code