@langchain/langgraph 0.2.27 → 0.2.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -240,6 +240,7 @@ class StateGraph extends graph_js_1.Graph {
240
240
  ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
241
241
  [runnable]
242
242
  : options?.subgraphs,
243
+ ends: options?.ends,
243
244
  };
244
245
  this.nodes[key] = nodeSpec;
245
246
  return this;
@@ -303,6 +304,14 @@ class StateGraph extends graph_js_1.Graph {
303
304
  for (const [key, node] of Object.entries(this.nodes)) {
304
305
  compiled.attachNode(key, node);
305
306
  }
307
+ compiled.attachBranch(graph_js_1.START, constants_js_1.SELF, _getControlBranch(), {
308
+ withReader: false,
309
+ });
310
+ for (const [key] of Object.entries(this.nodes)) {
311
+ compiled.attachBranch(key, constants_js_1.SELF, _getControlBranch(), {
312
+ withReader: false,
313
+ });
314
+ }
306
315
  for (const [start, end] of this.edges) {
307
316
  compiled.attachEdge(start, end);
308
317
  }
@@ -339,13 +348,30 @@ function _getChannels(schema) {
339
348
  class CompiledStateGraph extends graph_js_1.CompiledGraph {
340
349
  attachNode(key, node) {
341
350
  const stateKeys = Object.keys(this.builder.channels);
351
+ function _getRoot(input) {
352
+ if ((0, constants_js_1._isCommand)(input)) {
353
+ if (input.graph === constants_js_1.Command.PARENT) {
354
+ return write_js_1.SKIP_WRITE;
355
+ }
356
+ return input.update;
357
+ }
358
+ return input;
359
+ }
360
+ // to avoid name collision below
361
+ const nodeKey = key;
342
362
  function getStateKey(key, input) {
343
363
  if (!input) {
344
364
  return write_js_1.SKIP_WRITE;
345
365
  }
366
+ else if ((0, constants_js_1._isCommand)(input)) {
367
+ if (input.graph === constants_js_1.Command.PARENT) {
368
+ return write_js_1.SKIP_WRITE;
369
+ }
370
+ return getStateKey(key, input.update);
371
+ }
346
372
  else if (typeof input !== "object" || Array.isArray(input)) {
347
373
  const typeofInput = Array.isArray(input) ? "array" : typeof input;
348
- throw new errors_js_1.InvalidUpdateError(`Expected node "${key.toString()}" to return an object, received ${typeofInput}`, {
374
+ throw new errors_js_1.InvalidUpdateError(`Expected node "${nodeKey.toString()}" to return an object, received ${typeofInput}`, {
349
375
  lc_error_code: "INVALID_GRAPH_NODE_RETURN_VALUE",
350
376
  });
351
377
  }
@@ -355,7 +381,16 @@ class CompiledStateGraph extends graph_js_1.CompiledGraph {
355
381
  }
356
382
  // state updaters
357
383
  const stateWriteEntries = stateKeys.map((key) => key === ROOT
358
- ? { channel: key, value: write_js_1.PASSTHROUGH, skipNone: true }
384
+ ? {
385
+ channel: key,
386
+ value: write_js_1.PASSTHROUGH,
387
+ skipNone: true,
388
+ mapper: new utils_js_1.RunnableCallable({
389
+ func: _getRoot,
390
+ trace: false,
391
+ recurse: false,
392
+ }),
393
+ }
359
394
  : {
360
395
  channel: key,
361
396
  value: write_js_1.PASSTHROUGH,
@@ -397,6 +432,7 @@ class CompiledStateGraph extends graph_js_1.CompiledGraph {
397
432
  metadata: node?.metadata,
398
433
  retryPolicy: node?.retryPolicy,
399
434
  subgraphs: node?.subgraphs,
435
+ ends: node?.ends,
400
436
  });
401
437
  }
402
438
  }
@@ -430,28 +466,29 @@ class CompiledStateGraph extends graph_js_1.CompiledGraph {
430
466
  this.nodes[end].triggers.push(start);
431
467
  }
432
468
  }
433
- attachBranch(start, name, branch) {
434
- // attach branch publisher
435
- this.nodes[start].writers.push(branch.compile(
436
- // writer
437
- (dests) => {
438
- const filteredDests = dests.filter((dest) => dest !== graph_js_1.END);
439
- if (!filteredDests.length) {
469
+ attachBranch(start, name, branch, options = { withReader: true }) {
470
+ const branchWriter = async (packets, config) => {
471
+ const filteredPackets = packets.filter((p) => p !== graph_js_1.END);
472
+ if (!filteredPackets.length) {
440
473
  return;
441
474
  }
442
- const writes = filteredDests.map((dest) => {
443
- if ((0, constants_js_1._isSend)(dest)) {
444
- return dest;
475
+ const writes = filteredPackets.map((p) => {
476
+ if ((0, constants_js_1._isSend)(p)) {
477
+ return p;
445
478
  }
446
479
  return {
447
- channel: `branch:${start}:${name}:${dest}`,
480
+ channel: `branch:${start}:${name}:${p}`,
448
481
  value: start,
449
482
  };
450
483
  });
451
- return new write_js_1.ChannelWrite(writes, [constants_js_1.TAG_HIDDEN]);
452
- },
484
+ await write_js_1.ChannelWrite.doWrite({ ...config, tags: (config.tags ?? []).concat([constants_js_1.TAG_HIDDEN]) }, writes);
485
+ };
486
+ // attach branch publisher
487
+ this.nodes[start].writers.push(branch.run(branchWriter,
453
488
  // reader
454
- (config) => read_js_1.ChannelRead.doRead(config, this.streamChannels ?? this.outputChannels, true)));
489
+ options.withReader
490
+ ? (config) => read_js_1.ChannelRead.doRead(config, this.streamChannels ?? this.outputChannels, true)
491
+ : undefined));
455
492
  // attach branch subscribers
456
493
  const ends = branch.ends
457
494
  ? Object.values(branch.ends)
@@ -499,3 +536,28 @@ function isStateGraphArgsWithInputOutputSchemas(obj) {
499
536
  obj.input !== undefined &&
500
537
  obj.output !== undefined);
501
538
  }
539
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
540
+ function _controlBranch(value) {
541
+ if ((0, constants_js_1._isSend)(value)) {
542
+ return [value];
543
+ }
544
+ if (!(0, constants_js_1._isCommand)(value)) {
545
+ return [];
546
+ }
547
+ if (value.graph === constants_js_1.Command.PARENT) {
548
+ throw new errors_js_1.ParentCommand(value);
549
+ }
550
+ return Array.isArray(value.goto) ? value.goto : [value.goto];
551
+ }
552
+ function _getControlBranch() {
553
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
554
+ const CONTROL_BRANCH_PATH = new utils_js_1.RunnableCallable({
555
+ func: _controlBranch,
556
+ tags: [constants_js_1.TAG_HIDDEN],
557
+ trace: false,
558
+ recurse: false,
559
+ });
560
+ return new graph_js_1.Branch({
561
+ path: CONTROL_BRANCH_PATH,
562
+ });
563
+ }
@@ -133,5 +133,7 @@ export declare class CompiledStateGraph<S, U, N extends string = typeof START, I
133
133
  attachNode(key: typeof START, node?: never): void;
134
134
  attachNode(key: N, node: StateGraphNodeSpec<S, U>): void;
135
135
  attachEdge(start: N | N[] | "__start__", end: N | "__end__"): void;
136
- attachBranch(start: N | typeof START, name: string, branch: Branch<S, N>): void;
136
+ attachBranch(start: N | typeof START, name: string, branch: Branch<S, N>, options?: {
137
+ withReader?: boolean;
138
+ }): void;
137
139
  }
@@ -1,14 +1,14 @@
1
1
  /* eslint-disable @typescript-eslint/no-use-before-define */
2
2
  import { _coerceToRunnable, Runnable, } from "@langchain/core/runnables";
3
3
  import { isBaseChannel } from "../channels/base.js";
4
- import { END, CompiledGraph, Graph, START, } from "./graph.js";
4
+ import { END, CompiledGraph, Graph, START, Branch, } from "./graph.js";
5
5
  import { ChannelWrite, PASSTHROUGH, SKIP_WRITE, } from "../pregel/write.js";
6
6
  import { ChannelRead, PregelNode } from "../pregel/read.js";
7
7
  import { NamedBarrierValue } from "../channels/named_barrier_value.js";
8
8
  import { EphemeralValue } from "../channels/ephemeral_value.js";
9
9
  import { RunnableCallable } from "../utils.js";
10
- import { _isSend, CHECKPOINT_NAMESPACE_END, CHECKPOINT_NAMESPACE_SEPARATOR, TAG_HIDDEN, } from "../constants.js";
11
- import { InvalidUpdateError } from "../errors.js";
10
+ import { _isCommand, _isSend, CHECKPOINT_NAMESPACE_END, CHECKPOINT_NAMESPACE_SEPARATOR, Command, SELF, TAG_HIDDEN, } from "../constants.js";
11
+ import { InvalidUpdateError, ParentCommand } from "../errors.js";
12
12
  import { getChannel, } from "./annotation.js";
13
13
  import { isConfiguredManagedValue } from "../managed/base.js";
14
14
  import { isPregelLike } from "../pregel/utils/subgraph.js";
@@ -237,6 +237,7 @@ export class StateGraph extends Graph {
237
237
  ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
238
238
  [runnable]
239
239
  : options?.subgraphs,
240
+ ends: options?.ends,
240
241
  };
241
242
  this.nodes[key] = nodeSpec;
242
243
  return this;
@@ -300,6 +301,14 @@ export class StateGraph extends Graph {
300
301
  for (const [key, node] of Object.entries(this.nodes)) {
301
302
  compiled.attachNode(key, node);
302
303
  }
304
+ compiled.attachBranch(START, SELF, _getControlBranch(), {
305
+ withReader: false,
306
+ });
307
+ for (const [key] of Object.entries(this.nodes)) {
308
+ compiled.attachBranch(key, SELF, _getControlBranch(), {
309
+ withReader: false,
310
+ });
311
+ }
303
312
  for (const [start, end] of this.edges) {
304
313
  compiled.attachEdge(start, end);
305
314
  }
@@ -335,13 +344,30 @@ function _getChannels(schema) {
335
344
  export class CompiledStateGraph extends CompiledGraph {
336
345
  attachNode(key, node) {
337
346
  const stateKeys = Object.keys(this.builder.channels);
347
+ function _getRoot(input) {
348
+ if (_isCommand(input)) {
349
+ if (input.graph === Command.PARENT) {
350
+ return SKIP_WRITE;
351
+ }
352
+ return input.update;
353
+ }
354
+ return input;
355
+ }
356
+ // to avoid name collision below
357
+ const nodeKey = key;
338
358
  function getStateKey(key, input) {
339
359
  if (!input) {
340
360
  return SKIP_WRITE;
341
361
  }
362
+ else if (_isCommand(input)) {
363
+ if (input.graph === Command.PARENT) {
364
+ return SKIP_WRITE;
365
+ }
366
+ return getStateKey(key, input.update);
367
+ }
342
368
  else if (typeof input !== "object" || Array.isArray(input)) {
343
369
  const typeofInput = Array.isArray(input) ? "array" : typeof input;
344
- throw new InvalidUpdateError(`Expected node "${key.toString()}" to return an object, received ${typeofInput}`, {
370
+ throw new InvalidUpdateError(`Expected node "${nodeKey.toString()}" to return an object, received ${typeofInput}`, {
345
371
  lc_error_code: "INVALID_GRAPH_NODE_RETURN_VALUE",
346
372
  });
347
373
  }
@@ -351,7 +377,16 @@ export class CompiledStateGraph extends CompiledGraph {
351
377
  }
352
378
  // state updaters
353
379
  const stateWriteEntries = stateKeys.map((key) => key === ROOT
354
- ? { channel: key, value: PASSTHROUGH, skipNone: true }
380
+ ? {
381
+ channel: key,
382
+ value: PASSTHROUGH,
383
+ skipNone: true,
384
+ mapper: new RunnableCallable({
385
+ func: _getRoot,
386
+ trace: false,
387
+ recurse: false,
388
+ }),
389
+ }
355
390
  : {
356
391
  channel: key,
357
392
  value: PASSTHROUGH,
@@ -393,6 +428,7 @@ export class CompiledStateGraph extends CompiledGraph {
393
428
  metadata: node?.metadata,
394
429
  retryPolicy: node?.retryPolicy,
395
430
  subgraphs: node?.subgraphs,
431
+ ends: node?.ends,
396
432
  });
397
433
  }
398
434
  }
@@ -426,28 +462,29 @@ export class CompiledStateGraph extends CompiledGraph {
426
462
  this.nodes[end].triggers.push(start);
427
463
  }
428
464
  }
429
- attachBranch(start, name, branch) {
430
- // attach branch publisher
431
- this.nodes[start].writers.push(branch.compile(
432
- // writer
433
- (dests) => {
434
- const filteredDests = dests.filter((dest) => dest !== END);
435
- if (!filteredDests.length) {
465
+ attachBranch(start, name, branch, options = { withReader: true }) {
466
+ const branchWriter = async (packets, config) => {
467
+ const filteredPackets = packets.filter((p) => p !== END);
468
+ if (!filteredPackets.length) {
436
469
  return;
437
470
  }
438
- const writes = filteredDests.map((dest) => {
439
- if (_isSend(dest)) {
440
- return dest;
471
+ const writes = filteredPackets.map((p) => {
472
+ if (_isSend(p)) {
473
+ return p;
441
474
  }
442
475
  return {
443
- channel: `branch:${start}:${name}:${dest}`,
476
+ channel: `branch:${start}:${name}:${p}`,
444
477
  value: start,
445
478
  };
446
479
  });
447
- return new ChannelWrite(writes, [TAG_HIDDEN]);
448
- },
480
+ await ChannelWrite.doWrite({ ...config, tags: (config.tags ?? []).concat([TAG_HIDDEN]) }, writes);
481
+ };
482
+ // attach branch publisher
483
+ this.nodes[start].writers.push(branch.run(branchWriter,
449
484
  // reader
450
- (config) => ChannelRead.doRead(config, this.streamChannels ?? this.outputChannels, true)));
485
+ options.withReader
486
+ ? (config) => ChannelRead.doRead(config, this.streamChannels ?? this.outputChannels, true)
487
+ : undefined));
451
488
  // attach branch subscribers
452
489
  const ends = branch.ends
453
490
  ? Object.values(branch.ends)
@@ -494,3 +531,28 @@ function isStateGraphArgsWithInputOutputSchemas(obj) {
494
531
  obj.input !== undefined &&
495
532
  obj.output !== undefined);
496
533
  }
534
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
535
+ function _controlBranch(value) {
536
+ if (_isSend(value)) {
537
+ return [value];
538
+ }
539
+ if (!_isCommand(value)) {
540
+ return [];
541
+ }
542
+ if (value.graph === Command.PARENT) {
543
+ throw new ParentCommand(value);
544
+ }
545
+ return Array.isArray(value.goto) ? value.goto : [value.goto];
546
+ }
547
+ function _getControlBranch() {
548
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
549
+ const CONTROL_BRANCH_PATH = new RunnableCallable({
550
+ func: _controlBranch,
551
+ tags: [TAG_HIDDEN],
552
+ trace: false,
553
+ recurse: false,
554
+ });
555
+ return new Branch({
556
+ path: CONTROL_BRANCH_PATH,
557
+ });
558
+ }
@@ -4,6 +4,7 @@ exports.toolsCondition = exports.ToolNode = void 0;
4
4
  const messages_1 = require("@langchain/core/messages");
5
5
  const utils_js_1 = require("../utils.cjs");
6
6
  const graph_js_1 = require("../graph/graph.cjs");
7
+ const errors_js_1 = require("../errors.cjs");
7
8
  /**
8
9
  * A node that runs the tools requested in the last AIMessage. It can be used
9
10
  * either in StateGraph with a "messages" key or in MessageGraph. If multiple
@@ -178,6 +179,12 @@ class ToolNode extends utils_js_1.RunnableCallable {
178
179
  if (!this.handleToolErrors) {
179
180
  throw e;
180
181
  }
182
+ if ((0, errors_js_1.isGraphInterrupt)(e.name)) {
183
+ // `NodeInterrupt` errors are a breakpoint to bring a human into the loop.
184
+ // As such, they are not recoverable by the agent and shouldn't be fed
185
+ // back. Instead, re-throw these errors even when `handleToolErrors = true`.
186
+ throw e;
187
+ }
181
188
  return new messages_1.ToolMessage({
182
189
  content: `Error: ${e.message}\n Please fix your mistakes.`,
183
190
  name: call.name,
@@ -1,6 +1,7 @@
1
1
  import { ToolMessage, isBaseMessage, } from "@langchain/core/messages";
2
2
  import { RunnableCallable } from "../utils.js";
3
3
  import { END } from "../graph/graph.js";
4
+ import { isGraphInterrupt } from "../errors.js";
4
5
  /**
5
6
  * A node that runs the tools requested in the last AIMessage. It can be used
6
7
  * either in StateGraph with a "messages" key or in MessageGraph. If multiple
@@ -175,6 +176,12 @@ export class ToolNode extends RunnableCallable {
175
176
  if (!this.handleToolErrors) {
176
177
  throw e;
177
178
  }
179
+ if (isGraphInterrupt(e.name)) {
180
+ // `NodeInterrupt` errors are a breakpoint to bring a human into the loop.
181
+ // As such, they are not recoverable by the agent and shouldn't be fed
182
+ // back. Instead, re-throw these errors even when `handleToolErrors = true`.
183
+ throw e;
184
+ }
178
185
  return new ToolMessage({
179
186
  content: `Error: ${e.message}\n Please fix your mistakes.`,
180
187
  name: call.name,
@@ -100,6 +100,21 @@ const IGNORE = new Set([constants_js_1.PUSH, constants_js_1.RESUME, constants_js
100
100
  function _applyWrites(checkpoint, channels, tasks,
101
101
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
102
  getNextVersion) {
103
+ // Sort tasks by first 3 path elements for deterministic order
104
+ // Later path parts (like task IDs) are ignored for sorting
105
+ tasks.sort((a, b) => {
106
+ const aPath = a.path?.slice(0, 3) || [];
107
+ const bPath = b.path?.slice(0, 3) || [];
108
+ // Compare each path element
109
+ for (let i = 0; i < Math.min(aPath.length, bPath.length); i += 1) {
110
+ if (aPath[i] < bPath[i])
111
+ return -1;
112
+ if (aPath[i] > bPath[i])
113
+ return 1;
114
+ }
115
+ // If one path is shorter, it comes first
116
+ return aPath.length - bPath.length;
117
+ });
103
118
  // if no task has triggers this is applying writes from the null task only
104
119
  // so we don't do anything other than update the channels written to
105
120
  const bumpStep = tasks.some((task) => task.triggers.length > 0);
@@ -146,6 +161,7 @@ getNextVersion) {
146
161
  // do nothing
147
162
  }
148
163
  else if (chan === constants_js_1.TASKS) {
164
+ // TODO: remove branch in 1.0
149
165
  checkpoint.pending_sends.push({
150
166
  node: val.node,
151
167
  args: val.args,
@@ -214,6 +230,11 @@ getNextVersion) {
214
230
  return pendingWritesByManaged;
215
231
  }
216
232
  exports._applyWrites = _applyWrites;
233
+ /**
234
+ * Prepare the set of tasks that will make up the next Pregel step.
235
+ * This is the union of all PUSH tasks (Sends) and PULL tasks (nodes triggered
236
+ * by edges).
237
+ */
217
238
  function _prepareNextTasks(checkpoint, pendingWrites, processes, channels, managed, config, forExecution, extra) {
218
239
  const tasks = {};
219
240
  // Consume pending packets
@@ -234,12 +255,18 @@ function _prepareNextTasks(checkpoint, pendingWrites, processes, channels, manag
234
255
  return tasks;
235
256
  }
236
257
  exports._prepareNextTasks = _prepareNextTasks;
258
+ /**
259
+ * Prepares a single task for the next Pregel step, given a task path, which
260
+ * uniquely identifies a PUSH or PULL task within the graph.
261
+ */
237
262
  function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, channels, managed, config, forExecution, extra) {
238
263
  const { step, checkpointer, manager } = extra;
239
264
  const configurable = config.configurable ?? {};
240
265
  const parentNamespace = configurable.checkpoint_ns ?? "";
241
266
  if (taskPath[0] === constants_js_1.PUSH) {
242
- const index = typeof taskPath[1] === "number" ? taskPath[1] : parseInt(taskPath[1], 10);
267
+ const index = typeof taskPath[1] === "number"
268
+ ? taskPath[1]
269
+ : parseInt(taskPath[1], 10);
243
270
  if (index >= checkpoint.pending_sends.length) {
244
271
  return undefined;
245
272
  }
@@ -302,6 +329,7 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
302
329
  name: packet.node,
303
330
  writes: writes,
304
331
  triggers,
332
+ path: taskPath,
305
333
  }, select_, fresh_),
306
334
  [constants_js_1.CONFIG_KEY_CHECKPOINTER]: checkpointer ?? configurable[constants_js_1.CONFIG_KEY_CHECKPOINTER],
307
335
  [constants_js_1.CONFIG_KEY_CHECKPOINT_MAP]: {
@@ -319,6 +347,7 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
319
347
  retry_policy: proc.retryPolicy,
320
348
  id: taskId,
321
349
  path: taskPath,
350
+ writers: proc.getWriters(),
322
351
  };
323
352
  }
324
353
  }
@@ -405,6 +434,7 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
405
434
  name,
406
435
  writes: writes,
407
436
  triggers,
437
+ path: taskPath,
408
438
  }, select_, fresh_),
409
439
  [constants_js_1.CONFIG_KEY_CHECKPOINTER]: checkpointer ?? configurable[constants_js_1.CONFIG_KEY_CHECKPOINTER],
410
440
  [constants_js_1.CONFIG_KEY_CHECKPOINT_MAP]: {
@@ -422,6 +452,7 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
422
452
  retry_policy: proc.retryPolicy,
423
453
  id: taskId,
424
454
  path: taskPath,
455
+ writers: proc.getWriters(),
425
456
  };
426
457
  }
427
458
  }
@@ -15,6 +15,7 @@ export type WritesProtocol<C = string> = {
15
15
  name: string;
16
16
  writes: PendingWrite<C>[];
17
17
  triggers: string[];
18
+ path?: [string, ...(string | number)[]];
18
19
  };
19
20
  export declare const increment: (current?: number) => number;
20
21
  export declare function shouldInterrupt<N extends PropertyKey, C extends PropertyKey>(checkpoint: Checkpoint, interruptNodes: All | N[], tasks: PregelExecutableTask<N, C>[]): boolean;
@@ -37,5 +38,5 @@ export type NextTaskExtraFieldsWithoutStore = NextTaskExtraFields & {
37
38
  export declare function _prepareNextTasks<Nn extends StrRecord<string, PregelNode>, Cc extends StrRecord<string, BaseChannel>>(checkpoint: ReadonlyCheckpoint, pendingWrites: [string, string, unknown][] | undefined, processes: Nn, channels: Cc, managed: ManagedValueMapping, config: RunnableConfig, forExecution: false, extra: NextTaskExtraFieldsWithoutStore): Record<string, PregelTaskDescription>;
38
39
  export declare function _prepareNextTasks<Nn extends StrRecord<string, PregelNode>, Cc extends StrRecord<string, BaseChannel>>(checkpoint: ReadonlyCheckpoint, pendingWrites: [string, string, unknown][] | undefined, processes: Nn, channels: Cc, managed: ManagedValueMapping, config: RunnableConfig, forExecution: true, extra: NextTaskExtraFieldsWithStore): Record<string, PregelExecutableTask<keyof Nn, keyof Cc>>;
39
40
  export declare function _prepareSingleTask<Nn extends StrRecord<string, PregelNode>, Cc extends StrRecord<string, BaseChannel>>(taskPath: [string, string | number], checkpoint: ReadonlyCheckpoint, pendingWrites: [string, string, unknown][] | undefined, processes: Nn, channels: Cc, managed: ManagedValueMapping, config: RunnableConfig, forExecution: false, extra: NextTaskExtraFields): PregelTaskDescription | undefined;
40
- export declare function _prepareSingleTask<Nn extends StrRecord<string, PregelNode>, Cc extends StrRecord<string, BaseChannel>>(taskPath: [string, string | number], checkpoint: ReadonlyCheckpoint, pendingWrites: [string, string, unknown][] | undefined, processes: Nn, channels: Cc, managed: ManagedValueMapping, config: RunnableConfig, forExecution: true, extra: NextTaskExtraFields): PregelExecutableTask<keyof Nn, keyof Cc> | undefined;
41
- export declare function _prepareSingleTask<Nn extends StrRecord<string, PregelNode>, Cc extends StrRecord<string, BaseChannel>>(taskPath: [string, string | number], checkpoint: ReadonlyCheckpoint, pendingWrites: [string, string, unknown][] | undefined, processes: Nn, channels: Cc, managed: ManagedValueMapping, config: RunnableConfig, forExecution: boolean, extra: NextTaskExtraFieldsWithStore): PregelTaskDescription | PregelExecutableTask<keyof Nn, keyof Cc> | undefined;
41
+ export declare function _prepareSingleTask<Nn extends StrRecord<string, PregelNode>, Cc extends StrRecord<string, BaseChannel>>(taskPath: [string, ...(string | number)[]], checkpoint: ReadonlyCheckpoint, pendingWrites: [string, string, unknown][] | undefined, processes: Nn, channels: Cc, managed: ManagedValueMapping, config: RunnableConfig, forExecution: true, extra: NextTaskExtraFields): PregelExecutableTask<keyof Nn, keyof Cc> | undefined;
42
+ export declare function _prepareSingleTask<Nn extends StrRecord<string, PregelNode>, Cc extends StrRecord<string, BaseChannel>>(taskPath: [string, ...(string | number)[]], checkpoint: ReadonlyCheckpoint, pendingWrites: [string, string, unknown][] | undefined, processes: Nn, channels: Cc, managed: ManagedValueMapping, config: RunnableConfig, forExecution: boolean, extra: NextTaskExtraFieldsWithStore): PregelTaskDescription | PregelExecutableTask<keyof Nn, keyof Cc> | undefined;
@@ -93,6 +93,21 @@ const IGNORE = new Set([PUSH, RESUME, INTERRUPT]);
93
93
  export function _applyWrites(checkpoint, channels, tasks,
94
94
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
95
  getNextVersion) {
96
+ // Sort tasks by first 3 path elements for deterministic order
97
+ // Later path parts (like task IDs) are ignored for sorting
98
+ tasks.sort((a, b) => {
99
+ const aPath = a.path?.slice(0, 3) || [];
100
+ const bPath = b.path?.slice(0, 3) || [];
101
+ // Compare each path element
102
+ for (let i = 0; i < Math.min(aPath.length, bPath.length); i += 1) {
103
+ if (aPath[i] < bPath[i])
104
+ return -1;
105
+ if (aPath[i] > bPath[i])
106
+ return 1;
107
+ }
108
+ // If one path is shorter, it comes first
109
+ return aPath.length - bPath.length;
110
+ });
96
111
  // if no task has triggers this is applying writes from the null task only
97
112
  // so we don't do anything other than update the channels written to
98
113
  const bumpStep = tasks.some((task) => task.triggers.length > 0);
@@ -139,6 +154,7 @@ getNextVersion) {
139
154
  // do nothing
140
155
  }
141
156
  else if (chan === TASKS) {
157
+ // TODO: remove branch in 1.0
142
158
  checkpoint.pending_sends.push({
143
159
  node: val.node,
144
160
  args: val.args,
@@ -206,6 +222,11 @@ getNextVersion) {
206
222
  // Return managed values writes to be applied externally
207
223
  return pendingWritesByManaged;
208
224
  }
225
+ /**
226
+ * Prepare the set of tasks that will make up the next Pregel step.
227
+ * This is the union of all PUSH tasks (Sends) and PULL tasks (nodes triggered
228
+ * by edges).
229
+ */
209
230
  export function _prepareNextTasks(checkpoint, pendingWrites, processes, channels, managed, config, forExecution, extra) {
210
231
  const tasks = {};
211
232
  // Consume pending packets
@@ -225,12 +246,18 @@ export function _prepareNextTasks(checkpoint, pendingWrites, processes, channels
225
246
  }
226
247
  return tasks;
227
248
  }
249
+ /**
250
+ * Prepares a single task for the next Pregel step, given a task path, which
251
+ * uniquely identifies a PUSH or PULL task within the graph.
252
+ */
228
253
  export function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, channels, managed, config, forExecution, extra) {
229
254
  const { step, checkpointer, manager } = extra;
230
255
  const configurable = config.configurable ?? {};
231
256
  const parentNamespace = configurable.checkpoint_ns ?? "";
232
257
  if (taskPath[0] === PUSH) {
233
- const index = typeof taskPath[1] === "number" ? taskPath[1] : parseInt(taskPath[1], 10);
258
+ const index = typeof taskPath[1] === "number"
259
+ ? taskPath[1]
260
+ : parseInt(taskPath[1], 10);
234
261
  if (index >= checkpoint.pending_sends.length) {
235
262
  return undefined;
236
263
  }
@@ -293,6 +320,7 @@ export function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processe
293
320
  name: packet.node,
294
321
  writes: writes,
295
322
  triggers,
323
+ path: taskPath,
296
324
  }, select_, fresh_),
297
325
  [CONFIG_KEY_CHECKPOINTER]: checkpointer ?? configurable[CONFIG_KEY_CHECKPOINTER],
298
326
  [CONFIG_KEY_CHECKPOINT_MAP]: {
@@ -310,6 +338,7 @@ export function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processe
310
338
  retry_policy: proc.retryPolicy,
311
339
  id: taskId,
312
340
  path: taskPath,
341
+ writers: proc.getWriters(),
313
342
  };
314
343
  }
315
344
  }
@@ -396,6 +425,7 @@ export function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processe
396
425
  name,
397
426
  writes: writes,
398
427
  triggers,
428
+ path: taskPath,
399
429
  }, select_, fresh_),
400
430
  [CONFIG_KEY_CHECKPOINTER]: checkpointer ?? configurable[CONFIG_KEY_CHECKPOINTER],
401
431
  [CONFIG_KEY_CHECKPOINT_MAP]: {
@@ -413,6 +443,7 @@ export function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processe
413
443
  retry_policy: proc.retryPolicy,
414
444
  id: taskId,
415
445
  path: taskPath,
446
+ writers: proc.getWriters(),
416
447
  };
417
448
  }
418
449
  }
@@ -602,6 +602,7 @@ class Pregel extends runnables_1.Runnable {
602
602
  writes: [],
603
603
  triggers: [constants_js_1.INTERRUPT],
604
604
  id: (0, langgraph_checkpoint_1.uuid5)(constants_js_1.INTERRUPT, checkpoint.id),
605
+ writers: [],
605
606
  };
606
607
  // execute task
607
608
  await task.proc.invoke(task.input, (0, runnables_1.patchConfig)({
@@ -617,8 +618,15 @@ class Pregel extends runnables_1.Runnable {
617
618
  },
618
619
  }));
619
620
  // save task writes
620
- if (saved !== undefined) {
621
- await checkpointer.putWrites(checkpointConfig, task.writes, task.id);
621
+ // channel writes are saved to current checkpoint
622
+ // push writes are saved to next checkpoint
623
+ const [channelWrites, pushWrites] = [
624
+ task.writes.filter((w) => w[0] !== constants_js_1.PUSH),
625
+ task.writes.filter((w) => w[0] === constants_js_1.PUSH),
626
+ ];
627
+ // save task writes
628
+ if (saved !== undefined && channelWrites.length > 0) {
629
+ await checkpointer.putWrites(checkpointConfig, channelWrites, task.id);
622
630
  }
623
631
  // apply to checkpoint
624
632
  // TODO: Why does keyof StrRecord allow number and symbol?
@@ -630,6 +638,9 @@ class Pregel extends runnables_1.Runnable {
630
638
  writes: { [asNode]: values },
631
639
  parents: saved?.metadata?.parents ?? {},
632
640
  }, newVersions);
641
+ if (pushWrites.length > 0) {
642
+ await checkpointer.putWrites(nextConfig, pushWrites, task.id);
643
+ }
633
644
  return (0, index_js_1.patchCheckpointMap)(nextConfig, saved ? saved.metadata : undefined);
634
645
  }
635
646
  _defaults(config) {
@@ -7,7 +7,7 @@ import { validateGraph, validateKeys } from "./validate.js";
7
7
  import { readChannels } from "./io.js";
8
8
  import { printStepCheckpoint, printStepTasks, printStepWrites, tasksWithWrites, } from "./debug.js";
9
9
  import { ChannelWrite, PASSTHROUGH } from "./write.js";
10
- import { CONFIG_KEY_CHECKPOINTER, CONFIG_KEY_READ, CONFIG_KEY_SEND, ERROR, INTERRUPT, CHECKPOINT_NAMESPACE_SEPARATOR, CHECKPOINT_NAMESPACE_END, CONFIG_KEY_STREAM, CONFIG_KEY_TASK_ID, NULL_TASK_ID, INPUT, } from "../constants.js";
10
+ import { CONFIG_KEY_CHECKPOINTER, CONFIG_KEY_READ, CONFIG_KEY_SEND, ERROR, INTERRUPT, CHECKPOINT_NAMESPACE_SEPARATOR, CHECKPOINT_NAMESPACE_END, CONFIG_KEY_STREAM, CONFIG_KEY_TASK_ID, NULL_TASK_ID, INPUT, PUSH, } from "../constants.js";
11
11
  import { GraphRecursionError, GraphValueError, InvalidUpdateError, isGraphBubbleUp, isGraphInterrupt, } from "../errors.js";
12
12
  import { _prepareNextTasks, _localRead, _applyWrites, } from "./algo.js";
13
13
  import { _coerceToDict, getNewChannelVersions, patchCheckpointMap, } from "./utils/index.js";
@@ -598,6 +598,7 @@ export class Pregel extends Runnable {
598
598
  writes: [],
599
599
  triggers: [INTERRUPT],
600
600
  id: uuid5(INTERRUPT, checkpoint.id),
601
+ writers: [],
601
602
  };
602
603
  // execute task
603
604
  await task.proc.invoke(task.input, patchConfig({
@@ -613,8 +614,15 @@ export class Pregel extends Runnable {
613
614
  },
614
615
  }));
615
616
  // save task writes
616
- if (saved !== undefined) {
617
- await checkpointer.putWrites(checkpointConfig, task.writes, task.id);
617
+ // channel writes are saved to current checkpoint
618
+ // push writes are saved to next checkpoint
619
+ const [channelWrites, pushWrites] = [
620
+ task.writes.filter((w) => w[0] !== PUSH),
621
+ task.writes.filter((w) => w[0] === PUSH),
622
+ ];
623
+ // save task writes
624
+ if (saved !== undefined && channelWrites.length > 0) {
625
+ await checkpointer.putWrites(checkpointConfig, channelWrites, task.id);
618
626
  }
619
627
  // apply to checkpoint
620
628
  // TODO: Why does keyof StrRecord allow number and symbol?
@@ -626,6 +634,9 @@ export class Pregel extends Runnable {
626
634
  writes: { [asNode]: values },
627
635
  parents: saved?.metadata?.parents ?? {},
628
636
  }, newVersions);
637
+ if (pushWrites.length > 0) {
638
+ await checkpointer.putWrites(nextConfig, pushWrites, task.id);
639
+ }
629
640
  return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
630
641
  }
631
642
  _defaults(config) {