@langchain/langgraph 0.2.54 → 0.2.56

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