INTEGRATION

LangGraph + Actae.

LangGraph owns the graph execution and its message state. Actae supplies the Checkpointer, so every graph checkpoint becomes an immutable, cursor-aligned snapshot that survives a process crash and can be forked.

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_id loads 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

bash
pip 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.

python
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:

python
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)
typescript
import { 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.

python
# 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 restart

If 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.

python
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:

FieldMeaning
vEnvelope version (currently 1)
thread_id, checkpoint_nsIdentity, including subgraph namespaces
checkpoint_id, parent_checkpoint_idLangGraph checkpoint UUID and its parent
checkpointThe graph state, serialized with dumps_typed
metadataCheckpointMetadata (source, step, parents)
pending_writesMerged task writes that restore interrupted steps
event_cursor, event_idThe Actae event the snapshot is aligned to

The 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 ActaeLangGraphSyncError rather than deadlocking.
  • The extra is optional. The module imports without LangGraph installed; constructing the saver then raises ImportError with 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.