@langchain/langgraph 0.2.29 → 0.2.30

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,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports._isCommand = exports.Command = exports._isSend = exports.Send = exports._isSendInterface = exports.CHECKPOINT_NAMESPACE_END = exports.CHECKPOINT_NAMESPACE_SEPARATOR = exports.RESERVED = exports.NULL_TASK_ID = exports.TASK_NAMESPACE = exports.PULL = exports.PUSH = exports.TASKS = exports.SELF = exports.TAG_NOSTREAM = exports.TAG_HIDDEN = exports.RECURSION_LIMIT_DEFAULT = exports.RUNTIME_PLACEHOLDER = exports.RESUME = exports.INTERRUPT = exports.CONFIG_KEY_CHECKPOINT_MAP = exports.CONFIG_KEY_RESUME_VALUE = exports.CONFIG_KEY_STREAM = exports.CONFIG_KEY_TASK_ID = exports.CONFIG_KEY_RESUMING = exports.CONFIG_KEY_CHECKPOINTER = exports.CONFIG_KEY_READ = exports.CONFIG_KEY_SEND = exports.ERROR = exports.INPUT = exports.MISSING = void 0;
3
+ exports._isCommand = exports.Command = exports._isSend = exports.Send = exports._isSendInterface = exports.CHECKPOINT_NAMESPACE_END = exports.CHECKPOINT_NAMESPACE_SEPARATOR = exports.RESERVED = exports.NULL_TASK_ID = exports.TASK_NAMESPACE = exports.PULL = exports.PUSH = exports.TASKS = exports.SELF = exports.TAG_NOSTREAM = exports.TAG_HIDDEN = exports.RECURSION_LIMIT_DEFAULT = exports.RUNTIME_PLACEHOLDER = exports.RESUME = exports.INTERRUPT = exports.CONFIG_KEY_CHECKPOINT_MAP = exports.CONFIG_KEY_CHECKPOINT_NS = exports.CONFIG_KEY_SCRATCHPAD = exports.CONFIG_KEY_WRITES = exports.CONFIG_KEY_RESUME_VALUE = exports.CONFIG_KEY_STREAM = exports.CONFIG_KEY_TASK_ID = exports.CONFIG_KEY_RESUMING = exports.CONFIG_KEY_CHECKPOINTER = exports.CONFIG_KEY_READ = exports.CONFIG_KEY_SEND = exports.ERROR = exports.INPUT = exports.MISSING = void 0;
4
4
  exports.MISSING = Symbol.for("__missing__");
5
5
  exports.INPUT = "__input__";
6
6
  exports.ERROR = "__error__";
@@ -11,6 +11,9 @@ exports.CONFIG_KEY_RESUMING = "__pregel_resuming";
11
11
  exports.CONFIG_KEY_TASK_ID = "__pregel_task_id";
12
12
  exports.CONFIG_KEY_STREAM = "__pregel_stream";
13
13
  exports.CONFIG_KEY_RESUME_VALUE = "__pregel_resume_value";
14
+ exports.CONFIG_KEY_WRITES = "__pregel_writes";
15
+ exports.CONFIG_KEY_SCRATCHPAD = "__pregel_scratchpad";
16
+ exports.CONFIG_KEY_CHECKPOINT_NS = "checkpoint_ns";
14
17
  // this one is part of public API
15
18
  exports.CONFIG_KEY_CHECKPOINT_MAP = "checkpoint_map";
16
19
  exports.INTERRUPT = "__interrupt__";
@@ -8,6 +8,9 @@ export declare const CONFIG_KEY_RESUMING = "__pregel_resuming";
8
8
  export declare const CONFIG_KEY_TASK_ID = "__pregel_task_id";
9
9
  export declare const CONFIG_KEY_STREAM = "__pregel_stream";
10
10
  export declare const CONFIG_KEY_RESUME_VALUE = "__pregel_resume_value";
11
+ export declare const CONFIG_KEY_WRITES = "__pregel_writes";
12
+ export declare const CONFIG_KEY_SCRATCHPAD = "__pregel_scratchpad";
13
+ export declare const CONFIG_KEY_CHECKPOINT_NS = "checkpoint_ns";
11
14
  export declare const CONFIG_KEY_CHECKPOINT_MAP = "checkpoint_map";
12
15
  export declare const INTERRUPT = "__interrupt__";
13
16
  export declare const RESUME = "__resume__";
package/dist/constants.js CHANGED
@@ -8,6 +8,9 @@ export const CONFIG_KEY_RESUMING = "__pregel_resuming";
8
8
  export const CONFIG_KEY_TASK_ID = "__pregel_task_id";
9
9
  export const CONFIG_KEY_STREAM = "__pregel_stream";
10
10
  export const CONFIG_KEY_RESUME_VALUE = "__pregel_resume_value";
11
+ export const CONFIG_KEY_WRITES = "__pregel_writes";
12
+ export const CONFIG_KEY_SCRATCHPAD = "__pregel_scratchpad";
13
+ export const CONFIG_KEY_CHECKPOINT_NS = "checkpoint_ns";
11
14
  // this one is part of public API
12
15
  export const CONFIG_KEY_CHECKPOINT_MAP = "checkpoint_map";
13
16
  export const INTERRUPT = "__interrupt__";
@@ -4,24 +4,98 @@ exports.interrupt = void 0;
4
4
  const singletons_1 = require("@langchain/core/singletons");
5
5
  const errors_js_1 = require("./errors.cjs");
6
6
  const constants_js_1 = require("./constants.cjs");
7
+ /**
8
+ * Interrupts the execution of a graph node.
9
+ * This function can be used to pause execution of a node, and return the value of the `resume`
10
+ * input when the graph is re-invoked using `Command`.
11
+ * Multiple interrupts can be called within a single node, and each will be handled sequentially.
12
+ *
13
+ * When an interrupt is called:
14
+ * 1. If there's a `resume` value available (from a previous `Command`), it returns that value.
15
+ * 2. Otherwise, it throws a `GraphInterrupt` with the provided value
16
+ * 3. The graph can be resumed by passing a `Command` with a `resume` value
17
+ *
18
+ * @param value - The value to include in the interrupt. This will be available in task.interrupts[].value
19
+ * @returns The `resume` value provided when the graph is re-invoked with a Command
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * // Define a node that uses multiple interrupts
24
+ * const nodeWithInterrupts = () => {
25
+ * // First interrupt - will pause execution and include {value: 1} in task values
26
+ * const answer1 = interrupt({ value: 1 });
27
+ *
28
+ * // Second interrupt - only called after first interrupt is resumed
29
+ * const answer2 = interrupt({ value: 2 });
30
+ *
31
+ * // Use the resume values
32
+ * return { myKey: answer1 + " " + answer2 };
33
+ * };
34
+ *
35
+ * // Resume the graph after first interrupt
36
+ * await graph.stream(new Command({ resume: "answer 1" }));
37
+ *
38
+ * // Resume the graph after second interrupt
39
+ * await graph.stream(new Command({ resume: "answer 2" }));
40
+ * // Final result: { myKey: "answer 1 answer 2" }
41
+ * ```
42
+ *
43
+ * @throws {Error} If called outside the context of a graph
44
+ * @throws {GraphInterrupt} When no resume value is available
45
+ */
7
46
  function interrupt(value) {
8
47
  const config = singletons_1.AsyncLocalStorageProviderSingleton.getRunnableConfig();
9
48
  if (!config) {
10
49
  throw new Error("Called interrupt() outside the context of a graph.");
11
50
  }
12
- const resume = config.configurable?.[constants_js_1.CONFIG_KEY_RESUME_VALUE];
13
- if (resume !== constants_js_1.MISSING) {
14
- return resume;
51
+ // Track interrupt index
52
+ const scratchpad = config.configurable?.[constants_js_1.CONFIG_KEY_SCRATCHPAD];
53
+ if (scratchpad.interruptCounter === undefined) {
54
+ scratchpad.interruptCounter = 0;
15
55
  }
16
56
  else {
17
- throw new errors_js_1.GraphInterrupt([
18
- {
19
- value,
20
- when: "during",
21
- resumable: true,
22
- ns: config.configurable?.checkpoint_ns?.split("|"),
23
- },
24
- ]);
57
+ scratchpad.interruptCounter += 1;
25
58
  }
59
+ const idx = scratchpad.interruptCounter;
60
+ // Find previous resume values
61
+ const taskId = config.configurable?.[constants_js_1.CONFIG_KEY_TASK_ID];
62
+ const writes = config.configurable?.[constants_js_1.CONFIG_KEY_WRITES] ?? [];
63
+ if (!scratchpad.resume) {
64
+ const newResume = (writes.find((w) => w[0] === taskId && w[1] === constants_js_1.RESUME)?.[2] || []);
65
+ scratchpad.resume = Array.isArray(newResume) ? newResume : [newResume];
66
+ }
67
+ if (scratchpad.resume) {
68
+ if (idx < scratchpad.resume.length) {
69
+ return scratchpad.resume[idx];
70
+ }
71
+ }
72
+ // Find current resume value
73
+ if (!scratchpad.usedNullResume) {
74
+ scratchpad.usedNullResume = true;
75
+ const sortedWrites = [...writes].sort((a, b) => b[0].localeCompare(a[0]) // Sort in reverse order
76
+ );
77
+ for (const [tid, c, v] of sortedWrites) {
78
+ if (tid === constants_js_1.NULL_TASK_ID && c === constants_js_1.RESUME) {
79
+ if (scratchpad.resume.length !== idx) {
80
+ throw new Error(`Resume length mismatch: ${scratchpad.resume.length} !== ${idx}`);
81
+ }
82
+ scratchpad.resume.push(v);
83
+ const send = config.configurable?.[constants_js_1.CONFIG_KEY_SEND];
84
+ if (send) {
85
+ send([[constants_js_1.RESUME, scratchpad.resume]]);
86
+ }
87
+ return v;
88
+ }
89
+ }
90
+ }
91
+ // No resume value found
92
+ throw new errors_js_1.GraphInterrupt([
93
+ {
94
+ value,
95
+ when: "during",
96
+ resumable: true,
97
+ ns: config.configurable?.[constants_js_1.CONFIG_KEY_CHECKPOINT_NS]?.split(constants_js_1.CHECKPOINT_NAMESPACE_SEPARATOR),
98
+ },
99
+ ]);
26
100
  }
27
101
  exports.interrupt = interrupt;
@@ -1 +1,40 @@
1
+ /**
2
+ * Interrupts the execution of a graph node.
3
+ * This function can be used to pause execution of a node, and return the value of the `resume`
4
+ * input when the graph is re-invoked using `Command`.
5
+ * Multiple interrupts can be called within a single node, and each will be handled sequentially.
6
+ *
7
+ * When an interrupt is called:
8
+ * 1. If there's a `resume` value available (from a previous `Command`), it returns that value.
9
+ * 2. Otherwise, it throws a `GraphInterrupt` with the provided value
10
+ * 3. The graph can be resumed by passing a `Command` with a `resume` value
11
+ *
12
+ * @param value - The value to include in the interrupt. This will be available in task.interrupts[].value
13
+ * @returns The `resume` value provided when the graph is re-invoked with a Command
14
+ *
15
+ * @example
16
+ * ```typescript
17
+ * // Define a node that uses multiple interrupts
18
+ * const nodeWithInterrupts = () => {
19
+ * // First interrupt - will pause execution and include {value: 1} in task values
20
+ * const answer1 = interrupt({ value: 1 });
21
+ *
22
+ * // Second interrupt - only called after first interrupt is resumed
23
+ * const answer2 = interrupt({ value: 2 });
24
+ *
25
+ * // Use the resume values
26
+ * return { myKey: answer1 + " " + answer2 };
27
+ * };
28
+ *
29
+ * // Resume the graph after first interrupt
30
+ * await graph.stream(new Command({ resume: "answer 1" }));
31
+ *
32
+ * // Resume the graph after second interrupt
33
+ * await graph.stream(new Command({ resume: "answer 2" }));
34
+ * // Final result: { myKey: "answer 1 answer 2" }
35
+ * ```
36
+ *
37
+ * @throws {Error} If called outside the context of a graph
38
+ * @throws {GraphInterrupt} When no resume value is available
39
+ */
1
40
  export declare function interrupt<I = unknown, R = unknown>(value: I): R;
package/dist/interrupt.js CHANGED
@@ -1,23 +1,97 @@
1
1
  import { AsyncLocalStorageProviderSingleton } from "@langchain/core/singletons";
2
2
  import { GraphInterrupt } from "./errors.js";
3
- import { CONFIG_KEY_RESUME_VALUE, MISSING } from "./constants.js";
3
+ import { CONFIG_KEY_CHECKPOINT_NS, CONFIG_KEY_SCRATCHPAD, CONFIG_KEY_TASK_ID, CONFIG_KEY_WRITES, CONFIG_KEY_SEND, CHECKPOINT_NAMESPACE_SEPARATOR, NULL_TASK_ID, RESUME, } from "./constants.js";
4
+ /**
5
+ * Interrupts the execution of a graph node.
6
+ * This function can be used to pause execution of a node, and return the value of the `resume`
7
+ * input when the graph is re-invoked using `Command`.
8
+ * Multiple interrupts can be called within a single node, and each will be handled sequentially.
9
+ *
10
+ * When an interrupt is called:
11
+ * 1. If there's a `resume` value available (from a previous `Command`), it returns that value.
12
+ * 2. Otherwise, it throws a `GraphInterrupt` with the provided value
13
+ * 3. The graph can be resumed by passing a `Command` with a `resume` value
14
+ *
15
+ * @param value - The value to include in the interrupt. This will be available in task.interrupts[].value
16
+ * @returns The `resume` value provided when the graph is re-invoked with a Command
17
+ *
18
+ * @example
19
+ * ```typescript
20
+ * // Define a node that uses multiple interrupts
21
+ * const nodeWithInterrupts = () => {
22
+ * // First interrupt - will pause execution and include {value: 1} in task values
23
+ * const answer1 = interrupt({ value: 1 });
24
+ *
25
+ * // Second interrupt - only called after first interrupt is resumed
26
+ * const answer2 = interrupt({ value: 2 });
27
+ *
28
+ * // Use the resume values
29
+ * return { myKey: answer1 + " " + answer2 };
30
+ * };
31
+ *
32
+ * // Resume the graph after first interrupt
33
+ * await graph.stream(new Command({ resume: "answer 1" }));
34
+ *
35
+ * // Resume the graph after second interrupt
36
+ * await graph.stream(new Command({ resume: "answer 2" }));
37
+ * // Final result: { myKey: "answer 1 answer 2" }
38
+ * ```
39
+ *
40
+ * @throws {Error} If called outside the context of a graph
41
+ * @throws {GraphInterrupt} When no resume value is available
42
+ */
4
43
  export function interrupt(value) {
5
44
  const config = AsyncLocalStorageProviderSingleton.getRunnableConfig();
6
45
  if (!config) {
7
46
  throw new Error("Called interrupt() outside the context of a graph.");
8
47
  }
9
- const resume = config.configurable?.[CONFIG_KEY_RESUME_VALUE];
10
- if (resume !== MISSING) {
11
- return resume;
48
+ // Track interrupt index
49
+ const scratchpad = config.configurable?.[CONFIG_KEY_SCRATCHPAD];
50
+ if (scratchpad.interruptCounter === undefined) {
51
+ scratchpad.interruptCounter = 0;
12
52
  }
13
53
  else {
14
- throw new GraphInterrupt([
15
- {
16
- value,
17
- when: "during",
18
- resumable: true,
19
- ns: config.configurable?.checkpoint_ns?.split("|"),
20
- },
21
- ]);
54
+ scratchpad.interruptCounter += 1;
22
55
  }
56
+ const idx = scratchpad.interruptCounter;
57
+ // Find previous resume values
58
+ const taskId = config.configurable?.[CONFIG_KEY_TASK_ID];
59
+ const writes = config.configurable?.[CONFIG_KEY_WRITES] ?? [];
60
+ if (!scratchpad.resume) {
61
+ const newResume = (writes.find((w) => w[0] === taskId && w[1] === RESUME)?.[2] || []);
62
+ scratchpad.resume = Array.isArray(newResume) ? newResume : [newResume];
63
+ }
64
+ if (scratchpad.resume) {
65
+ if (idx < scratchpad.resume.length) {
66
+ return scratchpad.resume[idx];
67
+ }
68
+ }
69
+ // Find current resume value
70
+ if (!scratchpad.usedNullResume) {
71
+ scratchpad.usedNullResume = true;
72
+ const sortedWrites = [...writes].sort((a, b) => b[0].localeCompare(a[0]) // Sort in reverse order
73
+ );
74
+ for (const [tid, c, v] of sortedWrites) {
75
+ if (tid === NULL_TASK_ID && c === RESUME) {
76
+ if (scratchpad.resume.length !== idx) {
77
+ throw new Error(`Resume length mismatch: ${scratchpad.resume.length} !== ${idx}`);
78
+ }
79
+ scratchpad.resume.push(v);
80
+ const send = config.configurable?.[CONFIG_KEY_SEND];
81
+ if (send) {
82
+ send([[RESUME, scratchpad.resume]]);
83
+ }
84
+ return v;
85
+ }
86
+ }
87
+ }
88
+ // No resume value found
89
+ throw new GraphInterrupt([
90
+ {
91
+ value,
92
+ when: "during",
93
+ resumable: true,
94
+ ns: config.configurable?.[CONFIG_KEY_CHECKPOINT_NS]?.split(CHECKPOINT_NAMESPACE_SEPARATOR),
95
+ },
96
+ ]);
23
97
  }
@@ -75,7 +75,7 @@ function _localRead(step, checkpoint, channels, managed, task, select, fresh = f
75
75
  exports._localRead = _localRead;
76
76
  function _localWrite(step,
77
77
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
78
- commit, processes, channels, managed,
78
+ commit, processes, managed,
79
79
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
80
  writes) {
81
81
  for (const [chan, value] of writes) {
@@ -89,9 +89,6 @@ writes) {
89
89
  // replace any runtime values with placeholders
90
90
  managed.replaceRuntimeValues(step, value.args);
91
91
  }
92
- else if (!(chan in channels) && !managed.get(chan)) {
93
- console.warn(`Skipping write for channel '${chan}' which has no readers`);
94
- }
95
92
  }
96
93
  commit(writes);
97
94
  }
@@ -307,7 +304,6 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
307
304
  metadata = { ...metadata, ...proc.metadata };
308
305
  }
309
306
  const writes = [];
310
- const resume = pendingWrites?.find((w) => [taskId, constants_js_1.NULL_TASK_ID].includes(w[0]) && w[1] === constants_js_1.RESUME);
311
307
  return {
312
308
  name: packet.node,
313
309
  input: packet.args,
@@ -324,7 +320,7 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
324
320
  configurable: {
325
321
  [constants_js_1.CONFIG_KEY_TASK_ID]: taskId,
326
322
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
327
- [constants_js_1.CONFIG_KEY_SEND]: (writes_) => _localWrite(step, (items) => writes.push(...items), processes, channels, managed, writes_),
323
+ [constants_js_1.CONFIG_KEY_SEND]: (writes_) => _localWrite(step, (items) => writes.push(...items), processes, managed, writes_),
328
324
  [constants_js_1.CONFIG_KEY_READ]: (select_, fresh_ = false) => _localRead(step, checkpoint, channels, managed, {
329
325
  name: packet.node,
330
326
  writes: writes,
@@ -336,9 +332,11 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
336
332
  ...configurable[constants_js_1.CONFIG_KEY_CHECKPOINT_MAP],
337
333
  [parentNamespace]: checkpoint.id,
338
334
  },
339
- [constants_js_1.CONFIG_KEY_RESUME_VALUE]: resume
340
- ? resume[2]
341
- : configurable[constants_js_1.CONFIG_KEY_RESUME_VALUE] ?? constants_js_1.MISSING,
335
+ [constants_js_1.CONFIG_KEY_WRITES]: [
336
+ ...(pendingWrites || []),
337
+ ...(configurable[constants_js_1.CONFIG_KEY_WRITES] || []),
338
+ ].filter((w) => w[0] === constants_js_1.NULL_TASK_ID || w[0] === taskId),
339
+ [constants_js_1.CONFIG_KEY_SCRATCHPAD]: {},
342
340
  checkpoint_id: undefined,
343
341
  checkpoint_ns: taskCheckpointNamespace,
344
342
  },
@@ -409,7 +407,6 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
409
407
  metadata = { ...metadata, ...proc.metadata };
410
408
  }
411
409
  const writes = [];
412
- const resume = pendingWrites?.find((w) => [taskId, constants_js_1.NULL_TASK_ID].includes(w[0]) && w[1] === constants_js_1.RESUME);
413
410
  const taskCheckpointNamespace = `${checkpointNamespace}${constants_js_1.CHECKPOINT_NAMESPACE_END}${taskId}`;
414
411
  return {
415
412
  name,
@@ -429,7 +426,7 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
429
426
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
430
427
  [constants_js_1.CONFIG_KEY_SEND]: (writes_) => _localWrite(step, (items) => {
431
428
  writes.push(...items);
432
- }, processes, channels, managed, writes_),
429
+ }, processes, managed, writes_),
433
430
  [constants_js_1.CONFIG_KEY_READ]: (select_, fresh_ = false) => _localRead(step, checkpoint, channels, managed, {
434
431
  name,
435
432
  writes: writes,
@@ -441,9 +438,11 @@ function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processes, chan
441
438
  ...configurable[constants_js_1.CONFIG_KEY_CHECKPOINT_MAP],
442
439
  [parentNamespace]: checkpoint.id,
443
440
  },
444
- [constants_js_1.CONFIG_KEY_RESUME_VALUE]: resume
445
- ? resume[2]
446
- : configurable[constants_js_1.CONFIG_KEY_RESUME_VALUE] ?? constants_js_1.MISSING,
441
+ [constants_js_1.CONFIG_KEY_WRITES]: [
442
+ ...(pendingWrites || []),
443
+ ...(configurable[constants_js_1.CONFIG_KEY_WRITES] || []),
444
+ ].filter((w) => w[0] === constants_js_1.NULL_TASK_ID || w[0] === taskId),
445
+ [constants_js_1.CONFIG_KEY_SCRATCHPAD]: {},
447
446
  checkpoint_id: undefined,
448
447
  checkpoint_ns: taskCheckpointNamespace,
449
448
  },
@@ -20,7 +20,7 @@ export type WritesProtocol<C = string> = {
20
20
  export declare const increment: (current?: number) => number;
21
21
  export declare function shouldInterrupt<N extends PropertyKey, C extends PropertyKey>(checkpoint: Checkpoint, interruptNodes: All | N[], tasks: PregelExecutableTask<N, C>[]): boolean;
22
22
  export declare function _localRead<Cc extends Record<string, BaseChannel>>(step: number, checkpoint: ReadonlyCheckpoint, channels: Cc, managed: ManagedValueMapping, task: WritesProtocol<keyof Cc>, select: Array<keyof Cc> | keyof Cc, fresh?: boolean): Record<string, unknown> | unknown;
23
- export declare function _localWrite(step: number, commit: (writes: [string, any][]) => any, processes: Record<string, PregelNode>, channels: Record<string, BaseChannel>, managed: ManagedValueMapping, writes: [string, any][]): void;
23
+ export declare function _localWrite(step: number, commit: (writes: [string, any][]) => any, processes: Record<string, PregelNode>, managed: ManagedValueMapping, writes: [string, any][]): void;
24
24
  export declare function _applyWrites<Cc extends Record<string, BaseChannel>>(checkpoint: Checkpoint, channels: Cc, tasks: WritesProtocol<keyof Cc>[], getNextVersion?: (version: any, channel: BaseChannel) => any): Record<string, PendingWriteValue[]>;
25
25
  export type NextTaskExtraFields = {
26
26
  step: number;
@@ -3,7 +3,7 @@ import { mergeConfigs, patchConfig, } from "@langchain/core/runnables";
3
3
  import { copyCheckpoint, uuid5, maxChannelVersion, } from "@langchain/langgraph-checkpoint";
4
4
  import { createCheckpoint, emptyChannels, isBaseChannel, } from "../channels/base.js";
5
5
  import { readChannel, readChannels } from "./io.js";
6
- import { _isSend, _isSendInterface, CONFIG_KEY_CHECKPOINT_MAP, CHECKPOINT_NAMESPACE_SEPARATOR, CONFIG_KEY_CHECKPOINTER, CONFIG_KEY_READ, CONFIG_KEY_TASK_ID, CONFIG_KEY_SEND, INTERRUPT, RESERVED, TAG_HIDDEN, TASKS, CHECKPOINT_NAMESPACE_END, PUSH, PULL, RESUME, CONFIG_KEY_RESUME_VALUE, NULL_TASK_ID, MISSING, } from "../constants.js";
6
+ import { _isSend, _isSendInterface, CONFIG_KEY_CHECKPOINT_MAP, CHECKPOINT_NAMESPACE_SEPARATOR, CONFIG_KEY_CHECKPOINTER, CONFIG_KEY_READ, CONFIG_KEY_TASK_ID, CONFIG_KEY_SEND, INTERRUPT, RESERVED, TAG_HIDDEN, TASKS, CHECKPOINT_NAMESPACE_END, PUSH, PULL, RESUME, NULL_TASK_ID, CONFIG_KEY_SCRATCHPAD, CONFIG_KEY_WRITES, } from "../constants.js";
7
7
  import { EmptyChannelError, InvalidUpdateError } from "../errors.js";
8
8
  import { getNullChannelVersion } from "./utils/index.js";
9
9
  export const increment = (current) => {
@@ -69,7 +69,7 @@ export function _localRead(step, checkpoint, channels, managed, task, select, fr
69
69
  }
70
70
  export function _localWrite(step,
71
71
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
72
- commit, processes, channels, managed,
72
+ commit, processes, managed,
73
73
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
74
74
  writes) {
75
75
  for (const [chan, value] of writes) {
@@ -83,9 +83,6 @@ writes) {
83
83
  // replace any runtime values with placeholders
84
84
  managed.replaceRuntimeValues(step, value.args);
85
85
  }
86
- else if (!(chan in channels) && !managed.get(chan)) {
87
- console.warn(`Skipping write for channel '${chan}' which has no readers`);
88
- }
89
86
  }
90
87
  commit(writes);
91
88
  }
@@ -298,7 +295,6 @@ export function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processe
298
295
  metadata = { ...metadata, ...proc.metadata };
299
296
  }
300
297
  const writes = [];
301
- const resume = pendingWrites?.find((w) => [taskId, NULL_TASK_ID].includes(w[0]) && w[1] === RESUME);
302
298
  return {
303
299
  name: packet.node,
304
300
  input: packet.args,
@@ -315,7 +311,7 @@ export function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processe
315
311
  configurable: {
316
312
  [CONFIG_KEY_TASK_ID]: taskId,
317
313
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
318
- [CONFIG_KEY_SEND]: (writes_) => _localWrite(step, (items) => writes.push(...items), processes, channels, managed, writes_),
314
+ [CONFIG_KEY_SEND]: (writes_) => _localWrite(step, (items) => writes.push(...items), processes, managed, writes_),
319
315
  [CONFIG_KEY_READ]: (select_, fresh_ = false) => _localRead(step, checkpoint, channels, managed, {
320
316
  name: packet.node,
321
317
  writes: writes,
@@ -327,9 +323,11 @@ export function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processe
327
323
  ...configurable[CONFIG_KEY_CHECKPOINT_MAP],
328
324
  [parentNamespace]: checkpoint.id,
329
325
  },
330
- [CONFIG_KEY_RESUME_VALUE]: resume
331
- ? resume[2]
332
- : configurable[CONFIG_KEY_RESUME_VALUE] ?? MISSING,
326
+ [CONFIG_KEY_WRITES]: [
327
+ ...(pendingWrites || []),
328
+ ...(configurable[CONFIG_KEY_WRITES] || []),
329
+ ].filter((w) => w[0] === NULL_TASK_ID || w[0] === taskId),
330
+ [CONFIG_KEY_SCRATCHPAD]: {},
333
331
  checkpoint_id: undefined,
334
332
  checkpoint_ns: taskCheckpointNamespace,
335
333
  },
@@ -400,7 +398,6 @@ export function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processe
400
398
  metadata = { ...metadata, ...proc.metadata };
401
399
  }
402
400
  const writes = [];
403
- const resume = pendingWrites?.find((w) => [taskId, NULL_TASK_ID].includes(w[0]) && w[1] === RESUME);
404
401
  const taskCheckpointNamespace = `${checkpointNamespace}${CHECKPOINT_NAMESPACE_END}${taskId}`;
405
402
  return {
406
403
  name,
@@ -420,7 +417,7 @@ export function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processe
420
417
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
421
418
  [CONFIG_KEY_SEND]: (writes_) => _localWrite(step, (items) => {
422
419
  writes.push(...items);
423
- }, processes, channels, managed, writes_),
420
+ }, processes, managed, writes_),
424
421
  [CONFIG_KEY_READ]: (select_, fresh_ = false) => _localRead(step, checkpoint, channels, managed, {
425
422
  name,
426
423
  writes: writes,
@@ -432,9 +429,11 @@ export function _prepareSingleTask(taskPath, checkpoint, pendingWrites, processe
432
429
  ...configurable[CONFIG_KEY_CHECKPOINT_MAP],
433
430
  [parentNamespace]: checkpoint.id,
434
431
  },
435
- [CONFIG_KEY_RESUME_VALUE]: resume
436
- ? resume[2]
437
- : configurable[CONFIG_KEY_RESUME_VALUE] ?? MISSING,
432
+ [CONFIG_KEY_WRITES]: [
433
+ ...(pendingWrites || []),
434
+ ...(configurable[CONFIG_KEY_WRITES] || []),
435
+ ].filter((w) => w[0] === NULL_TASK_ID || w[0] === taskId),
436
+ [CONFIG_KEY_SCRATCHPAD]: {},
438
437
  checkpoint_id: undefined,
439
438
  checkpoint_ns: taskCheckpointNamespace,
440
439
  },
@@ -863,7 +863,12 @@ class Pregel extends runnables_1.Runnable {
863
863
  throw error;
864
864
  }
865
865
  if ((0, errors_js_1.isGraphInterrupt)(error) && error.interrupts.length) {
866
- loop.putWrites(task.id, error.interrupts.map((interrupt) => [constants_js_1.INTERRUPT, interrupt]));
866
+ const interrupts = error.interrupts.map((interrupt) => [constants_js_1.INTERRUPT, interrupt]);
867
+ const resumes = task.writes.filter((w) => w[0] === constants_js_1.RESUME);
868
+ if (resumes.length) {
869
+ interrupts.push(...resumes);
870
+ }
871
+ loop.putWrites(task.id, interrupts);
867
872
  }
868
873
  }
869
874
  else {
@@ -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, PUSH, } 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, RESUME, 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";
@@ -859,7 +859,12 @@ export class Pregel extends Runnable {
859
859
  throw error;
860
860
  }
861
861
  if (isGraphInterrupt(error) && error.interrupts.length) {
862
- loop.putWrites(task.id, error.interrupts.map((interrupt) => [INTERRUPT, interrupt]));
862
+ const interrupts = error.interrupts.map((interrupt) => [INTERRUPT, interrupt]);
863
+ const resumes = task.writes.filter((w) => w[0] === RESUME);
864
+ if (resumes.length) {
865
+ interrupts.push(...resumes);
866
+ }
867
+ loop.putWrites(task.id, interrupts);
863
868
  }
864
869
  }
865
870
  else {
@@ -49,7 +49,7 @@ exports.readChannels = readChannels;
49
49
  /**
50
50
  * Map input chunk to a sequence of pending writes in the form (channel, value).
51
51
  */
52
- function* mapCommand(cmd) {
52
+ function* mapCommand(cmd, pendingWrites) {
53
53
  if (cmd.graph === constants_js_1.Command.PARENT) {
54
54
  throw new errors_js_1.InvalidUpdateError("There is no parent graph.");
55
55
  }
@@ -80,7 +80,11 @@ function* mapCommand(cmd) {
80
80
  Object.keys(cmd.resume).length &&
81
81
  Object.keys(cmd.resume).every(uuid_1.validate)) {
82
82
  for (const [tid, resume] of Object.entries(cmd.resume)) {
83
- yield [tid, constants_js_1.RESUME, resume];
83
+ // Find existing resume values for this task ID
84
+ const existing = (pendingWrites.find(([id, type]) => id === tid && type === constants_js_1.RESUME)?.[2] ?? []);
85
+ // Ensure we have an array and append the resume value
86
+ existing.push(resume);
87
+ yield [tid, constants_js_1.RESUME, existing];
84
88
  }
85
89
  }
86
90
  else {
@@ -1,4 +1,4 @@
1
- import type { PendingWrite } from "@langchain/langgraph-checkpoint";
1
+ import type { CheckpointPendingWrite, PendingWrite } from "@langchain/langgraph-checkpoint";
2
2
  import type { BaseChannel } from "../channels/base.js";
3
3
  import type { PregelExecutableTask } from "./types.js";
4
4
  import { Command } from "../constants.js";
@@ -7,7 +7,7 @@ export declare function readChannels<C extends PropertyKey>(channels: Record<C,
7
7
  /**
8
8
  * Map input chunk to a sequence of pending writes in the form (channel, value).
9
9
  */
10
- export declare function mapCommand(cmd: Command): Generator<[string, string, unknown]>;
10
+ export declare function mapCommand(cmd: Command, pendingWrites: CheckpointPendingWrite<string>[]): Generator<[string, string, unknown]>;
11
11
  /**
12
12
  * Map input chunk to a sequence of pending writes in the form [channel, value].
13
13
  */
package/dist/pregel/io.js CHANGED
@@ -44,7 +44,7 @@ export function readChannels(channels, select, skipEmpty = true
44
44
  /**
45
45
  * Map input chunk to a sequence of pending writes in the form (channel, value).
46
46
  */
47
- export function* mapCommand(cmd) {
47
+ export function* mapCommand(cmd, pendingWrites) {
48
48
  if (cmd.graph === Command.PARENT) {
49
49
  throw new InvalidUpdateError("There is no parent graph.");
50
50
  }
@@ -75,7 +75,11 @@ export function* mapCommand(cmd) {
75
75
  Object.keys(cmd.resume).length &&
76
76
  Object.keys(cmd.resume).every(validate)) {
77
77
  for (const [tid, resume] of Object.entries(cmd.resume)) {
78
- yield [tid, RESUME, resume];
78
+ // Find existing resume values for this task ID
79
+ const existing = (pendingWrites.find(([id, type]) => id === tid && type === RESUME)?.[2] ?? []);
80
+ // Ensure we have an array and append the resume value
81
+ existing.push(resume);
82
+ yield [tid, RESUME, existing];
79
83
  }
80
84
  }
81
85
  else {
@@ -399,14 +399,26 @@ class PregelLoop {
399
399
  * @param writes
400
400
  */
401
401
  putWrites(taskId, writes) {
402
- if (writes.length === 0) {
402
+ let writesCopy = writes;
403
+ if (writesCopy.length === 0) {
403
404
  return;
404
405
  }
406
+ // deduplicate writes to special channels, last write wins
407
+ if (writesCopy.every(([key]) => key in langgraph_checkpoint_1.WRITES_IDX_MAP)) {
408
+ writesCopy = Array.from(new Map(writesCopy.map((w) => [w[0], w])).values());
409
+ }
405
410
  // save writes
406
- const pendingWrites = writes.map(([key, value]) => {
407
- return [taskId, key, value];
408
- });
409
- this.checkpointPendingWrites.push(...pendingWrites);
411
+ for (const [c, v] of writesCopy) {
412
+ if (c in langgraph_checkpoint_1.WRITES_IDX_MAP) {
413
+ const idx = this.checkpointPendingWrites.findIndex((w) => w[0] === taskId && w[1] === c);
414
+ if (idx !== -1) {
415
+ this.checkpointPendingWrites[idx] = [taskId, c, v];
416
+ }
417
+ else {
418
+ this.checkpointPendingWrites.push([taskId, c, v]);
419
+ }
420
+ }
421
+ }
410
422
  const putWritePromise = this.checkpointer?.putWrites({
411
423
  ...this.checkpointConfig,
412
424
  configurable: {
@@ -414,12 +426,12 @@ class PregelLoop {
414
426
  checkpoint_ns: this.config.configurable?.checkpoint_ns ?? "",
415
427
  checkpoint_id: this.checkpoint.id,
416
428
  },
417
- }, writes, taskId);
429
+ }, writesCopy, taskId);
418
430
  if (putWritePromise !== undefined) {
419
431
  this.checkpointerPromises.push(putWritePromise);
420
432
  }
421
433
  if (this.tasks) {
422
- this._outputWrites(taskId, writes);
434
+ this._outputWrites(taskId, writesCopy);
423
435
  }
424
436
  }
425
437
  _outputWrites(taskId, writes, cached = false) {
@@ -457,7 +469,7 @@ class PregelLoop {
457
469
  if (![INPUT_DONE, INPUT_RESUMING].includes(this.input)) {
458
470
  await this._first(inputKeys);
459
471
  }
460
- else if (Object.values(this.tasks).every((task) => task.writes.length > 0)) {
472
+ else if (Object.values(this.tasks).every((task) => task.writes.filter(([c]) => !(c in langgraph_checkpoint_1.WRITES_IDX_MAP)).length > 0)) {
461
473
  const writes = Object.values(this.tasks).flatMap((t) => t.writes);
462
474
  // All tasks have finished
463
475
  const managedValueWrites = (0, algo_js_1._applyWrites)(this.checkpoint, this.channels, Object.values(this.tasks), this.checkpointerGetNextVersion);
@@ -581,7 +593,7 @@ class PregelLoop {
581
593
  if ((0, constants_js_1._isCommand)(this.input)) {
582
594
  const writes = {};
583
595
  // group writes by task id
584
- for (const [tid, key, value] of (0, io_js_1.mapCommand)(this.input)) {
596
+ for (const [tid, key, value] of (0, io_js_1.mapCommand)(this.input, this.checkpointPendingWrites)) {
585
597
  if (writes[tid] === undefined) {
586
598
  writes[tid] = [];
587
599
  }
@@ -1,5 +1,5 @@
1
1
  import { IterableReadableStream } from "@langchain/core/utils/stream";
2
- import { copyCheckpoint, emptyCheckpoint, AsyncBatchedStore, } from "@langchain/langgraph-checkpoint";
2
+ import { copyCheckpoint, emptyCheckpoint, AsyncBatchedStore, WRITES_IDX_MAP, } from "@langchain/langgraph-checkpoint";
3
3
  import { createCheckpoint, emptyChannels, } from "../channels/base.js";
4
4
  import { _isCommand, CHECKPOINT_NAMESPACE_SEPARATOR, CONFIG_KEY_CHECKPOINT_MAP, CONFIG_KEY_READ, CONFIG_KEY_RESUMING, CONFIG_KEY_STREAM, ERROR, INPUT, INTERRUPT, RESUME, TAG_HIDDEN, } from "../constants.js";
5
5
  import { _applyWrites, _prepareNextTasks, increment, shouldInterrupt, } from "./algo.js";
@@ -395,14 +395,26 @@ export class PregelLoop {
395
395
  * @param writes
396
396
  */
397
397
  putWrites(taskId, writes) {
398
- if (writes.length === 0) {
398
+ let writesCopy = writes;
399
+ if (writesCopy.length === 0) {
399
400
  return;
400
401
  }
402
+ // deduplicate writes to special channels, last write wins
403
+ if (writesCopy.every(([key]) => key in WRITES_IDX_MAP)) {
404
+ writesCopy = Array.from(new Map(writesCopy.map((w) => [w[0], w])).values());
405
+ }
401
406
  // save writes
402
- const pendingWrites = writes.map(([key, value]) => {
403
- return [taskId, key, value];
404
- });
405
- this.checkpointPendingWrites.push(...pendingWrites);
407
+ for (const [c, v] of writesCopy) {
408
+ if (c in WRITES_IDX_MAP) {
409
+ const idx = this.checkpointPendingWrites.findIndex((w) => w[0] === taskId && w[1] === c);
410
+ if (idx !== -1) {
411
+ this.checkpointPendingWrites[idx] = [taskId, c, v];
412
+ }
413
+ else {
414
+ this.checkpointPendingWrites.push([taskId, c, v]);
415
+ }
416
+ }
417
+ }
406
418
  const putWritePromise = this.checkpointer?.putWrites({
407
419
  ...this.checkpointConfig,
408
420
  configurable: {
@@ -410,12 +422,12 @@ export class PregelLoop {
410
422
  checkpoint_ns: this.config.configurable?.checkpoint_ns ?? "",
411
423
  checkpoint_id: this.checkpoint.id,
412
424
  },
413
- }, writes, taskId);
425
+ }, writesCopy, taskId);
414
426
  if (putWritePromise !== undefined) {
415
427
  this.checkpointerPromises.push(putWritePromise);
416
428
  }
417
429
  if (this.tasks) {
418
- this._outputWrites(taskId, writes);
430
+ this._outputWrites(taskId, writesCopy);
419
431
  }
420
432
  }
421
433
  _outputWrites(taskId, writes, cached = false) {
@@ -453,7 +465,7 @@ export class PregelLoop {
453
465
  if (![INPUT_DONE, INPUT_RESUMING].includes(this.input)) {
454
466
  await this._first(inputKeys);
455
467
  }
456
- else if (Object.values(this.tasks).every((task) => task.writes.length > 0)) {
468
+ else if (Object.values(this.tasks).every((task) => task.writes.filter(([c]) => !(c in WRITES_IDX_MAP)).length > 0)) {
457
469
  const writes = Object.values(this.tasks).flatMap((t) => t.writes);
458
470
  // All tasks have finished
459
471
  const managedValueWrites = _applyWrites(this.checkpoint, this.channels, Object.values(this.tasks), this.checkpointerGetNextVersion);
@@ -577,7 +589,7 @@ export class PregelLoop {
577
589
  if (_isCommand(this.input)) {
578
590
  const writes = {};
579
591
  // group writes by task id
580
- for (const [tid, key, value] of mapCommand(this.input)) {
592
+ for (const [tid, key, value] of mapCommand(this.input, this.checkpointPendingWrites)) {
581
593
  if (writes[tid] === undefined) {
582
594
  writes[tid] = [];
583
595
  }
@@ -144,4 +144,9 @@ export interface StateSnapshot {
144
144
  */
145
145
  readonly tasks: PregelTaskDescription[];
146
146
  }
147
+ export type PregelScratchpad<Resume> = {
148
+ interruptCounter: number;
149
+ usedNullResume: boolean;
150
+ resume: Resume[];
151
+ };
147
152
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@langchain/langgraph",
3
- "version": "0.2.29",
3
+ "version": "0.2.30",
4
4
  "description": "LangGraph",
5
5
  "type": "module",
6
6
  "engines": {