What this integration is
ActaeCheckpointSaver implements LangGraph’s BaseCheckpointSaver protocol on top of Actae. LangGraph still decides when to checkpoint and what a checkpoint contains; Actae is the storage and recovery backend. Nothing about your graph, nodes, or model calls changes.
What you gain over an in-memory or single-file saver:
- Cross-process resume. A new worker with the same
thread_idloads the last checkpoint from Actae and continues. - Immutable, versioned history. Every save is a new snapshot pinned to a real event cursor, so you can inspect any prior state, not just the latest.
- Interrupted-step recovery. Pending writes are merged idempotently and replayed, so a step interrupted between the model call and the tool call is restored rather than restarted.
- Exact-boundary forks. Fork a thread into a new one and refine a later node while keeping the full inherited message context.
Install and wire it up
bashpip install 'actae-client[langgraph]'
The high-level Actae facade is the shortest path. It only connects when the graph runs, and reads ACTAE_URL and ACTAE_API_KEY from the environment.
from actae_client import Actae
from langgraph.graph import StateGraph
actae = Actae.from_env()
graph = builder.compile(checkpointer=actae.langgraph.checkpointer())
result = await graph.ainvoke(input, actae.langgraph.config("ticket-42"))actae.langgraph.config(thread_id) returns the LangGraph-shaped config ({"configurable": {"thread_id": ...}}), so you do not hand-write nested config dicts. The lower-level form is equally supported:
from actae_client.adapters.langgraph import ActaeCheckpointSaver
saver = ActaeCheckpointSaver(client, channel="langgraph")
graph = builder.compile(checkpointer=saver)
config = {"configurable": {"thread_id": "ticket-42"}}
result = await graph.ainvoke(input, config)typescriptimport { Actae } from '@actae/sdk';
const actae = Actae.fromEnv();
const graph = builder.compile({ checkpointer: actae.langgraph.checkpointer() });
await graph.invoke(input, actae.langgraph.config('ticket-42'));Resume after a crash
On restart, point a new process at the same Actae and the same thread_id. The saver loads the last checkpoint; passing None as input resumes rather than starting over.
# New process, same Actae, same thread_id.
config = {"configurable": {"thread_id": "ticket-42"}}
previous = graph.get_state(config) # reads the checkpoint from Actae
if previous and previous.next: # there is unfinished work
result = await graph.ainvoke(None, config) # resume, do not restartIf the process died mid-node, the recorded pending writes restore the interrupted step. Writes are merged idempotently per (task_id, idx) — the last batch for a task wins, matching LangGraph’s in-memory semantics — and saved as a new snapshot version of the same checkpoint.
Why the cursor matters
Every snapshot is aligned to the exact Actae event cursor returned when the checkpoint event was recorded. Because cursors are strictly increasing across a thread, replay is deterministic and a snapshot can never be attributed to the wrong point in history.
Fork a run and refine a later node
This is the flagship fork-and-resume workflow applied to LangGraph: run the graph, fork the thread at its latest checkpoint (or an explicit checkpoint_id), then run a modified graph from the fork. The inherited message context is preserved, so only the changed part re-runs.
cfg = {"configurable": {"thread_id": "run-1"}}
await graph.ainvoke(input, cfg) # full run
fork_cfg = await saver.fork_thread(
cfg, new_thread_id="run-1-fix", reason="refine node 5",
)
# Build the graph with the changed node; resume from the fork's checkpoint.
fork_graph = build_modified_graph(saver=saver)
result = await fork_graph.ainvoke(None, fork_cfg)fork_thread is the async form; fork_thread_sync is the bridge for graph.invoke users. Pass a checkpoint_id inside the config to fork at an intermediate step instead of the latest one.
What is stored, per snapshot
Each Actae state snapshot is an envelope (version 1) with LangGraph’s own lossless serialization:
vEnvelope version (currently 1)thread_id, checkpoint_nsIdentity, including subgraph namespacescheckpoint_id, parent_checkpoint_idLangGraph checkpoint UUID and its parentcheckpointThe graph state, serialized with dumps_typedmetadataCheckpointMetadata (source, step, parents)pending_writesMerged task writes that restore interrupted stepsevent_cursor, event_idThe Actae event the snapshot is aligned toThe effective channel is deterministic per (thread_id, checkpoint_ns): f"{prefix}:{sha256(thread_id + NUL + checkpoint_ns)[:16]}". You can override it entirely with a channel_resolver callable, which must be deterministic and return a non-empty string.
Limitations, stated plainly
- Sync calls inside a running loop fail. The sync protocol methods run their async implementation in a fresh event loop. Calling them from inside an active loop raises
ActaeLangGraphSyncErrorrather than deadlocking. - The extra is optional. The module imports without LangGraph installed; constructing the saver then raises
ImportErrorwith the install command. - No silent migration. Data written by the pre-1.1.0 fixed-channel adapter is retained for audit but is not read by the current implementation.
- Actae does not run the graph. Scheduling, retries, and node execution remain LangGraph’s responsibility; Actae persists the boundaries.
For the underlying model, see forking & recovery and sessions & adapters.