@langchain/langgraph-checkpoint-redis 0.0.2 → 1.0.0
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/CHANGELOG.md +11 -0
- package/dist/_virtual/rolldown_runtime.cjs +25 -0
- package/dist/index.cjs +525 -699
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +41 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +36 -29
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +522 -693
- package/dist/index.js.map +1 -1
- package/dist/shallow.cjs +403 -536
- package/dist/shallow.cjs.map +1 -0
- package/dist/shallow.d.cts +46 -0
- package/dist/shallow.d.cts.map +1 -0
- package/dist/shallow.d.ts +32 -25
- package/dist/shallow.d.ts.map +1 -1
- package/dist/shallow.js +401 -531
- package/dist/shallow.js.map +1 -1
- package/dist/store.cjs +616 -811
- package/dist/store.cjs.map +1 -0
- package/dist/store.d.cts +138 -0
- package/dist/store.d.cts.map +1 -0
- package/dist/store.d.ts +120 -114
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +613 -807
- package/dist/store.js.map +1 -1
- package/package.json +36 -39
- package/index.cjs +0 -1
- package/index.d.cts +0 -1
- package/index.d.ts +0 -1
- package/index.js +0 -1
- package/shallow.cjs +0 -1
- package/shallow.d.cts +0 -1
- package/shallow.d.ts +0 -1
- package/shallow.js +0 -1
- package/store.cjs +0 -1
- package/store.d.cts +0 -1
- package/store.d.ts +0 -1
- package/store.js +0 -1
package/dist/shallow.js
CHANGED
|
@@ -1,536 +1,406 @@
|
|
|
1
|
-
import { BaseCheckpointSaver, uuid6
|
|
1
|
+
import { BaseCheckpointSaver, uuid6 } from "@langchain/langgraph-checkpoint";
|
|
2
2
|
import { createClient } from "redis";
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
//#region src/shallow.ts
|
|
4
5
|
function deterministicStringify(obj) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const keys = Object.keys(value).sort();
|
|
20
|
-
for (const k of keys) {
|
|
21
|
-
sorted[k] = value[k];
|
|
22
|
-
}
|
|
23
|
-
return sorted;
|
|
24
|
-
}
|
|
25
|
-
return value;
|
|
26
|
-
});
|
|
6
|
+
if (obj === null || typeof obj !== "object") return JSON.stringify(obj);
|
|
7
|
+
if (Array.isArray(obj)) return JSON.stringify(obj.map((item) => deterministicStringify(item)));
|
|
8
|
+
const sortedObj = {};
|
|
9
|
+
const sortedKeys = Object.keys(obj).sort();
|
|
10
|
+
for (const key of sortedKeys) sortedObj[key] = obj[key];
|
|
11
|
+
return JSON.stringify(sortedObj, (_, value) => {
|
|
12
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
13
|
+
const sorted = {};
|
|
14
|
+
const keys = Object.keys(value).sort();
|
|
15
|
+
for (const k of keys) sorted[k] = value[k];
|
|
16
|
+
return sorted;
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
});
|
|
27
20
|
}
|
|
28
|
-
const SCHEMAS = [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
21
|
+
const SCHEMAS = [{
|
|
22
|
+
index: "checkpoints",
|
|
23
|
+
prefix: "checkpoint:",
|
|
24
|
+
schema: {
|
|
25
|
+
"$.thread_id": {
|
|
26
|
+
type: "TAG",
|
|
27
|
+
AS: "thread_id"
|
|
28
|
+
},
|
|
29
|
+
"$.checkpoint_ns": {
|
|
30
|
+
type: "TAG",
|
|
31
|
+
AS: "checkpoint_ns"
|
|
32
|
+
},
|
|
33
|
+
"$.checkpoint_id": {
|
|
34
|
+
type: "TAG",
|
|
35
|
+
AS: "checkpoint_id"
|
|
36
|
+
},
|
|
37
|
+
"$.parent_checkpoint_id": {
|
|
38
|
+
type: "TAG",
|
|
39
|
+
AS: "parent_checkpoint_id"
|
|
40
|
+
},
|
|
41
|
+
"$.checkpoint_ts": {
|
|
42
|
+
type: "NUMERIC",
|
|
43
|
+
AS: "checkpoint_ts"
|
|
44
|
+
},
|
|
45
|
+
"$.has_writes": {
|
|
46
|
+
type: "TAG",
|
|
47
|
+
AS: "has_writes"
|
|
48
|
+
},
|
|
49
|
+
"$.source": {
|
|
50
|
+
type: "TAG",
|
|
51
|
+
AS: "source"
|
|
52
|
+
},
|
|
53
|
+
"$.step": {
|
|
54
|
+
type: "NUMERIC",
|
|
55
|
+
AS: "step"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}, {
|
|
59
|
+
index: "checkpoint_writes",
|
|
60
|
+
prefix: "checkpoint_write:",
|
|
61
|
+
schema: {
|
|
62
|
+
"$.thread_id": {
|
|
63
|
+
type: "TAG",
|
|
64
|
+
AS: "thread_id"
|
|
65
|
+
},
|
|
66
|
+
"$.checkpoint_ns": {
|
|
67
|
+
type: "TAG",
|
|
68
|
+
AS: "checkpoint_ns"
|
|
69
|
+
},
|
|
70
|
+
"$.checkpoint_id": {
|
|
71
|
+
type: "TAG",
|
|
72
|
+
AS: "checkpoint_id"
|
|
73
|
+
},
|
|
74
|
+
"$.task_id": {
|
|
75
|
+
type: "TAG",
|
|
76
|
+
AS: "task_id"
|
|
77
|
+
},
|
|
78
|
+
"$.idx": {
|
|
79
|
+
type: "NUMERIC",
|
|
80
|
+
AS: "idx"
|
|
81
|
+
},
|
|
82
|
+
"$.channel": {
|
|
83
|
+
type: "TAG",
|
|
84
|
+
AS: "channel"
|
|
85
|
+
},
|
|
86
|
+
"$.type": {
|
|
87
|
+
type: "TAG",
|
|
88
|
+
AS: "type"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}];
|
|
57
92
|
/**
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
// Delete write registries
|
|
373
|
-
const zsetPattern = `write_keys_zset:${threadId}:*`;
|
|
374
|
-
const zsetKeys = await this.client.keys(zsetPattern);
|
|
375
|
-
if (zsetKeys.length > 0) {
|
|
376
|
-
await this.client.del(zsetKeys);
|
|
377
|
-
}
|
|
378
|
-
}
|
|
379
|
-
async end() {
|
|
380
|
-
await this.client.quit();
|
|
381
|
-
}
|
|
382
|
-
// Helper method to add searchable metadata fields
|
|
383
|
-
addSearchableMetadataFields(jsonDoc, metadata) {
|
|
384
|
-
if (!metadata)
|
|
385
|
-
return;
|
|
386
|
-
// Add common searchable fields at top level
|
|
387
|
-
if ("source" in metadata) {
|
|
388
|
-
jsonDoc.source = metadata.source;
|
|
389
|
-
}
|
|
390
|
-
if ("step" in metadata) {
|
|
391
|
-
jsonDoc.step = metadata.step;
|
|
392
|
-
}
|
|
393
|
-
if ("writes" in metadata) {
|
|
394
|
-
// Writes field needs to be JSON stringified for TAG search
|
|
395
|
-
jsonDoc.writes =
|
|
396
|
-
typeof metadata.writes === "object"
|
|
397
|
-
? JSON.stringify(metadata.writes)
|
|
398
|
-
: metadata.writes;
|
|
399
|
-
}
|
|
400
|
-
if ("score" in metadata) {
|
|
401
|
-
jsonDoc.score = metadata.score;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
// Helper method to create checkpoint tuple from json document
|
|
405
|
-
createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
|
|
406
|
-
return {
|
|
407
|
-
config: {
|
|
408
|
-
configurable: {
|
|
409
|
-
thread_id: jsonDoc.thread_id,
|
|
410
|
-
checkpoint_ns: jsonDoc.checkpoint_ns,
|
|
411
|
-
checkpoint_id: jsonDoc.checkpoint_id,
|
|
412
|
-
},
|
|
413
|
-
},
|
|
414
|
-
checkpoint,
|
|
415
|
-
metadata: jsonDoc.metadata,
|
|
416
|
-
parentConfig: jsonDoc.parent_checkpoint_id
|
|
417
|
-
? {
|
|
418
|
-
configurable: {
|
|
419
|
-
thread_id: jsonDoc.thread_id,
|
|
420
|
-
checkpoint_ns: jsonDoc.checkpoint_ns,
|
|
421
|
-
checkpoint_id: jsonDoc.parent_checkpoint_id,
|
|
422
|
-
},
|
|
423
|
-
}
|
|
424
|
-
: undefined,
|
|
425
|
-
pendingWrites,
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
// Helper method to apply TTL to keys
|
|
429
|
-
async applyTTL(...keys) {
|
|
430
|
-
if (!this.ttlConfig?.defaultTTL)
|
|
431
|
-
return;
|
|
432
|
-
const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);
|
|
433
|
-
const results = await Promise.allSettled(keys.map((key) => this.client.expire(key, ttlSeconds)));
|
|
434
|
-
// Log any failures but don't throw - TTL is best effort
|
|
435
|
-
for (let i = 0; i < results.length; i++) {
|
|
436
|
-
if (results[i].status === "rejected") {
|
|
437
|
-
console.warn(`Failed to set TTL for key ${keys[i]}:`, results[i].reason);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
}
|
|
441
|
-
// Helper method to load pending writes
|
|
442
|
-
async loadPendingWrites(threadId, checkpointNs, checkpointId) {
|
|
443
|
-
const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;
|
|
444
|
-
const writeKeys = await this.client.zRange(zsetKey, 0, -1);
|
|
445
|
-
if (writeKeys.length === 0) {
|
|
446
|
-
return undefined;
|
|
447
|
-
}
|
|
448
|
-
const pendingWrites = [];
|
|
449
|
-
for (const writeKey of writeKeys) {
|
|
450
|
-
const writeDoc = await this.client.json.get(writeKey);
|
|
451
|
-
if (writeDoc) {
|
|
452
|
-
pendingWrites.push([
|
|
453
|
-
writeDoc.task_id,
|
|
454
|
-
writeDoc.channel,
|
|
455
|
-
writeDoc.value,
|
|
456
|
-
]);
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
return pendingWrites;
|
|
460
|
-
}
|
|
461
|
-
// Helper method to check metadata filter matches
|
|
462
|
-
checkMetadataFilterMatch(metadata, filter) {
|
|
463
|
-
for (const [key, value] of Object.entries(filter)) {
|
|
464
|
-
const metadataValue = metadata?.[key];
|
|
465
|
-
if (value === null) {
|
|
466
|
-
if (!(key in (metadata || {})) || metadataValue !== null) {
|
|
467
|
-
return false;
|
|
468
|
-
}
|
|
469
|
-
}
|
|
470
|
-
else if (typeof value === "object" && !Array.isArray(value)) {
|
|
471
|
-
// Deep comparison for objects with deterministic key ordering
|
|
472
|
-
if (typeof metadataValue !== "object" || metadataValue === null) {
|
|
473
|
-
return false;
|
|
474
|
-
}
|
|
475
|
-
if (deterministicStringify(value) !==
|
|
476
|
-
deterministicStringify(metadataValue)) {
|
|
477
|
-
return false;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
else if (metadataValue !== value) {
|
|
481
|
-
return false;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
return true;
|
|
485
|
-
}
|
|
486
|
-
async cleanupOldCheckpoint(threadId, checkpointNs, oldCheckpointId) {
|
|
487
|
-
// Clean up old writes
|
|
488
|
-
const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;
|
|
489
|
-
const oldWriteKeys = await this.client.keys(writePattern);
|
|
490
|
-
if (oldWriteKeys.length > 0) {
|
|
491
|
-
await this.client.del(oldWriteKeys);
|
|
492
|
-
}
|
|
493
|
-
// Clean up write registry
|
|
494
|
-
const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${oldCheckpointId}`;
|
|
495
|
-
await this.client.del(zsetKey);
|
|
496
|
-
// Note: We don't clean up blob keys in shallow mode since we store inline
|
|
497
|
-
// But for completeness, clean up any legacy blob keys if they exist
|
|
498
|
-
const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;
|
|
499
|
-
const oldBlobKeys = await this.client.keys(blobPattern);
|
|
500
|
-
if (oldBlobKeys.length > 0) {
|
|
501
|
-
await this.client.del(oldBlobKeys);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
504
|
-
sanitizeMetadata(metadata) {
|
|
505
|
-
if (!metadata)
|
|
506
|
-
return {};
|
|
507
|
-
const sanitized = {};
|
|
508
|
-
for (const [key, value] of Object.entries(metadata)) {
|
|
509
|
-
// Remove null characters from keys and string values
|
|
510
|
-
// eslint-disable-next-line no-control-regex
|
|
511
|
-
const sanitizedKey = key.replace(/\x00/g, "");
|
|
512
|
-
sanitized[sanitizedKey] =
|
|
513
|
-
// eslint-disable-next-line no-control-regex
|
|
514
|
-
typeof value === "string" ? value.replace(/\x00/g, "") : value;
|
|
515
|
-
}
|
|
516
|
-
return sanitized;
|
|
517
|
-
}
|
|
518
|
-
async ensureIndexes() {
|
|
519
|
-
for (const schema of SCHEMAS) {
|
|
520
|
-
try {
|
|
521
|
-
// Try to create the index
|
|
522
|
-
await this.client.ft.create(schema.index, schema.schema, {
|
|
523
|
-
ON: "JSON",
|
|
524
|
-
PREFIX: schema.prefix,
|
|
525
|
-
});
|
|
526
|
-
}
|
|
527
|
-
catch (error) {
|
|
528
|
-
// Ignore if index already exists
|
|
529
|
-
if (!error.message?.includes("Index already exists")) {
|
|
530
|
-
console.error(`Failed to create index ${schema.index}:`, error.message);
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
}
|
|
93
|
+
* ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.
|
|
94
|
+
*
|
|
95
|
+
* This is a memory-optimized variant that:
|
|
96
|
+
* - Only stores the most recent checkpoint for each thread
|
|
97
|
+
* - Stores channel values inline (no separate blob storage)
|
|
98
|
+
* - Automatically cleans up old checkpoints and writes when new ones are added
|
|
99
|
+
* - Reduces storage usage for applications that don't need checkpoint history
|
|
100
|
+
*/
|
|
101
|
+
var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
|
|
102
|
+
client;
|
|
103
|
+
ttlConfig;
|
|
104
|
+
constructor(client, ttlConfig) {
|
|
105
|
+
super();
|
|
106
|
+
this.client = client;
|
|
107
|
+
this.ttlConfig = ttlConfig;
|
|
108
|
+
}
|
|
109
|
+
static async fromUrl(url, ttlConfig) {
|
|
110
|
+
const client = createClient({ url });
|
|
111
|
+
await client.connect();
|
|
112
|
+
const saver = new ShallowRedisSaver(client, ttlConfig);
|
|
113
|
+
await saver.ensureIndexes();
|
|
114
|
+
return saver;
|
|
115
|
+
}
|
|
116
|
+
async get(config) {
|
|
117
|
+
const tuple = await this.getTuple(config);
|
|
118
|
+
return tuple?.checkpoint;
|
|
119
|
+
}
|
|
120
|
+
async put(config, checkpoint, metadata, _newVersions) {
|
|
121
|
+
await this.ensureIndexes();
|
|
122
|
+
const threadId = config.configurable?.thread_id;
|
|
123
|
+
const checkpointNs = config.configurable?.checkpoint_ns ?? "";
|
|
124
|
+
const parentCheckpointId = config.configurable?.checkpoint_id;
|
|
125
|
+
if (!threadId) throw new Error("thread_id is required");
|
|
126
|
+
const checkpointId = checkpoint.id || uuid6(0);
|
|
127
|
+
const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;
|
|
128
|
+
let prevCheckpointData = null;
|
|
129
|
+
let prevCheckpointId = null;
|
|
130
|
+
try {
|
|
131
|
+
prevCheckpointData = await this.client.json.get(key);
|
|
132
|
+
if (prevCheckpointData && typeof prevCheckpointData === "object") prevCheckpointId = prevCheckpointData.checkpoint_id;
|
|
133
|
+
} catch (error) {}
|
|
134
|
+
if (prevCheckpointId && prevCheckpointId !== checkpointId) await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);
|
|
135
|
+
const checkpointCopy = {
|
|
136
|
+
...checkpoint,
|
|
137
|
+
channel_values: checkpoint.channel_values || {},
|
|
138
|
+
channel_blobs: void 0
|
|
139
|
+
};
|
|
140
|
+
const jsonDoc = {
|
|
141
|
+
thread_id: threadId,
|
|
142
|
+
checkpoint_ns: checkpointNs,
|
|
143
|
+
checkpoint_id: checkpointId,
|
|
144
|
+
parent_checkpoint_id: parentCheckpointId || null,
|
|
145
|
+
checkpoint: checkpointCopy,
|
|
146
|
+
metadata: this.sanitizeMetadata(metadata),
|
|
147
|
+
checkpoint_ts: Date.now(),
|
|
148
|
+
has_writes: "false"
|
|
149
|
+
};
|
|
150
|
+
this.addSearchableMetadataFields(jsonDoc, metadata);
|
|
151
|
+
await this.client.json.set(key, "$", jsonDoc);
|
|
152
|
+
if (this.ttlConfig?.defaultTTL) await this.applyTTL(key);
|
|
153
|
+
return { configurable: {
|
|
154
|
+
thread_id: threadId,
|
|
155
|
+
checkpoint_ns: checkpointNs,
|
|
156
|
+
checkpoint_id: checkpointId
|
|
157
|
+
} };
|
|
158
|
+
}
|
|
159
|
+
async getTuple(config) {
|
|
160
|
+
const threadId = config.configurable?.thread_id;
|
|
161
|
+
const checkpointNs = config.configurable?.checkpoint_ns ?? "";
|
|
162
|
+
const checkpointId = config.configurable?.checkpoint_id;
|
|
163
|
+
if (!threadId) return void 0;
|
|
164
|
+
const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;
|
|
165
|
+
const jsonDoc = await this.client.json.get(key);
|
|
166
|
+
if (!jsonDoc) return void 0;
|
|
167
|
+
if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) return void 0;
|
|
168
|
+
if (this.ttlConfig?.refreshOnRead && this.ttlConfig?.defaultTTL) await this.applyTTL(key);
|
|
169
|
+
const checkpoint = {
|
|
170
|
+
...jsonDoc.checkpoint,
|
|
171
|
+
channel_values: jsonDoc.checkpoint.channel_values || {}
|
|
172
|
+
};
|
|
173
|
+
let pendingWrites;
|
|
174
|
+
if (jsonDoc.has_writes === "true") pendingWrites = await this.loadPendingWrites(jsonDoc.thread_id, jsonDoc.checkpoint_ns, jsonDoc.checkpoint_id);
|
|
175
|
+
return this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
|
|
176
|
+
}
|
|
177
|
+
async *list(config, options) {
|
|
178
|
+
await this.ensureIndexes();
|
|
179
|
+
if (config?.configurable?.thread_id) {
|
|
180
|
+
const tuple = await this.getTuple(config);
|
|
181
|
+
if (tuple) if (options?.filter) {
|
|
182
|
+
if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) yield tuple;
|
|
183
|
+
} else yield tuple;
|
|
184
|
+
} else {
|
|
185
|
+
const queryParts = [];
|
|
186
|
+
if (options?.filter) {
|
|
187
|
+
for (const [key, value] of Object.entries(options.filter)) if (value === void 0) {} else if (value === null) {} else if (typeof value === "string") queryParts.push(`(@${key}:{${value}})`);
|
|
188
|
+
else if (typeof value === "number") queryParts.push(`(@${key}:[${value} ${value}])`);
|
|
189
|
+
}
|
|
190
|
+
if (queryParts.length === 0) queryParts.push("*");
|
|
191
|
+
const query = queryParts.join(" ");
|
|
192
|
+
const limit = options?.limit ?? 10;
|
|
193
|
+
try {
|
|
194
|
+
const results = await this.client.ft.search("checkpoints", query, {
|
|
195
|
+
LIMIT: {
|
|
196
|
+
from: 0,
|
|
197
|
+
size: limit * 2
|
|
198
|
+
},
|
|
199
|
+
SORTBY: {
|
|
200
|
+
BY: "checkpoint_ts",
|
|
201
|
+
DIRECTION: "DESC"
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
const seenThreads = /* @__PURE__ */ new Set();
|
|
205
|
+
let yieldCount = 0;
|
|
206
|
+
for (const doc of results.documents) {
|
|
207
|
+
if (yieldCount >= limit) break;
|
|
208
|
+
const jsonDoc = doc.value;
|
|
209
|
+
const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;
|
|
210
|
+
if (seenThreads.has(threadKey)) continue;
|
|
211
|
+
seenThreads.add(threadKey);
|
|
212
|
+
if (options?.filter) {
|
|
213
|
+
if (!this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)) continue;
|
|
214
|
+
}
|
|
215
|
+
const checkpoint = {
|
|
216
|
+
...jsonDoc.checkpoint,
|
|
217
|
+
channel_values: jsonDoc.checkpoint.channel_values || {}
|
|
218
|
+
};
|
|
219
|
+
yield this.createCheckpointTuple(jsonDoc, checkpoint);
|
|
220
|
+
yieldCount++;
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
if (error.message?.includes("no such index")) {
|
|
224
|
+
const pattern = `checkpoint:*:*:shallow`;
|
|
225
|
+
const keys = await this.client.keys(pattern);
|
|
226
|
+
if (keys.length === 0) return;
|
|
227
|
+
keys.sort().reverse();
|
|
228
|
+
const seenThreads = /* @__PURE__ */ new Set();
|
|
229
|
+
let yieldCount = 0;
|
|
230
|
+
const limit$1 = options?.limit ?? 10;
|
|
231
|
+
for (const key of keys) {
|
|
232
|
+
if (yieldCount >= limit$1) break;
|
|
233
|
+
const jsonDoc = await this.client.json.get(key);
|
|
234
|
+
if (!jsonDoc) continue;
|
|
235
|
+
const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;
|
|
236
|
+
if (seenThreads.has(threadKey)) continue;
|
|
237
|
+
seenThreads.add(threadKey);
|
|
238
|
+
if (options?.filter) {
|
|
239
|
+
if (!this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)) continue;
|
|
240
|
+
}
|
|
241
|
+
const checkpoint = {
|
|
242
|
+
...jsonDoc.checkpoint,
|
|
243
|
+
channel_values: jsonDoc.checkpoint.channel_values || {}
|
|
244
|
+
};
|
|
245
|
+
yield this.createCheckpointTuple(jsonDoc, checkpoint);
|
|
246
|
+
yieldCount++;
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
throw error;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async putWrites(config, writes, taskId) {
|
|
255
|
+
await this.ensureIndexes();
|
|
256
|
+
const threadId = config.configurable?.thread_id;
|
|
257
|
+
const checkpointNs = config.configurable?.checkpoint_ns ?? "";
|
|
258
|
+
const checkpointId = config.configurable?.checkpoint_id;
|
|
259
|
+
if (!threadId || !checkpointId) throw new Error("thread_id and checkpoint_id are required");
|
|
260
|
+
const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;
|
|
261
|
+
const oldWriteKeys = await this.client.keys(writePattern);
|
|
262
|
+
if (oldWriteKeys.length > 0) await this.client.del(oldWriteKeys);
|
|
263
|
+
const writeKeys = [];
|
|
264
|
+
for (let idx = 0; idx < writes.length; idx++) {
|
|
265
|
+
const [channel, value] = writes[idx];
|
|
266
|
+
const writeKey = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:${idx}`;
|
|
267
|
+
writeKeys.push(writeKey);
|
|
268
|
+
const writeDoc = {
|
|
269
|
+
thread_id: threadId,
|
|
270
|
+
checkpoint_ns: checkpointNs,
|
|
271
|
+
checkpoint_id: checkpointId,
|
|
272
|
+
task_id: taskId,
|
|
273
|
+
idx,
|
|
274
|
+
channel,
|
|
275
|
+
type: typeof value === "object" ? "json" : "string",
|
|
276
|
+
value
|
|
277
|
+
};
|
|
278
|
+
await this.client.json.set(writeKey, "$", writeDoc);
|
|
279
|
+
}
|
|
280
|
+
if (writeKeys.length > 0) {
|
|
281
|
+
const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;
|
|
282
|
+
const zaddArgs = {};
|
|
283
|
+
writeKeys.forEach((key, idx) => {
|
|
284
|
+
zaddArgs[key] = idx;
|
|
285
|
+
});
|
|
286
|
+
await this.client.zAdd(zsetKey, Object.entries(zaddArgs).map(([key, score]) => ({
|
|
287
|
+
score,
|
|
288
|
+
value: key
|
|
289
|
+
})));
|
|
290
|
+
if (this.ttlConfig?.defaultTTL) await this.applyTTL(...writeKeys, zsetKey);
|
|
291
|
+
}
|
|
292
|
+
const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;
|
|
293
|
+
const checkpointExists = await this.client.exists(checkpointKey);
|
|
294
|
+
if (checkpointExists) {
|
|
295
|
+
const currentDoc = await this.client.json.get(checkpointKey);
|
|
296
|
+
if (currentDoc) {
|
|
297
|
+
currentDoc.has_writes = "true";
|
|
298
|
+
await this.client.json.set(checkpointKey, "$", currentDoc);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
async deleteThread(threadId) {
|
|
303
|
+
const checkpointPattern = `checkpoint:${threadId}:*:shallow`;
|
|
304
|
+
const checkpointKeys = await this.client.keys(checkpointPattern);
|
|
305
|
+
if (checkpointKeys.length > 0) await this.client.del(checkpointKeys);
|
|
306
|
+
const writesPattern = `checkpoint_write:${threadId}:*`;
|
|
307
|
+
const writesKeys = await this.client.keys(writesPattern);
|
|
308
|
+
if (writesKeys.length > 0) await this.client.del(writesKeys);
|
|
309
|
+
const zsetPattern = `write_keys_zset:${threadId}:*`;
|
|
310
|
+
const zsetKeys = await this.client.keys(zsetPattern);
|
|
311
|
+
if (zsetKeys.length > 0) await this.client.del(zsetKeys);
|
|
312
|
+
}
|
|
313
|
+
async end() {
|
|
314
|
+
await this.client.quit();
|
|
315
|
+
}
|
|
316
|
+
addSearchableMetadataFields(jsonDoc, metadata) {
|
|
317
|
+
if (!metadata) return;
|
|
318
|
+
if ("source" in metadata) jsonDoc.source = metadata.source;
|
|
319
|
+
if ("step" in metadata) jsonDoc.step = metadata.step;
|
|
320
|
+
if ("writes" in metadata) jsonDoc.writes = typeof metadata.writes === "object" ? JSON.stringify(metadata.writes) : metadata.writes;
|
|
321
|
+
if ("score" in metadata) jsonDoc.score = metadata.score;
|
|
322
|
+
}
|
|
323
|
+
createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
|
|
324
|
+
return {
|
|
325
|
+
config: { configurable: {
|
|
326
|
+
thread_id: jsonDoc.thread_id,
|
|
327
|
+
checkpoint_ns: jsonDoc.checkpoint_ns,
|
|
328
|
+
checkpoint_id: jsonDoc.checkpoint_id
|
|
329
|
+
} },
|
|
330
|
+
checkpoint,
|
|
331
|
+
metadata: jsonDoc.metadata,
|
|
332
|
+
parentConfig: jsonDoc.parent_checkpoint_id ? { configurable: {
|
|
333
|
+
thread_id: jsonDoc.thread_id,
|
|
334
|
+
checkpoint_ns: jsonDoc.checkpoint_ns,
|
|
335
|
+
checkpoint_id: jsonDoc.parent_checkpoint_id
|
|
336
|
+
} } : void 0,
|
|
337
|
+
pendingWrites
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
async applyTTL(...keys) {
|
|
341
|
+
if (!this.ttlConfig?.defaultTTL) return;
|
|
342
|
+
const ttlSeconds = Math.floor(this.ttlConfig.defaultTTL * 60);
|
|
343
|
+
const results = await Promise.allSettled(keys.map((key) => this.client.expire(key, ttlSeconds)));
|
|
344
|
+
for (let i = 0; i < results.length; i++) if (results[i].status === "rejected") console.warn(`Failed to set TTL for key ${keys[i]}:`, results[i].reason);
|
|
345
|
+
}
|
|
346
|
+
async loadPendingWrites(threadId, checkpointNs, checkpointId) {
|
|
347
|
+
const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;
|
|
348
|
+
const writeKeys = await this.client.zRange(zsetKey, 0, -1);
|
|
349
|
+
if (writeKeys.length === 0) return void 0;
|
|
350
|
+
const pendingWrites = [];
|
|
351
|
+
for (const writeKey of writeKeys) {
|
|
352
|
+
const writeDoc = await this.client.json.get(writeKey);
|
|
353
|
+
if (writeDoc) pendingWrites.push([
|
|
354
|
+
writeDoc.task_id,
|
|
355
|
+
writeDoc.channel,
|
|
356
|
+
writeDoc.value
|
|
357
|
+
]);
|
|
358
|
+
}
|
|
359
|
+
return pendingWrites;
|
|
360
|
+
}
|
|
361
|
+
checkMetadataFilterMatch(metadata, filter) {
|
|
362
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
363
|
+
const metadataValue = metadata?.[key];
|
|
364
|
+
if (value === null) {
|
|
365
|
+
if (!(key in (metadata || {})) || metadataValue !== null) return false;
|
|
366
|
+
} else if (typeof value === "object" && !Array.isArray(value)) {
|
|
367
|
+
if (typeof metadataValue !== "object" || metadataValue === null) return false;
|
|
368
|
+
if (deterministicStringify(value) !== deterministicStringify(metadataValue)) return false;
|
|
369
|
+
} else if (metadataValue !== value) return false;
|
|
370
|
+
}
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
async cleanupOldCheckpoint(threadId, checkpointNs, oldCheckpointId) {
|
|
374
|
+
const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;
|
|
375
|
+
const oldWriteKeys = await this.client.keys(writePattern);
|
|
376
|
+
if (oldWriteKeys.length > 0) await this.client.del(oldWriteKeys);
|
|
377
|
+
const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${oldCheckpointId}`;
|
|
378
|
+
await this.client.del(zsetKey);
|
|
379
|
+
const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;
|
|
380
|
+
const oldBlobKeys = await this.client.keys(blobPattern);
|
|
381
|
+
if (oldBlobKeys.length > 0) await this.client.del(oldBlobKeys);
|
|
382
|
+
}
|
|
383
|
+
sanitizeMetadata(metadata) {
|
|
384
|
+
if (!metadata) return {};
|
|
385
|
+
const sanitized = {};
|
|
386
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
387
|
+
const sanitizedKey = key.replace(/\x00/g, "");
|
|
388
|
+
sanitized[sanitizedKey] = typeof value === "string" ? value.replace(/\x00/g, "") : value;
|
|
389
|
+
}
|
|
390
|
+
return sanitized;
|
|
391
|
+
}
|
|
392
|
+
async ensureIndexes() {
|
|
393
|
+
for (const schema of SCHEMAS) try {
|
|
394
|
+
await this.client.ft.create(schema.index, schema.schema, {
|
|
395
|
+
ON: "JSON",
|
|
396
|
+
PREFIX: schema.prefix
|
|
397
|
+
});
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (!error.message?.includes("Index already exists")) console.error(`Failed to create index ${schema.index}:`, error.message);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
};
|
|
403
|
+
|
|
404
|
+
//#endregion
|
|
405
|
+
export { ShallowRedisSaver };
|
|
536
406
|
//# sourceMappingURL=shallow.js.map
|