@langgraph-js/pure-graph 3.0.1 → 3.1.2

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,7 +4,7 @@ import { concat } from '@langchain/core/utils/stream';
4
4
  import { EventEmitter } from 'eventemitter3';
5
5
  import { load } from '@langchain/core/load';
6
6
  import { v7 } from 'uuid';
7
- import { MemorySaver } from '@langchain/langgraph-checkpoint';
7
+ import { BaseCheckpointSaver, TASKS, maxChannelVersion, getCheckpointId, copyCheckpoint, uuid6, WRITES_IDX_MAP } from '@langchain/langgraph-checkpoint';
8
8
 
9
9
  const getLangGraphCommand = (command) => {
10
10
  let goto = command.goto != null && !Array.isArray(command.goto) ? [command.goto] : command.goto;
@@ -820,6 +820,301 @@ class KyselyThreadsManager {
820
820
  }
821
821
  }
822
822
 
823
+ function _parseShallowKey(key) {
824
+ const [threadId, checkpointNs] = key.split("::");
825
+ return { threadId, checkpointNs: checkpointNs ?? "" };
826
+ }
827
+ function _getWritesKey(threadId, checkpointNs, checkpointId) {
828
+ return `${threadId}::${checkpointNs}::${checkpointId}`;
829
+ }
830
+ function deterministicStringify(obj) {
831
+ if (obj === null || typeof obj !== "object") {
832
+ return JSON.stringify(obj);
833
+ }
834
+ if (Array.isArray(obj)) {
835
+ return JSON.stringify(obj.map((item) => deterministicStringify(item)));
836
+ }
837
+ const sortedObj = {};
838
+ const sortedKeys = Object.keys(obj).sort();
839
+ for (const key of sortedKeys) {
840
+ sortedObj[key] = obj[key];
841
+ }
842
+ return JSON.stringify(sortedObj, (_, value) => {
843
+ if (value !== null && typeof value === "object" && !Array.isArray(value)) {
844
+ const sorted = {};
845
+ const keys = Object.keys(value).sort();
846
+ for (const k of keys) {
847
+ sorted[k] = value[k];
848
+ }
849
+ return sorted;
850
+ }
851
+ return value;
852
+ });
853
+ }
854
+ class ShallowMemorySaver extends BaseCheckpointSaver {
855
+ // thread ID -> checkpoint namespace -> checkpoint data
856
+ // Each namespace only keeps ONE checkpoint
857
+ storage = {};
858
+ // writes storage: composite key -> writes data
859
+ // Key format: thread_id::checkpoint_ns::checkpoint_id
860
+ writes = {};
861
+ constructor(serde) {
862
+ super(serde);
863
+ }
864
+ /**
865
+ * Get just the checkpoint without metadata
866
+ * Convenience method matching ShallowRedisSaver API
867
+ */
868
+ async get(config) {
869
+ const tuple = await this.getTuple(config);
870
+ return tuple?.checkpoint;
871
+ }
872
+ /**
873
+ * Migrate pending sends for checkpoints with version < 4
874
+ * @internal
875
+ */
876
+ async _migratePendingSends(mutableCheckpoint, threadId, checkpointNs, parentCheckpointId) {
877
+ const deseriablizableCheckpoint = mutableCheckpoint;
878
+ const parentKey = _getWritesKey(threadId, checkpointNs, parentCheckpointId);
879
+ const pendingSends = await Promise.all(
880
+ Object.values(this.writes[parentKey] ?? {}).filter(([_taskId, channel]) => channel === TASKS).map(async ([_taskId, _channel, writes]) => await this.serde.loadsTyped("json", writes))
881
+ );
882
+ deseriablizableCheckpoint.channel_values ??= {};
883
+ deseriablizableCheckpoint.channel_values[TASKS] = pendingSends;
884
+ deseriablizableCheckpoint.channel_versions ??= {};
885
+ deseriablizableCheckpoint.channel_versions[TASKS] = Object.keys(deseriablizableCheckpoint.channel_versions).length > 0 ? maxChannelVersion(...Object.values(deseriablizableCheckpoint.channel_versions)) : this.getNextVersion(void 0);
886
+ }
887
+ /**
888
+ * Load pending writes for a specific checkpoint
889
+ */
890
+ async _loadPendingWrites(threadId, checkpointNs, checkpointId) {
891
+ const key = _getWritesKey(threadId, checkpointNs, checkpointId);
892
+ const writes = this.writes[key] ?? {};
893
+ if (Object.keys(writes).length === 0) {
894
+ return [];
895
+ }
896
+ return await Promise.all(
897
+ Object.values(writes).map(async ([taskId, channel, value]) => {
898
+ return [taskId, channel, await this.serde.loadsTyped("json", value)];
899
+ })
900
+ );
901
+ }
902
+ /**
903
+ * Clean up old writes for a checkpoint
904
+ */
905
+ _cleanupOldWrites(threadId, checkpointNs, checkpointId) {
906
+ const key = _getWritesKey(threadId, checkpointNs, checkpointId);
907
+ delete this.writes[key];
908
+ }
909
+ /**
910
+ * Check metadata filter matches (with deep comparison support)
911
+ * Matches ShallowRedisSaver behavior
912
+ */
913
+ _checkMetadataFilterMatch(metadata, filter) {
914
+ for (const [key, value] of Object.entries(filter)) {
915
+ const metadataValue = metadata?.[key];
916
+ if (value === null) {
917
+ if (!(key in (metadata || {})) || metadataValue !== null) {
918
+ return false;
919
+ }
920
+ } else if (typeof value === "object" && !Array.isArray(value)) {
921
+ if (typeof metadataValue !== "object" || metadataValue === null) {
922
+ return false;
923
+ }
924
+ if (deterministicStringify(value) !== deterministicStringify(metadataValue)) {
925
+ return false;
926
+ }
927
+ } else if (metadataValue !== value) {
928
+ return false;
929
+ }
930
+ }
931
+ return true;
932
+ }
933
+ async getTuple(config) {
934
+ const threadId = config.configurable?.thread_id;
935
+ const checkpointNs = config.configurable?.checkpoint_ns ?? "";
936
+ const requestedCheckpointId = getCheckpointId(config);
937
+ if (threadId === void 0) {
938
+ return void 0;
939
+ }
940
+ const namespaceData = this.storage[threadId]?.[checkpointNs];
941
+ if (namespaceData === void 0) {
942
+ return void 0;
943
+ }
944
+ const { checkpoint, metadata, checkpoint_id, parent_checkpoint_id } = namespaceData;
945
+ if (requestedCheckpointId && checkpoint_id !== requestedCheckpointId) {
946
+ return void 0;
947
+ }
948
+ const deserializedCheckpoint = await this.serde.loadsTyped("json", checkpoint);
949
+ if (deserializedCheckpoint.v < 4 && parent_checkpoint_id !== void 0) {
950
+ await this._migratePendingSends(deserializedCheckpoint, threadId, checkpointNs, parent_checkpoint_id);
951
+ }
952
+ const pendingWrites = await this._loadPendingWrites(threadId, checkpointNs, checkpoint_id);
953
+ const deserializedMetadata = await this.serde.loadsTyped("json", metadata);
954
+ const checkpointTuple = {
955
+ config: {
956
+ configurable: {
957
+ thread_id: threadId,
958
+ checkpoint_ns: checkpointNs,
959
+ checkpoint_id
960
+ }
961
+ },
962
+ checkpoint: deserializedCheckpoint,
963
+ metadata: deserializedMetadata,
964
+ pendingWrites
965
+ };
966
+ if (parent_checkpoint_id !== void 0) {
967
+ checkpointTuple.parentConfig = {
968
+ configurable: {
969
+ thread_id: threadId,
970
+ checkpoint_ns: checkpointNs,
971
+ checkpoint_id: parent_checkpoint_id
972
+ }
973
+ };
974
+ }
975
+ return checkpointTuple;
976
+ }
977
+ async *list(config, options) {
978
+ let { before, limit, filter } = options ?? {};
979
+ const threadIds = config.configurable?.thread_id ? [config.configurable.thread_id] : Object.keys(this.storage);
980
+ const configCheckpointNamespace = config.configurable?.checkpoint_ns;
981
+ const configCheckpointId = config.configurable?.checkpoint_id;
982
+ const matchingCheckpoints = [];
983
+ for (const threadId of threadIds) {
984
+ const namespaces = this.storage[threadId];
985
+ if (namespaces === void 0) continue;
986
+ for (const checkpointNs of Object.keys(namespaces)) {
987
+ if (configCheckpointNamespace !== void 0 && checkpointNs !== configCheckpointNamespace) {
988
+ continue;
989
+ }
990
+ const namespaceData = namespaces[checkpointNs];
991
+ if (namespaceData === void 0) continue;
992
+ const { checkpoint_id } = namespaceData;
993
+ if (configCheckpointId && checkpoint_id !== configCheckpointId) {
994
+ continue;
995
+ }
996
+ if (before?.configurable?.checkpoint_id && checkpoint_id >= before.configurable.checkpoint_id) {
997
+ continue;
998
+ }
999
+ const deserializedMetadata = await this.serde.loadsTyped(
1000
+ "json",
1001
+ namespaceData.metadata
1002
+ );
1003
+ if (filter && !this._checkMetadataFilterMatch(deserializedMetadata, filter)) {
1004
+ continue;
1005
+ }
1006
+ matchingCheckpoints.push({ threadId, checkpointNs, namespaceData });
1007
+ }
1008
+ }
1009
+ matchingCheckpoints.sort((a, b) => b.namespaceData.checkpoint_ts - a.namespaceData.checkpoint_ts);
1010
+ let count = 0;
1011
+ for (const { threadId, checkpointNs, namespaceData } of matchingCheckpoints) {
1012
+ if (limit !== void 0 && count >= limit) {
1013
+ return;
1014
+ }
1015
+ const { checkpoint, metadata, checkpoint_id, parent_checkpoint_id } = namespaceData;
1016
+ const deserializedCheckpoint = await this.serde.loadsTyped("json", checkpoint);
1017
+ if (deserializedCheckpoint.v < 4 && parent_checkpoint_id !== void 0) {
1018
+ await this._migratePendingSends(deserializedCheckpoint, threadId, checkpointNs, parent_checkpoint_id);
1019
+ }
1020
+ const pendingWrites = await this._loadPendingWrites(threadId, checkpointNs, checkpoint_id);
1021
+ const deserializedMetadata = await this.serde.loadsTyped("json", metadata);
1022
+ const checkpointTuple = {
1023
+ config: {
1024
+ configurable: {
1025
+ thread_id: threadId,
1026
+ checkpoint_ns: checkpointNs,
1027
+ checkpoint_id
1028
+ }
1029
+ },
1030
+ checkpoint: deserializedCheckpoint,
1031
+ metadata: deserializedMetadata,
1032
+ pendingWrites
1033
+ };
1034
+ if (parent_checkpoint_id !== void 0) {
1035
+ checkpointTuple.parentConfig = {
1036
+ configurable: {
1037
+ thread_id: threadId,
1038
+ checkpoint_ns: checkpointNs,
1039
+ checkpoint_id: parent_checkpoint_id
1040
+ }
1041
+ };
1042
+ }
1043
+ count++;
1044
+ yield checkpointTuple;
1045
+ }
1046
+ }
1047
+ async put(config, checkpoint, metadata, _newVersions) {
1048
+ const preparedCheckpoint = copyCheckpoint(checkpoint);
1049
+ const threadId = config.configurable?.thread_id;
1050
+ const checkpointNamespace = config.configurable?.checkpoint_ns ?? "";
1051
+ const parentCheckpointId = config.configurable?.checkpoint_id;
1052
+ if (threadId === void 0) {
1053
+ throw new Error("thread_id is required");
1054
+ }
1055
+ const checkpointId = checkpoint.id || uuid6(0);
1056
+ if (!this.storage[threadId]) {
1057
+ this.storage[threadId] = {};
1058
+ }
1059
+ const oldNamespaceData = this.storage[threadId][checkpointNamespace];
1060
+ if (oldNamespaceData !== void 0 && oldNamespaceData.checkpoint_id !== checkpointId) {
1061
+ this._cleanupOldWrites(threadId, checkpointNamespace, oldNamespaceData.checkpoint_id);
1062
+ }
1063
+ const [[, serializedCheckpoint], [, serializedMetadata]] = await Promise.all([
1064
+ this.serde.dumpsTyped(preparedCheckpoint),
1065
+ this.serde.dumpsTyped(metadata)
1066
+ ]);
1067
+ this.storage[threadId][checkpointNamespace] = {
1068
+ checkpoint: serializedCheckpoint,
1069
+ metadata: serializedMetadata,
1070
+ checkpoint_id: checkpointId,
1071
+ parent_checkpoint_id: parentCheckpointId,
1072
+ checkpoint_ts: Date.now()
1073
+ // Add timestamp for sorting
1074
+ };
1075
+ return {
1076
+ configurable: {
1077
+ thread_id: threadId,
1078
+ checkpoint_ns: checkpointNamespace,
1079
+ checkpoint_id: checkpointId
1080
+ }
1081
+ };
1082
+ }
1083
+ async putWrites(config, writes, taskId) {
1084
+ const threadId = config.configurable?.thread_id;
1085
+ const checkpointNamespace = config.configurable?.checkpoint_ns ?? "";
1086
+ const checkpointId = config.configurable?.checkpoint_id;
1087
+ if (!threadId || !checkpointId) {
1088
+ throw new Error("thread_id and checkpoint_id are required");
1089
+ }
1090
+ const outerKey = _getWritesKey(threadId, checkpointNamespace, checkpointId);
1091
+ const outerWrites_ = this.writes[outerKey];
1092
+ if (this.writes[outerKey] === void 0) {
1093
+ this.writes[outerKey] = {};
1094
+ }
1095
+ await Promise.all(
1096
+ writes.map(async ([channel, value], idx) => {
1097
+ const [, serializedValue] = await this.serde.dumpsTyped(value);
1098
+ const innerKey = [taskId, WRITES_IDX_MAP[channel] || idx];
1099
+ const innerKeyStr = `${innerKey[0]},${innerKey[1]}`;
1100
+ if (innerKey[1] >= 0 && outerWrites_ && innerKeyStr in outerWrites_) {
1101
+ return;
1102
+ }
1103
+ this.writes[outerKey][innerKeyStr] = [taskId, channel, serializedValue];
1104
+ })
1105
+ );
1106
+ }
1107
+ async deleteThread(threadId) {
1108
+ delete this.storage[threadId];
1109
+ for (const key of Object.keys(this.writes)) {
1110
+ const { threadId: keyThreadId } = _parseShallowKey(key.split("::").slice(0, 2).join("::"));
1111
+ if (keyThreadId === threadId) {
1112
+ delete this.writes[key];
1113
+ }
1114
+ }
1115
+ }
1116
+ }
1117
+
823
1118
  class EventMessage {
824
1119
  event;
825
1120
  data;
@@ -1340,13 +1635,13 @@ const createCheckPointer = async () => {
1340
1635
  "\x1B[33m%s\x1B[0m",
1341
1636
  "LG | set DATABASE_URL=postgresql://user:pass@localhost:5432/db to your .env file to use \x1B[1mPostgreSQL\x1B[0m for prod!"
1342
1637
  );
1343
- return new MemorySaver();
1638
+ return new ShallowMemorySaver();
1344
1639
  };
1345
1640
  const createMessageQueue = async () => {
1346
1641
  let q;
1347
1642
  if (process.env.REDIS_URL) {
1348
1643
  console.debug("LG | Using redis as stream queue");
1349
- const { RedisStreamQueue } = await import('./queue-Bfg-8ehP.js');
1644
+ const { RedisStreamQueue } = await import('./queue-Cu_nO_wt.js');
1350
1645
  q = RedisStreamQueue;
1351
1646
  } else {
1352
1647
  q = MemoryStreamQueue;
@@ -1386,7 +1681,7 @@ const createThreadManager = async (config) => {
1386
1681
  }
1387
1682
  if (process.env.SQLITE_DATABASE_URI && config.checkpointer) {
1388
1683
  console.debug("LG | Using SQLite ThreadsManager");
1389
- const { SQLiteAdapter } = await import('./sqlite-adapter-Bbonr5S5.js');
1684
+ const { SQLiteAdapter } = await import('./sqlite-adapter-BKOLSdoL.js');
1390
1685
  const database = config.checkpointer.db;
1391
1686
  const threadsManager = new KyselyThreadsManager(new SQLiteAdapter(database));
1392
1687
  await threadsManager.setup();
@@ -1584,4 +1879,4 @@ async function* streamState(threads, run, payload, options) {
1584
1879
  }
1585
1880
 
1586
1881
  export { BaseStreamQueue as B, CancelEventMessage as C, GRAPHS as G, KyselyThreadsManager as K, LangGraphGlobal as L, streamState as a, getGraph as g, registerGraph as r, serialiseAsDict as s };
1587
- //# sourceMappingURL=stream-Crp1K8nB.js.map
1882
+ //# sourceMappingURL=stream-CsqWsCjt.js.map