@langchain/langgraph 0.2.55 → 0.2.57

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.
@@ -4,10 +4,10 @@ import { compareChannelVersions, copyCheckpoint, emptyCheckpoint, SCHEDULED, uui
4
4
  import { createCheckpoint, emptyChannels, isBaseChannel, } from "../channels/base.js";
5
5
  import { PregelNode } from "./read.js";
6
6
  import { validateGraph, validateKeys } from "./validate.js";
7
- import { readChannels } from "./io.js";
7
+ import { mapInput, 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, COPY, END, PUSH, } from "../constants.js";
11
11
  import { GraphRecursionError, GraphValueError, InvalidUpdateError, } from "../errors.js";
12
12
  import { _prepareNextTasks, _localRead, _applyWrites, } from "./algo.js";
13
13
  import { _coerceToDict, getNewChannelVersions, patchCheckpointMap, } from "./utils/index.js";
@@ -673,89 +673,192 @@ export class Pregel extends Runnable {
673
673
  }
674
674
  }
675
675
  /**
676
- * Updates the state of the graph with new values.
676
+ * Apply updates to the graph state in bulk.
677
677
  * Requires a checkpointer to be configured.
678
678
  *
679
- * This method can be used for:
680
- * - Implementing human-in-the-loop workflows
681
- * - Modifying graph state during breakpoints
682
- * - Integrating external inputs into the graph
679
+ * This method is useful for recreating a thread
680
+ * from a list of updates, especially if a checkpoint
681
+ * is created as a result of multiple tasks.
683
682
  *
684
- * @param inputConfig - Configuration for the update
685
- * @param values - The values to update the state with
686
- * @param asNode - Optional node name to attribute the update to
683
+ * @internal The API might change in the future.
684
+ *
685
+ * @param startConfig - Configuration for the update
686
+ * @param updates - The list of updates to apply to graph state
687
687
  * @returns Updated configuration
688
688
  * @throws {GraphValueError} If no checkpointer is configured
689
- * @throws {InvalidUpdateError} If the update cannot be attributed to a node
689
+ * @throws {InvalidUpdateError} If the update cannot be attributed to a node or an update can be only applied in sequence.
690
690
  */
691
- async updateState(inputConfig, values, asNode) {
692
- const checkpointer = inputConfig.configurable?.[CONFIG_KEY_CHECKPOINTER] ?? this.checkpointer;
691
+ async bulkUpdateState(startConfig, supersteps) {
692
+ const checkpointer = startConfig.configurable?.[CONFIG_KEY_CHECKPOINTER] ?? this.checkpointer;
693
693
  if (!checkpointer) {
694
694
  throw new GraphValueError("No checkpointer set");
695
695
  }
696
+ if (supersteps.length === 0) {
697
+ throw new Error("No supersteps provided");
698
+ }
699
+ if (supersteps.some((s) => s.updates.length === 0)) {
700
+ throw new Error("No updates provided");
701
+ }
696
702
  // delegate to subgraph
697
- const checkpointNamespace = inputConfig.configurable?.checkpoint_ns ?? "";
703
+ const checkpointNamespace = startConfig.configurable?.checkpoint_ns ?? "";
698
704
  if (checkpointNamespace !== "" &&
699
- inputConfig.configurable?.[CONFIG_KEY_CHECKPOINTER] === undefined) {
705
+ startConfig.configurable?.[CONFIG_KEY_CHECKPOINTER] === undefined) {
700
706
  // remove task_ids from checkpoint_ns
701
707
  const recastNamespace = recastCheckpointNamespace(checkpointNamespace);
702
708
  // find the subgraph with the matching name
703
709
  // eslint-disable-next-line no-unreachable-loop
704
710
  for await (const [, pregel] of this.getSubgraphsAsync(recastNamespace, true)) {
705
- return await pregel.updateState(patchConfigurable(inputConfig, {
711
+ return await pregel.bulkUpdateState(patchConfigurable(startConfig, {
706
712
  [CONFIG_KEY_CHECKPOINTER]: checkpointer,
707
- }), values, asNode);
713
+ }), supersteps);
708
714
  }
709
715
  throw new Error(`Subgraph "${recastNamespace}" not found`);
710
716
  }
711
- // get last checkpoint
712
- const config = this.config
713
- ? mergeConfigs(this.config, inputConfig)
714
- : inputConfig;
715
- const saved = await checkpointer.getTuple(config);
716
- const checkpoint = saved !== undefined
717
- ? copyCheckpoint(saved.checkpoint)
718
- : emptyCheckpoint();
719
- const checkpointPreviousVersions = {
720
- ...saved?.checkpoint.channel_versions,
721
- };
722
- const step = saved?.metadata?.step ?? -1;
723
- // merge configurable fields with previous checkpoint config
724
- let checkpointConfig = patchConfigurable(config, {
725
- checkpoint_ns: config.configurable?.checkpoint_ns ?? "",
726
- });
727
- let checkpointMetadata = config.metadata ?? {};
728
- if (saved?.config.configurable) {
729
- checkpointConfig = patchConfigurable(config, saved.config.configurable);
730
- checkpointMetadata = {
731
- ...saved.metadata,
732
- ...checkpointMetadata,
717
+ const updateSuperStep = async (inputConfig, updates) => {
718
+ // get last checkpoint
719
+ const config = this.config
720
+ ? mergeConfigs(this.config, inputConfig)
721
+ : inputConfig;
722
+ const saved = await checkpointer.getTuple(config);
723
+ const checkpoint = saved !== undefined
724
+ ? copyCheckpoint(saved.checkpoint)
725
+ : emptyCheckpoint();
726
+ const checkpointPreviousVersions = {
727
+ ...saved?.checkpoint.channel_versions,
733
728
  };
734
- }
735
- // Find last node that updated the state, if not provided
736
- if (values == null && asNode === undefined) {
737
- const nextConfig = await checkpointer.put(checkpointConfig, createCheckpoint(checkpoint, undefined, step), {
738
- source: "update",
739
- step: step + 1,
740
- writes: {},
741
- parents: saved?.metadata?.parents ?? {},
742
- }, {});
743
- return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
744
- }
745
- // update channels
746
- const channels = emptyChannels(this.channels, checkpoint);
747
- // Pass `skipManaged: true` as managed values are not used/relevant in update state calls.
748
- const { managed } = await this.prepareSpecs(config, { skipManaged: true });
749
- if (values === null && asNode === "__end__") {
750
- if (saved) {
729
+ const step = saved?.metadata?.step ?? -1;
730
+ // merge configurable fields with previous checkpoint config
731
+ let checkpointConfig = patchConfigurable(config, {
732
+ checkpoint_ns: config.configurable?.checkpoint_ns ?? "",
733
+ });
734
+ let checkpointMetadata = config.metadata ?? {};
735
+ if (saved?.config.configurable) {
736
+ checkpointConfig = patchConfigurable(config, saved.config.configurable);
737
+ checkpointMetadata = {
738
+ ...saved.metadata,
739
+ ...checkpointMetadata,
740
+ };
741
+ }
742
+ // Find last node that updated the state, if not provided
743
+ const { values, asNode } = updates[0];
744
+ if (values == null && asNode === undefined) {
745
+ if (updates.length > 1) {
746
+ throw new InvalidUpdateError(`Cannot create empty checkpoint with multiple updates`);
747
+ }
748
+ const nextConfig = await checkpointer.put(checkpointConfig, createCheckpoint(checkpoint, undefined, step), {
749
+ source: "update",
750
+ step: step + 1,
751
+ writes: {},
752
+ parents: saved?.metadata?.parents ?? {},
753
+ }, {});
754
+ return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
755
+ }
756
+ // update channels
757
+ const channels = emptyChannels(this.channels, checkpoint);
758
+ // Pass `skipManaged: true` as managed values are not used/relevant in update state calls.
759
+ const { managed } = await this.prepareSpecs(config, {
760
+ skipManaged: true,
761
+ });
762
+ if (values === null && asNode === END) {
763
+ if (updates.length > 1) {
764
+ throw new InvalidUpdateError(`Cannot apply multiple updates when clearing state`);
765
+ }
766
+ if (saved) {
767
+ // tasks for this checkpoint
768
+ const nextTasks = _prepareNextTasks(checkpoint, saved.pendingWrites || [], this.nodes, channels, managed, saved.config, true, {
769
+ step: (saved.metadata?.step ?? -1) + 1,
770
+ checkpointer: this.checkpointer || undefined,
771
+ store: this.store,
772
+ });
773
+ // apply null writes
774
+ const nullWrites = (saved.pendingWrites || [])
775
+ .filter((w) => w[0] === NULL_TASK_ID)
776
+ .map((w) => w.slice(1));
777
+ if (nullWrites.length > 0) {
778
+ _applyWrites(saved.checkpoint, channels, [
779
+ {
780
+ name: INPUT,
781
+ writes: nullWrites,
782
+ triggers: [],
783
+ },
784
+ ]);
785
+ }
786
+ // apply writes from tasks that already ran
787
+ for (const [taskId, k, v] of saved.pendingWrites || []) {
788
+ if ([ERROR, INTERRUPT, SCHEDULED].includes(k)) {
789
+ continue;
790
+ }
791
+ if (!(taskId in nextTasks)) {
792
+ continue;
793
+ }
794
+ nextTasks[taskId].writes.push([k, v]);
795
+ }
796
+ // clear all current tasks
797
+ _applyWrites(checkpoint, channels, Object.values(nextTasks));
798
+ }
799
+ // save checkpoint
800
+ const nextConfig = await checkpointer.put(checkpointConfig, createCheckpoint(checkpoint, undefined, step), {
801
+ ...checkpointMetadata,
802
+ source: "update",
803
+ step: step + 1,
804
+ writes: {},
805
+ parents: saved?.metadata?.parents ?? {},
806
+ }, {});
807
+ return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
808
+ }
809
+ if (values == null && asNode === COPY) {
810
+ if (updates.length > 1) {
811
+ throw new InvalidUpdateError(`Cannot copy checkpoint with multiple updates`);
812
+ }
813
+ const nextConfig = await checkpointer.put(saved?.parentConfig ?? checkpointConfig, createCheckpoint(checkpoint, undefined, step), {
814
+ source: "fork",
815
+ step: step + 1,
816
+ writes: {},
817
+ parents: saved?.metadata?.parents ?? {},
818
+ }, {});
819
+ return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
820
+ }
821
+ if (asNode === INPUT) {
822
+ if (updates.length > 1) {
823
+ throw new InvalidUpdateError(`Cannot apply multiple updates when updating as input`);
824
+ }
825
+ const inputWrites = await gatherIterator(mapInput(this.inputChannels, values));
826
+ if (inputWrites.length === 0) {
827
+ throw new InvalidUpdateError(`Received no input writes for ${JSON.stringify(this.inputChannels, null, 2)}`);
828
+ }
829
+ // apply to checkpoint
830
+ _applyWrites(checkpoint, channels, [
831
+ {
832
+ name: INPUT,
833
+ writes: inputWrites,
834
+ triggers: [],
835
+ },
836
+ ], checkpointer.getNextVersion.bind(this.checkpointer));
837
+ // apply input write to channels
838
+ const nextStep = saved?.metadata?.step != null ? saved.metadata.step + 1 : -1;
839
+ const nextConfig = await checkpointer.put(checkpointConfig, createCheckpoint(checkpoint, channels, nextStep), {
840
+ source: "input",
841
+ step: nextStep,
842
+ writes: Object.fromEntries(inputWrites),
843
+ parents: saved?.metadata?.parents ?? {},
844
+ }, getNewChannelVersions(checkpointPreviousVersions, checkpoint.channel_versions));
845
+ // Store the writes
846
+ await checkpointer.putWrites(nextConfig, inputWrites, uuid5(INPUT, checkpoint.id));
847
+ return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
848
+ }
849
+ // apply pending writes, if not on specific checkpoint
850
+ if (config.configurable?.checkpoint_id === undefined &&
851
+ saved?.pendingWrites !== undefined &&
852
+ saved.pendingWrites.length > 0) {
751
853
  // tasks for this checkpoint
752
- const nextTasks = _prepareNextTasks(checkpoint, saved.pendingWrites || [], this.nodes, channels, managed, saved.config, true, {
753
- step: (saved.metadata?.step ?? -1) + 1,
754
- checkpointer: this.checkpointer || undefined,
854
+ const nextTasks = _prepareNextTasks(checkpoint, saved.pendingWrites, this.nodes, channels, managed, saved.config, true, {
755
855
  store: this.store,
856
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
857
+ checkpointer: this.checkpointer,
858
+ step: (saved.metadata?.step ?? -1) + 1,
756
859
  });
757
860
  // apply null writes
758
- const nullWrites = (saved.pendingWrites || [])
861
+ const nullWrites = (saved.pendingWrites ?? [])
759
862
  .filter((w) => w[0] === NULL_TASK_ID)
760
863
  .map((w) => w.slice(1));
761
864
  if (nullWrites.length > 0) {
@@ -767,172 +870,165 @@ export class Pregel extends Runnable {
767
870
  },
768
871
  ]);
769
872
  }
770
- // apply writes from tasks that already ran
771
- for (const [taskId, k, v] of saved.pendingWrites || []) {
772
- if ([ERROR, INTERRUPT, SCHEDULED].includes(k)) {
773
- continue;
774
- }
775
- if (!(taskId in nextTasks)) {
873
+ // apply writes
874
+ for (const [tid, k, v] of saved.pendingWrites) {
875
+ if ([ERROR, INTERRUPT, SCHEDULED].includes(k) ||
876
+ nextTasks[tid] === undefined) {
776
877
  continue;
777
878
  }
778
- nextTasks[taskId].writes.push([k, v]);
879
+ nextTasks[tid].writes.push([k, v]);
880
+ }
881
+ const tasks = Object.values(nextTasks).filter((task) => {
882
+ return task.writes.length > 0;
883
+ });
884
+ if (tasks.length > 0) {
885
+ _applyWrites(checkpoint, channels, tasks);
779
886
  }
780
- // clear all current tasks
781
- _applyWrites(checkpoint, channels, Object.values(nextTasks));
782
887
  }
783
- // save checkpoint
784
- const nextConfig = await checkpointer.put(checkpointConfig, createCheckpoint(checkpoint, undefined, step), {
785
- ...checkpointMetadata,
786
- source: "update",
787
- step: step + 1,
788
- writes: {},
789
- parents: saved?.metadata?.parents ?? {},
790
- }, {});
791
- return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
792
- }
793
- if (values == null && asNode === "__copy__") {
794
- const nextConfig = await checkpointer.put(saved?.parentConfig ?? checkpointConfig, createCheckpoint(checkpoint, undefined, step), {
795
- source: "fork",
796
- step: step + 1,
797
- writes: {},
798
- parents: saved?.metadata?.parents ?? {},
799
- }, {});
800
- return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
801
- }
802
- // apply pending writes, if not on specific checkpoint
803
- if (config.configurable?.checkpoint_id === undefined &&
804
- saved?.pendingWrites !== undefined &&
805
- saved.pendingWrites.length > 0) {
806
- // tasks for this checkpoint
807
- const nextTasks = _prepareNextTasks(checkpoint, saved.pendingWrites, this.nodes, channels, managed, saved.config, true, {
808
- store: this.store,
809
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
810
- checkpointer: this.checkpointer,
811
- step: (saved.metadata?.step ?? -1) + 1,
812
- });
813
- // apply null writes
814
- const nullWrites = (saved.pendingWrites ?? [])
815
- .filter((w) => w[0] === NULL_TASK_ID)
816
- .map((w) => w.slice(1));
817
- if (nullWrites.length > 0) {
818
- _applyWrites(saved.checkpoint, channels, [
819
- {
820
- name: INPUT,
821
- writes: nullWrites,
822
- triggers: [],
823
- },
824
- ]);
888
+ const nonNullVersion = Object.values(checkpoint.versions_seen)
889
+ .map((seenVersions) => {
890
+ return Object.values(seenVersions);
891
+ })
892
+ .flat()
893
+ .find((v) => !!v);
894
+ const validUpdates = [];
895
+ if (updates.length === 1) {
896
+ // eslint-disable-next-line prefer-const
897
+ let { values, asNode } = updates[0];
898
+ if (asNode === undefined && nonNullVersion === undefined) {
899
+ if (typeof this.inputChannels === "string" &&
900
+ this.nodes[this.inputChannels] !== undefined) {
901
+ asNode = this.inputChannels;
902
+ }
903
+ }
904
+ else if (asNode === undefined) {
905
+ const lastSeenByNode = Object.entries(checkpoint.versions_seen)
906
+ .map(([n, seen]) => {
907
+ return Object.values(seen).map((v) => {
908
+ return [v, n];
909
+ });
910
+ })
911
+ .flat()
912
+ .sort(([aNumber], [bNumber]) => compareChannelVersions(aNumber, bNumber));
913
+ // if two nodes updated the state at the same time, it's ambiguous
914
+ if (lastSeenByNode) {
915
+ if (lastSeenByNode.length === 1) {
916
+ // eslint-disable-next-line prefer-destructuring
917
+ asNode = lastSeenByNode[0][1];
918
+ }
919
+ else if (lastSeenByNode[lastSeenByNode.length - 1][0] !==
920
+ lastSeenByNode[lastSeenByNode.length - 2][0]) {
921
+ // eslint-disable-next-line prefer-destructuring
922
+ asNode = lastSeenByNode[lastSeenByNode.length - 1][1];
923
+ }
924
+ }
925
+ }
926
+ if (asNode === undefined) {
927
+ throw new InvalidUpdateError(`Ambiguous update, specify "asNode"`);
928
+ }
929
+ validUpdates.push({ values, asNode });
825
930
  }
826
- // apply writes
827
- for (const [tid, k, v] of saved.pendingWrites) {
828
- if ([ERROR, INTERRUPT, SCHEDULED].includes(k) ||
829
- nextTasks[tid] === undefined) {
830
- continue;
931
+ else {
932
+ for (const { asNode, values } of updates) {
933
+ if (asNode == null) {
934
+ throw new InvalidUpdateError(`"asNode" is required when applying multiple updates`);
935
+ }
936
+ validUpdates.push({ values, asNode });
831
937
  }
832
- nextTasks[tid].writes.push([k, v]);
833
938
  }
834
- const tasks = Object.values(nextTasks).filter((task) => {
835
- return task.writes.length > 0;
836
- });
837
- if (tasks.length > 0) {
838
- _applyWrites(checkpoint, channels, tasks);
939
+ const tasks = [];
940
+ for (const { asNode, values } of validUpdates) {
941
+ if (this.nodes[asNode] === undefined) {
942
+ throw new InvalidUpdateError(`Node "${asNode.toString()}" does not exist`);
943
+ }
944
+ // run all writers of the chosen node
945
+ const writers = this.nodes[asNode].getWriters();
946
+ if (!writers.length) {
947
+ throw new InvalidUpdateError(`No writers found for node "${asNode.toString()}"`);
948
+ }
949
+ tasks.push({
950
+ name: asNode,
951
+ input: values,
952
+ proc: writers.length > 1
953
+ ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
954
+ RunnableSequence.from(writers, {
955
+ omitSequenceTags: true,
956
+ })
957
+ : writers[0],
958
+ writes: [],
959
+ triggers: [INTERRUPT],
960
+ id: uuid5(INTERRUPT, checkpoint.id),
961
+ writers: [],
962
+ });
839
963
  }
840
- }
841
- const nonNullVersion = Object.values(checkpoint.versions_seen)
842
- .map((seenVersions) => {
843
- return Object.values(seenVersions);
844
- })
845
- .flat()
846
- .find((v) => !!v);
847
- if (asNode === undefined && nonNullVersion === undefined) {
848
- if (typeof this.inputChannels === "string" &&
849
- this.nodes[this.inputChannels] !== undefined) {
850
- asNode = this.inputChannels;
964
+ for (const task of tasks) {
965
+ // execute task
966
+ await task.proc.invoke(task.input, patchConfig({
967
+ ...config,
968
+ store: config?.store ?? this.store,
969
+ }, {
970
+ runName: config.runName ?? `${this.getName()}UpdateState`,
971
+ configurable: {
972
+ [CONFIG_KEY_SEND]: (items) => task.writes.push(...items),
973
+ [CONFIG_KEY_READ]: (select_, fresh_ = false) => _localRead(step, checkpoint, channels, managed,
974
+ // TODO: Why does keyof StrRecord allow number and symbol?
975
+ task, select_, fresh_),
976
+ },
977
+ }));
851
978
  }
852
- }
853
- else if (asNode === undefined) {
854
- const lastSeenByNode = Object.entries(checkpoint.versions_seen)
855
- .map(([n, seen]) => {
856
- return Object.values(seen).map((v) => {
857
- return [v, n];
858
- });
859
- })
860
- .flat()
861
- .sort(([aNumber], [bNumber]) => compareChannelVersions(aNumber, bNumber));
862
- // if two nodes updated the state at the same time, it's ambiguous
863
- if (lastSeenByNode) {
864
- if (lastSeenByNode.length === 1) {
865
- // eslint-disable-next-line prefer-destructuring
866
- asNode = lastSeenByNode[0][1];
979
+ for (const task of tasks) {
980
+ // channel writes are saved to current checkpoint
981
+ const channelWrites = task.writes.filter((w) => w[0] !== PUSH);
982
+ // save task writes
983
+ if (saved !== undefined && channelWrites.length > 0) {
984
+ await checkpointer.putWrites(checkpointConfig, channelWrites, task.id);
867
985
  }
868
- else if (lastSeenByNode[lastSeenByNode.length - 1][0] !==
869
- lastSeenByNode[lastSeenByNode.length - 2][0]) {
870
- // eslint-disable-next-line prefer-destructuring
871
- asNode = lastSeenByNode[lastSeenByNode.length - 1][1];
986
+ }
987
+ // apply to checkpoint
988
+ // TODO: Why does keyof StrRecord allow number and symbol?
989
+ _applyWrites(checkpoint, channels, tasks, checkpointer.getNextVersion.bind(this.checkpointer));
990
+ const newVersions = getNewChannelVersions(checkpointPreviousVersions, checkpoint.channel_versions);
991
+ const nextConfig = await checkpointer.put(checkpointConfig, createCheckpoint(checkpoint, channels, step + 1), {
992
+ source: "update",
993
+ step: step + 1,
994
+ writes: Object.fromEntries(validUpdates.map((update) => [update.asNode, update.values])),
995
+ parents: saved?.metadata?.parents ?? {},
996
+ }, newVersions);
997
+ for (const task of tasks) {
998
+ // push writes are saved to next checkpoint
999
+ const pushWrites = task.writes.filter((w) => w[0] === PUSH);
1000
+ if (pushWrites.length > 0) {
1001
+ await checkpointer.putWrites(nextConfig, pushWrites, task.id);
872
1002
  }
873
1003
  }
874
- }
875
- if (asNode === undefined) {
876
- throw new InvalidUpdateError(`Ambiguous update, specify "asNode"`);
877
- }
878
- if (this.nodes[asNode] === undefined) {
879
- throw new InvalidUpdateError(`Node "${asNode.toString()}" does not exist`);
880
- }
881
- // run all writers of the chosen node
882
- const writers = this.nodes[asNode].getWriters();
883
- if (!writers.length) {
884
- throw new InvalidUpdateError(`No writers found for node "${asNode.toString()}"`);
885
- }
886
- const task = {
887
- name: asNode,
888
- input: values,
889
- proc: writers.length > 1
890
- ? // eslint-disable-next-line @typescript-eslint/no-explicit-any
891
- RunnableSequence.from(writers, { omitSequenceTags: true })
892
- : writers[0],
893
- writes: [],
894
- triggers: [INTERRUPT],
895
- id: uuid5(INTERRUPT, checkpoint.id),
896
- writers: [],
1004
+ return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
897
1005
  };
898
- // execute task
899
- await task.proc.invoke(task.input, patchConfig({
900
- ...config,
901
- store: config?.store ?? this.store,
902
- }, {
903
- runName: config.runName ?? `${this.getName()}UpdateState`,
904
- configurable: {
905
- [CONFIG_KEY_SEND]: (items) => task.writes.push(...items),
906
- [CONFIG_KEY_READ]: (select_, fresh_ = false) => _localRead(step, checkpoint, channels, managed,
907
- // TODO: Why does keyof StrRecord allow number and symbol?
908
- task, select_, fresh_),
909
- },
910
- }));
911
- // save task writes
912
- // channel writes are saved to current checkpoint
913
- // push writes are saved to next checkpoint
914
- const [channelWrites, pushWrites] = [
915
- task.writes.filter((w) => w[0] !== PUSH),
916
- task.writes.filter((w) => w[0] === PUSH),
917
- ];
918
- // save task writes
919
- if (saved !== undefined && channelWrites.length > 0) {
920
- await checkpointer.putWrites(checkpointConfig, channelWrites, task.id);
1006
+ let currentConfig = startConfig;
1007
+ for (const { updates } of supersteps) {
1008
+ currentConfig = await updateSuperStep(currentConfig, updates);
921
1009
  }
922
- // apply to checkpoint
923
- // TODO: Why does keyof StrRecord allow number and symbol?
924
- _applyWrites(checkpoint, channels, [task], checkpointer.getNextVersion.bind(this.checkpointer));
925
- const newVersions = getNewChannelVersions(checkpointPreviousVersions, checkpoint.channel_versions);
926
- const nextConfig = await checkpointer.put(checkpointConfig, createCheckpoint(checkpoint, channels, step + 1), {
927
- source: "update",
928
- step: step + 1,
929
- writes: { [asNode]: values },
930
- parents: saved?.metadata?.parents ?? {},
931
- }, newVersions);
932
- if (pushWrites.length > 0) {
933
- await checkpointer.putWrites(nextConfig, pushWrites, task.id);
934
- }
935
- return patchCheckpointMap(nextConfig, saved ? saved.metadata : undefined);
1010
+ return currentConfig;
1011
+ }
1012
+ /**
1013
+ * Updates the state of the graph with new values.
1014
+ * Requires a checkpointer to be configured.
1015
+ *
1016
+ * This method can be used for:
1017
+ * - Implementing human-in-the-loop workflows
1018
+ * - Modifying graph state during breakpoints
1019
+ * - Integrating external inputs into the graph
1020
+ *
1021
+ * @param inputConfig - Configuration for the update
1022
+ * @param values - The values to update the state with
1023
+ * @param asNode - Optional node name to attribute the update to
1024
+ * @returns Updated configuration
1025
+ * @throws {GraphValueError} If no checkpointer is configured
1026
+ * @throws {InvalidUpdateError} If the update cannot be attributed to a node
1027
+ */
1028
+ async updateState(inputConfig, values, asNode) {
1029
+ return this.bulkUpdateState(inputConfig, [
1030
+ { updates: [{ values, asNode }] },
1031
+ ]);
936
1032
  }
937
1033
  /**
938
1034
  * Gets the default values for various graph configuration options.