@langchain/langgraph-checkpoint-redis 1.0.1 → 1.0.3
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.
- package/LICENSE +21 -0
- package/dist/constants.cjs +7 -0
- package/dist/constants.cjs.map +1 -0
- package/dist/constants.js +6 -0
- package/dist/constants.js.map +1 -0
- package/dist/index.cjs +42 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -4
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -35
- package/dist/index.js.map +1 -1
- package/dist/shallow.cjs +31 -24
- package/dist/shallow.cjs.map +1 -1
- package/dist/shallow.d.cts +2 -4
- package/dist/shallow.d.cts.map +1 -1
- package/dist/shallow.d.ts +2 -4
- package/dist/shallow.d.ts.map +1 -1
- package/dist/shallow.js +26 -19
- package/dist/shallow.js.map +1 -1
- package/dist/store.cjs +39 -51
- package/dist/store.cjs.map +1 -1
- package/dist/store.d.cts +2 -5
- package/dist/store.d.cts.map +1 -1
- package/dist/store.d.ts +2 -5
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +31 -43
- package/dist/store.js.map +1 -1
- package/dist/utils.cjs +22 -0
- package/dist/utils.cjs.map +1 -0
- package/dist/utils.js +21 -0
- package/dist/utils.js.map +1 -0
- package/package.json +22 -21
- package/CHANGELOG.md +0 -24
- package/dist/_virtual/rolldown_runtime.cjs +0 -25
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 LangChain
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.cjs","names":[],"sources":["../src/constants.ts"],"sourcesContent":["export const WRITE_KEYS_ZSET_PREFIX = \"write_keys_zset\";\n"],"mappings":";;AAAA,MAAa,yBAAyB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.js","names":[],"sources":["../src/constants.ts"],"sourcesContent":["export const WRITE_KEYS_ZSET_PREFIX = \"write_keys_zset\";\n"],"mappings":";AAAA,MAAa,yBAAyB"}
|
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
3
|
-
const
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2
|
+
const require_utils = require('./utils.cjs');
|
|
3
|
+
const require_constants = require('./constants.cjs');
|
|
4
|
+
let _langchain_langgraph_checkpoint = require("@langchain/langgraph-checkpoint");
|
|
5
|
+
let redis = require("redis");
|
|
4
6
|
|
|
5
7
|
//#region src/index.ts
|
|
6
8
|
const SCHEMAS = [
|
|
@@ -107,7 +109,7 @@ const SCHEMAS = [
|
|
|
107
109
|
}
|
|
108
110
|
}
|
|
109
111
|
];
|
|
110
|
-
var RedisSaver = class RedisSaver extends
|
|
112
|
+
var RedisSaver = class RedisSaver extends _langchain_langgraph_checkpoint.BaseCheckpointSaver {
|
|
111
113
|
client;
|
|
112
114
|
ttlConfig;
|
|
113
115
|
constructor(client, ttlConfig) {
|
|
@@ -130,14 +132,13 @@ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseC
|
|
|
130
132
|
return saver;
|
|
131
133
|
}
|
|
132
134
|
async get(config) {
|
|
133
|
-
|
|
134
|
-
return tuple?.checkpoint;
|
|
135
|
+
return (await this.getTuple(config))?.checkpoint;
|
|
135
136
|
}
|
|
136
137
|
async getTuple(config) {
|
|
137
138
|
const threadId = config.configurable?.thread_id;
|
|
138
139
|
const checkpointNs = config.configurable?.checkpoint_ns ?? "";
|
|
139
140
|
const checkpointId = config.configurable?.checkpoint_id;
|
|
140
|
-
if (!threadId) return
|
|
141
|
+
if (!threadId) return;
|
|
141
142
|
let key;
|
|
142
143
|
let jsonDoc;
|
|
143
144
|
if (checkpointId) {
|
|
@@ -146,12 +147,12 @@ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseC
|
|
|
146
147
|
} else {
|
|
147
148
|
const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;
|
|
148
149
|
const keys = await this.client.keys(pattern);
|
|
149
|
-
if (keys.length === 0) return
|
|
150
|
+
if (keys.length === 0) return;
|
|
150
151
|
keys.sort();
|
|
151
152
|
key = keys[keys.length - 1];
|
|
152
153
|
jsonDoc = await this.client.json.get(key);
|
|
153
154
|
}
|
|
154
|
-
if (!jsonDoc) return
|
|
155
|
+
if (!jsonDoc) return;
|
|
155
156
|
if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) await this.applyTTL(key);
|
|
156
157
|
const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(jsonDoc);
|
|
157
158
|
return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
|
|
@@ -162,15 +163,17 @@ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseC
|
|
|
162
163
|
const checkpointNs = config.configurable?.checkpoint_ns ?? "";
|
|
163
164
|
const parentCheckpointId = config.configurable?.checkpoint_id;
|
|
164
165
|
if (!threadId) throw new Error("thread_id is required");
|
|
165
|
-
const checkpointId = checkpoint.id || (0,
|
|
166
|
+
const checkpointId = checkpoint.id || (0, _langchain_langgraph_checkpoint.uuid6)(0);
|
|
166
167
|
const key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;
|
|
167
|
-
const storedCheckpoint = (0,
|
|
168
|
+
const storedCheckpoint = (0, _langchain_langgraph_checkpoint.copyCheckpoint)(checkpoint);
|
|
168
169
|
if (storedCheckpoint.channel_values && newVersions !== void 0) if (Object.keys(newVersions).length === 0) storedCheckpoint.channel_values = {};
|
|
169
170
|
else {
|
|
170
171
|
const filteredChannelValues = {};
|
|
171
172
|
for (const channel of Object.keys(newVersions)) if (channel in storedCheckpoint.channel_values) filteredChannelValues[channel] = storedCheckpoint.channel_values[channel];
|
|
172
173
|
storedCheckpoint.channel_values = filteredChannelValues;
|
|
173
174
|
}
|
|
175
|
+
const zsetKey = `${require_constants.WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;
|
|
176
|
+
const writesExist = await this.client.exists(zsetKey);
|
|
174
177
|
const jsonDoc = {
|
|
175
178
|
thread_id: threadId,
|
|
176
179
|
checkpoint_ns: checkpointNs === "" ? "__empty__" : checkpointNs,
|
|
@@ -179,7 +182,7 @@ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseC
|
|
|
179
182
|
checkpoint: storedCheckpoint,
|
|
180
183
|
metadata,
|
|
181
184
|
checkpoint_ts: Date.now(),
|
|
182
|
-
has_writes: "false"
|
|
185
|
+
has_writes: writesExist ? "true" : "false"
|
|
183
186
|
};
|
|
184
187
|
this.addSearchableMetadataFields(jsonDoc, metadata);
|
|
185
188
|
await this.client.json.set(key, "$", jsonDoc);
|
|
@@ -196,28 +199,33 @@ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseC
|
|
|
196
199
|
const hasNullFilter = Object.values(options.filter).some((v) => v === null);
|
|
197
200
|
const queryParts = [];
|
|
198
201
|
if (config?.configurable?.thread_id) {
|
|
199
|
-
const threadId = config.configurable.thread_id
|
|
202
|
+
const threadId = require_utils.escapeRediSearchTagValue(config.configurable.thread_id);
|
|
200
203
|
queryParts.push(`(@thread_id:{${threadId}})`);
|
|
201
204
|
}
|
|
202
205
|
if (config?.configurable?.checkpoint_ns !== void 0) {
|
|
203
206
|
const checkpointNs = config.configurable.checkpoint_ns;
|
|
204
207
|
if (checkpointNs === "") queryParts.push(`(@checkpoint_ns:{__empty__})`);
|
|
205
208
|
else {
|
|
206
|
-
const escapedNs =
|
|
209
|
+
const escapedNs = require_utils.escapeRediSearchTagValue(checkpointNs);
|
|
207
210
|
queryParts.push(`(@checkpoint_ns:{${escapedNs}})`);
|
|
208
211
|
}
|
|
209
212
|
}
|
|
210
213
|
if (!options?.before && options?.filter) {
|
|
211
|
-
for (const [key, value] of Object.entries(options.filter)) if (value === void 0) {} else if (value === null) {} else if (typeof value === "string")
|
|
212
|
-
|
|
213
|
-
|
|
214
|
+
for (const [key, value] of Object.entries(options.filter)) if (value === void 0) {} else if (value === null) {} else if (typeof value === "string") {
|
|
215
|
+
const escapedKey = require_utils.escapeRediSearchTagValue(key);
|
|
216
|
+
const escapedValue = require_utils.escapeRediSearchTagValue(value);
|
|
217
|
+
queryParts.push(`(@${escapedKey}:{${escapedValue}})`);
|
|
218
|
+
} else if (typeof value === "number") {
|
|
219
|
+
const escapedKey = require_utils.escapeRediSearchTagValue(key);
|
|
220
|
+
queryParts.push(`(@${escapedKey}:[${value} ${value}])`);
|
|
221
|
+
} else if (typeof value === "object" && Object.keys(value).length === 0) {}
|
|
214
222
|
}
|
|
215
223
|
if (queryParts.length === 0) queryParts.push("*");
|
|
216
224
|
const query = queryParts.join(" ");
|
|
217
225
|
const limit = options?.limit ?? 10;
|
|
218
226
|
try {
|
|
219
227
|
const fetchLimit = options?.before && !config?.configurable?.thread_id ? 1e3 : options?.before ? limit * 10 : limit;
|
|
220
|
-
|
|
228
|
+
let documents = (await this.client.ft.search("checkpoints", query, {
|
|
221
229
|
LIMIT: {
|
|
222
230
|
from: 0,
|
|
223
231
|
size: fetchLimit
|
|
@@ -226,22 +234,18 @@ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseC
|
|
|
226
234
|
BY: "checkpoint_ts",
|
|
227
235
|
DIRECTION: "DESC"
|
|
228
236
|
}
|
|
229
|
-
});
|
|
230
|
-
let documents = results.documents;
|
|
237
|
+
})).documents;
|
|
231
238
|
let yieldedCount = 0;
|
|
232
239
|
for (const doc of documents) {
|
|
233
240
|
if (yieldedCount >= limit) break;
|
|
234
241
|
if (options?.before?.configurable?.checkpoint_id) {
|
|
235
|
-
|
|
236
|
-
const beforeCheckpointId = options.before.configurable.checkpoint_id;
|
|
237
|
-
if (currentCheckpointId >= beforeCheckpointId) continue;
|
|
242
|
+
if (doc.value.checkpoint_id >= options.before.configurable.checkpoint_id) continue;
|
|
238
243
|
}
|
|
239
244
|
const jsonDoc = doc.value;
|
|
240
245
|
let matches = true;
|
|
241
246
|
if ((hasNullFilter || options?.before) && options?.filter) {
|
|
242
247
|
for (const [filterKey, filterValue] of Object.entries(options.filter)) if (filterValue === null) {
|
|
243
|
-
|
|
244
|
-
if (metadataValue !== null) {
|
|
248
|
+
if (jsonDoc.metadata?.[filterKey] !== null) {
|
|
245
249
|
matches = false;
|
|
246
250
|
break;
|
|
247
251
|
}
|
|
@@ -275,15 +279,13 @@ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseC
|
|
|
275
279
|
keys.sort().reverse();
|
|
276
280
|
let filteredKeys = keys;
|
|
277
281
|
if (options?.before?.configurable?.checkpoint_id) {
|
|
278
|
-
const
|
|
279
|
-
const beforeCheckpointNs = options.before.configurable.checkpoint_ns ?? checkpointNs;
|
|
280
|
-
const beforeKey = `checkpoint:${beforeThreadId}:${beforeCheckpointNs}:${options.before.configurable.checkpoint_id}`;
|
|
282
|
+
const beforeKey = `checkpoint:${options.before.configurable.thread_id || threadId}:${options.before.configurable.checkpoint_ns ?? checkpointNs}:${options.before.configurable.checkpoint_id}`;
|
|
281
283
|
const beforeIndex = keys.indexOf(beforeKey);
|
|
282
284
|
if (beforeIndex > 0) filteredKeys = keys.slice(beforeIndex + 1);
|
|
283
285
|
else if (beforeIndex === 0) filteredKeys = [];
|
|
284
286
|
}
|
|
285
|
-
const limit
|
|
286
|
-
const limitedKeys = filteredKeys.slice(0, limit
|
|
287
|
+
const limit = options?.limit ?? 10;
|
|
288
|
+
const limitedKeys = filteredKeys.slice(0, limit);
|
|
287
289
|
for (const key of limitedKeys) {
|
|
288
290
|
const jsonDoc = await this.client.json.get(key);
|
|
289
291
|
if (jsonDoc) {
|
|
@@ -318,19 +320,16 @@ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseC
|
|
|
318
320
|
}
|
|
319
321
|
allDocuments.sort((a, b) => b.doc.checkpoint_ts - a.doc.checkpoint_ts);
|
|
320
322
|
let yieldedCount = 0;
|
|
321
|
-
const limit
|
|
323
|
+
const limit = options?.limit ?? 10;
|
|
322
324
|
for (const { doc: jsonDoc } of allDocuments) {
|
|
323
|
-
if (yieldedCount >= limit
|
|
325
|
+
if (yieldedCount >= limit) break;
|
|
324
326
|
if (options?.before?.configurable?.checkpoint_id) {
|
|
325
|
-
|
|
326
|
-
const beforeCheckpointId = options.before.configurable.checkpoint_id;
|
|
327
|
-
if (currentCheckpointId >= beforeCheckpointId) continue;
|
|
327
|
+
if (jsonDoc.checkpoint_id >= options.before.configurable.checkpoint_id) continue;
|
|
328
328
|
}
|
|
329
329
|
let matches = true;
|
|
330
330
|
if (options?.filter) {
|
|
331
331
|
for (const [filterKey, filterValue] of Object.entries(options.filter)) if (filterValue === null) {
|
|
332
|
-
|
|
333
|
-
if (metadataValue !== null) {
|
|
332
|
+
if (jsonDoc.metadata?.[filterKey] !== null) {
|
|
334
333
|
matches = false;
|
|
335
334
|
break;
|
|
336
335
|
}
|
|
@@ -388,7 +387,7 @@ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseC
|
|
|
388
387
|
await this.client.json.set(writeKey, "$", writeDoc);
|
|
389
388
|
}
|
|
390
389
|
if (writeKeys.length > 0) {
|
|
391
|
-
const zsetKey =
|
|
390
|
+
const zsetKey = `${require_constants.WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;
|
|
392
391
|
const zaddArgs = {};
|
|
393
392
|
writeKeys.forEach((key, idx) => {
|
|
394
393
|
zaddArgs[key] = baseTimestamp + idx;
|
|
@@ -400,8 +399,7 @@ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseC
|
|
|
400
399
|
if (this.ttlConfig?.defaultTTL) await this.applyTTL(...writeKeys, zsetKey);
|
|
401
400
|
}
|
|
402
401
|
const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;
|
|
403
|
-
|
|
404
|
-
if (checkpointExists) {
|
|
402
|
+
if (await this.client.exists(checkpointKey)) {
|
|
405
403
|
const currentDoc = await this.client.json.get(checkpointKey);
|
|
406
404
|
if (currentDoc) {
|
|
407
405
|
currentDoc.has_writes = "true";
|
|
@@ -423,7 +421,7 @@ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseC
|
|
|
423
421
|
async loadPendingWrites(threadId, checkpointNs, checkpointId) {
|
|
424
422
|
const pattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:*`;
|
|
425
423
|
const writeKeys = await this.client.keys(pattern);
|
|
426
|
-
if (writeKeys.length === 0) return
|
|
424
|
+
if (writeKeys.length === 0) return;
|
|
427
425
|
const writeDocuments = [];
|
|
428
426
|
for (const writeKey of writeKeys) {
|
|
429
427
|
const writeDoc = await this.client.json.get(writeKey);
|
|
@@ -460,13 +458,13 @@ var RedisSaver = class RedisSaver extends __langchain_langgraph_checkpoint.BaseC
|
|
|
460
458
|
async migratePendingSends(checkpoint, threadId, checkpointNs, parentCheckpointId) {
|
|
461
459
|
const parentWrites = await this.loadPendingWrites(threadId, checkpointNs, parentCheckpointId);
|
|
462
460
|
if (!parentWrites || parentWrites.length === 0) return;
|
|
463
|
-
const taskWrites = parentWrites.filter(([, channel]) => channel ===
|
|
461
|
+
const taskWrites = parentWrites.filter(([, channel]) => channel === _langchain_langgraph_checkpoint.TASKS);
|
|
464
462
|
if (taskWrites.length === 0) return;
|
|
465
463
|
const allTasks = [];
|
|
466
464
|
for (const [, , value] of taskWrites) allTasks.push(value);
|
|
467
465
|
checkpoint.channel_values ??= {};
|
|
468
|
-
checkpoint.channel_values[
|
|
469
|
-
checkpoint.channel_versions[
|
|
466
|
+
checkpoint.channel_values[_langchain_langgraph_checkpoint.TASKS] = allTasks;
|
|
467
|
+
checkpoint.channel_versions[_langchain_langgraph_checkpoint.TASKS] = Object.keys(checkpoint.channel_versions).length > 0 ? (0, _langchain_langgraph_checkpoint.maxChannelVersion)(...Object.values(checkpoint.channel_versions)) : 1;
|
|
470
468
|
}
|
|
471
469
|
async createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
|
|
472
470
|
const checkpointNs = jsonDoc.checkpoint_ns === "__empty__" ? "" : jsonDoc.checkpoint_ns;
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":["BaseCheckpointSaver","key: string","jsonDoc: CheckpointDocument | null","filteredChannelValues: Record<string, any>","jsonDoc: CheckpointDocument","queryParts: string[]","error: any","limit","allDocuments: { key: string; doc: CheckpointDocument }[]","searchOptions: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n }","writeKeys: string[]","zaddArgs: Record<string, number>","writeDocuments: any[]","pendingWrites: Array<[string, string, any]>","checkpoint: Checkpoint","pendingWrites: Array<[string, string, any]> | undefined","TASKS","allTasks: any[]","sortedObj: Record<string, any>","sorted: Record<string, any>"],"sources":["../src/index.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n TASKS,\n maxChannelVersion,\n copyCheckpoint,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient, createCluster } from \"redis\";\n\n// Type for Redis client - supports both standalone and cluster\nexport type RedisClientType =\n | ReturnType<typeof createClient>\n | ReturnType<typeof createCluster>;\n\nconst SCHEMAS = [\n {\n index: \"checkpoints\",\n prefix: \"checkpoint:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.parent_checkpoint_id\": { type: \"TAG\", AS: \"parent_checkpoint_id\" },\n \"$.checkpoint_ts\": { type: \"NUMERIC\", AS: \"checkpoint_ts\" },\n \"$.has_writes\": { type: \"TAG\", AS: \"has_writes\" },\n \"$.source\": { type: \"TAG\", AS: \"source\" },\n \"$.step\": { type: \"NUMERIC\", AS: \"step\" },\n },\n },\n {\n index: \"checkpoint_blobs\",\n prefix: \"checkpoint_blob:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.version\": { type: \"TAG\", AS: \"version\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\n },\n },\n {\n index: \"checkpoint_writes\",\n prefix: \"checkpoint_write:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.task_id\": { type: \"TAG\", AS: \"task_id\" },\n \"$.idx\": { type: \"NUMERIC\", AS: \"idx\" },\n \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\n },\n },\n];\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n\ninterface CheckpointDocument {\n thread_id: string;\n checkpoint_ns: string;\n checkpoint_id: string;\n parent_checkpoint_id: string | null;\n checkpoint: Checkpoint & {\n channel_values?: Record<string, any>;\n channel_blobs?: Record<string, { __blob__: boolean; key: string }>;\n };\n metadata: CheckpointMetadata;\n checkpoint_ts: number;\n has_writes: string;\n source?: string;\n step?: number;\n [key: string]: any; // Allow additional fields for metadata\n}\n\nexport class RedisSaver extends BaseCheckpointSaver {\n private client: RedisClientType;\n private ttlConfig?: TTLConfig;\n\n constructor(client: RedisClientType, ttlConfig?: TTLConfig) {\n super();\n this.client = client;\n this.ttlConfig = ttlConfig;\n }\n\n static async fromUrl(\n url: string,\n ttlConfig?: TTLConfig\n ): Promise<RedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new RedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n static async fromCluster(\n rootNodes: Array<{ url: string }>,\n ttlConfig?: TTLConfig\n ): Promise<RedisSaver> {\n const client = createCluster({ rootNodes });\n await client.connect();\n const saver = new RedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n async get(config: RunnableConfig): Promise<Checkpoint | undefined> {\n const tuple = await this.getTuple(config);\n return tuple?.checkpoint;\n }\n\n async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n return undefined;\n }\n\n let key: string;\n let jsonDoc: CheckpointDocument | null;\n\n if (checkpointId) {\n // Get specific checkpoint\n key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;\n jsonDoc = (await this.client.json.get(key)) as CheckpointDocument | null;\n } else {\n // Get latest checkpoint - need to search\n const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;\n // Use keys for simplicity - scan would be better for large datasets\n const keys = await (this.client as any).keys(pattern);\n\n if (keys.length === 0) {\n return undefined;\n }\n\n // Sort by key to get latest\n keys.sort();\n key = keys[keys.length - 1];\n jsonDoc = (await this.client.json.get(key)) as CheckpointDocument | null;\n }\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // Refresh TTL if configured\n if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n // Load checkpoint with pending writes\n const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(\n jsonDoc\n );\n\n return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async put(\n config: RunnableConfig,\n checkpoint: Checkpoint,\n metadata: CheckpointMetadata,\n newVersions: ChannelVersions\n ): Promise<RunnableConfig> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const parentCheckpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n throw new Error(\"thread_id is required\");\n }\n\n const checkpointId = checkpoint.id || uuid6(0);\n const key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Copy checkpoint and filter channel_values to only include changed channels\n const storedCheckpoint = copyCheckpoint(checkpoint);\n\n // If newVersions is provided and has keys, only store those channels that changed\n // If newVersions is empty {}, store no channel values\n // If newVersions is not provided (undefined), keep all channel_values as-is\n if (storedCheckpoint.channel_values && newVersions !== undefined) {\n if (Object.keys(newVersions).length === 0) {\n // Empty newVersions means no channels changed - store empty channel_values\n storedCheckpoint.channel_values = {};\n } else {\n // Only store the channels that are in newVersions\n const filteredChannelValues: Record<string, any> = {};\n for (const channel of Object.keys(newVersions)) {\n if (channel in storedCheckpoint.channel_values) {\n filteredChannelValues[channel] =\n storedCheckpoint.channel_values[channel];\n }\n }\n storedCheckpoint.channel_values = filteredChannelValues;\n }\n }\n // If newVersions is undefined, keep all channel_values as-is (for backward compatibility)\n\n // Structure matching Python implementation\n const jsonDoc: CheckpointDocument = {\n thread_id: threadId,\n // Store empty namespace as \"__empty__\" for RediSearch compatibility\n checkpoint_ns: checkpointNs === \"\" ? \"__empty__\" : checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: storedCheckpoint,\n metadata: metadata,\n checkpoint_ts: Date.now(),\n has_writes: \"false\",\n };\n\n // Store metadata fields at top-level for searching\n this.addSearchableMetadataFields(jsonDoc, metadata);\n\n // Use Redis JSON commands\n await this.client.json.set(key, \"$\", jsonDoc as any);\n\n // Apply TTL if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n return {\n configurable: {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n },\n };\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // If filter is provided (even if empty), use search functionality\n if (options?.filter !== undefined) {\n // Check if we have null values in the filter which RediSearch can't handle\n const hasNullFilter = Object.values(options.filter).some(\n (v) => v === null\n );\n\n // Build search query\n const queryParts: string[] = [];\n\n // Add thread_id constraint if provided\n if (config?.configurable?.thread_id) {\n const threadId = config.configurable.thread_id.replace(\n /[-.@]/g,\n \"\\\\$&\"\n );\n queryParts.push(`(@thread_id:{${threadId}})`);\n }\n\n // Add checkpoint_ns constraint if provided\n if (config?.configurable?.checkpoint_ns !== undefined) {\n const checkpointNs = config.configurable.checkpoint_ns;\n if (checkpointNs === \"\") {\n // Empty string needs special handling in RediSearch\n // We'll store it as \"__empty__\" in the index\n queryParts.push(`(@checkpoint_ns:{__empty__})`);\n } else {\n const escapedNs = checkpointNs.replace(/[-.@]/g, \"\\\\$&\");\n queryParts.push(`(@checkpoint_ns:{${escapedNs}})`);\n }\n }\n\n // Skip metadata filters in search query when 'before' parameter is used\n // We'll apply them after the before filtering to get correct results\n if (!options?.before && options?.filter) {\n // Add metadata filters (but skip null values)\n for (const [key, value] of Object.entries(options.filter)) {\n if (value === undefined) {\n // Skip undefined filters\n } else if (value === null) {\n // Skip null values for RediSearch query, will handle in post-processing\n } else if (typeof value === \"string\") {\n // Don't escape, just wrap in braces for exact match\n queryParts.push(`(@${key}:{${value}})`);\n } else if (typeof value === \"number\") {\n queryParts.push(`(@${key}:[${value} ${value}])`);\n } else if (\n typeof value === \"object\" &&\n Object.keys(value).length === 0\n ) {\n // Skip empty objects\n }\n }\n }\n\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n const query = queryParts.join(\" \");\n const limit = options?.limit ?? 10;\n\n try {\n // Fetch more results than the limit to handle post-filtering for 'before'\n // When no thread_id is specified but 'before' is used, we need to fetch all results\n const fetchLimit =\n options?.before && !config?.configurable?.thread_id\n ? 1000 // Fetch many results for global search with 'before' filtering\n : options?.before\n ? limit * 10\n : limit;\n\n const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: fetchLimit },\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n let documents = results.documents;\n\n let yieldedCount = 0;\n\n for (const doc of documents) {\n if (yieldedCount >= limit) break;\n\n // Handle 'before' parameter - filter based on checkpoint_id comparison\n // UUID6 IDs are time-sortable, so string comparison works for ordering\n if (options?.before?.configurable?.checkpoint_id) {\n const currentCheckpointId = doc.value.checkpoint_id as string;\n const beforeCheckpointId =\n options.before.configurable.checkpoint_id;\n\n // Skip checkpoints that are not before the specified checkpoint\n if (currentCheckpointId >= beforeCheckpointId) {\n continue;\n }\n }\n\n const jsonDoc = doc.value;\n\n // Apply metadata filters manually (either for null filters or when before parameter was used)\n let matches = true;\n if ((hasNullFilter || options?.before) && options?.filter) {\n for (const [filterKey, filterValue] of Object.entries(\n options.filter\n )) {\n if (filterValue === null) {\n // Check if the field exists and is null in metadata\n // This should only match explicit null, not missing fields\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n if (metadataValue !== null) {\n matches = false;\n break;\n }\n } else if (filterValue !== undefined) {\n // Check other metadata values\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n // For objects, do deep equality check with deterministic key ordering\n if (typeof filterValue === \"object\" && filterValue !== null) {\n if (\n deterministicStringify(metadataValue) !==\n deterministicStringify(filterValue)\n ) {\n matches = false;\n break;\n }\n } else if (metadataValue !== filterValue) {\n matches = false;\n break;\n }\n }\n }\n if (!matches) continue;\n }\n\n // Load checkpoint with pending writes and migrate sends\n const { checkpoint, pendingWrites } =\n await this.loadCheckpointWithWrites(jsonDoc);\n yield await this.createCheckpointTuple(\n jsonDoc,\n checkpoint,\n pendingWrites\n );\n yieldedCount++;\n }\n\n // Search succeeded, return without falling through\n return;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall through to regular listing\n } else {\n throw error;\n }\n }\n\n // If search failed due to missing index, fall through to regular listing\n if (config?.configurable?.thread_id) {\n // Fall back to regular listing with manual filtering when thread_id is specified\n const threadId = config.configurable.thread_id;\n const checkpointNs = config.configurable.checkpoint_ns ?? \"\";\n const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;\n // Use scan for better performance and cluster compatibility\n // Use keys for simplicity - scan would be better for large datasets\n const keys = await (this.client as any).keys(pattern);\n\n keys.sort().reverse();\n\n let filteredKeys = keys;\n\n // Handle 'before' parameter\n if (options?.before?.configurable?.checkpoint_id) {\n const beforeThreadId =\n options.before.configurable.thread_id || threadId;\n const beforeCheckpointNs =\n options.before.configurable.checkpoint_ns ?? checkpointNs;\n const beforeKey = `checkpoint:${beforeThreadId}:${beforeCheckpointNs}:${options.before.configurable.checkpoint_id}`;\n\n const beforeIndex = keys.indexOf(beforeKey);\n if (beforeIndex > 0) {\n // Return all items that come after the found index (i.e., before in time)\n filteredKeys = keys.slice(beforeIndex + 1);\n } else if (beforeIndex === 0) {\n // Nothing before the first item (most recent)\n filteredKeys = [];\n }\n // If not found, return all\n }\n\n const limit = options?.limit ?? 10;\n const limitedKeys = filteredKeys.slice(0, limit);\n\n for (const key of limitedKeys) {\n const jsonDoc = (await this.client.json.get(\n key\n )) as CheckpointDocument | null;\n if (jsonDoc) {\n // Check if metadata matches filter\n let matches = true;\n for (const [filterKey, filterValue] of Object.entries(\n options.filter\n )) {\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n if (filterValue === null) {\n if (metadataValue !== null) {\n matches = false;\n break;\n }\n } else if (metadataValue !== filterValue) {\n matches = false;\n break;\n }\n }\n\n if (!matches) continue;\n\n // Load checkpoint with pending writes and migrate sends\n const { checkpoint, pendingWrites } =\n await this.loadCheckpointWithWrites(jsonDoc);\n yield await this.createCheckpointTuple(\n jsonDoc,\n checkpoint,\n pendingWrites\n );\n }\n }\n } else {\n // Fall back to global search when thread_id is undefined\n // This is needed for validation tests that search globally with 'before' parameter\n const globalPattern =\n config?.configurable?.checkpoint_ns !== undefined\n ? `checkpoint:*:${\n config.configurable.checkpoint_ns === \"\"\n ? \"__empty__\"\n : config.configurable.checkpoint_ns\n }:*`\n : \"checkpoint:*\";\n\n const allKeys = await (this.client as any).keys(globalPattern);\n const allDocuments: { key: string; doc: CheckpointDocument }[] = [];\n\n // Load all matching documents\n for (const key of allKeys) {\n const jsonDoc = (await this.client.json.get(\n key\n )) as CheckpointDocument | null;\n if (jsonDoc) {\n allDocuments.push({ key, doc: jsonDoc });\n }\n }\n\n // Sort by timestamp (descending) to match the search behavior\n allDocuments.sort((a, b) => b.doc.checkpoint_ts - a.doc.checkpoint_ts);\n\n let yieldedCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const { doc: jsonDoc } of allDocuments) {\n if (yieldedCount >= limit) break;\n\n // Handle 'before' parameter - filter based on checkpoint_id comparison\n if (options?.before?.configurable?.checkpoint_id) {\n const currentCheckpointId = jsonDoc.checkpoint_id;\n const beforeCheckpointId =\n options.before.configurable.checkpoint_id;\n\n // Skip checkpoints that are not before the specified checkpoint\n if (currentCheckpointId >= beforeCheckpointId) {\n continue;\n }\n }\n\n // Apply metadata filters manually\n let matches = true;\n if (options?.filter) {\n for (const [filterKey, filterValue] of Object.entries(\n options.filter\n )) {\n if (filterValue === null) {\n // Check if the field exists and is null in metadata\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n if (metadataValue !== null) {\n matches = false;\n break;\n }\n } else if (filterValue !== undefined) {\n // Check other metadata values\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n // For objects, do deep equality check with deterministic key ordering\n if (typeof filterValue === \"object\" && filterValue !== null) {\n if (\n deterministicStringify(metadataValue) !==\n deterministicStringify(filterValue)\n ) {\n matches = false;\n break;\n }\n } else if (metadataValue !== filterValue) {\n matches = false;\n break;\n }\n }\n }\n if (!matches) continue;\n }\n\n // Load checkpoint with pending writes and migrate sends\n const { checkpoint, pendingWrites } =\n await this.loadCheckpointWithWrites(jsonDoc);\n yield await this.createCheckpointTuple(\n jsonDoc,\n checkpoint,\n pendingWrites\n );\n yieldedCount++;\n }\n }\n\n return;\n }\n\n // Regular listing without filter - use search with empty filter instead\n // This ensures consistent behavior between filter={} and filter=undefined\n const searchOptions: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n } = {\n ...options,\n filter: {} as CheckpointMetadata,\n };\n\n // Delegate to the search path\n yield* this.list(config, searchOptions);\n return;\n }\n\n async putWrites(\n config: RunnableConfig,\n writes: PendingWrite[],\n taskId: string\n ): Promise<void> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId || !checkpointId) {\n throw new Error(\"thread_id and checkpoint_id are required\");\n }\n\n // Collect write keys for sorted set tracking\n const writeKeys: string[] = [];\n\n // Use high-resolution timestamp to ensure unique ordering across putWrites calls\n const baseTimestamp = performance.now() * 1000; // Microsecond precision\n\n // Store each write as a separate indexed JSON document\n for (let idx = 0; idx < writes.length; idx++) {\n const [channel, value] = writes[idx];\n const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;\n writeKeys.push(writeKey);\n\n const writeDoc = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n task_id: taskId,\n idx: idx,\n channel: channel,\n type: typeof value === \"object\" ? \"json\" : \"string\",\n value: value,\n timestamp: baseTimestamp,\n global_idx: baseTimestamp + idx, // Add microseconds for sub-millisecond ordering\n };\n\n await this.client.json.set(writeKey, \"$\", writeDoc as any);\n }\n\n // Register write keys in sorted set for efficient retrieval\n if (writeKeys.length > 0) {\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Use timestamp + idx for scoring to maintain correct order\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = baseTimestamp + idx;\n });\n await this.client.zAdd(\n zsetKey,\n Object.entries(zaddArgs).map(([key, score]) => ({ score, value: key }))\n );\n\n // Apply TTL to write keys and zset if configured\n if (this.ttlConfig?.defaultTTL) {\n // Apply TTL to write keys and zset\n await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n // Get the current document and update it\n const currentDoc = (await this.client.json.get(\n checkpointKey\n )) as CheckpointDocument | null;\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc as any);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*`;\n // Use scan for better performance and cluster compatibility\n // Use keys for simplicity - scan would be better for large datasets\n const checkpointKeys = await (this.client as any).keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `writes:${threadId}:*`;\n // Use scan for better performance and cluster compatibility\n // Use keys for simplicity - scan would be better for large datasets\n const writesKeys = await (this.client as any).keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\n }\n\n // Helper method to load channel blobs (simplified - no blob support for now)\n // private async loadChannelBlobs(\n // checkpoint: Checkpoint & { channel_blobs?: any }\n // ): Promise<Checkpoint> {\n // // Since we're not using blobs anymore, just return the checkpoint as-is\n // return checkpoint;\n // }\n\n // Helper method to load pending writes\n private async loadPendingWrites(\n threadId: string,\n checkpointNs: string,\n checkpointId: string\n ): Promise<Array<[string, string, any]> | undefined> {\n // Search for all write documents for this checkpoint\n const pattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:*`;\n const writeKeys = await (this.client as any).keys(pattern);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const writeDocuments: any[] = [];\n for (const writeKey of writeKeys) {\n const writeDoc = (await this.client.json.get(writeKey)) as any;\n if (writeDoc) {\n writeDocuments.push(writeDoc);\n }\n }\n\n // Sort by global_idx (which represents insertion order across all putWrites calls)\n // This matches how SQLite would naturally order by insertion time + idx\n writeDocuments.sort((a, b) => (a.global_idx || 0) - (b.global_idx || 0));\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeDoc of writeDocuments) {\n // Deserialize write value using serde to restore LangChain objects\n const deserializedValue = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(writeDoc.value)\n );\n pendingWrites.push([\n writeDoc.task_id,\n writeDoc.channel,\n deserializedValue,\n ]);\n }\n\n return pendingWrites;\n }\n\n // Helper method to load checkpoint with pending writes\n private async loadCheckpointWithWrites(jsonDoc: any): Promise<{\n checkpoint: Checkpoint;\n pendingWrites?: Array<[string, string, any]>;\n }> {\n // Deserialize checkpoint using serde to restore LangChain objects\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n // Migrate pending sends ONLY for OLD checkpoint versions (v < 4) with parents\n // Modern checkpoints (v >= 4) should NEVER have pending sends migrated\n if (checkpoint.v < 4 && jsonDoc.parent_checkpoint_id != null) {\n // Convert back from \"__empty__\" to empty string for migration\n const actualNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\n await this.migratePendingSends(\n checkpoint,\n jsonDoc.thread_id,\n actualNs,\n jsonDoc.parent_checkpoint_id\n );\n }\n\n // Load this checkpoint's own pending writes (but don't migrate them)\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n // Convert back from \"__empty__\" to empty string for key lookup\n const actualNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n actualNs,\n jsonDoc.checkpoint_id\n );\n }\n\n return { checkpoint, pendingWrites };\n }\n\n // Migrate pending sends from parent checkpoint (matches SQLite implementation)\n private async migratePendingSends(\n checkpoint: Checkpoint,\n threadId: string,\n checkpointNs: string,\n parentCheckpointId: string\n ): Promise<void> {\n // Load pending writes from parent checkpoint that have TASKS channel\n const parentWrites = await this.loadPendingWrites(\n threadId,\n checkpointNs,\n parentCheckpointId\n );\n\n if (!parentWrites || parentWrites.length === 0) {\n return;\n }\n\n // Filter for TASKS channel writes only\n const taskWrites = parentWrites.filter(([, channel]) => channel === TASKS);\n\n if (taskWrites.length === 0) {\n return;\n }\n\n // Collect all task values in order\n const allTasks: any[] = [];\n for (const [, , value] of taskWrites) {\n allTasks.push(value);\n }\n\n // Add pending sends to checkpoint\n checkpoint.channel_values ??= {};\n checkpoint.channel_values[TASKS] = allTasks;\n\n // Add to versions (matches SQLite logic)\n checkpoint.channel_versions[TASKS] =\n Object.keys(checkpoint.channel_versions).length > 0\n ? maxChannelVersion(...Object.values(checkpoint.channel_versions))\n : 1;\n }\n\n // Helper method to create checkpoint tuple from json document\n private async createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): Promise<CheckpointTuple> {\n // Convert back from \"__empty__\" to empty string\n const checkpointNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\n\n // Deserialize metadata using serde\n const metadata = (await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.metadata)\n )) as CheckpointMetadata;\n\n return {\n config: {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: checkpointNs,\n checkpoint_id: jsonDoc.checkpoint_id,\n },\n },\n checkpoint,\n metadata,\n parentConfig: jsonDoc.parent_checkpoint_id\n ? {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: checkpointNs,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\n };\n }\n\n // Helper method to add searchable metadata fields\n private addSearchableMetadataFields(\n jsonDoc: any,\n metadata?: CheckpointMetadata\n ): void {\n if (!metadata) return;\n\n // Add common searchable fields at top level\n if (\"source\" in metadata) {\n jsonDoc.source = metadata.source;\n }\n if (\"step\" in metadata) {\n jsonDoc.step = metadata.step;\n }\n if (\"writes\" in metadata) {\n // Writes field needs to be JSON stringified for TAG search\n jsonDoc.writes =\n typeof metadata.writes === \"object\"\n ? JSON.stringify(metadata.writes)\n : metadata.writes;\n }\n if (\"score\" in metadata) {\n jsonDoc.score = metadata.score;\n }\n }\n\n // Helper method to apply TTL to keys\n private async applyTTL(...keys: string[]): Promise<void> {\n if (!this.ttlConfig?.defaultTTL) return;\n\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n const results = await Promise.allSettled(\n keys.map((key) => this.client.expire(key, ttlSeconds))\n );\n\n // Log any failures but don't throw - TTL is best effort\n for (let i = 0; i < results.length; i++) {\n if (results[i].status === \"rejected\") {\n console.warn(\n `Failed to set TTL for key ${keys[i]}:`,\n (results[i] as PromiseRejectedResult).reason\n );\n }\n }\n }\n\n private async ensureIndexes(): Promise<void> {\n for (const schema of SCHEMAS) {\n try {\n // Try to create the index\n await this.client.ft.create(schema.index, schema.schema as any, {\n ON: \"JSON\",\n PREFIX: schema.prefix,\n });\n } catch (error: any) {\n // Ignore if index already exists\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\n `Failed to create index ${schema.index}:`,\n error.message\n );\n }\n }\n }\n }\n}\n\n// Helper function for deterministic object comparison\nfunction deterministicStringify(obj: any): string {\n if (obj === null || typeof obj !== \"object\") {\n return JSON.stringify(obj);\n }\n if (Array.isArray(obj)) {\n return JSON.stringify(obj.map((item) => deterministicStringify(item)));\n }\n const sortedObj: Record<string, any> = {};\n const sortedKeys = Object.keys(obj).sort();\n for (const key of sortedKeys) {\n sortedObj[key] = obj[key];\n }\n return JSON.stringify(sortedObj, (_, value) => {\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const sorted: Record<string, any> = {};\n const keys = Object.keys(value).sort();\n for (const k of keys) {\n sorted[k] = value[k];\n }\n return sorted;\n }\n return value;\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["BaseCheckpointSaver","WRITE_KEYS_ZSET_PREFIX","escapeRediSearchTagValue","TASKS"],"sources":["../src/index.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n TASKS,\n maxChannelVersion,\n copyCheckpoint,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient, createCluster } from \"redis\";\nimport { escapeRediSearchTagValue } from \"./utils.js\";\nimport { WRITE_KEYS_ZSET_PREFIX } from \"./constants.js\";\n\n// Type for Redis client - supports both standalone and cluster\nexport type RedisClientType =\n | ReturnType<typeof createClient>\n | ReturnType<typeof createCluster>;\n\nconst SCHEMAS = [\n {\n index: \"checkpoints\",\n prefix: \"checkpoint:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.parent_checkpoint_id\": { type: \"TAG\", AS: \"parent_checkpoint_id\" },\n \"$.checkpoint_ts\": { type: \"NUMERIC\", AS: \"checkpoint_ts\" },\n \"$.has_writes\": { type: \"TAG\", AS: \"has_writes\" },\n \"$.source\": { type: \"TAG\", AS: \"source\" },\n \"$.step\": { type: \"NUMERIC\", AS: \"step\" },\n },\n },\n {\n index: \"checkpoint_blobs\",\n prefix: \"checkpoint_blob:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.version\": { type: \"TAG\", AS: \"version\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\n },\n },\n {\n index: \"checkpoint_writes\",\n prefix: \"checkpoint_write:\",\n schema: {\n \"$.thread_id\": { type: \"TAG\", AS: \"thread_id\" },\n \"$.checkpoint_ns\": { type: \"TAG\", AS: \"checkpoint_ns\" },\n \"$.checkpoint_id\": { type: \"TAG\", AS: \"checkpoint_id\" },\n \"$.task_id\": { type: \"TAG\", AS: \"task_id\" },\n \"$.idx\": { type: \"NUMERIC\", AS: \"idx\" },\n \"$.channel\": { type: \"TAG\", AS: \"channel\" },\n \"$.type\": { type: \"TAG\", AS: \"type\" },\n },\n },\n];\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\n}\n\ninterface CheckpointDocument {\n thread_id: string;\n checkpoint_ns: string;\n checkpoint_id: string;\n parent_checkpoint_id: string | null;\n checkpoint: Checkpoint & {\n channel_values?: Record<string, any>;\n channel_blobs?: Record<string, { __blob__: boolean; key: string }>;\n };\n metadata: CheckpointMetadata;\n checkpoint_ts: number;\n has_writes: string;\n source?: string;\n step?: number;\n [key: string]: any; // Allow additional fields for metadata\n}\n\nexport class RedisSaver extends BaseCheckpointSaver {\n private client: RedisClientType;\n private ttlConfig?: TTLConfig;\n\n constructor(client: RedisClientType, ttlConfig?: TTLConfig) {\n super();\n this.client = client;\n this.ttlConfig = ttlConfig;\n }\n\n static async fromUrl(\n url: string,\n ttlConfig?: TTLConfig\n ): Promise<RedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new RedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n static async fromCluster(\n rootNodes: Array<{ url: string }>,\n ttlConfig?: TTLConfig\n ): Promise<RedisSaver> {\n const client = createCluster({ rootNodes });\n await client.connect();\n const saver = new RedisSaver(client, ttlConfig);\n await saver.ensureIndexes();\n return saver;\n }\n\n async get(config: RunnableConfig): Promise<Checkpoint | undefined> {\n const tuple = await this.getTuple(config);\n return tuple?.checkpoint;\n }\n\n async getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined> {\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n return undefined;\n }\n\n let key: string;\n let jsonDoc: CheckpointDocument | null;\n\n if (checkpointId) {\n // Get specific checkpoint\n key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;\n jsonDoc = (await this.client.json.get(key)) as CheckpointDocument | null;\n } else {\n // Get latest checkpoint - need to search\n const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;\n // Use keys for simplicity - scan would be better for large datasets\n const keys = await (this.client as any).keys(pattern);\n\n if (keys.length === 0) {\n return undefined;\n }\n\n // Sort by key to get latest\n keys.sort();\n key = keys[keys.length - 1];\n jsonDoc = (await this.client.json.get(key)) as CheckpointDocument | null;\n }\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // Refresh TTL if configured\n if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n // Load checkpoint with pending writes\n const { checkpoint, pendingWrites } = await this.loadCheckpointWithWrites(\n jsonDoc\n );\n\n return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async put(\n config: RunnableConfig,\n checkpoint: Checkpoint,\n metadata: CheckpointMetadata,\n newVersions: ChannelVersions\n ): Promise<RunnableConfig> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const parentCheckpointId = config.configurable?.checkpoint_id;\n\n if (!threadId) {\n throw new Error(\"thread_id is required\");\n }\n\n const checkpointId = checkpoint.id || uuid6(0);\n const key = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Copy checkpoint and filter channel_values to only include changed channels\n const storedCheckpoint = copyCheckpoint(checkpoint);\n\n // If newVersions is provided and has keys, only store those channels that changed\n // If newVersions is empty {}, store no channel values\n // If newVersions is not provided (undefined), keep all channel_values as-is\n if (storedCheckpoint.channel_values && newVersions !== undefined) {\n if (Object.keys(newVersions).length === 0) {\n // Empty newVersions means no channels changed - store empty channel_values\n storedCheckpoint.channel_values = {};\n } else {\n // Only store the channels that are in newVersions\n const filteredChannelValues: Record<string, any> = {};\n for (const channel of Object.keys(newVersions)) {\n if (channel in storedCheckpoint.channel_values) {\n filteredChannelValues[channel] =\n storedCheckpoint.channel_values[channel];\n }\n }\n storedCheckpoint.channel_values = filteredChannelValues;\n }\n }\n // If newVersions is undefined, keep all channel_values as-is (for backward compatibility)\n\n // Check if writes already exist for this checkpoint (handles putWrites-before-put ordering)\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n const writesExist = await this.client.exists(zsetKey);\n\n // Structure matching Python implementation\n const jsonDoc: CheckpointDocument = {\n thread_id: threadId,\n // Store empty namespace as \"__empty__\" for RediSearch compatibility\n checkpoint_ns: checkpointNs === \"\" ? \"__empty__\" : checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: storedCheckpoint,\n metadata: metadata,\n checkpoint_ts: Date.now(),\n has_writes: writesExist ? \"true\" : \"false\",\n };\n\n // Store metadata fields at top-level for searching\n this.addSearchableMetadataFields(jsonDoc, metadata);\n\n // Use Redis JSON commands\n await this.client.json.set(key, \"$\", jsonDoc as any);\n\n // Apply TTL if configured\n if (this.ttlConfig?.defaultTTL) {\n await this.applyTTL(key);\n }\n\n return {\n configurable: {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n },\n };\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // If filter is provided (even if empty), use search functionality\n if (options?.filter !== undefined) {\n // Check if we have null values in the filter which RediSearch can't handle\n const hasNullFilter = Object.values(options.filter).some(\n (v) => v === null\n );\n\n // Build search query\n const queryParts: string[] = [];\n\n // Add thread_id constraint if provided\n if (config?.configurable?.thread_id) {\n const threadId = escapeRediSearchTagValue(\n config.configurable.thread_id\n );\n queryParts.push(`(@thread_id:{${threadId}})`);\n }\n\n // Add checkpoint_ns constraint if provided\n if (config?.configurable?.checkpoint_ns !== undefined) {\n const checkpointNs = config.configurable.checkpoint_ns;\n if (checkpointNs === \"\") {\n // Empty string needs special handling in RediSearch\n // We'll store it as \"__empty__\" in the index\n queryParts.push(`(@checkpoint_ns:{__empty__})`);\n } else {\n const escapedNs = escapeRediSearchTagValue(checkpointNs);\n queryParts.push(`(@checkpoint_ns:{${escapedNs}})`);\n }\n }\n\n // Skip metadata filters in search query when 'before' parameter is used\n // We'll apply them after the before filtering to get correct results\n if (!options?.before && options?.filter) {\n // Add metadata filters (but skip null values)\n for (const [key, value] of Object.entries(options.filter)) {\n if (value === undefined) {\n // Skip undefined filters\n } else if (value === null) {\n // Skip null values for RediSearch query, will handle in post-processing\n } else if (typeof value === \"string\") {\n // Escape both key and value to prevent RediSearch query injection\n const escapedKey = escapeRediSearchTagValue(key);\n const escapedValue = escapeRediSearchTagValue(value);\n queryParts.push(`(@${escapedKey}:{${escapedValue}})`);\n } else if (typeof value === \"number\") {\n // Escape key to prevent injection; numbers don't need value escaping\n const escapedKey = escapeRediSearchTagValue(key);\n queryParts.push(`(@${escapedKey}:[${value} ${value}])`);\n } else if (\n typeof value === \"object\" &&\n Object.keys(value).length === 0\n ) {\n // Skip empty objects\n }\n }\n }\n\n if (queryParts.length === 0) {\n queryParts.push(\"*\");\n }\n\n const query = queryParts.join(\" \");\n const limit = options?.limit ?? 10;\n\n try {\n // Fetch more results than the limit to handle post-filtering for 'before'\n // When no thread_id is specified but 'before' is used, we need to fetch all results\n const fetchLimit =\n options?.before && !config?.configurable?.thread_id\n ? 1000 // Fetch many results for global search with 'before' filtering\n : options?.before\n ? limit * 10\n : limit;\n\n const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: fetchLimit },\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n let documents = results.documents;\n\n let yieldedCount = 0;\n\n for (const doc of documents) {\n if (yieldedCount >= limit) break;\n\n // Handle 'before' parameter - filter based on checkpoint_id comparison\n // UUID6 IDs are time-sortable, so string comparison works for ordering\n if (options?.before?.configurable?.checkpoint_id) {\n const currentCheckpointId = doc.value.checkpoint_id as string;\n const beforeCheckpointId =\n options.before.configurable.checkpoint_id;\n\n // Skip checkpoints that are not before the specified checkpoint\n if (currentCheckpointId >= beforeCheckpointId) {\n continue;\n }\n }\n\n const jsonDoc = doc.value;\n\n // Apply metadata filters manually (either for null filters or when before parameter was used)\n let matches = true;\n if ((hasNullFilter || options?.before) && options?.filter) {\n for (const [filterKey, filterValue] of Object.entries(\n options.filter\n )) {\n if (filterValue === null) {\n // Check if the field exists and is null in metadata\n // This should only match explicit null, not missing fields\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n if (metadataValue !== null) {\n matches = false;\n break;\n }\n } else if (filterValue !== undefined) {\n // Check other metadata values\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n // For objects, do deep equality check with deterministic key ordering\n if (typeof filterValue === \"object\" && filterValue !== null) {\n if (\n deterministicStringify(metadataValue) !==\n deterministicStringify(filterValue)\n ) {\n matches = false;\n break;\n }\n } else if (metadataValue !== filterValue) {\n matches = false;\n break;\n }\n }\n }\n if (!matches) continue;\n }\n\n // Load checkpoint with pending writes and migrate sends\n const { checkpoint, pendingWrites } =\n await this.loadCheckpointWithWrites(jsonDoc);\n yield await this.createCheckpointTuple(\n jsonDoc,\n checkpoint,\n pendingWrites\n );\n yieldedCount++;\n }\n\n // Search succeeded, return without falling through\n return;\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall through to regular listing\n } else {\n throw error;\n }\n }\n\n // If search failed due to missing index, fall through to regular listing\n if (config?.configurable?.thread_id) {\n // Fall back to regular listing with manual filtering when thread_id is specified\n const threadId = config.configurable.thread_id;\n const checkpointNs = config.configurable.checkpoint_ns ?? \"\";\n const pattern = `checkpoint:${threadId}:${checkpointNs}:*`;\n // Use scan for better performance and cluster compatibility\n // Use keys for simplicity - scan would be better for large datasets\n const keys = await (this.client as any).keys(pattern);\n\n keys.sort().reverse();\n\n let filteredKeys = keys;\n\n // Handle 'before' parameter\n if (options?.before?.configurable?.checkpoint_id) {\n const beforeThreadId =\n options.before.configurable.thread_id || threadId;\n const beforeCheckpointNs =\n options.before.configurable.checkpoint_ns ?? checkpointNs;\n const beforeKey = `checkpoint:${beforeThreadId}:${beforeCheckpointNs}:${options.before.configurable.checkpoint_id}`;\n\n const beforeIndex = keys.indexOf(beforeKey);\n if (beforeIndex > 0) {\n // Return all items that come after the found index (i.e., before in time)\n filteredKeys = keys.slice(beforeIndex + 1);\n } else if (beforeIndex === 0) {\n // Nothing before the first item (most recent)\n filteredKeys = [];\n }\n // If not found, return all\n }\n\n const limit = options?.limit ?? 10;\n const limitedKeys = filteredKeys.slice(0, limit);\n\n for (const key of limitedKeys) {\n const jsonDoc = (await this.client.json.get(\n key\n )) as CheckpointDocument | null;\n if (jsonDoc) {\n // Check if metadata matches filter\n let matches = true;\n for (const [filterKey, filterValue] of Object.entries(\n options.filter\n )) {\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n if (filterValue === null) {\n if (metadataValue !== null) {\n matches = false;\n break;\n }\n } else if (metadataValue !== filterValue) {\n matches = false;\n break;\n }\n }\n\n if (!matches) continue;\n\n // Load checkpoint with pending writes and migrate sends\n const { checkpoint, pendingWrites } =\n await this.loadCheckpointWithWrites(jsonDoc);\n yield await this.createCheckpointTuple(\n jsonDoc,\n checkpoint,\n pendingWrites\n );\n }\n }\n } else {\n // Fall back to global search when thread_id is undefined\n // This is needed for validation tests that search globally with 'before' parameter\n const globalPattern =\n config?.configurable?.checkpoint_ns !== undefined\n ? `checkpoint:*:${\n config.configurable.checkpoint_ns === \"\"\n ? \"__empty__\"\n : config.configurable.checkpoint_ns\n }:*`\n : \"checkpoint:*\";\n\n const allKeys = await (this.client as any).keys(globalPattern);\n const allDocuments: { key: string; doc: CheckpointDocument }[] = [];\n\n // Load all matching documents\n for (const key of allKeys) {\n const jsonDoc = (await this.client.json.get(\n key\n )) as CheckpointDocument | null;\n if (jsonDoc) {\n allDocuments.push({ key, doc: jsonDoc });\n }\n }\n\n // Sort by timestamp (descending) to match the search behavior\n allDocuments.sort((a, b) => b.doc.checkpoint_ts - a.doc.checkpoint_ts);\n\n let yieldedCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const { doc: jsonDoc } of allDocuments) {\n if (yieldedCount >= limit) break;\n\n // Handle 'before' parameter - filter based on checkpoint_id comparison\n if (options?.before?.configurable?.checkpoint_id) {\n const currentCheckpointId = jsonDoc.checkpoint_id;\n const beforeCheckpointId =\n options.before.configurable.checkpoint_id;\n\n // Skip checkpoints that are not before the specified checkpoint\n if (currentCheckpointId >= beforeCheckpointId) {\n continue;\n }\n }\n\n // Apply metadata filters manually\n let matches = true;\n if (options?.filter) {\n for (const [filterKey, filterValue] of Object.entries(\n options.filter\n )) {\n if (filterValue === null) {\n // Check if the field exists and is null in metadata\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n if (metadataValue !== null) {\n matches = false;\n break;\n }\n } else if (filterValue !== undefined) {\n // Check other metadata values\n const metadataValue = (jsonDoc.metadata as any)?.[filterKey];\n // For objects, do deep equality check with deterministic key ordering\n if (typeof filterValue === \"object\" && filterValue !== null) {\n if (\n deterministicStringify(metadataValue) !==\n deterministicStringify(filterValue)\n ) {\n matches = false;\n break;\n }\n } else if (metadataValue !== filterValue) {\n matches = false;\n break;\n }\n }\n }\n if (!matches) continue;\n }\n\n // Load checkpoint with pending writes and migrate sends\n const { checkpoint, pendingWrites } =\n await this.loadCheckpointWithWrites(jsonDoc);\n yield await this.createCheckpointTuple(\n jsonDoc,\n checkpoint,\n pendingWrites\n );\n yieldedCount++;\n }\n }\n\n return;\n }\n\n // Regular listing without filter - use search with empty filter instead\n // This ensures consistent behavior between filter={} and filter=undefined\n const searchOptions: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n } = {\n ...options,\n filter: {} as CheckpointMetadata,\n };\n\n // Delegate to the search path\n yield* this.list(config, searchOptions);\n return;\n }\n\n async putWrites(\n config: RunnableConfig,\n writes: PendingWrite[],\n taskId: string\n ): Promise<void> {\n await this.ensureIndexes();\n\n const threadId = config.configurable?.thread_id;\n const checkpointNs = config.configurable?.checkpoint_ns ?? \"\";\n const checkpointId = config.configurable?.checkpoint_id;\n\n if (!threadId || !checkpointId) {\n throw new Error(\"thread_id and checkpoint_id are required\");\n }\n\n // Collect write keys for sorted set tracking\n const writeKeys: string[] = [];\n\n // Use high-resolution timestamp to ensure unique ordering across putWrites calls\n const baseTimestamp = performance.now() * 1000; // Microsecond precision\n\n // Store each write as a separate indexed JSON document\n for (let idx = 0; idx < writes.length; idx++) {\n const [channel, value] = writes[idx];\n const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;\n writeKeys.push(writeKey);\n\n const writeDoc = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n task_id: taskId,\n idx: idx,\n channel: channel,\n type: typeof value === \"object\" ? \"json\" : \"string\",\n value: value,\n timestamp: baseTimestamp,\n global_idx: baseTimestamp + idx, // Add microseconds for sub-millisecond ordering\n };\n\n await this.client.json.set(writeKey, \"$\", writeDoc as any);\n }\n\n // Register write keys in sorted set for efficient retrieval\n if (writeKeys.length > 0) {\n const zsetKey = `${WRITE_KEYS_ZSET_PREFIX}:${threadId}:${checkpointNs}:${checkpointId}`;\n\n // Use timestamp + idx for scoring to maintain correct order\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = baseTimestamp + idx;\n });\n await this.client.zAdd(\n zsetKey,\n Object.entries(zaddArgs).map(([key, score]) => ({ score, value: key }))\n );\n\n // Apply TTL to write keys and zset if configured\n if (this.ttlConfig?.defaultTTL) {\n // Apply TTL to write keys and zset\n await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:${checkpointId}`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n // Get the current document and update it\n const currentDoc = (await this.client.json.get(\n checkpointKey\n )) as CheckpointDocument | null;\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc as any);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*`;\n // Use scan for better performance and cluster compatibility\n // Use keys for simplicity - scan would be better for large datasets\n const checkpointKeys = await (this.client as any).keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `writes:${threadId}:*`;\n // Use scan for better performance and cluster compatibility\n // Use keys for simplicity - scan would be better for large datasets\n const writesKeys = await (this.client as any).keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\n }\n\n // Helper method to load channel blobs (simplified - no blob support for now)\n // private async loadChannelBlobs(\n // checkpoint: Checkpoint & { channel_blobs?: any }\n // ): Promise<Checkpoint> {\n // // Since we're not using blobs anymore, just return the checkpoint as-is\n // return checkpoint;\n // }\n\n // Helper method to load pending writes\n private async loadPendingWrites(\n threadId: string,\n checkpointNs: string,\n checkpointId: string\n ): Promise<Array<[string, string, any]> | undefined> {\n // Search for all write documents for this checkpoint\n const pattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:*`;\n const writeKeys = await (this.client as any).keys(pattern);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const writeDocuments: any[] = [];\n for (const writeKey of writeKeys) {\n const writeDoc = (await this.client.json.get(writeKey)) as any;\n if (writeDoc) {\n writeDocuments.push(writeDoc);\n }\n }\n\n // Sort by global_idx (which represents insertion order across all putWrites calls)\n // This matches how SQLite would naturally order by insertion time + idx\n writeDocuments.sort((a, b) => (a.global_idx || 0) - (b.global_idx || 0));\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeDoc of writeDocuments) {\n // Deserialize write value using serde to restore LangChain objects\n const deserializedValue = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(writeDoc.value)\n );\n pendingWrites.push([\n writeDoc.task_id,\n writeDoc.channel,\n deserializedValue,\n ]);\n }\n\n return pendingWrites;\n }\n\n // Helper method to load checkpoint with pending writes\n private async loadCheckpointWithWrites(jsonDoc: any): Promise<{\n checkpoint: Checkpoint;\n pendingWrites?: Array<[string, string, any]>;\n }> {\n // Deserialize checkpoint using serde to restore LangChain objects\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n // Migrate pending sends ONLY for OLD checkpoint versions (v < 4) with parents\n // Modern checkpoints (v >= 4) should NEVER have pending sends migrated\n if (checkpoint.v < 4 && jsonDoc.parent_checkpoint_id != null) {\n // Convert back from \"__empty__\" to empty string for migration\n const actualNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\n await this.migratePendingSends(\n checkpoint,\n jsonDoc.thread_id,\n actualNs,\n jsonDoc.parent_checkpoint_id\n );\n }\n\n // Load this checkpoint's own pending writes (but don't migrate them)\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n // Convert back from \"__empty__\" to empty string for key lookup\n const actualNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n actualNs,\n jsonDoc.checkpoint_id\n );\n }\n\n return { checkpoint, pendingWrites };\n }\n\n // Migrate pending sends from parent checkpoint (matches SQLite implementation)\n private async migratePendingSends(\n checkpoint: Checkpoint,\n threadId: string,\n checkpointNs: string,\n parentCheckpointId: string\n ): Promise<void> {\n // Load pending writes from parent checkpoint that have TASKS channel\n const parentWrites = await this.loadPendingWrites(\n threadId,\n checkpointNs,\n parentCheckpointId\n );\n\n if (!parentWrites || parentWrites.length === 0) {\n return;\n }\n\n // Filter for TASKS channel writes only\n const taskWrites = parentWrites.filter(([, channel]) => channel === TASKS);\n\n if (taskWrites.length === 0) {\n return;\n }\n\n // Collect all task values in order\n const allTasks: any[] = [];\n for (const [, , value] of taskWrites) {\n allTasks.push(value);\n }\n\n // Add pending sends to checkpoint\n checkpoint.channel_values ??= {};\n checkpoint.channel_values[TASKS] = allTasks;\n\n // Add to versions (matches SQLite logic)\n checkpoint.channel_versions[TASKS] =\n Object.keys(checkpoint.channel_versions).length > 0\n ? maxChannelVersion(...Object.values(checkpoint.channel_versions))\n : 1;\n }\n\n // Helper method to create checkpoint tuple from json document\n private async createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): Promise<CheckpointTuple> {\n // Convert back from \"__empty__\" to empty string\n const checkpointNs =\n jsonDoc.checkpoint_ns === \"__empty__\" ? \"\" : jsonDoc.checkpoint_ns;\n\n // Deserialize metadata using serde\n const metadata = (await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.metadata)\n )) as CheckpointMetadata;\n\n return {\n config: {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: checkpointNs,\n checkpoint_id: jsonDoc.checkpoint_id,\n },\n },\n checkpoint,\n metadata,\n parentConfig: jsonDoc.parent_checkpoint_id\n ? {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: checkpointNs,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\n };\n }\n\n // Helper method to add searchable metadata fields\n private addSearchableMetadataFields(\n jsonDoc: any,\n metadata?: CheckpointMetadata\n ): void {\n if (!metadata) return;\n\n // Add common searchable fields at top level\n if (\"source\" in metadata) {\n jsonDoc.source = metadata.source;\n }\n if (\"step\" in metadata) {\n jsonDoc.step = metadata.step;\n }\n if (\"writes\" in metadata) {\n // Writes field needs to be JSON stringified for TAG search\n jsonDoc.writes =\n typeof metadata.writes === \"object\"\n ? JSON.stringify(metadata.writes)\n : metadata.writes;\n }\n if (\"score\" in metadata) {\n jsonDoc.score = metadata.score;\n }\n }\n\n // Helper method to apply TTL to keys\n private async applyTTL(...keys: string[]): Promise<void> {\n if (!this.ttlConfig?.defaultTTL) return;\n\n const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);\n const results = await Promise.allSettled(\n keys.map((key) => this.client.expire(key, ttlSeconds))\n );\n\n // Log any failures but don't throw - TTL is best effort\n for (let i = 0; i < results.length; i++) {\n if (results[i].status === \"rejected\") {\n console.warn(\n `Failed to set TTL for key ${keys[i]}:`,\n (results[i] as PromiseRejectedResult).reason\n );\n }\n }\n }\n\n private async ensureIndexes(): Promise<void> {\n for (const schema of SCHEMAS) {\n try {\n // Try to create the index\n await this.client.ft.create(schema.index, schema.schema as any, {\n ON: \"JSON\",\n PREFIX: schema.prefix,\n });\n } catch (error: any) {\n // Ignore if index already exists\n if (!error.message?.includes(\"Index already exists\")) {\n console.error(\n `Failed to create index ${schema.index}:`,\n error.message\n );\n }\n }\n }\n }\n}\n\n// Helper function for deterministic object comparison\nfunction deterministicStringify(obj: any): string {\n if (obj === null || typeof obj !== \"object\") {\n return JSON.stringify(obj);\n }\n if (Array.isArray(obj)) {\n return JSON.stringify(obj.map((item) => deterministicStringify(item)));\n }\n const sortedObj: Record<string, any> = {};\n const sortedKeys = Object.keys(obj).sort();\n for (const key of sortedKeys) {\n sortedObj[key] = obj[key];\n }\n return JSON.stringify(sortedObj, (_, value) => {\n if (value !== null && typeof value === \"object\" && !Array.isArray(value)) {\n const sorted: Record<string, any> = {};\n const keys = Object.keys(value).sort();\n for (const k of keys) {\n sorted[k] = value[k];\n }\n return sorted;\n }\n return value;\n });\n}\n"],"mappings":"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"}
|
package/dist/index.d.cts
CHANGED
|
@@ -3,11 +3,10 @@ import { RunnableConfig } from "@langchain/core/runnables";
|
|
|
3
3
|
import { createClient, createCluster } from "redis";
|
|
4
4
|
|
|
5
5
|
//#region src/index.d.ts
|
|
6
|
-
// Type for Redis client - supports both standalone and cluster
|
|
7
6
|
type RedisClientType = ReturnType<typeof createClient> | ReturnType<typeof createCluster>;
|
|
8
7
|
interface TTLConfig {
|
|
9
|
-
defaultTTL?: number;
|
|
10
|
-
refreshOnRead?: boolean;
|
|
8
|
+
defaultTTL?: number;
|
|
9
|
+
refreshOnRead?: boolean;
|
|
11
10
|
}
|
|
12
11
|
declare class RedisSaver extends BaseCheckpointSaver {
|
|
13
12
|
private client;
|
|
@@ -30,7 +29,6 @@ declare class RedisSaver extends BaseCheckpointSaver {
|
|
|
30
29
|
private loadCheckpointWithWrites;
|
|
31
30
|
private migratePendingSends;
|
|
32
31
|
private createCheckpointTuple;
|
|
33
|
-
// Helper method to add searchable metadata fields
|
|
34
32
|
private addSearchableMetadataFields;
|
|
35
33
|
private applyTTL;
|
|
36
34
|
private ensureIndexes;
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[
|
|
1
|
+
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;KAmBY,eAAA,GACR,UAAA,QAAkB,YAAA,IAClB,UAAA,QAAkB,aAAA;AAAA,UA4CL,SAAA;EACf,UAAA;EACA,aAAA;AAAA;AAAA,cAoBW,UAAA,SAAmB,mBAAA;EAAA,QACtB,MAAA;EAAA,QACA,SAAA;EAER,WAAA,CAAY,MAAA,EAAQ,eAAA,EAAiB,SAAA,GAAY,SAAA;EAAA,OAMpC,OAAA,CACX,GAAA,UACA,SAAA,GAAY,SAAA,GACX,OAAA,CAAQ,UAAA;EAAA,OAQE,WAAA,CACX,SAAA,EAAW,KAAA;IAAQ,GAAA;EAAA,IACnB,SAAA,GAAY,SAAA,GACX,OAAA,CAAQ,UAAA;EAQL,GAAA,CAAI,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,UAAA;EAKrC,QAAA,CAAS,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,eAAA;EAiD1C,GAAA,CACJ,MAAA,EAAQ,cAAA,EACR,UAAA,EAAY,UAAA,EACZ,QAAA,EAAU,kBAAA,EACV,WAAA,EAAa,eAAA,GACZ,OAAA,CAAQ,cAAA;EA2EJ,IAAA,CACL,MAAA,EAAQ,cAAA,SACR,OAAA,GAAU,qBAAA;IAA0B,MAAA,GAAS,kBAAA;EAAA,IAC5C,cAAA,CAAe,eAAA;EAoVZ,SAAA,CACJ,MAAA,EAAQ,cAAA,EACR,MAAA,EAAQ,YAAA,IACR,MAAA,WACC,OAAA;EA2EG,YAAA,CAAa,QAAA,WAAmB,OAAA;EAsBhC,GAAA,CAAA,GAAO,OAAA;EAAA,QAaC,iBAAA;EAAA,QA2CA,wBAAA;EAAA,QAyCA,mBAAA;EAAA,QA0CA,qBAAA;EAAA,QAuCN,2BAAA;EAAA,QA0BM,QAAA;EAAA,QAmBA,aAAA;AAAA"}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,11 +3,10 @@ import { createClient, createCluster } from "redis";
|
|
|
3
3
|
import { RunnableConfig } from "@langchain/core/runnables";
|
|
4
4
|
|
|
5
5
|
//#region src/index.d.ts
|
|
6
|
-
// Type for Redis client - supports both standalone and cluster
|
|
7
6
|
type RedisClientType = ReturnType<typeof createClient> | ReturnType<typeof createCluster>;
|
|
8
7
|
interface TTLConfig {
|
|
9
|
-
defaultTTL?: number;
|
|
10
|
-
refreshOnRead?: boolean;
|
|
8
|
+
defaultTTL?: number;
|
|
9
|
+
refreshOnRead?: boolean;
|
|
11
10
|
}
|
|
12
11
|
declare class RedisSaver extends BaseCheckpointSaver {
|
|
13
12
|
private client;
|
|
@@ -30,7 +29,6 @@ declare class RedisSaver extends BaseCheckpointSaver {
|
|
|
30
29
|
private loadCheckpointWithWrites;
|
|
31
30
|
private migratePendingSends;
|
|
32
31
|
private createCheckpointTuple;
|
|
33
|
-
// Helper method to add searchable metadata fields
|
|
34
32
|
private addSearchableMetadataFields;
|
|
35
33
|
private applyTTL;
|
|
36
34
|
private ensureIndexes;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;KAmBY,eAAA,GACR,UAAA,QAAkB,YAAA,IAClB,UAAA,QAAkB,aAAA;AAAA,UA4CL,SAAA;EACf,UAAA;EACA,aAAA;AAAA;AAAA,cAoBW,UAAA,SAAmB,mBAAA;EAAA,QACtB,MAAA;EAAA,QACA,SAAA;EAER,WAAA,CAAY,MAAA,EAAQ,eAAA,EAAiB,SAAA,GAAY,SAAA;EAAA,OAMpC,OAAA,CACX,GAAA,UACA,SAAA,GAAY,SAAA,GACX,OAAA,CAAQ,UAAA;EAAA,OAQE,WAAA,CACX,SAAA,EAAW,KAAA;IAAQ,GAAA;EAAA,IACnB,SAAA,GAAY,SAAA,GACX,OAAA,CAAQ,UAAA;EAQL,GAAA,CAAI,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,UAAA;EAKrC,QAAA,CAAS,MAAA,EAAQ,cAAA,GAAiB,OAAA,CAAQ,eAAA;EAiD1C,GAAA,CACJ,MAAA,EAAQ,cAAA,EACR,UAAA,EAAY,UAAA,EACZ,QAAA,EAAU,kBAAA,EACV,WAAA,EAAa,eAAA,GACZ,OAAA,CAAQ,cAAA;EA2EJ,IAAA,CACL,MAAA,EAAQ,cAAA,SACR,OAAA,GAAU,qBAAA;IAA0B,MAAA,GAAS,kBAAA;EAAA,IAC5C,cAAA,CAAe,eAAA;EAoVZ,SAAA,CACJ,MAAA,EAAQ,cAAA,EACR,MAAA,EAAQ,YAAA,IACR,MAAA,WACC,OAAA;EA2EG,YAAA,CAAa,QAAA,WAAmB,OAAA;EAsBhC,GAAA,CAAA,GAAO,OAAA;EAAA,QAaC,iBAAA;EAAA,QA2CA,wBAAA;EAAA,QAyCA,mBAAA;EAAA,QA0CA,qBAAA;EAAA,QAuCN,2BAAA;EAAA,QA0BM,QAAA;EAAA,QAmBA,aAAA;AAAA"}
|