@langchain/langgraph 0.2.26 → 0.2.28

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.
@@ -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
  }
@@ -472,8 +472,13 @@ class Pregel extends runnables_1.Runnable {
472
472
  let checkpointConfig = (0, utils_js_1.patchConfigurable)(config, {
473
473
  checkpoint_ns: config.configurable?.checkpoint_ns ?? "",
474
474
  });
475
+ let checkpointMetadata = config.metadata ?? {};
475
476
  if (saved?.config.configurable) {
476
477
  checkpointConfig = (0, utils_js_1.patchConfigurable)(config, saved.config.configurable);
478
+ checkpointMetadata = {
479
+ ...saved.metadata,
480
+ ...checkpointMetadata,
481
+ };
477
482
  }
478
483
  // Find last node that updated the state, if not provided
479
484
  if (values == null && asNode === undefined) {
@@ -485,6 +490,54 @@ class Pregel extends runnables_1.Runnable {
485
490
  }, {});
486
491
  return (0, index_js_1.patchCheckpointMap)(nextConfig, saved ? saved.metadata : undefined);
487
492
  }
493
+ // update channels
494
+ const channels = (0, base_js_1.emptyChannels)(this.channels, checkpoint);
495
+ // Pass `skipManaged: true` as managed values are not used/relevant in update state calls.
496
+ const { managed } = await this.prepareSpecs(config, { skipManaged: true });
497
+ if (values === null && asNode === "__end__") {
498
+ if (saved) {
499
+ // tasks for this checkpoint
500
+ const nextTasks = (0, algo_js_1._prepareNextTasks)(checkpoint, saved.pendingWrites || [], this.nodes, channels, managed, saved.config, true, {
501
+ step: (saved.metadata?.step ?? -1) + 1,
502
+ checkpointer: this.checkpointer || undefined,
503
+ store: this.store,
504
+ });
505
+ // apply null writes
506
+ const nullWrites = (saved.pendingWrites || [])
507
+ .filter((w) => w[0] === constants_js_1.NULL_TASK_ID)
508
+ .flatMap((w) => w.slice(1));
509
+ if (nullWrites.length > 0) {
510
+ (0, algo_js_1._applyWrites)(saved.checkpoint, channels, [
511
+ {
512
+ name: constants_js_1.INPUT,
513
+ writes: nullWrites,
514
+ triggers: [],
515
+ },
516
+ ]);
517
+ }
518
+ // apply writes from tasks that already ran
519
+ for (const [taskId, k, v] of saved.pendingWrites || []) {
520
+ if ([constants_js_1.ERROR, constants_js_1.INTERRUPT, langgraph_checkpoint_1.SCHEDULED].includes(k)) {
521
+ continue;
522
+ }
523
+ if (!(taskId in nextTasks)) {
524
+ continue;
525
+ }
526
+ nextTasks[taskId].writes.push([k, v]);
527
+ }
528
+ // clear all current tasks
529
+ (0, algo_js_1._applyWrites)(checkpoint, channels, Object.values(nextTasks));
530
+ }
531
+ // save checkpoint
532
+ const nextConfig = await checkpointer.put(checkpointConfig, (0, base_js_1.createCheckpoint)(checkpoint, undefined, step), {
533
+ ...checkpointMetadata,
534
+ source: "update",
535
+ step: step + 1,
536
+ writes: {},
537
+ parents: saved?.metadata?.parents ?? {},
538
+ }, {});
539
+ return (0, index_js_1.patchCheckpointMap)(nextConfig, saved ? saved.metadata : undefined);
540
+ }
488
541
  if (values == null && asNode === "__copy__") {
489
542
  const nextConfig = await checkpointer.put(saved?.parentConfig ?? checkpointConfig, (0, base_js_1.createCheckpoint)(checkpoint, undefined, step), {
490
543
  source: "fork",
@@ -534,10 +587,6 @@ class Pregel extends runnables_1.Runnable {
534
587
  if (this.nodes[asNode] === undefined) {
535
588
  throw new errors_js_1.InvalidUpdateError(`Node "${asNode.toString()}" does not exist`);
536
589
  }
537
- // update channels
538
- const channels = (0, base_js_1.emptyChannels)(this.channels, checkpoint);
539
- // Pass `skipManaged: true` as managed values are not used/relevant in update state calls.
540
- const { managed } = await this.prepareSpecs(config, { skipManaged: true });
541
590
  // run all writers of the chosen node
542
591
  const writers = this.nodes[asNode].getWriters();
543
592
  if (!writers.length) {
@@ -553,6 +602,7 @@ class Pregel extends runnables_1.Runnable {
553
602
  writes: [],
554
603
  triggers: [constants_js_1.INTERRUPT],
555
604
  id: (0, langgraph_checkpoint_1.uuid5)(constants_js_1.INTERRUPT, checkpoint.id),
605
+ writers: [],
556
606
  };
557
607
  // execute task
558
608
  await task.proc.invoke(task.input, (0, runnables_1.patchConfig)({
@@ -568,8 +618,15 @@ class Pregel extends runnables_1.Runnable {
568
618
  },
569
619
  }));
570
620
  // save task writes
571
- if (saved !== undefined) {
572
- 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);
573
630
  }
574
631
  // apply to checkpoint
575
632
  // TODO: Why does keyof StrRecord allow number and symbol?
@@ -581,6 +638,9 @@ class Pregel extends runnables_1.Runnable {
581
638
  writes: { [asNode]: values },
582
639
  parents: saved?.metadata?.parents ?? {},
583
640
  }, newVersions);
641
+ if (pushWrites.length > 0) {
642
+ await checkpointer.putWrites(nextConfig, pushWrites, task.id);
643
+ }
584
644
  return (0, index_js_1.patchCheckpointMap)(nextConfig, saved ? saved.metadata : undefined);
585
645
  }
586
646
  _defaults(config) {
@@ -1,13 +1,13 @@
1
1
  /* eslint-disable no-param-reassign */
2
2
  import { Runnable, RunnableSequence, getCallbackManagerForConfig, mergeConfigs, patchConfig, _coerceToRunnable, } from "@langchain/core/runnables";
3
- import { compareChannelVersions, copyCheckpoint, emptyCheckpoint, uuid5, } from "@langchain/langgraph-checkpoint";
3
+ import { compareChannelVersions, copyCheckpoint, emptyCheckpoint, SCHEDULED, uuid5, } from "@langchain/langgraph-checkpoint";
4
4
  import { createCheckpoint, emptyChannels, isBaseChannel, } from "../channels/base.js";
5
5
  import { PregelNode } from "./read.js";
6
6
  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, } 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";
@@ -468,8 +468,13 @@ export class Pregel extends Runnable {
468
468
  let checkpointConfig = patchConfigurable(config, {
469
469
  checkpoint_ns: config.configurable?.checkpoint_ns ?? "",
470
470
  });
471
+ let checkpointMetadata = config.metadata ?? {};
471
472
  if (saved?.config.configurable) {
472
473
  checkpointConfig = patchConfigurable(config, saved.config.configurable);
474
+ checkpointMetadata = {
475
+ ...saved.metadata,
476
+ ...checkpointMetadata,
477
+ };
473
478
  }
474
479
  // Find last node that updated the state, if not provided
475
480
  if (values == null && asNode === undefined) {
@@ -481,6 +486,54 @@ export class Pregel extends Runnable {
481
486
  }, {});
482
487
  return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
483
488
  }
489
+ // update channels
490
+ const channels = emptyChannels(this.channels, checkpoint);
491
+ // Pass `skipManaged: true` as managed values are not used/relevant in update state calls.
492
+ const { managed } = await this.prepareSpecs(config, { skipManaged: true });
493
+ if (values === null && asNode === "__end__") {
494
+ if (saved) {
495
+ // tasks for this checkpoint
496
+ const nextTasks = _prepareNextTasks(checkpoint, saved.pendingWrites || [], this.nodes, channels, managed, saved.config, true, {
497
+ step: (saved.metadata?.step ?? -1) + 1,
498
+ checkpointer: this.checkpointer || undefined,
499
+ store: this.store,
500
+ });
501
+ // apply null writes
502
+ const nullWrites = (saved.pendingWrites || [])
503
+ .filter((w) => w[0] === NULL_TASK_ID)
504
+ .flatMap((w) => w.slice(1));
505
+ if (nullWrites.length > 0) {
506
+ _applyWrites(saved.checkpoint, channels, [
507
+ {
508
+ name: INPUT,
509
+ writes: nullWrites,
510
+ triggers: [],
511
+ },
512
+ ]);
513
+ }
514
+ // apply writes from tasks that already ran
515
+ for (const [taskId, k, v] of saved.pendingWrites || []) {
516
+ if ([ERROR, INTERRUPT, SCHEDULED].includes(k)) {
517
+ continue;
518
+ }
519
+ if (!(taskId in nextTasks)) {
520
+ continue;
521
+ }
522
+ nextTasks[taskId].writes.push([k, v]);
523
+ }
524
+ // clear all current tasks
525
+ _applyWrites(checkpoint, channels, Object.values(nextTasks));
526
+ }
527
+ // save checkpoint
528
+ const nextConfig = await checkpointer.put(checkpointConfig, createCheckpoint(checkpoint, undefined, step), {
529
+ ...checkpointMetadata,
530
+ source: "update",
531
+ step: step + 1,
532
+ writes: {},
533
+ parents: saved?.metadata?.parents ?? {},
534
+ }, {});
535
+ return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
536
+ }
484
537
  if (values == null && asNode === "__copy__") {
485
538
  const nextConfig = await checkpointer.put(saved?.parentConfig ?? checkpointConfig, createCheckpoint(checkpoint, undefined, step), {
486
539
  source: "fork",
@@ -530,10 +583,6 @@ export class Pregel extends Runnable {
530
583
  if (this.nodes[asNode] === undefined) {
531
584
  throw new InvalidUpdateError(`Node "${asNode.toString()}" does not exist`);
532
585
  }
533
- // update channels
534
- const channels = emptyChannels(this.channels, checkpoint);
535
- // Pass `skipManaged: true` as managed values are not used/relevant in update state calls.
536
- const { managed } = await this.prepareSpecs(config, { skipManaged: true });
537
586
  // run all writers of the chosen node
538
587
  const writers = this.nodes[asNode].getWriters();
539
588
  if (!writers.length) {
@@ -549,6 +598,7 @@ export class Pregel extends Runnable {
549
598
  writes: [],
550
599
  triggers: [INTERRUPT],
551
600
  id: uuid5(INTERRUPT, checkpoint.id),
601
+ writers: [],
552
602
  };
553
603
  // execute task
554
604
  await task.proc.invoke(task.input, patchConfig({
@@ -564,8 +614,15 @@ export class Pregel extends Runnable {
564
614
  },
565
615
  }));
566
616
  // save task writes
567
- if (saved !== undefined) {
568
- 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);
569
626
  }
570
627
  // apply to checkpoint
571
628
  // TODO: Why does keyof StrRecord allow number and symbol?
@@ -577,6 +634,9 @@ export class Pregel extends Runnable {
577
634
  writes: { [asNode]: values },
578
635
  parents: saved?.metadata?.parents ?? {},
579
636
  }, newVersions);
637
+ if (pushWrites.length > 0) {
638
+ await checkpointer.putWrites(nextConfig, pushWrites, task.id);
639
+ }
580
640
  return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
581
641
  }
582
642
  _defaults(config) {
@@ -46,7 +46,34 @@ function readChannels(channels, select, skipEmpty = true
46
46
  }
47
47
  }
48
48
  exports.readChannels = readChannels;
49
+ /**
50
+ * Map input chunk to a sequence of pending writes in the form (channel, value).
51
+ */
49
52
  function* mapCommand(cmd) {
53
+ if (cmd.graph === constants_js_1.Command.PARENT) {
54
+ throw new errors_js_1.InvalidUpdateError("There is no parent graph.");
55
+ }
56
+ if (cmd.goto) {
57
+ let sends;
58
+ if (Array.isArray(cmd.goto)) {
59
+ sends = cmd.goto;
60
+ }
61
+ else {
62
+ sends = [cmd.goto];
63
+ }
64
+ for (const send of sends) {
65
+ if ((0, constants_js_1._isSend)(send)) {
66
+ yield [constants_js_1.NULL_TASK_ID, constants_js_1.TASKS, send];
67
+ }
68
+ else if (typeof send === "string") {
69
+ yield [constants_js_1.NULL_TASK_ID, `branch:__start__:${constants_js_1.SELF}:${send}`, "__start__"];
70
+ }
71
+ else {
72
+ throw new Error(`In Command.send, expected Send or string, got ${typeof send}`);
73
+ }
74
+ }
75
+ // TODO: handle goto str for state graph
76
+ }
50
77
  if (cmd.resume) {
51
78
  if (typeof cmd.resume === "object" &&
52
79
  !!cmd.resume &&
@@ -60,6 +87,14 @@ function* mapCommand(cmd) {
60
87
  yield [constants_js_1.NULL_TASK_ID, constants_js_1.RESUME, cmd.resume];
61
88
  }
62
89
  }
90
+ if (cmd.update) {
91
+ if (typeof cmd.update !== "object" || !cmd.update) {
92
+ throw new Error("Expected cmd.update to be a dict mapping channel names to update values");
93
+ }
94
+ for (const [k, v] of Object.entries(cmd.update)) {
95
+ yield [constants_js_1.NULL_TASK_ID, k, v];
96
+ }
97
+ }
63
98
  }
64
99
  exports.mapCommand = mapCommand;
65
100
  /**
@@ -4,6 +4,9 @@ import type { PregelExecutableTask } from "./types.js";
4
4
  import { Command } from "../constants.js";
5
5
  export declare function readChannel<C extends PropertyKey>(channels: Record<C, BaseChannel>, chan: C, catchErrors?: boolean, returnException?: boolean): unknown | null;
6
6
  export declare function readChannels<C extends PropertyKey>(channels: Record<C, BaseChannel>, select: C | Array<C>, skipEmpty?: boolean): Record<string, any> | any;
7
+ /**
8
+ * Map input chunk to a sequence of pending writes in the form (channel, value).
9
+ */
7
10
  export declare function mapCommand(cmd: Command): Generator<[string, string, unknown]>;
8
11
  /**
9
12
  * Map input chunk to a sequence of pending writes in the form [channel, value].
package/dist/pregel/io.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { validate } from "uuid";
2
- import { NULL_TASK_ID, RESUME, TAG_HIDDEN } from "../constants.js";
3
- import { EmptyChannelError } from "../errors.js";
2
+ import { _isSend, Command, NULL_TASK_ID, RESUME, SELF, TAG_HIDDEN, TASKS, } from "../constants.js";
3
+ import { EmptyChannelError, InvalidUpdateError } from "../errors.js";
4
4
  export function readChannel(channels, chan, catchErrors = true, returnException = false) {
5
5
  try {
6
6
  return channels[chan].get();
@@ -41,7 +41,34 @@ export function readChannels(channels, select, skipEmpty = true
41
41
  return readChannel(channels, select);
42
42
  }
43
43
  }
44
+ /**
45
+ * Map input chunk to a sequence of pending writes in the form (channel, value).
46
+ */
44
47
  export function* mapCommand(cmd) {
48
+ if (cmd.graph === Command.PARENT) {
49
+ throw new InvalidUpdateError("There is no parent graph.");
50
+ }
51
+ if (cmd.goto) {
52
+ let sends;
53
+ if (Array.isArray(cmd.goto)) {
54
+ sends = cmd.goto;
55
+ }
56
+ else {
57
+ sends = [cmd.goto];
58
+ }
59
+ for (const send of sends) {
60
+ if (_isSend(send)) {
61
+ yield [NULL_TASK_ID, TASKS, send];
62
+ }
63
+ else if (typeof send === "string") {
64
+ yield [NULL_TASK_ID, `branch:__start__:${SELF}:${send}`, "__start__"];
65
+ }
66
+ else {
67
+ throw new Error(`In Command.send, expected Send or string, got ${typeof send}`);
68
+ }
69
+ }
70
+ // TODO: handle goto str for state graph
71
+ }
45
72
  if (cmd.resume) {
46
73
  if (typeof cmd.resume === "object" &&
47
74
  !!cmd.resume &&
@@ -55,6 +82,14 @@ export function* mapCommand(cmd) {
55
82
  yield [NULL_TASK_ID, RESUME, cmd.resume];
56
83
  }
57
84
  }
85
+ if (cmd.update) {
86
+ if (typeof cmd.update !== "object" || !cmd.update) {
87
+ throw new Error("Expected cmd.update to be a dict mapping channel names to update values");
88
+ }
89
+ for (const [k, v] of Object.entries(cmd.update)) {
90
+ yield [NULL_TASK_ID, k, v];
91
+ }
92
+ }
58
93
  }
59
94
  /**
60
95
  * Map input chunk to a sequence of pending writes in the form [channel, value].
@@ -576,22 +576,9 @@ class PregelLoop {
576
576
  async _first(inputKeys) {
577
577
  const isResuming = Object.keys(this.checkpoint.channel_versions).length !== 0 &&
578
578
  (this.config.configurable?.[constants_js_1.CONFIG_KEY_RESUMING] !== undefined ||
579
- this.input === null);
580
- if (isResuming) {
581
- for (const channelName of Object.keys(this.channels)) {
582
- if (this.checkpoint.channel_versions[channelName] !== undefined) {
583
- const version = this.checkpoint.channel_versions[channelName];
584
- this.checkpoint.versions_seen[constants_js_1.INTERRUPT] = {
585
- ...this.checkpoint.versions_seen[constants_js_1.INTERRUPT],
586
- [channelName]: version,
587
- };
588
- }
589
- }
590
- // produce values output
591
- const valuesOutput = await (0, utils_js_1.gatherIterator)((0, utils_js_1.prefixGenerator)((0, io_js_1.mapOutputValues)(this.outputKeys, true, this.channels), "values"));
592
- this._emit(valuesOutput);
593
- }
594
- else if ((0, constants_js_1._isCommand)(this.input)) {
579
+ this.input === null ||
580
+ (0, constants_js_1._isCommand)(this.input));
581
+ if ((0, constants_js_1._isCommand)(this.input)) {
595
582
  const writes = {};
596
583
  // group writes by task id
597
584
  for (const [tid, key, value] of (0, io_js_1.mapCommand)(this.input)) {
@@ -608,6 +595,20 @@ class PregelLoop {
608
595
  this.putWrites(tid, ws);
609
596
  }
610
597
  }
598
+ if (isResuming) {
599
+ for (const channelName of Object.keys(this.channels)) {
600
+ if (this.checkpoint.channel_versions[channelName] !== undefined) {
601
+ const version = this.checkpoint.channel_versions[channelName];
602
+ this.checkpoint.versions_seen[constants_js_1.INTERRUPT] = {
603
+ ...this.checkpoint.versions_seen[constants_js_1.INTERRUPT],
604
+ [channelName]: version,
605
+ };
606
+ }
607
+ }
608
+ // produce values output
609
+ const valuesOutput = await (0, utils_js_1.gatherIterator)((0, utils_js_1.prefixGenerator)((0, io_js_1.mapOutputValues)(this.outputKeys, true, this.channels), "values"));
610
+ this._emit(valuesOutput);
611
+ }
611
612
  else {
612
613
  // map inputs to channel updates
613
614
  const inputWrites = await (0, utils_js_1.gatherIterator)((0, io_js_1.mapInput)(inputKeys, this.input));
@@ -572,22 +572,9 @@ export class PregelLoop {
572
572
  async _first(inputKeys) {
573
573
  const isResuming = Object.keys(this.checkpoint.channel_versions).length !== 0 &&
574
574
  (this.config.configurable?.[CONFIG_KEY_RESUMING] !== undefined ||
575
- this.input === null);
576
- if (isResuming) {
577
- for (const channelName of Object.keys(this.channels)) {
578
- if (this.checkpoint.channel_versions[channelName] !== undefined) {
579
- const version = this.checkpoint.channel_versions[channelName];
580
- this.checkpoint.versions_seen[INTERRUPT] = {
581
- ...this.checkpoint.versions_seen[INTERRUPT],
582
- [channelName]: version,
583
- };
584
- }
585
- }
586
- // produce values output
587
- const valuesOutput = await gatherIterator(prefixGenerator(mapOutputValues(this.outputKeys, true, this.channels), "values"));
588
- this._emit(valuesOutput);
589
- }
590
- else if (_isCommand(this.input)) {
575
+ this.input === null ||
576
+ _isCommand(this.input));
577
+ if (_isCommand(this.input)) {
591
578
  const writes = {};
592
579
  // group writes by task id
593
580
  for (const [tid, key, value] of mapCommand(this.input)) {
@@ -604,6 +591,20 @@ export class PregelLoop {
604
591
  this.putWrites(tid, ws);
605
592
  }
606
593
  }
594
+ if (isResuming) {
595
+ for (const channelName of Object.keys(this.channels)) {
596
+ if (this.checkpoint.channel_versions[channelName] !== undefined) {
597
+ const version = this.checkpoint.channel_versions[channelName];
598
+ this.checkpoint.versions_seen[INTERRUPT] = {
599
+ ...this.checkpoint.versions_seen[INTERRUPT],
600
+ [channelName]: version,
601
+ };
602
+ }
603
+ }
604
+ // produce values output
605
+ const valuesOutput = await gatherIterator(prefixGenerator(mapOutputValues(this.outputKeys, true, this.channels), "values"));
606
+ this._emit(valuesOutput);
607
+ }
607
608
  else {
608
609
  // map inputs to channel updates
609
610
  const inputWrites = await gatherIterator(mapInput(inputKeys, this.input));