@langchain/langgraph 0.2.27 → 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.
@@ -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";
@@ -300,6 +300,14 @@ export class StateGraph extends Graph {
300
300
  for (const [key, node] of Object.entries(this.nodes)) {
301
301
  compiled.attachNode(key, node);
302
302
  }
303
+ compiled.attachBranch(START, SELF, _getControlBranch(), {
304
+ withReader: false,
305
+ });
306
+ for (const [key] of Object.entries(this.nodes)) {
307
+ compiled.attachBranch(key, SELF, _getControlBranch(), {
308
+ withReader: false,
309
+ });
310
+ }
303
311
  for (const [start, end] of this.edges) {
304
312
  compiled.attachEdge(start, end);
305
313
  }
@@ -335,13 +343,30 @@ function _getChannels(schema) {
335
343
  export class CompiledStateGraph extends CompiledGraph {
336
344
  attachNode(key, node) {
337
345
  const stateKeys = Object.keys(this.builder.channels);
346
+ function _getRoot(input) {
347
+ if (_isCommand(input)) {
348
+ if (input.graph === Command.PARENT) {
349
+ return SKIP_WRITE;
350
+ }
351
+ return input.update;
352
+ }
353
+ return input;
354
+ }
355
+ // to avoid name collision below
356
+ const nodeKey = key;
338
357
  function getStateKey(key, input) {
339
358
  if (!input) {
340
359
  return SKIP_WRITE;
341
360
  }
361
+ else if (_isCommand(input)) {
362
+ if (input.graph === Command.PARENT) {
363
+ return SKIP_WRITE;
364
+ }
365
+ return getStateKey(key, input.update);
366
+ }
342
367
  else if (typeof input !== "object" || Array.isArray(input)) {
343
368
  const typeofInput = Array.isArray(input) ? "array" : typeof input;
344
- throw new InvalidUpdateError(`Expected node "${key.toString()}" to return an object, received ${typeofInput}`, {
369
+ throw new InvalidUpdateError(`Expected node "${nodeKey.toString()}" to return an object, received ${typeofInput}`, {
345
370
  lc_error_code: "INVALID_GRAPH_NODE_RETURN_VALUE",
346
371
  });
347
372
  }
@@ -351,7 +376,16 @@ export class CompiledStateGraph extends CompiledGraph {
351
376
  }
352
377
  // state updaters
353
378
  const stateWriteEntries = stateKeys.map((key) => key === ROOT
354
- ? { channel: key, value: PASSTHROUGH, skipNone: true }
379
+ ? {
380
+ channel: key,
381
+ value: PASSTHROUGH,
382
+ skipNone: true,
383
+ mapper: new RunnableCallable({
384
+ func: _getRoot,
385
+ trace: false,
386
+ recurse: false,
387
+ }),
388
+ }
355
389
  : {
356
390
  channel: key,
357
391
  value: PASSTHROUGH,
@@ -426,28 +460,29 @@ export class CompiledStateGraph extends CompiledGraph {
426
460
  this.nodes[end].triggers.push(start);
427
461
  }
428
462
  }
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) {
463
+ attachBranch(start, name, branch, options = { withReader: true }) {
464
+ const branchWriter = async (packets, config) => {
465
+ const filteredPackets = packets.filter((p) => p !== END);
466
+ if (!filteredPackets.length) {
436
467
  return;
437
468
  }
438
- const writes = filteredDests.map((dest) => {
439
- if (_isSend(dest)) {
440
- return dest;
469
+ const writes = filteredPackets.map((p) => {
470
+ if (_isSend(p)) {
471
+ return p;
441
472
  }
442
473
  return {
443
- channel: `branch:${start}:${name}:${dest}`,
474
+ channel: `branch:${start}:${name}:${p}`,
444
475
  value: start,
445
476
  };
446
477
  });
447
- return new ChannelWrite(writes, [TAG_HIDDEN]);
448
- },
478
+ await ChannelWrite.doWrite({ ...config, tags: (config.tags ?? []).concat([TAG_HIDDEN]) }, writes);
479
+ };
480
+ // attach branch publisher
481
+ this.nodes[start].writers.push(branch.run(branchWriter,
449
482
  // reader
450
- (config) => ChannelRead.doRead(config, this.streamChannels ?? this.outputChannels, true)));
483
+ options.withReader
484
+ ? (config) => ChannelRead.doRead(config, this.streamChannels ?? this.outputChannels, true)
485
+ : undefined));
451
486
  // attach branch subscribers
452
487
  const ends = branch.ends
453
488
  ? Object.values(branch.ends)
@@ -494,3 +529,28 @@ function isStateGraphArgsWithInputOutputSchemas(obj) {
494
529
  obj.input !== undefined &&
495
530
  obj.output !== undefined);
496
531
  }
532
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
533
+ function _controlBranch(value) {
534
+ if (_isSend(value)) {
535
+ return [value];
536
+ }
537
+ if (!_isCommand(value)) {
538
+ return [];
539
+ }
540
+ if (value.graph === Command.PARENT) {
541
+ throw new ParentCommand(value);
542
+ }
543
+ return Array.isArray(value.goto) ? value.goto : [value.goto];
544
+ }
545
+ function _getControlBranch() {
546
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
547
+ const CONTROL_BRANCH_PATH = new RunnableCallable({
548
+ func: _controlBranch,
549
+ tags: [TAG_HIDDEN],
550
+ trace: false,
551
+ recurse: false,
552
+ });
553
+ return new Branch({
554
+ path: CONTROL_BRANCH_PATH,
555
+ });
556
+ }
@@ -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) {
@@ -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));