@langchain/langgraph 0.2.28 → 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__";
@@ -121,6 +124,9 @@ function _isSend(x) {
121
124
  return operation !== undefined && operation.lg_name === "Send";
122
125
  }
123
126
  exports._isSend = _isSend;
127
+ /**
128
+ * One or more commands to update the graph's state and send messages to nodes.
129
+ */
124
130
  class Command {
125
131
  constructor(args) {
126
132
  Object.defineProperty(this, "lg_name", {
@@ -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__";
@@ -89,11 +92,32 @@ export type Interrupt = {
89
92
  ns?: string[];
90
93
  };
91
94
  export type CommandParams<R> = {
95
+ /**
96
+ * Value to resume execution with. To be used together with {@link interrupt}.
97
+ */
92
98
  resume?: R;
99
+ /**
100
+ * Graph to send the command to. Supported values are:
101
+ * - None: the current graph (default)
102
+ * - GraphCommand.PARENT: closest parent graph
103
+ */
93
104
  graph?: string;
105
+ /**
106
+ * Update to apply to the graph's state.
107
+ */
94
108
  update?: Record<string, any>;
109
+ /**
110
+ * Can be one of the following:
111
+ * - name of the node to navigate to next (any node that belongs to the specified `graph`)
112
+ * - sequence of node names to navigate to next
113
+ * - `Send` object (to execute a node with the input provided)
114
+ * - sequence of `Send` objects
115
+ */
95
116
  goto?: string | Send | (string | Send)[];
96
117
  };
118
+ /**
119
+ * One or more commands to update the graph's state and send messages to nodes.
120
+ */
97
121
  export declare class Command<R = unknown> {
98
122
  lg_name: string;
99
123
  resume?: R;
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__";
@@ -115,6 +118,9 @@ export function _isSend(x) {
115
118
  const operation = x;
116
119
  return operation !== undefined && operation.lg_name === "Send";
117
120
  }
121
+ /**
122
+ * One or more commands to update the graph's state and send messages to nodes.
123
+ */
118
124
  export class Command {
119
125
  constructor(args) {
120
126
  Object.defineProperty(this, "lg_name", {
@@ -159,6 +159,7 @@ class Graph {
159
159
  runnable,
160
160
  metadata: options?.metadata,
161
161
  subgraphs: (0, subgraph_js_1.isPregelLike)(runnable) ? [runnable] : options?.subgraphs,
162
+ ends: options?.ends,
162
163
  };
163
164
  return this;
164
165
  }
@@ -290,6 +291,11 @@ class Graph {
290
291
  }
291
292
  }
292
293
  }
294
+ for (const node of Object.values(this.nodes)) {
295
+ for (const target of node.ends ?? []) {
296
+ allTargets.add(target);
297
+ }
298
+ }
293
299
  // validate targets
294
300
  for (const node of Object.keys(this.nodes)) {
295
301
  if (!allTargets.has(node)) {
@@ -331,6 +337,7 @@ class CompiledGraph extends index_js_1.Pregel {
331
337
  triggers: [],
332
338
  metadata: node.metadata,
333
339
  subgraphs: node.subgraphs,
340
+ ends: node.ends,
334
341
  })
335
342
  .pipe(node.runnable)
336
343
  .pipe(new write_js_1.ChannelWrite([{ channel: key, value: write_js_1.PASSTHROUGH }], [constants_js_1.TAG_HIDDEN]));
@@ -526,6 +533,13 @@ class CompiledGraph extends index_js_1.Pregel {
526
533
  }
527
534
  }
528
535
  }
536
+ for (const [key, node] of Object.entries(this.builder.nodes)) {
537
+ if (node.ends !== undefined) {
538
+ for (const end of node.ends) {
539
+ addEdge(_escapeMermaidKeywords(key), _escapeMermaidKeywords(end), undefined, true);
540
+ }
541
+ }
542
+ }
529
543
  return graph;
530
544
  }
531
545
  /**
@@ -30,10 +30,12 @@ export type NodeSpec<RunInput, RunOutput> = {
30
30
  runnable: Runnable<RunInput, RunOutput>;
31
31
  metadata?: Record<string, unknown>;
32
32
  subgraphs?: Pregel<any, any>[];
33
+ ends?: string[];
33
34
  };
34
35
  export type AddNodeOptions = {
35
36
  metadata?: Record<string, unknown>;
36
37
  subgraphs?: Pregel<any, any>[];
38
+ ends?: string[];
37
39
  };
38
40
  export declare class Graph<N extends string = typeof END, RunInput = any, RunOutput = any, NodeSpecType extends NodeSpec<RunInput, RunOutput> = NodeSpec<RunInput, RunOutput>, C extends StateDefinition = StateDefinition> {
39
41
  nodes: Record<N, NodeSpecType>;
@@ -155,6 +155,7 @@ export class Graph {
155
155
  runnable,
156
156
  metadata: options?.metadata,
157
157
  subgraphs: isPregelLike(runnable) ? [runnable] : options?.subgraphs,
158
+ ends: options?.ends,
158
159
  };
159
160
  return this;
160
161
  }
@@ -286,6 +287,11 @@ export class Graph {
286
287
  }
287
288
  }
288
289
  }
290
+ for (const node of Object.values(this.nodes)) {
291
+ for (const target of node.ends ?? []) {
292
+ allTargets.add(target);
293
+ }
294
+ }
289
295
  // validate targets
290
296
  for (const node of Object.keys(this.nodes)) {
291
297
  if (!allTargets.has(node)) {
@@ -326,6 +332,7 @@ export class CompiledGraph extends Pregel {
326
332
  triggers: [],
327
333
  metadata: node.metadata,
328
334
  subgraphs: node.subgraphs,
335
+ ends: node.ends,
329
336
  })
330
337
  .pipe(node.runnable)
331
338
  .pipe(new ChannelWrite([{ channel: key, value: PASSTHROUGH }], [TAG_HIDDEN]));
@@ -521,6 +528,13 @@ export class CompiledGraph extends Pregel {
521
528
  }
522
529
  }
523
530
  }
531
+ for (const [key, node] of Object.entries(this.builder.nodes)) {
532
+ if (node.ends !== undefined) {
533
+ for (const end of node.ends) {
534
+ addEdge(_escapeMermaidKeywords(key), _escapeMermaidKeywords(end), undefined, true);
535
+ }
536
+ }
537
+ }
524
538
  return graph;
525
539
  }
526
540
  /**
@@ -240,6 +240,7 @@ class StateGraph extends graph_js_1.Graph {
240
240
  ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
241
241
  [runnable]
242
242
  : options?.subgraphs,
243
+ ends: options?.ends,
243
244
  };
244
245
  this.nodes[key] = nodeSpec;
245
246
  return this;
@@ -431,6 +432,7 @@ class CompiledStateGraph extends graph_js_1.CompiledGraph {
431
432
  metadata: node?.metadata,
432
433
  retryPolicy: node?.retryPolicy,
433
434
  subgraphs: node?.subgraphs,
435
+ ends: node?.ends,
434
436
  });
435
437
  }
436
438
  }
@@ -237,6 +237,7 @@ export class StateGraph extends Graph {
237
237
  ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
238
238
  [runnable]
239
239
  : options?.subgraphs,
240
+ ends: options?.ends,
240
241
  };
241
242
  this.nodes[key] = nodeSpec;
242
243
  return this;
@@ -427,6 +428,7 @@ export class CompiledStateGraph extends CompiledGraph {
427
428
  metadata: node?.metadata,
428
429
  retryPolicy: node?.retryPolicy,
429
430
  subgraphs: node?.subgraphs,
431
+ ends: node?.ends,
430
432
  });
431
433
  }
432
434
  }
@@ -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
  }
@@ -63,7 +63,7 @@ const defaultRunnableBound =
63
63
  /* #__PURE__ */ new runnables_1.RunnablePassthrough();
64
64
  class PregelNode extends runnables_1.RunnableBinding {
65
65
  constructor(fields) {
66
- const { channels, triggers, mapper, writers, bound, kwargs, metadata, retryPolicy, tags, subgraphs, } = fields;
66
+ const { channels, triggers, mapper, writers, bound, kwargs, metadata, retryPolicy, tags, subgraphs, ends, } = fields;
67
67
  const mergedTags = [
68
68
  ...(fields.config?.tags ? fields.config.tags : []),
69
69
  ...(tags ?? []),
@@ -145,6 +145,12 @@ class PregelNode extends runnables_1.RunnableBinding {
145
145
  writable: true,
146
146
  value: void 0
147
147
  });
148
+ Object.defineProperty(this, "ends", {
149
+ enumerable: true,
150
+ configurable: true,
151
+ writable: true,
152
+ value: void 0
153
+ });
148
154
  this.channels = channels;
149
155
  this.triggers = triggers;
150
156
  this.mapper = mapper;
@@ -155,6 +161,7 @@ class PregelNode extends runnables_1.RunnableBinding {
155
161
  this.tags = mergedTags;
156
162
  this.retryPolicy = retryPolicy;
157
163
  this.subgraphs = subgraphs;
164
+ this.ends = ends;
158
165
  }
159
166
  getWriters() {
160
167
  const newWriters = [...this.writers];
@@ -21,6 +21,7 @@ interface PregelNodeArgs<RunInput, RunOutput> extends Partial<RunnableBindingArg
21
21
  metadata?: Record<string, unknown>;
22
22
  retryPolicy?: RetryPolicy;
23
23
  subgraphs?: Runnable[];
24
+ ends?: string[];
24
25
  }
25
26
  export type PregelNodeInputType = any;
26
27
  export type PregelNodeOutputType = any;
@@ -36,6 +37,7 @@ export declare class PregelNode<RunInput = PregelNodeInputType, RunOutput = Preg
36
37
  tags: string[];
37
38
  retryPolicy?: RetryPolicy;
38
39
  subgraphs?: Runnable[];
40
+ ends?: string[];
39
41
  constructor(fields: PregelNodeArgs<RunInput, RunOutput>);
40
42
  getWriters(): Array<Runnable>;
41
43
  getNode(): Runnable<RunInput, RunOutput> | undefined;
@@ -59,7 +59,7 @@ const defaultRunnableBound =
59
59
  /* #__PURE__ */ new RunnablePassthrough();
60
60
  export class PregelNode extends RunnableBinding {
61
61
  constructor(fields) {
62
- const { channels, triggers, mapper, writers, bound, kwargs, metadata, retryPolicy, tags, subgraphs, } = fields;
62
+ const { channels, triggers, mapper, writers, bound, kwargs, metadata, retryPolicy, tags, subgraphs, ends, } = fields;
63
63
  const mergedTags = [
64
64
  ...(fields.config?.tags ? fields.config.tags : []),
65
65
  ...(tags ?? []),
@@ -141,6 +141,12 @@ export class PregelNode extends RunnableBinding {
141
141
  writable: true,
142
142
  value: void 0
143
143
  });
144
+ Object.defineProperty(this, "ends", {
145
+ enumerable: true,
146
+ configurable: true,
147
+ writable: true,
148
+ value: void 0
149
+ });
144
150
  this.channels = channels;
145
151
  this.triggers = triggers;
146
152
  this.mapper = mapper;
@@ -151,6 +157,7 @@ export class PregelNode extends RunnableBinding {
151
157
  this.tags = mergedTags;
152
158
  this.retryPolicy = retryPolicy;
153
159
  this.subgraphs = subgraphs;
160
+ this.ends = ends;
154
161
  }
155
162
  getWriters() {
156
163
  const newWriters = [...this.writers];
@@ -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.28",
3
+ "version": "0.2.30",
4
4
  "description": "LangGraph",
5
5
  "type": "module",
6
6
  "engines": {