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