LangGraph "does not provide an export named 'LastValue'": ten classes declared but not exported
On @langchain/langgraph 1.4.19 the package root's type declarations export 212 names and its runtime exports 95. Ten of the gap are classes, tsc compiles the broken import without complaint, and one of the imports that fails is copied verbatim from the vendor's own JavaScript documentation.
Updated on October 10, 2026
On this page
Quick answer (October 2026). On
@langchain/langgraph 1.4.19, the package root's TypeScript declaration file exports 212 names while the root's runtime build exports 95. Of the 117 that are declared and not exported, 107 are type-only and correctly absent. Ten are classes, and importing any of them from the package root throws SyntaxError: The requested module '@langchain/langgraph' does not provide an export named 'LastValue' under ESM, or silently yields undefined under CommonJS.
tsc compiles the broken import with zero errors. One of the imports that fails is copied verbatim from the vendor's own JavaScript documentation. The ten classes do exist, on the @langchain/langgraph/channels and @langchain/langgraph/pregel subpaths.
This tutorial reproduces all of that from a clean directory, then shows a second and more damaging trap waiting on the other side of the corrected import: a Topic channel whose checkpoint has two possible shapes, where the restore path guesses between them structurally and can guess wrong.
The error, as you meet it
You are defining graph state and you want a channel class rather than a reducer. You copy the import from the docs, run it, and get this:
SyntaxError: The requested module '@langchain/langgraph' does not provide an export named 'LastValue'
Nothing is wrong with your install, your Node version, or your module resolution. The name really is exported by the type declarations and really is not exported by the runtime build.
Where that import came from
It is not invented. The vendor's JavaScript graph API page, under a heading reading Using channel classes directly, introduces a sample with the sentence "For more control, you can instantiate channel classes directly:" and the sample opens with these two lines:
import { BaseMessage } from "@langchain/core/messages";
import { StateGraph, LastValue, BinaryOperatorAggregate, Topic } from "@langchain/langgraph";
I am quoting the two import lines only. The rest of that sample uses TypeScript generic syntax, which this site does not render reliably, so I am describing it instead of reproducing it: it declares an interface for the workflow state, then constructs a StateGraph whose channels object sets messages to a new BinaryOperatorAggregate(...), question to a new LastValue(), and events to a new Topic().
Three of those four imported names behave differently at runtime. StateGraph and BinaryOperatorAggregate are real runtime exports of the package root. LastValue and Topic are not. ESM reports the first missing binding it reaches, which is why the error names LastValue and never mentions Topic.
That page never mentions the @langchain/langgraph/channels subpath. I counted: @langchain/langgraph/channels occurs zero times in its visible text. The subpath is documented, but on a different page.
Install
npm init -y
npm install --save-exact @langchain/langgraph@1.4.19 zod@4.6.5 typescript@5.9.3
What is actually missing
The measurement that matters is not "117 names are missing", because most of a declaration file is types and types are supposed to be absent from a runtime build. The question is how many value declarations are missing. This script reads the root declaration file's export block, classifies every name by how it is declared anywhere in the shipped .d.ts files, and diffs that against the live runtime exports.
Two controls keep the answer honest. The classifier has to recognise classes that are present, otherwise a count of ten could just mean it never matches anything. And nothing in the absent set may come back unclassified, otherwise ten could be an undercount.
// doc-import.mjs
// The import line exactly as the vendor documents it under "Using channel
// classes directly" on docs.langchain.com/oss/javascript/langgraph/use-graph-api
import { StateGraph, LastValue, BinaryOperatorAggregate, Topic } from "@langchain/langgraph";
export const loaded = {
StateGraph: typeof StateGraph,
LastValue: typeof LastValue,
BinaryOperatorAggregate: typeof BinaryOperatorAggregate,
Topic: typeof Topic,
};
// exports.mjs
import fs from 'node:fs';
import path from 'node:path';
import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
const lines = [];
function say(s) { lines.push(s); }
// ---------- measured versions, not asserted ----------
function version(spec) {
let dir = path.dirname(require.resolve(spec));
for (let i = 0; i !== 8; i += 1) {
const f = path.join(dir, 'package.json');
if (fs.existsSync(f)) {
const j = JSON.parse(fs.readFileSync(f, 'utf8'));
if (j.name) return j.name + '@' + j.version;
}
dir = path.dirname(dir);
}
return 'unknown';
}
say('node ' + process.version);
say('langgraph ' + version('@langchain/langgraph'));
say('');
const PKG = 'node_modules/@langchain/langgraph';
const DIST = PKG + '/dist';
// ---------- 1. subpath census, and is any of them a wildcard ----------
const manifest = JSON.parse(fs.readFileSync(PKG + '/package.json', 'utf8'));
const subpaths = Object.keys(manifest.exports || {});
say('1. SUBPATHS declared: ' + subpaths.length);
say(' ' + subpaths.join(' '));
say(' any wildcard subpath: ' + subpaths.some(function (k) { return k.includes('*'); }));
say('');
// ---------- 2. declared at the root vs exported at runtime ----------
function allDts(dir, acc) {
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
const p = dir + '/' + e.name;
if (e.isDirectory()) allDts(p, acc);
else if (e.name.endsWith('.d.ts')) acc.push(p);
}
return acc;
}
const everyDts = allDts(DIST, []).map(function (f) { return fs.readFileSync(f, 'utf8'); }).join('\n');
const rootDts = fs.readFileSync(DIST + '/index.d.ts', 'utf8');
const block = rootDts.match(/export \{([^}]*)\};/s);
const declared = block[1]
.split(',')
.map(function (s) { return s.trim(); })
.filter(Boolean)
.map(function (s) { return s.includes(' as ') ? s.split(' as ')[1].trim() : s; });
const runtime = new Set(Object.keys(await import('@langchain/langgraph')));
// Order matters, and no angle bracket is needed: a trailing non-word char suffices.
function declarationKind(name) {
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
const tail = '(?![A-Za-z0-9_])';
if (new RegExp('declare class ' + esc + tail).test(everyDts)) return 'class';
if (new RegExp('declare function ' + esc + tail).test(everyDts)) return 'function';
if (new RegExp('declare (?:const|let|var) ' + esc + tail).test(everyDts)) return 'const';
if (new RegExp('interface ' + esc + tail).test(everyDts)) return 'TYPE-interface';
if (new RegExp('type ' + esc + tail).test(everyDts)) return 'TYPE-alias';
return 'UNCLASSIFIED';
}
const rows = declared.map(function (n) {
return { name: n, kind: declarationKind(n), atRuntime: runtime.has(n) };
});
const absent = rows.filter(function (r) { return !r.atRuntime; });
say('2. ROOT ENTRY POINT');
say(' names in the root .d.ts export block : ' + rows.length);
say(' names present at runtime : ' + (rows.length - absent.length));
say(' declared but ABSENT at runtime : ' + absent.length);
const tally = {};
for (const r of absent) { tally[r.kind] = (tally[r.kind] || 0) + 1; }
say(' absent, by declaration kind:');
for (const k of Object.keys(tally).sort()) { say(' ' + k + ': ' + tally[k]); }
// A type-only name is CORRECTLY absent from a runtime build. Only value
// declarations are supposed to be there.
const VALUE_KINDS = ['class', 'function', 'const'];
const valuesAbsent = absent.filter(function (r) { return VALUE_KINDS.includes(r.kind); });
say('');
say(' VALUE declarations exported by the .d.ts and missing at runtime: ' + valuesAbsent.length);
for (const r of valuesAbsent) { say(' ' + r.kind + ' ' + r.name); }
// CONTROL: the classifier must recognise classes that ARE present, or a zero
// above would just mean the classifier never matches anything. And nothing in
// the absent set may be unclassified, or the 10 could be an undercount.
const classesPresent = rows.filter(function (r) { return r.atRuntime && r.kind === 'class'; });
const unclassifiedAbsent = absent.filter(function (r) { return r.kind === 'UNCLASSIFIED'; });
const unclassifiedPresent = rows.filter(function (r) { return r.atRuntime && r.kind === 'UNCLASSIFIED'; });
say('');
say(' CONTROL runtime-present names classified as class : ' + classesPresent.length);
say(' CONTROL unclassified among the ABSENT set : ' + unclassifiedAbsent.length);
say(' CONTROL unclassified among runtime-present names : ' + unclassifiedPresent.length);
say(' CONTROL absent tally sums to absent total : ' +
(Object.keys(tally).reduce(function (a, k) { return a + tally[k]; }, 0) === absent.length));
say('');
// ---------- 3. the three import arms ----------
say('3. IMPORT ARMS');
say(' A1 the vendor documented import, ESM');
try {
const m = await import('./doc-import.mjs');
say(' loaded: ' + JSON.stringify(m.loaded));
} catch (e) {
say(' ' + e.constructor.name + ': ' + e.message);
}
say(' A2 CONTROL the same import minus the two missing names');
try {
const m = await import('@langchain/langgraph');
say(' StateGraph ' + typeof m.StateGraph);
say(' BinaryOperatorAggregate ' + typeof m.BinaryOperatorAggregate);
} catch (e) {
say(' ' + e.constructor.name + ': ' + e.message);
}
say(' A3 CONTROL the channels subpath, which is where they live');
try {
const m = await import('@langchain/langgraph/channels');
say(' LastValue ' + typeof m.LastValue + ' Topic ' + typeof m.Topic);
say(' channels subpath export count: ' + Object.keys(m).length);
} catch (e) {
say(' ' + e.constructor.name + ': ' + e.message);
}
say(' A4 the same documented names under CommonJS');
const cjs = require('@langchain/langgraph');
for (const n of ['StateGraph', 'LastValue', 'BinaryOperatorAggregate', 'Topic']) {
say(' ' + n.padEnd(24) + typeof cjs[n]);
}
try {
const unused = new cjs.LastValue();
say(' constructed LastValue: ' + typeof unused);
} catch (e) {
say(' new cjs.LastValue() -- ' + e.constructor.name + ': ' + e.message);
}
fs.writeSync(1, lines.join('\n') + '\n');
The TypeScript half
This is the part that makes the defect survive code review. The broken import type-checks cleanly under
strict with nodenext resolution, because the declaration file genuinely does export the name.
// tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "nodenext",
"moduleResolution": "nodenext",
"strict": true,
"noEmit": true,
"skipLibCheck": true
},
"files": ["check-root.ts"]
}
// check-root.ts
// Same four names the vendor documents importing from the package root.
import { StateGraph, LastValue, BinaryOperatorAggregate, Topic } from "@langchain/langgraph";
const graph = StateGraph;
const lastValue = new LastValue();
const aggregate = new BinaryOperatorAggregate(function (a, b) { return a; });
const topic = new Topic({ accumulate: true });
console.log(typeof graph, typeof lastValue, typeof aggregate, typeof topic);
The second trap, on the corrected import
Fix the import to @langchain/langgraph/channels and Topic works. Now look at what it stores.
Topic serialises itself two different ways. With unique: false, which is the default, checkpoint() returns a flat list of the buffered values. With unique: true it returns a two-element tuple of [seen, values], so that deduplication history survives a resume. The restore path has to tell those two shapes apart, and it does so structurally, not by reading the flag: a checkpoint is treated as the tuple form when it is an array of length two whose first and second elements are both arrays.
A non-unique Topic holding exactly two values that are themselves arrays therefore produces a flat checkpoint that is indistinguishable from the tuple form. On restore it is read as [seen, values]: the first value is absorbed into the dedupe set and thrown away, and the second value is unwrapped into the buffer.
The vendor's own source comment shows this collision was already known in one direction. It records that the checkpoint used to always return the tuple, so an empty topic serialised as two empty arrays and a Python checkpointer rejected it. The producer was changed to emit the flat form. The consumer still reads two empty arrays as a tuple.
// resume.mjs
import fs from 'node:fs';
import { StateGraph, Annotation, MemorySaver } from '@langchain/langgraph';
import { Topic } from '@langchain/langgraph/channels';
const lines = [];
function say(s) { lines.push(s); }
// ---------- part 1: the channel contract on its own ----------
// checkpoint() then fromCheckpoint() is exactly what a checkpointer does.
function roundTrip(label, fields, writes) {
const ch = new Topic(fields);
ch.update(writes);
const before = JSON.stringify(ch.values);
const stored = ch.checkpoint();
const after = JSON.stringify(ch.fromCheckpoint(stored).values);
say(' ' + label);
say(' buffer before : ' + before);
say(' checkpoint : ' + JSON.stringify(stored));
say(' buffer after : ' + after);
say(' IDENTICAL : ' + (before === after));
}
say('1. CHANNEL ROUND TRIP, unique defaults to false');
// update() flattens one level, so each element of the written array is one value.
roundTrip('B1 CONTROL three array values', { accumulate: true }, [[[1, 2]], [[3, 4]], [[5, 6]]]);
roundTrip('B2 exactly TWO array values', { accumulate: true }, [[[1, 2]], [[3, 4]]]);
roundTrip('B3 exactly TWO empty arrays', { accumulate: true }, [[[]], [[]]]);
roundTrip('B4 CONTROL two scalar values', { accumulate: true }, [1, 2]);
roundTrip('B5 CONTROL unique true, two arrays', { accumulate: true, unique: true }, [[[1, 2]], [[3, 4]]]);
say('');
// ---------- part 2: the same thing through a real graph ----------
const Spec = Annotation.Root({
batches: function () { return new Topic({ accumulate: true }); },
});
function build(saver) {
const g = new StateGraph(Spec);
g.addNode('step', function (state) {
say(' node reads batches = ' + JSON.stringify(state.batches));
return {};
});
g.addEdge('__start__', 'step');
g.addEdge('step', '__end__');
return g.compile({ checkpointer: saver });
}
async function acrossResume(label, write) {
const saver = new MemorySaver();
const cfg = { configurable: { thread_id: 'thread-1' } };
say(' ' + label + ' input ' + JSON.stringify(write));
const first = await build(saver).invoke({ batches: write }, cfg);
say(' state after turn 1 = ' + JSON.stringify(first.batches));
// A brand new compiled graph with brand new channels, same checkpointer and
// same thread. This is what a process restart looks like.
const second = await build(saver).invoke({ batches: [] }, cfg);
say(' state after turn 2 = ' + JSON.stringify(second.batches));
say(' SURVIVED RESUME : ' +
(JSON.stringify(first.batches) === JSON.stringify(second.batches)));
}
say('2. THE SAME CHANNEL INSIDE A GRAPH, ACROSS A RESUME');
await acrossResume('B6 CONTROL three array values', [[1, 2], [3, 4], [5, 6]]);
await acrossResume('B7 exactly TWO array values', [[1, 2], [3, 4]]);
await acrossResume('B8 exactly TWO empty arrays', [[], []]);
await acrossResume('B9 CONTROL two scalar values', [1, 2]);
fs.writeSync(1, lines.join('\n') + '\n');
Run it
npx tsc -p tsconfig.json && echo "tsc: 0 errors"
node exports.mjs
node resume.mjs
Output from a clean directory, verbatim:
tsc: 0 errors
node v24.8.0
langgraph @langchain/langgraph@1.4.19
1. SUBPATHS declared: 10
. ./web ./channels ./pregel ./prebuilt ./remote ./stream ./zod ./zod/schema ./package.json
any wildcard subpath: false
2. ROOT ENTRY POINT
names in the root .d.ts export block : 212
names present at runtime : 95
declared but ABSENT at runtime : 117
absent, by declaration kind:
TYPE-alias: 77
TYPE-interface: 30
class: 10
VALUE declarations exported by the .d.ts and missing at runtime: 10
class AnnotationRoot
class AnyValue
class CompiledGraph
class DynamicBarrierValue
class EphemeralValue
class LastValue
class NamedBarrierValue
class Pregel
class PregelNode
class Topic
CONTROL runtime-present names classified as class : 37
CONTROL unclassified among the ABSENT set : 0
CONTROL unclassified among runtime-present names : 11
CONTROL absent tally sums to absent total : true
3. IMPORT ARMS
A1 the vendor documented import, ESM
SyntaxError: The requested module '@langchain/langgraph' does not provide an export named 'LastValue'
A2 CONTROL the same import minus the two missing names
StateGraph function
BinaryOperatorAggregate function
A3 CONTROL the channels subpath, which is where they live
LastValue function Topic function
channels subpath export count: 18
A4 the same documented names under CommonJS
StateGraph function
LastValue undefined
BinaryOperatorAggregate function
Topic undefined
new cjs.LastValue() -- TypeError: cjs.LastValue is not a constructor
1. CHANNEL ROUND TRIP, unique defaults to false
B1 CONTROL three array values
buffer before : [[1,2],[3,4],[5,6]]
checkpoint : [[1,2],[3,4],[5,6]]
buffer after : [[1,2],[3,4],[5,6]]
IDENTICAL : true
B2 exactly TWO array values
buffer before : [[1,2],[3,4]]
checkpoint : [[1,2],[3,4]]
buffer after : [3,4]
IDENTICAL : false
B3 exactly TWO empty arrays
buffer before : [[],[]]
checkpoint : [[],[]]
buffer after : []
IDENTICAL : false
B4 CONTROL two scalar values
buffer before : [1,2]
checkpoint : [1,2]
buffer after : [1,2]
IDENTICAL : true
B5 CONTROL unique true, two arrays
buffer before : [[1,2],[3,4]]
checkpoint : [[[1,2],[3,4]],[[1,2],[3,4]]]
buffer after : [[1,2],[3,4]]
IDENTICAL : true
2. THE SAME CHANNEL INSIDE A GRAPH, ACROSS A RESUME
B6 CONTROL three array values input [[1,2],[3,4],[5,6]]
node reads batches = [[1,2],[3,4],[5,6]]
state after turn 1 = [[1,2],[3,4],[5,6]]
node reads batches = [[1,2],[3,4],[5,6]]
state after turn 2 = [[1,2],[3,4],[5,6]]
SURVIVED RESUME : true
B7 exactly TWO array values input [[1,2],[3,4]]
node reads batches = [[1,2],[3,4]]
state after turn 1 = [[1,2],[3,4]]
node reads batches = [3,4]
state after turn 2 = [3,4]
SURVIVED RESUME : false
B8 exactly TWO empty arrays input [[],[]]
node reads batches = [[],[]]
state after turn 1 = [[],[]]
node reads batches = undefined
state after turn 2 = undefined
SURVIVED RESUME : false
B9 CONTROL two scalar values input [1,2]
node reads batches = [1,2]
state after turn 1 = [1,2]
node reads batches = [1,2]
state after turn 2 = [1,2]
SURVIVED RESUME : true
Reading the result
The export half. 212 declared, 95 at runtime, 117 absent, of which 77 are type aliases and 30 are interfaces. That leaves exactly ten classes: AnnotationRoot, AnyValue, CompiledGraph, DynamicBarrierValue, EphemeralValue, LastValue, NamedBarrierValue, Pregel, PregelNode, Topic. The controls fire: 37 runtime-present names classify as classes, nothing in the absent set is unclassified, and the per-kind tally sums back to the absent total. Arm A2 shows the same import succeeds once the two missing names are removed, and arm A3 shows both classes load fine from the channels subpath, so the classes exist and only the root path is wrong.
The CommonJS half. Arm A4 is the one to worry about. Under
require, the two names are not an error, they are undefined. Nothing fails at import time. The failure surfaces later and somewhere else, as TypeError: cjs.LastValue is not a constructor, or worse, as undefined being handed to something that stores it.
The channel half. B2 is the quiet one. Two array values go in, the same two come back out of checkpoint(), and after the restore the buffer holds [3,4]: two scalars where there were two arrays. The value count is unchanged, so anything that checks a length sees nothing wrong. B3 is the loud one in slow motion: two empty arrays go in and the buffer comes back empty. B1 shows three array values survive, and B4 shows two scalars survive, so the trigger really is the pair of conditions and not arrays generally or pairs generally. B5 shows unique: true round-trips correctly, because the tuple it emits is unambiguous.
Inside a graph. B7 and B8 are the same thing through StateGraph with a checkpointer and a fresh graph object on the same thread, which is what a process restart looks like. In B7 the node reads [[1,2],[3,4]] before and [3,4] after. In B8 the node reads undefined after: the state field is simply gone. No error is raised on either.
Why exactly two, and why arrays
Because the structural test asks for an array of length two whose both elements are arrays. Three values fail the length test. Two scalars fail the element test. Two arrays satisfy both, and unique: false is the default, so the dedupe set that receives the discarded first value is never read again.
One detail that decides whether you can hit this at all: Topic.update() flattens its writes by one level. Writing the array [1,2] as a single value gives you two scalar entries, not one array entry. To get array-valued entries you write an array of arrays, which is what the graph scenarios above pass in. That is ordinary for a topic used to collect batches, matrices, embedding windows, or tool-call groups.
What I would do
Import channel classes from @langchain/langgraph/channels and Pregel and friends from @langchain/langgraph/pregel, not from the package root, regardless of what the type declarations allow. If you are on CommonJS, assert that the import is a function before you use it, because you will not get an error otherwise.
For Topic specifically, if your values can be arrays, set unique: true when the semantics allow it, since the tuple form is unambiguous and round-trips correctly. If they cannot, wrap each value in an object rather than pushing bare arrays. An object is not an array, so the structural test cannot match it.
What I checked that turned out fine
Reporting the hypotheses that died, because they bound the finding.
- The
./webbuild is not a reduced build. The obvious guess was that the browser entry point omits things. It does not:.and./webboth export exactly 95 names, with zero names on either side that the other lacks. - There is no wildcard subpath. The package declares 10 subpaths and none contains a
*, so a subpath census is meaningful here rather than a sample. - ESM and CommonJS are not divergent from each other. Both runtime builds expose the identical 95 names. The divergence is between the declarations and the runtime, not between the two builds.
unique: trueis not affected. It always emits the tuple, so it is always detected correctly.BinaryOperatorAggregatereally does take a positional operator, as that documentation sample uses it, not an options object.
Prior art
The error text is not new, but the cause here is. The closest report is an issue against the LangGraph CLI API package, which shows the same does not provide an export named text for Command. That one is a version skew: a bundled package importing a name from a separately installed, older @langchain/langgraph that genuinely did not export it yet. An older issue on the JS repo has the same shape for InMemoryStore against @langchain/langgraph-checkpoint, resolved by pinning.
What is measured here is different: a single installed version, 1.4.19, whose own declaration file and own runtime build disagree with each other. No second package and no version mismatch is involved. On the checkpoint side I found no prior report at all; the disambiguating helper's name returns zero results across that repository's issues and pull requests.
Versions and method
Measured on @langchain/langgraph 1.4.19 with
zod 4.6.5 and
typescript 5.9.3, on Node 24.8.0, in October 2026. Both versions are printed by the harness rather than asserted. Every number in this tutorial comes from the output block above, produced by running exactly the files shown from an empty directory. No API key is used anywhere and nothing here contacts a model provider.
Related reading on this site: how a LangGraph delta channel can replay writes in larger batches than your graph produced, and what a LangGraph checkpointer actually stores.
Sources: the documented sample is on the vendor's JavaScript graph API page; the working subpath import is documented on the Pregel page; the nearest prior report is langgraphjs-api issue 69 and the older one is langgraphjs issue 571; the named-export resolution behaviour is specified in the Node.js ESM documentation.
Written by
Ren OkabeRen builds agent infrastructure and writes copy-paste tutorials for engineers shipping LLM tool-use systems.
Frequently asked questions
Why does LangGraph say it does not provide an export named 'LastValue'?
Because on @langchain/langgraph 1.4.19 the package root's runtime build does not export it, even though the root's TypeScript declaration file does. Measured, the root .d.ts export block lists 212 names and the runtime exposes 95. Ten of the missing names are classes, including LastValue and Topic. Import them from @langchain/langgraph/channels instead.
Which classes are affected?
Exactly ten, measured by classifying every declared name by how it is declared: AnnotationRoot, AnyValue, CompiledGraph, DynamicBarrierValue, EphemeralValue, LastValue, NamedBarrierValue, Pregel, PregelNode and Topic. The other 107 missing names are type aliases and interfaces, which are correctly absent from a runtime build.
Why did TypeScript not catch this?
Because the declaration file genuinely exports the name, so there is nothing for the compiler to object to. The harness in this tutorial type-checks the broken import under strict mode with nodenext resolution and tsc reports zero errors. The failure only appears when Node resolves the named binding at runtime.
Does this behave differently under CommonJS?
Yes, and worse. Under ESM you get an immediate SyntaxError naming the first missing binding. Under require() the missing names are simply undefined, so nothing fails at import time. The failure surfaces later as TypeError: LastValue is not a constructor, or not at all if the undefined value is just stored.
Why does the error name LastValue rather than Topic?
ESM reports the first missing binding it reaches in the import list. In the vendor's documented sample the order is StateGraph, LastValue, BinaryOperatorAggregate, Topic, and StateGraph is a real export, so LastValue is the first name that fails. Topic is equally missing but is never mentioned in the error.
What is the Topic checkpoint problem?
Topic serialises two ways: a flat value list when unique is false, and a [seen, values] tuple when unique is true. The restore path tells them apart structurally, by checking for an array of length two whose both elements are arrays. A non-unique Topic holding exactly two array values produces a flat checkpoint that matches that test, so on resume the first value is absorbed into the dedupe set and discarded.
How much data does that actually lose?
In the measured run, two array values go in and the buffer comes back holding two scalars instead, so a length check sees nothing wrong. Two empty arrays go in and the buffer comes back empty. Inside a real graph with a checkpointer, the node reads undefined for that state field after the resume. No error is raised in either case.
How do I avoid the Topic issue?
Set unique: true where the semantics allow it, because the tuple form it emits is unambiguous and round-trips correctly. If you need unique false, wrap each value in an object rather than pushing bare arrays, since an object is not an array and cannot satisfy the structural test. Three or more values, or scalar values, are also unaffected.
Related tutorials
How to test whether a LangGraph DeltaValue reducer survives a resume
On @langchain/langgraph 1.4.19 a DeltaValue field replays checkpointed writes through your reducer in larger batches than your graph produced. If the reducer reads the batching, the resumed state is not the state you ran, and nothing errors. Three runnable scripts: a ten-line invariance check, a run where six turns become one, and a sweep showing the damage sits in the tail.
LangGraph Checkpointer: Persist and Resume Agent State (2026)
Make a LangGraph agent durable with a checkpointer: attach SqliteSaver in one line, crash the graph on purpose, and resume from disk without re-running the Claude call.
LangGraph Checkpointer Requires 'configurable' Keys: The Real Fix (2026)
"Checkpointer requires one or more of the following 'configurable' keys" means you compiled with a checkpointer and invoked without one. The message names three keys and only thread_id works. Measured in Python and TypeScript (2026).