@langchain/langgraph 0.0.33 → 0.0.34

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.
@@ -7,7 +7,7 @@ import { PregelNode } from "./read.js";
7
7
  import { validateGraph, validateKeys } from "./validate.js";
8
8
  import { mapInput, mapOutputUpdates, mapOutputValues, readChannel, readChannels, single, } from "./io.js";
9
9
  import { ChannelWrite, PASSTHROUGH } from "./write.js";
10
- import { CONFIG_KEY_READ, CONFIG_KEY_SEND, INTERRUPT, TAG_HIDDEN, TASKS, } from "../constants.js";
10
+ import { _isSend, _isSendInterface, CONFIG_KEY_READ, CONFIG_KEY_SEND, INTERRUPT, TAG_HIDDEN, TASKS, } from "../constants.js";
11
11
  import { EmptyChannelError, GraphRecursionError, GraphValueError, InvalidUpdateError, } from "../errors.js";
12
12
  const DEFAULT_LOOP_LIMIT = 25;
13
13
  function isString(value) {
@@ -209,7 +209,7 @@ export class Pregel extends Runnable {
209
209
  const checkpoint = saved ? saved.checkpoint : emptyCheckpoint();
210
210
  const channels = emptyChannels(this.channels, checkpoint);
211
211
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
212
- const [_, nextTasks] = _prepareNextTasks(checkpoint, this.nodes, channels, false);
212
+ const [_, nextTasks] = _prepareNextTasks(checkpoint, this.nodes, channels, false, { step: -1 });
213
213
  return {
214
214
  values: readChannels(channels, this.streamChannelsAsIs),
215
215
  next: nextTasks.map((task) => task.name),
@@ -226,7 +226,7 @@ export class Pregel extends Runnable {
226
226
  for await (const saved of this.checkpointer.list(config, limit, before)) {
227
227
  const channels = emptyChannels(this.channels, saved.checkpoint);
228
228
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
229
- const [_, nextTasks] = _prepareNextTasks(saved.checkpoint, this.nodes, channels, false);
229
+ const [_, nextTasks] = _prepareNextTasks(saved.checkpoint, this.nodes, channels, false, { step: -1 });
230
230
  yield {
231
231
  values: readChannels(channels, this.streamChannelsAsIs),
232
232
  next: nextTasks.map((task) => task.name),
@@ -376,7 +376,7 @@ export class Pregel extends Runnable {
376
376
  }
377
377
  if (inputPendingWrites.length) {
378
378
  // discard any unfinished tasks from previous checkpoint
379
- const discarded = _prepareNextTasks(checkpoint, processes, channels, true);
379
+ const discarded = _prepareNextTasks(checkpoint, processes, channels, true, { step: -1 });
380
380
  checkpoint = discarded[0]; // eslint-disable-line prefer-destructuring
381
381
  // apply input writes
382
382
  _applyWrites(checkpoint, channels, inputPendingWrites);
@@ -415,7 +415,7 @@ export class Pregel extends Runnable {
415
415
  // with channel updates applied only at the transition between steps
416
416
  const stop = start + (config.recursionLimit ?? DEFAULT_LOOP_LIMIT);
417
417
  for (let step = start; step < stop + 1; step += 1) {
418
- const [nextCheckpoint, nextTasks] = _prepareNextTasks(checkpoint, processes, channels, true);
418
+ const [nextCheckpoint, nextTasks] = _prepareNextTasks(checkpoint, processes, channels, true, { step });
419
419
  // if no more tasks, we're done
420
420
  if (nextTasks.length === 0 && step === start) {
421
421
  throw new GraphValueError(`No tasks to run in graph.`);
@@ -474,7 +474,10 @@ export class Pregel extends Runnable {
474
474
  yield* mapOutputValues(outputKeys, pendingWrites, channels);
475
475
  }
476
476
  else if (streamMode === "updates") {
477
- yield* mapOutputUpdates(outputKeys, nextTasks);
477
+ // TODO: Refactor
478
+ for await (const task of nextTasks) {
479
+ yield* mapOutputUpdates(outputKeys, [task]);
480
+ }
478
481
  }
479
482
  // save end of step checkpoint
480
483
  if (this.checkpointer) {
@@ -580,15 +583,46 @@ export function _localRead(checkpoint, channels, writes, select, fresh = false)
580
583
  return readChannels(channels, select);
581
584
  }
582
585
  }
586
+ export function _localWrite(
587
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
588
+ commit, processes, channels,
589
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
590
+ writes) {
591
+ for (const [chan, value] of writes) {
592
+ if (chan === TASKS) {
593
+ if (!_isSend(value)) {
594
+ throw new InvalidUpdateError(`Invalid packet type, expected SendProtocol, got ${JSON.stringify(value)}`);
595
+ }
596
+ if (!(value.node in processes)) {
597
+ throw new InvalidUpdateError(`Invalid node name ${value.node} in packet`);
598
+ }
599
+ }
600
+ else if (!(chan in channels)) {
601
+ console.warn(`Skipping write for channel '${chan}' which has no readers`);
602
+ }
603
+ }
604
+ commit(writes);
605
+ }
583
606
  export function _applyWrites(checkpoint, channels, pendingWrites) {
607
+ if (checkpoint.pending_sends) {
608
+ checkpoint.pending_sends = [];
609
+ }
584
610
  const pendingWritesByChannel = {};
585
611
  // Group writes by channel
586
612
  for (const [chan, val] of pendingWrites) {
587
- if (chan in pendingWritesByChannel) {
588
- pendingWritesByChannel[chan].push(val);
613
+ if (chan === TASKS) {
614
+ checkpoint.pending_sends.push({
615
+ node: val.node,
616
+ args: val.args,
617
+ });
589
618
  }
590
619
  else {
591
- pendingWritesByChannel[chan] = [val];
620
+ if (chan in pendingWritesByChannel) {
621
+ pendingWritesByChannel[chan].push(val);
622
+ }
623
+ else {
624
+ pendingWritesByChannel[chan] = [val];
625
+ }
592
626
  }
593
627
  }
594
628
  // find the highest version of all channels
@@ -607,7 +641,7 @@ export function _applyWrites(checkpoint, channels, pendingWrites) {
607
641
  }
608
642
  catch (e) {
609
643
  if (e.name === InvalidUpdateError.unminifiable_name) {
610
- throw new InvalidUpdateError(`Invalid update for channel ${chan}. Values: ${vals}`);
644
+ throw new InvalidUpdateError(`Invalid update for channel ${chan}. Values: ${vals}\n\nError: ${e.message}`);
611
645
  }
612
646
  }
613
647
  // side effect: update checkpoint channel versions
@@ -626,15 +660,60 @@ export function _applyWrites(checkpoint, channels, pendingWrites) {
626
660
  }
627
661
  }
628
662
  }
629
- export function _prepareNextTasks(checkpoint, processes, channels, forExecution) {
663
+ export function _prepareNextTasks(checkpoint, processes, channels, forExecution, extra) {
630
664
  const newCheckpoint = copyCheckpoint(checkpoint);
631
665
  const tasks = [];
632
666
  const taskDescriptions = [];
667
+ for (const packet of checkpoint.pending_sends) {
668
+ if (!_isSendInterface(packet)) {
669
+ console.warn(`Ignoring invalid packet ${JSON.stringify(packet)} in pending sends.`);
670
+ continue;
671
+ }
672
+ if (!(packet.node in processes)) {
673
+ console.warn(`Ignoring unknown node name ${packet.node} in pending sends.`);
674
+ continue;
675
+ }
676
+ if (forExecution) {
677
+ const proc = processes[packet.node];
678
+ const node = proc.getNode();
679
+ if (node !== undefined) {
680
+ const triggers = [TASKS];
681
+ const metadata = {
682
+ langgraph_step: extra.step,
683
+ langgraph_node: packet.node,
684
+ langgraph_triggers: triggers,
685
+ langgraph_task_idx: tasks.length,
686
+ };
687
+ const writes = [];
688
+ tasks.push({
689
+ name: packet.node,
690
+ input: packet.args,
691
+ proc: node,
692
+ writes,
693
+ config: patchConfig(mergeConfigs(proc.config, processes[packet.node].config, {
694
+ metadata,
695
+ }), {
696
+ runName: packet.node,
697
+ // callbacks:
698
+ configurable: {
699
+ [CONFIG_KEY_SEND]: _localWrite.bind(undefined, (items) => writes.push(...items), processes, channels),
700
+ [CONFIG_KEY_READ]: _localRead.bind(undefined, checkpoint, channels, writes),
701
+ },
702
+ }),
703
+ });
704
+ }
705
+ }
706
+ else {
707
+ taskDescriptions.push({
708
+ name: packet.node,
709
+ input: packet.args,
710
+ });
711
+ }
712
+ }
633
713
  // Check if any processes should be run in next step
634
714
  // If so, prepare the values to be passed to them
635
715
  for (const [name, proc] of Object.entries(processes)) {
636
- // If any of the channels read by this process were updated
637
- if (proc.triggers
716
+ const hasUpdatedChannels = proc.triggers
638
717
  .filter((chan) => {
639
718
  try {
640
719
  readChannel(channels, chan, false);
@@ -645,7 +724,9 @@ export function _prepareNextTasks(checkpoint, processes, channels, forExecution)
645
724
  }
646
725
  })
647
726
  .some((chan) => getChannelVersion(newCheckpoint, chan) >
648
- getVersionSeen(newCheckpoint, name, chan))) {
727
+ getVersionSeen(newCheckpoint, name, chan));
728
+ // If any of the channels read by this process were updated
729
+ if (hasUpdatedChannels) {
649
730
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
650
731
  let val;
651
732
  // If all trigger channels subscribed by this process are not empty
@@ -710,12 +791,25 @@ export function _prepareNextTasks(checkpoint, processes, channels, forExecution)
710
791
  });
711
792
  const node = proc.getNode();
712
793
  if (node !== undefined) {
794
+ const metadata = {
795
+ langgraph_step: extra.step,
796
+ langgraph_node: name,
797
+ langgraph_triggers: proc.triggers,
798
+ langgraph_task_idx: tasks.length,
799
+ };
800
+ const writes = [];
713
801
  tasks.push({
714
802
  name,
715
803
  input: val,
716
804
  proc: node,
717
- writes: [],
718
- config: proc.config,
805
+ writes,
806
+ config: patchConfig(mergeConfigs(proc.config, { metadata }), {
807
+ runName: name,
808
+ configurable: {
809
+ [CONFIG_KEY_SEND]: _localWrite.bind(undefined, (items) => writes.push(...items), processes, channels),
810
+ [CONFIG_KEY_READ]: _localRead.bind(undefined, checkpoint, channels, writes),
811
+ },
812
+ }),
719
813
  });
720
814
  }
721
815
  }
@@ -98,39 +98,42 @@ function* mapOutputUpdates(outputChannels, tasks
98
98
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
99
  ) {
100
100
  const outputTasks = tasks.filter((task) => task.config === undefined || !task.config.tags?.includes(constants_js_1.TAG_HIDDEN));
101
- if (Array.isArray(outputChannels)) {
102
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
- const updated = {};
104
- for (const task of outputTasks) {
105
- if (task.writes.some(([chan, _]) => outputChannels.includes(chan))) {
106
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
107
- const nodes = {};
108
- for (const [chan, value] of task.writes) {
109
- if (outputChannels.includes(chan)) {
110
- nodes[chan] = value;
111
- }
112
- }
113
- updated[task.name] = nodes;
114
- }
115
- }
116
- if (Object.keys(updated).length > 0) {
117
- yield updated;
118
- }
101
+ if (!outputTasks.length) {
102
+ return;
103
+ }
104
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
105
+ let updated;
106
+ if (!Array.isArray(outputChannels)) {
107
+ updated = outputTasks.flatMap((task) => task.writes
108
+ .filter(([chan, _]) => chan === outputChannels)
109
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
110
+ .map(([_, value]) => [task.name, value]));
119
111
  }
120
112
  else {
121
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
- const updated = {};
123
- for (const task of outputTasks) {
124
- for (const [chan, value] of task.writes) {
125
- if (chan === outputChannels) {
126
- updated[task.name] = value;
127
- }
128
- }
113
+ updated = outputTasks
114
+ .filter((task) => task.writes.some(([chan]) => outputChannels.includes(chan)))
115
+ .map((task) => [
116
+ task.name,
117
+ Object.fromEntries(task.writes.filter(([chan]) => outputChannels.includes(chan))),
118
+ ]);
119
+ }
120
+ const grouped = Object.fromEntries(outputTasks.map((t) => [t.name, []])
121
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
122
+ );
123
+ for (const [node, value] of updated) {
124
+ grouped[node].push(value);
125
+ }
126
+ for (const [node, value] of Object.entries(grouped)) {
127
+ if (value.length === 0) {
128
+ delete grouped[node];
129
129
  }
130
- if (Object.keys(updated).length > 0) {
131
- yield updated;
130
+ else if (value.length === 1) {
131
+ // TODO: Fix incorrect cast here
132
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
133
+ grouped[node] = value[0];
132
134
  }
133
135
  }
136
+ yield grouped;
134
137
  }
135
138
  exports.mapOutputUpdates = mapOutputUpdates;
136
139
  function single(iter) {
@@ -13,5 +13,5 @@ export declare function mapOutputValues<C extends PropertyKey>(outputChannels: C
13
13
  /**
14
14
  * Map pending writes (a sequence of tuples (channel, value)) to output chunk.
15
15
  */
16
- export declare function mapOutputUpdates<N extends PropertyKey, C extends PropertyKey>(outputChannels: C | Array<C>, tasks: readonly PregelExecutableTask<N, C>[]): Generator<Record<N, any | Record<string, any>>>;
16
+ export declare function mapOutputUpdates<N extends PropertyKey, C extends PropertyKey>(outputChannels: C | Array<C>, tasks: readonly PregelExecutableTask<N, C>[]): Generator<Record<N, Record<string, any> | Record<string, any>[]>>;
17
17
  export declare function single<T>(iter: IterableIterator<T>): T | null;
package/dist/pregel/io.js CHANGED
@@ -91,39 +91,42 @@ export function* mapOutputUpdates(outputChannels, tasks
91
91
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
92
  ) {
93
93
  const outputTasks = tasks.filter((task) => task.config === undefined || !task.config.tags?.includes(TAG_HIDDEN));
94
- if (Array.isArray(outputChannels)) {
95
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
96
- const updated = {};
97
- for (const task of outputTasks) {
98
- if (task.writes.some(([chan, _]) => outputChannels.includes(chan))) {
99
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
- const nodes = {};
101
- for (const [chan, value] of task.writes) {
102
- if (outputChannels.includes(chan)) {
103
- nodes[chan] = value;
104
- }
105
- }
106
- updated[task.name] = nodes;
107
- }
108
- }
109
- if (Object.keys(updated).length > 0) {
110
- yield updated;
111
- }
94
+ if (!outputTasks.length) {
95
+ return;
96
+ }
97
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
98
+ let updated;
99
+ if (!Array.isArray(outputChannels)) {
100
+ updated = outputTasks.flatMap((task) => task.writes
101
+ .filter(([chan, _]) => chan === outputChannels)
102
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
103
+ .map(([_, value]) => [task.name, value]));
112
104
  }
113
105
  else {
114
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
115
- const updated = {};
116
- for (const task of outputTasks) {
117
- for (const [chan, value] of task.writes) {
118
- if (chan === outputChannels) {
119
- updated[task.name] = value;
120
- }
121
- }
106
+ updated = outputTasks
107
+ .filter((task) => task.writes.some(([chan]) => outputChannels.includes(chan)))
108
+ .map((task) => [
109
+ task.name,
110
+ Object.fromEntries(task.writes.filter(([chan]) => outputChannels.includes(chan))),
111
+ ]);
112
+ }
113
+ const grouped = Object.fromEntries(outputTasks.map((t) => [t.name, []])
114
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
115
+ );
116
+ for (const [node, value] of updated) {
117
+ grouped[node].push(value);
118
+ }
119
+ for (const [node, value] of Object.entries(grouped)) {
120
+ if (value.length === 0) {
121
+ delete grouped[node];
122
122
  }
123
- if (Object.keys(updated).length > 0) {
124
- yield updated;
123
+ else if (value.length === 1) {
124
+ // TODO: Fix incorrect cast here
125
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
126
+ grouped[node] = value[0];
125
127
  }
126
128
  }
129
+ yield grouped;
127
130
  }
128
131
  export function single(iter) {
129
132
  // eslint-disable-next-line no-unreachable-loop
@@ -136,7 +136,10 @@ class PregelNode extends runnables_1.RunnableBinding {
136
136
  // eslint-disable-next-line no-instanceof/no-instanceof
137
137
  newWriters[newWriters.length - 2] instanceof write_js_1.ChannelWrite) {
138
138
  // we can combine writes if they are consecutive
139
- newWriters[newWriters.length - 2].writes.push(...newWriters[newWriters.length - 1].writes);
139
+ // careful to not modify the original writers list or ChannelWrite
140
+ const endWriters = newWriters.slice(-2);
141
+ const combinedWrites = endWriters[0].writes.concat(endWriters[1].writes);
142
+ newWriters[newWriters.length - 2] = new write_js_1.ChannelWrite(combinedWrites, endWriters[0].config?.tags);
140
143
  newWriters.pop();
141
144
  }
142
145
  return newWriters;
@@ -132,7 +132,10 @@ export class PregelNode extends RunnableBinding {
132
132
  // eslint-disable-next-line no-instanceof/no-instanceof
133
133
  newWriters[newWriters.length - 2] instanceof ChannelWrite) {
134
134
  // we can combine writes if they are consecutive
135
- newWriters[newWriters.length - 2].writes.push(...newWriters[newWriters.length - 1].writes);
135
+ // careful to not modify the original writers list or ChannelWrite
136
+ const endWriters = newWriters.slice(-2);
137
+ const combinedWrites = endWriters[0].writes.concat(endWriters[1].writes);
138
+ newWriters[newWriters.length - 2] = new ChannelWrite(combinedWrites, endWriters[0].config?.tags);
136
139
  newWriters.pop();
137
140
  }
138
141
  return newWriters;
@@ -3,8 +3,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ChannelWrite = exports.PASSTHROUGH = exports.SKIP_WRITE = void 0;
4
4
  const constants_js_1 = require("../constants.cjs");
5
5
  const utils_js_1 = require("../utils.cjs");
6
- exports.SKIP_WRITE = {};
7
- exports.PASSTHROUGH = {};
6
+ const errors_js_1 = require("../errors.cjs");
7
+ exports.SKIP_WRITE = {
8
+ [Symbol.for("LG_SKIP_WRITE")]: true,
9
+ };
10
+ function _isSkipWrite(x) {
11
+ return (typeof x === "object" &&
12
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
13
+ x?.[Symbol.for("LG_SKIP_WRITE")] !== undefined);
14
+ }
15
+ exports.PASSTHROUGH = {
16
+ [Symbol.for("LG_PASSTHROUGH")]: true,
17
+ };
18
+ function _isPassthrough(x) {
19
+ return (typeof x === "object" &&
20
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
21
+ x?.[Symbol.for("LG_PASSTHROUGH")] !== undefined);
22
+ }
8
23
  const IS_WRITER = Symbol("IS_WRITER");
9
24
  /**
10
25
  * Mapping of write channels to Runnables that return the value to be written,
@@ -13,7 +28,9 @@ const IS_WRITER = Symbol("IS_WRITER");
13
28
  class ChannelWrite extends utils_js_1.RunnableCallable {
14
29
  constructor(writes, tags) {
15
30
  const name = `ChannelWrite<${writes
16
- .map(({ channel }) => channel)
31
+ .map((packet) => {
32
+ return (0, constants_js_1._isSend)(packet) ? packet.node : packet.channel;
33
+ })
17
34
  .join(",")}>`;
18
35
  super({
19
36
  ...{ writes, name, tags },
@@ -28,34 +45,53 @@ class ChannelWrite extends utils_js_1.RunnableCallable {
28
45
  this.writes = writes;
29
46
  }
30
47
  async _getWriteValues(input, config) {
31
- return Promise.all(this.writes
32
- .map((write) => ({
33
- channel: write.channel,
34
- value: write.value === exports.PASSTHROUGH ? input : write.value,
35
- skipNone: write.skipNone,
36
- mapper: write.mapper,
37
- }))
38
- .map(async (write) => ({
39
- channel: write.channel,
40
- value: write.mapper
41
- ? await write.mapper.invoke(write.value, config)
42
- : write.value,
43
- skipNone: write.skipNone,
44
- mapper: write.mapper,
45
- }))).then((writes) => writes
46
- .filter((write) => !write.skipNone || write.value !== null)
47
- .reduce((acc, write) => {
48
- acc[write.channel] = write.value;
49
- return acc;
50
- }, {}));
48
+ const writes = this.writes
49
+ .filter(constants_js_1._isSend)
50
+ .map((packet) => {
51
+ return [constants_js_1.TASKS, packet];
52
+ });
53
+ const entries = this.writes.filter((write) => {
54
+ return !(0, constants_js_1._isSend)(write);
55
+ });
56
+ const invalidEntry = entries.find((write) => {
57
+ return write.channel === constants_js_1.TASKS;
58
+ });
59
+ if (invalidEntry) {
60
+ throw new errors_js_1.InvalidUpdateError(`Cannot write to the reserved channel ${constants_js_1.TASKS}`);
61
+ }
62
+ const values = await Promise.all(entries.map(async (write) => {
63
+ let value;
64
+ if (_isPassthrough(write.value)) {
65
+ value = input;
66
+ }
67
+ else {
68
+ value = write.value;
69
+ }
70
+ const mappedValue = write.mapper
71
+ ? await write.mapper.invoke(value, config)
72
+ : value;
73
+ return {
74
+ ...write,
75
+ value: mappedValue,
76
+ };
77
+ })).then((writes) => {
78
+ return writes
79
+ .filter((write) => !write.skipNone || write.value !== null)
80
+ .map((write) => {
81
+ return [write.channel, write.value];
82
+ });
83
+ });
84
+ return [...writes, ...values];
51
85
  }
52
86
  async _write(input, config) {
53
87
  const values = await this._getWriteValues(input, config);
54
88
  ChannelWrite.doWrite(config, values);
55
89
  }
90
+ // TODO: Support requireAtLeastOneOf
56
91
  static doWrite(config, values) {
57
92
  const write = config.configurable?.[constants_js_1.CONFIG_KEY_SEND];
58
- write(Object.entries(values).filter(([_channel, value]) => value !== exports.SKIP_WRITE));
93
+ const filtered = values.filter(([_, value]) => !_isSkipWrite(value));
94
+ write(filtered);
59
95
  }
60
96
  static isWriter(runnable) {
61
97
  return (
@@ -1,17 +1,22 @@
1
1
  import { Runnable, RunnableConfig, RunnableLike } from "@langchain/core/runnables";
2
+ import { Send } from "../constants.js";
2
3
  import { RunnableCallable } from "../utils.js";
3
- export declare const SKIP_WRITE: {};
4
- export declare const PASSTHROUGH: {};
4
+ export declare const SKIP_WRITE: {
5
+ [x: symbol]: boolean;
6
+ };
7
+ export declare const PASSTHROUGH: {
8
+ [x: symbol]: boolean;
9
+ };
5
10
  /**
6
11
  * Mapping of write channels to Runnables that return the value to be written,
7
12
  * or None to skip writing.
8
13
  */
9
14
  export declare class ChannelWrite<RunInput = any> extends RunnableCallable {
10
- writes: Array<ChannelWriteEntry>;
11
- constructor(writes: Array<ChannelWriteEntry>, tags?: string[]);
12
- _getWriteValues(input: unknown, config: RunnableConfig): Promise<Record<string, unknown>>;
15
+ writes: Array<ChannelWriteEntry | Send>;
16
+ constructor(writes: Array<ChannelWriteEntry | Send>, tags?: string[]);
17
+ _getWriteValues(input: unknown, config: RunnableConfig): Promise<[string, unknown][]>;
13
18
  _write(input: unknown, config: RunnableConfig): Promise<void>;
14
- static doWrite(config: RunnableConfig, values: Record<string, unknown>): void;
19
+ static doWrite(config: RunnableConfig, values: [string, unknown][]): void;
15
20
  static isWriter(runnable: RunnableLike): boolean;
16
21
  static registerWriter<T extends Runnable>(runnable: T): T;
17
22
  }
@@ -1,7 +1,22 @@
1
- import { CONFIG_KEY_SEND } from "../constants.js";
1
+ import { _isSend, CONFIG_KEY_SEND, TASKS } from "../constants.js";
2
2
  import { RunnableCallable } from "../utils.js";
3
- export const SKIP_WRITE = {};
4
- export const PASSTHROUGH = {};
3
+ import { InvalidUpdateError } from "../errors.js";
4
+ export const SKIP_WRITE = {
5
+ [Symbol.for("LG_SKIP_WRITE")]: true,
6
+ };
7
+ function _isSkipWrite(x) {
8
+ return (typeof x === "object" &&
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ x?.[Symbol.for("LG_SKIP_WRITE")] !== undefined);
11
+ }
12
+ export const PASSTHROUGH = {
13
+ [Symbol.for("LG_PASSTHROUGH")]: true,
14
+ };
15
+ function _isPassthrough(x) {
16
+ return (typeof x === "object" &&
17
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
18
+ x?.[Symbol.for("LG_PASSTHROUGH")] !== undefined);
19
+ }
5
20
  const IS_WRITER = Symbol("IS_WRITER");
6
21
  /**
7
22
  * Mapping of write channels to Runnables that return the value to be written,
@@ -10,7 +25,9 @@ const IS_WRITER = Symbol("IS_WRITER");
10
25
  export class ChannelWrite extends RunnableCallable {
11
26
  constructor(writes, tags) {
12
27
  const name = `ChannelWrite<${writes
13
- .map(({ channel }) => channel)
28
+ .map((packet) => {
29
+ return _isSend(packet) ? packet.node : packet.channel;
30
+ })
14
31
  .join(",")}>`;
15
32
  super({
16
33
  ...{ writes, name, tags },
@@ -25,34 +42,53 @@ export class ChannelWrite extends RunnableCallable {
25
42
  this.writes = writes;
26
43
  }
27
44
  async _getWriteValues(input, config) {
28
- return Promise.all(this.writes
29
- .map((write) => ({
30
- channel: write.channel,
31
- value: write.value === PASSTHROUGH ? input : write.value,
32
- skipNone: write.skipNone,
33
- mapper: write.mapper,
34
- }))
35
- .map(async (write) => ({
36
- channel: write.channel,
37
- value: write.mapper
38
- ? await write.mapper.invoke(write.value, config)
39
- : write.value,
40
- skipNone: write.skipNone,
41
- mapper: write.mapper,
42
- }))).then((writes) => writes
43
- .filter((write) => !write.skipNone || write.value !== null)
44
- .reduce((acc, write) => {
45
- acc[write.channel] = write.value;
46
- return acc;
47
- }, {}));
45
+ const writes = this.writes
46
+ .filter(_isSend)
47
+ .map((packet) => {
48
+ return [TASKS, packet];
49
+ });
50
+ const entries = this.writes.filter((write) => {
51
+ return !_isSend(write);
52
+ });
53
+ const invalidEntry = entries.find((write) => {
54
+ return write.channel === TASKS;
55
+ });
56
+ if (invalidEntry) {
57
+ throw new InvalidUpdateError(`Cannot write to the reserved channel ${TASKS}`);
58
+ }
59
+ const values = await Promise.all(entries.map(async (write) => {
60
+ let value;
61
+ if (_isPassthrough(write.value)) {
62
+ value = input;
63
+ }
64
+ else {
65
+ value = write.value;
66
+ }
67
+ const mappedValue = write.mapper
68
+ ? await write.mapper.invoke(value, config)
69
+ : value;
70
+ return {
71
+ ...write,
72
+ value: mappedValue,
73
+ };
74
+ })).then((writes) => {
75
+ return writes
76
+ .filter((write) => !write.skipNone || write.value !== null)
77
+ .map((write) => {
78
+ return [write.channel, write.value];
79
+ });
80
+ });
81
+ return [...writes, ...values];
48
82
  }
49
83
  async _write(input, config) {
50
84
  const values = await this._getWriteValues(input, config);
51
85
  ChannelWrite.doWrite(config, values);
52
86
  }
87
+ // TODO: Support requireAtLeastOneOf
53
88
  static doWrite(config, values) {
54
89
  const write = config.configurable?.[CONFIG_KEY_SEND];
55
- write(Object.entries(values).filter(([_channel, value]) => value !== SKIP_WRITE));
90
+ const filtered = values.filter(([_, value]) => !_isSkipWrite(value));
91
+ write(filtered);
56
92
  }
57
93
  static isWriter(runnable) {
58
94
  return (