@langchain/langgraph-checkpoint-redis 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/index.cjs +51 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -5
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +2 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -43
- package/dist/index.js.map +1 -1
- package/dist/shallow.cjs +40 -41
- package/dist/shallow.cjs.map +1 -1
- package/dist/shallow.d.cts +2 -5
- package/dist/shallow.d.cts.map +1 -1
- package/dist/shallow.d.ts +2 -5
- package/dist/shallow.d.ts.map +1 -1
- package/dist/shallow.js +35 -36
- 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 -18
- package/dist/_virtual/rolldown_runtime.cjs +0 -25
package/dist/shallow.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shallow.cjs","names":["sortedObj: Record<string, any>","sorted: Record<string, any>","BaseCheckpointSaver","prevCheckpointData: any","prevCheckpointId: string | null","jsonDoc: any","pendingWrites: Array<[string, string, any]> | undefined","queryParts: string[]","error: any","limit","writeKeys: string[]","zaddArgs: Record<string, number>","pendingWrites: Array<[string, string, any]>","sanitized: any"],"sources":["../src/shallow.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient } from \"redis\";\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\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\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_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\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport class ShallowRedisSaver extends BaseCheckpointSaver {\n private client: any;\n private ttlConfig?: TTLConfig;\n\n constructor(client: any, 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<ShallowRedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new ShallowRedisSaver(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 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\n // In shallow mode, we use a single key per thread (no checkpoint_id in key)\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n\n // Get the previous checkpoint to know what to clean up\n let prevCheckpointData: any = null;\n let prevCheckpointId: string | null = null;\n try {\n prevCheckpointData = await this.client.json.get(key);\n if (prevCheckpointData && typeof prevCheckpointData === \"object\") {\n prevCheckpointId = prevCheckpointData.checkpoint_id;\n }\n } catch (error) {\n // Key doesn't exist yet, that's fine\n }\n\n // Clean up old checkpoint and related data if it exists\n if (prevCheckpointId && prevCheckpointId !== checkpointId) {\n await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);\n }\n\n // Store channel values inline - no blob storage in shallow mode\n const checkpointCopy = {\n ...checkpoint,\n channel_values: checkpoint.channel_values || {},\n // Remove channel_blobs if present\n channel_blobs: undefined,\n };\n\n // Structure matching Python implementation\n const jsonDoc: any = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: checkpointCopy,\n metadata: this.sanitizeMetadata(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);\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 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 // In shallow mode, we use a single key per thread\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const jsonDoc = await this.client.json.get(key);\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // If a specific checkpoint_id was requested, check if it matches\n if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) {\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 // Channel values are stored inline in shallow mode\n const checkpoint = {\n ...jsonDoc.checkpoint,\n channel_values: jsonDoc.checkpoint.channel_values || {},\n };\n\n // Load pending writes if they exist\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n jsonDoc.checkpoint_ns,\n jsonDoc.checkpoint_id\n );\n }\n\n return this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // In shallow mode, we only return the latest checkpoint per thread\n if (config?.configurable?.thread_id) {\n // Single thread case\n const tuple = await this.getTuple(config);\n if (tuple) {\n // Apply filter if provided\n if (options?.filter) {\n if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) {\n yield tuple;\n }\n } else {\n yield tuple;\n }\n }\n } else {\n // All threads case - use search\n const queryParts: string[] = [];\n\n // Add metadata filters\n if (options?.filter) {\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 queryParts.push(`(@${key}:{${value}})`);\n } else if (typeof value === \"number\") {\n queryParts.push(`(@${key}:[${value} ${value}])`);\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 const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: limit * 2 }, // Get more since we'll deduplicate\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n // In shallow mode, deduplicate by thread_id\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n\n for (const doc of results.documents) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = doc.value;\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check null filters manually if needed\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint = {\n ...jsonDoc.checkpoint,\n channel_values: jsonDoc.checkpoint.channel_values || {},\n };\n\n yield this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall back to scanning all shallow checkpoints\n const pattern = `checkpoint:*:*:shallow`;\n const keys = await this.client.keys(pattern);\n\n if (keys.length === 0) {\n return;\n }\n\n // Sort keys to have consistent ordering\n keys.sort().reverse();\n\n // Get unique threads\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const key of keys) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = await this.client.json.get(key);\n if (!jsonDoc) continue;\n\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check filter if provided\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint = {\n ...jsonDoc.checkpoint,\n channel_values: jsonDoc.checkpoint.channel_values || {},\n };\n\n yield this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n return;\n }\n throw error;\n }\n }\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 // In shallow mode, we overwrite all writes for the task\n // First, clean up old writes for this task\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Store new writes\n const writeKeys: string[] = [];\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 };\n\n await this.client.json.set(writeKey, \"$\", writeDoc);\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 // Clear existing entries for this task and add new ones\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = 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 await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n const currentDoc = await this.client.json.get(checkpointKey);\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete shallow checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*:shallow`;\n const checkpointKeys = await this.client.keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `checkpoint_write:${threadId}:*`;\n const writesKeys = await this.client.keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n\n // Delete write registries\n const zsetPattern = `write_keys_zset:${threadId}:*`;\n const zsetKeys = await this.client.keys(zsetPattern);\n\n if (zsetKeys.length > 0) {\n await this.client.del(zsetKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\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 create checkpoint tuple from json document\n private createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): CheckpointTuple {\n return {\n config: {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.checkpoint_id,\n },\n },\n checkpoint,\n metadata: jsonDoc.metadata,\n parentConfig: jsonDoc.parent_checkpoint_id\n ? {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\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 // 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 const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n const writeKeys = await this.client.zRange(zsetKey, 0, -1);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeKey of writeKeys) {\n const writeDoc = await this.client.json.get(writeKey);\n if (writeDoc) {\n pendingWrites.push([\n writeDoc.task_id,\n writeDoc.channel,\n writeDoc.value,\n ]);\n }\n }\n\n return pendingWrites;\n }\n\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch(\n metadata: any,\n filter: CheckpointMetadata\n ): boolean {\n for (const [key, value] of Object.entries(filter)) {\n const metadataValue = metadata?.[key];\n if (value === null) {\n if (!(key in (metadata || {})) || metadataValue !== null) {\n return false;\n }\n } else if (typeof value === \"object\" && !Array.isArray(value)) {\n // Deep comparison for objects with deterministic key ordering\n if (typeof metadataValue !== \"object\" || metadataValue === null) {\n return false;\n }\n if (\n deterministicStringify(value) !==\n deterministicStringify(metadataValue)\n ) {\n return false;\n }\n } else if (metadataValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n private async cleanupOldCheckpoint(\n threadId: string,\n checkpointNs: string,\n oldCheckpointId: string\n ): Promise<void> {\n // Clean up old writes\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Clean up write registry\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${oldCheckpointId}`;\n await this.client.del(zsetKey);\n\n // Note: We don't clean up blob keys in shallow mode since we store inline\n // But for completeness, clean up any legacy blob keys if they exist\n const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldBlobKeys = await this.client.keys(blobPattern);\n if (oldBlobKeys.length > 0) {\n await this.client.del(oldBlobKeys);\n }\n }\n\n private sanitizeMetadata(metadata: CheckpointMetadata): CheckpointMetadata {\n if (!metadata) return {} as CheckpointMetadata;\n\n const sanitized: any = {};\n for (const [key, value] of Object.entries(metadata)) {\n // Remove null characters from keys and string values\n // eslint-disable-next-line no-control-regex\n const sanitizedKey = key.replace(/\\x00/g, \"\");\n sanitized[sanitizedKey] =\n // eslint-disable-next-line no-control-regex\n typeof value === \"string\" ? value.replace(/\\x00/g, \"\") : value;\n }\n return sanitized as CheckpointMetadata;\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, {\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"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"shallow.cjs","names":["BaseCheckpointSaver","escapeRediSearchTagValue"],"sources":["../src/shallow.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient } from \"redis\";\nimport { escapeRediSearchTagValue } from \"./utils.js\";\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\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\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_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\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport class ShallowRedisSaver extends BaseCheckpointSaver {\n private client: any;\n private ttlConfig?: TTLConfig;\n\n constructor(client: any, 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<ShallowRedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new ShallowRedisSaver(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 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\n // In shallow mode, we use a single key per thread (no checkpoint_id in key)\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n\n // Get the previous checkpoint to know what to clean up\n let prevCheckpointData: any = null;\n let prevCheckpointId: string | null = null;\n try {\n prevCheckpointData = await this.client.json.get(key);\n if (prevCheckpointData && typeof prevCheckpointData === \"object\") {\n prevCheckpointId = prevCheckpointData.checkpoint_id;\n }\n } catch (error) {\n // Key doesn't exist yet, that's fine\n }\n\n // Clean up old checkpoint and related data if it exists\n if (prevCheckpointId && prevCheckpointId !== checkpointId) {\n await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);\n }\n\n // Store channel values inline - no blob storage in shallow mode\n const checkpointCopy = {\n ...checkpoint,\n channel_values: checkpoint.channel_values || {},\n // Remove channel_blobs if present\n channel_blobs: undefined,\n };\n\n // Structure matching Python implementation\n const jsonDoc: any = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: checkpointCopy,\n metadata: this.sanitizeMetadata(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);\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 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 // In shallow mode, we use a single key per thread\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const jsonDoc = await this.client.json.get(key);\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // If a specific checkpoint_id was requested, check if it matches\n if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) {\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 // 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 // Load pending writes if they exist\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n jsonDoc.checkpoint_ns,\n jsonDoc.checkpoint_id\n );\n }\n\n return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // In shallow mode, we only return the latest checkpoint per thread\n if (config?.configurable?.thread_id) {\n // Single thread case\n const tuple = await this.getTuple(config);\n if (tuple) {\n // Apply filter if provided\n if (options?.filter) {\n if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) {\n yield tuple;\n }\n } else {\n yield tuple;\n }\n }\n } else {\n // All threads case - use search\n const queryParts: string[] = [];\n\n // Add metadata filters\n if (options?.filter) {\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 }\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 const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: limit * 2 }, // Get more since we'll deduplicate\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n // In shallow mode, deduplicate by thread_id\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n\n for (const doc of results.documents) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = doc.value;\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check null filters manually if needed\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall back to scanning all shallow checkpoints\n const pattern = `checkpoint:*:*:shallow`;\n const keys = await this.client.keys(pattern);\n\n if (keys.length === 0) {\n return;\n }\n\n // Sort keys to have consistent ordering\n keys.sort().reverse();\n\n // Get unique threads\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const key of keys) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = await this.client.json.get(key);\n if (!jsonDoc) continue;\n\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check filter if provided\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n return;\n }\n throw error;\n }\n }\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 // In shallow mode, we overwrite all writes for the task\n // First, clean up old writes for this task\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Store new writes\n const writeKeys: string[] = [];\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 };\n\n await this.client.json.set(writeKey, \"$\", writeDoc);\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 // Clear existing entries for this task and add new ones\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = 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 await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n const currentDoc = await this.client.json.get(checkpointKey);\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete shallow checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*:shallow`;\n const checkpointKeys = await this.client.keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `checkpoint_write:${threadId}:*`;\n const writesKeys = await this.client.keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n\n // Delete write registries\n const zsetPattern = `write_keys_zset:${threadId}:*`;\n const zsetKeys = await this.client.keys(zsetPattern);\n\n if (zsetKeys.length > 0) {\n await this.client.del(zsetKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\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 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 // 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: jsonDoc.checkpoint_ns,\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: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\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 // 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 const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n const writeKeys = await this.client.zRange(zsetKey, 0, -1);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeKey of writeKeys) {\n const writeDoc = await this.client.json.get(writeKey);\n if (writeDoc) {\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\n return pendingWrites;\n }\n\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch(\n metadata: any,\n filter: CheckpointMetadata\n ): boolean {\n for (const [key, value] of Object.entries(filter)) {\n const metadataValue = metadata?.[key];\n if (value === null) {\n if (!(key in (metadata || {})) || metadataValue !== null) {\n return false;\n }\n } else if (typeof value === \"object\" && !Array.isArray(value)) {\n // Deep comparison for objects with deterministic key ordering\n if (typeof metadataValue !== \"object\" || metadataValue === null) {\n return false;\n }\n if (\n deterministicStringify(value) !==\n deterministicStringify(metadataValue)\n ) {\n return false;\n }\n } else if (metadataValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n private async cleanupOldCheckpoint(\n threadId: string,\n checkpointNs: string,\n oldCheckpointId: string\n ): Promise<void> {\n // Clean up old writes\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Clean up write registry\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${oldCheckpointId}`;\n await this.client.del(zsetKey);\n\n // Note: We don't clean up blob keys in shallow mode since we store inline\n // But for completeness, clean up any legacy blob keys if they exist\n const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldBlobKeys = await this.client.keys(blobPattern);\n if (oldBlobKeys.length > 0) {\n await this.client.del(oldBlobKeys);\n }\n }\n\n private sanitizeMetadata(metadata: CheckpointMetadata): CheckpointMetadata {\n if (!metadata) return {} as CheckpointMetadata;\n\n const sanitized: any = {};\n for (const [key, value] of Object.entries(metadata)) {\n // Remove null characters from keys and string values\n // eslint-disable-next-line no-control-regex\n const sanitizedKey = key.replace(/\\x00/g, \"\");\n sanitized[sanitizedKey] =\n // eslint-disable-next-line no-control-regex\n typeof value === \"string\" ? value.replace(/\\x00/g, \"\") : value;\n }\n return sanitized as CheckpointMetadata;\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, {\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"],"mappings":"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"}
|
package/dist/shallow.d.cts
CHANGED
|
@@ -3,8 +3,8 @@ import { RunnableConfig } from "@langchain/core/runnables";
|
|
|
3
3
|
|
|
4
4
|
//#region src/shallow.d.ts
|
|
5
5
|
interface TTLConfig {
|
|
6
|
-
defaultTTL?: number;
|
|
7
|
-
refreshOnRead?: boolean;
|
|
6
|
+
defaultTTL?: number;
|
|
7
|
+
refreshOnRead?: boolean;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.
|
|
@@ -29,13 +29,10 @@ declare class ShallowRedisSaver extends BaseCheckpointSaver {
|
|
|
29
29
|
putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;
|
|
30
30
|
deleteThread(threadId: string): Promise<void>;
|
|
31
31
|
end(): Promise<void>;
|
|
32
|
-
// Helper method to add searchable metadata fields
|
|
33
32
|
private addSearchableMetadataFields;
|
|
34
|
-
// Helper method to create checkpoint tuple from json document
|
|
35
33
|
private createCheckpointTuple;
|
|
36
34
|
private applyTTL;
|
|
37
35
|
private loadPendingWrites;
|
|
38
|
-
// Helper method to check metadata filter matches
|
|
39
36
|
private checkMetadataFilterMatch;
|
|
40
37
|
private cleanupOldCheckpoint;
|
|
41
38
|
private sanitizeMetadata;
|
package/dist/shallow.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shallow.d.cts","names":["BaseCheckpointSaver","ChannelVersions","Checkpoint","CheckpointListOptions","CheckpointMetadata","CheckpointTuple","PendingWrite","RunnableConfig","TTLConfig","ShallowRedisSaver","Promise","AsyncGenerator"],"sources":["../src/shallow.d.ts"],"sourcesContent":["import { BaseCheckpointSaver, ChannelVersions, Checkpoint, CheckpointListOptions, CheckpointMetadata, CheckpointTuple, PendingWrite } from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nexport interface TTLConfig {\n defaultTTL?: number
|
|
1
|
+
{"version":3,"file":"shallow.d.cts","names":["BaseCheckpointSaver","ChannelVersions","Checkpoint","CheckpointListOptions","CheckpointMetadata","CheckpointTuple","PendingWrite","RunnableConfig","TTLConfig","ShallowRedisSaver","Promise","AsyncGenerator"],"sources":["../src/shallow.d.ts"],"sourcesContent":["import { BaseCheckpointSaver, ChannelVersions, Checkpoint, CheckpointListOptions, CheckpointMetadata, CheckpointTuple, PendingWrite } from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nexport interface TTLConfig {\n defaultTTL?: number;\n refreshOnRead?: boolean;\n}\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport declare class ShallowRedisSaver extends BaseCheckpointSaver {\n private client;\n private ttlConfig?;\n constructor(client: any, ttlConfig?: TTLConfig);\n static fromUrl(url: string, ttlConfig?: TTLConfig): Promise<ShallowRedisSaver>;\n get(config: RunnableConfig): Promise<Checkpoint | undefined>;\n put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, _newVersions: ChannelVersions): Promise<RunnableConfig>;\n getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>;\n list(config: RunnableConfig | null, options?: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n }): AsyncGenerator<CheckpointTuple>;\n putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;\n deleteThread(threadId: string): Promise<void>;\n end(): Promise<void>;\n private addSearchableMetadataFields;\n private createCheckpointTuple;\n private applyTTL;\n private loadPendingWrites;\n private checkMetadataFilterMatch;\n private cleanupOldCheckpoint;\n private sanitizeMetadata;\n private ensureIndexes;\n}\n//# sourceMappingURL=shallow.d.ts.map"],"mappings":";;;;UAEiBQ,SAAAA;;EAAAA,aAAS,CAAA,EAAA,OAAA;AAa1B;;;;;;;;;;AAM4CN,cANvBO,iBAAAA,SAA0BT,mBAAAA,CAMHE;UAAsBE,MAAAA;UAAkCH,SAAAA;aAA0BM,CAAAA,MAAAA,EAAAA,GAAAA,EAAAA,SAAAA,CAAAA,EAHrFC,SAGqFD;SAARG,OAAAA,CAAAA,GAAAA,EAAAA,MAAAA,EAAAA,SAAAA,CAAAA,EAF1EF,SAE0EE,CAAAA,EAF9DA,OAE8DA,CAFtDD,iBAEsDC,CAAAA;KACjGH,CAAAA,MAAAA,EAFLA,cAEKA,CAAAA,EAFYG,OAEZH,CAFoBL,UAEpBK,GAAAA,SAAAA,CAAAA;KAAyBF,CAAAA,MAAAA,EAD9BE,cAC8BF,EAAAA,UAAAA,EADFH,UACEG,EAAAA,QAAAA,EADoBD,kBACpBC,EAAAA,YAAAA,EADsDJ,eACtDI,CAAAA,EADwEK,OACxEL,CADgFE,cAChFF,CAAAA;UAARK,CAAAA,MAAAA,EAAjBH,cAAiBG,CAAAA,EAAAA,OAAAA,CAAQL,eAARK,GAAAA,SAAAA,CAAAA;MACrBH,CAAAA,MAAAA,EAAAA,cAAAA,GAAAA,IAAAA,EAAAA,QAAAA,EAAiCJ,qBAAjCI,GAAAA;IAAiCJ,MAAAA,CAAAA,EACjCC,kBADiCD;MAE1CQ,cADSP,CACMC,eADND,CAAAA;WACMC,CAAAA,MAAAA,EACDE,cADCF,EAAAA,MAAAA,EACuBC,YADvBD,EAAAA,EAAAA,MAAAA,EAAAA,MAAAA,CAAAA,EACwDK,OADxDL,CAAAA,IAAAA,CAAAA;cAAfM,CAAAA,QAAAA,EAAAA,MAAAA,CAAAA,EAE4BD,OAF5BC,CAAAA,IAAAA,CAAAA;KACcJ,CAAAA,CAAAA,EAEXG,OAFWH,CAAAA,IAAAA,CAAAA;UAAwBD,2BAAAA;UAAiCI,qBAAAA;UAC3CA,QAAAA;UACzBA,iBAAAA;UAboCV,wBAAAA;EAAmB,QAAA,oBAAA"}
|
package/dist/shallow.d.ts
CHANGED
|
@@ -3,8 +3,8 @@ import { RunnableConfig } from "@langchain/core/runnables";
|
|
|
3
3
|
|
|
4
4
|
//#region src/shallow.d.ts
|
|
5
5
|
interface TTLConfig {
|
|
6
|
-
defaultTTL?: number;
|
|
7
|
-
refreshOnRead?: boolean;
|
|
6
|
+
defaultTTL?: number;
|
|
7
|
+
refreshOnRead?: boolean;
|
|
8
8
|
}
|
|
9
9
|
/**
|
|
10
10
|
* ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.
|
|
@@ -29,13 +29,10 @@ declare class ShallowRedisSaver extends BaseCheckpointSaver {
|
|
|
29
29
|
putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;
|
|
30
30
|
deleteThread(threadId: string): Promise<void>;
|
|
31
31
|
end(): Promise<void>;
|
|
32
|
-
// Helper method to add searchable metadata fields
|
|
33
32
|
private addSearchableMetadataFields;
|
|
34
|
-
// Helper method to create checkpoint tuple from json document
|
|
35
33
|
private createCheckpointTuple;
|
|
36
34
|
private applyTTL;
|
|
37
35
|
private loadPendingWrites;
|
|
38
|
-
// Helper method to check metadata filter matches
|
|
39
36
|
private checkMetadataFilterMatch;
|
|
40
37
|
private cleanupOldCheckpoint;
|
|
41
38
|
private sanitizeMetadata;
|
package/dist/shallow.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shallow.d.ts","names":["BaseCheckpointSaver","ChannelVersions","Checkpoint","CheckpointListOptions","CheckpointMetadata","CheckpointTuple","PendingWrite","RunnableConfig","TTLConfig","ShallowRedisSaver","Promise","AsyncGenerator"],"sources":["../src/shallow.d.ts"],"sourcesContent":["import { BaseCheckpointSaver, ChannelVersions, Checkpoint, CheckpointListOptions, CheckpointMetadata, CheckpointTuple, PendingWrite } from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nexport interface TTLConfig {\n defaultTTL?: number
|
|
1
|
+
{"version":3,"file":"shallow.d.ts","names":["BaseCheckpointSaver","ChannelVersions","Checkpoint","CheckpointListOptions","CheckpointMetadata","CheckpointTuple","PendingWrite","RunnableConfig","TTLConfig","ShallowRedisSaver","Promise","AsyncGenerator"],"sources":["../src/shallow.d.ts"],"sourcesContent":["import { BaseCheckpointSaver, ChannelVersions, Checkpoint, CheckpointListOptions, CheckpointMetadata, CheckpointTuple, PendingWrite } from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nexport interface TTLConfig {\n defaultTTL?: number;\n refreshOnRead?: boolean;\n}\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport declare class ShallowRedisSaver extends BaseCheckpointSaver {\n private client;\n private ttlConfig?;\n constructor(client: any, ttlConfig?: TTLConfig);\n static fromUrl(url: string, ttlConfig?: TTLConfig): Promise<ShallowRedisSaver>;\n get(config: RunnableConfig): Promise<Checkpoint | undefined>;\n put(config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata, _newVersions: ChannelVersions): Promise<RunnableConfig>;\n getTuple(config: RunnableConfig): Promise<CheckpointTuple | undefined>;\n list(config: RunnableConfig | null, options?: CheckpointListOptions & {\n filter?: CheckpointMetadata;\n }): AsyncGenerator<CheckpointTuple>;\n putWrites(config: RunnableConfig, writes: PendingWrite[], taskId: string): Promise<void>;\n deleteThread(threadId: string): Promise<void>;\n end(): Promise<void>;\n private addSearchableMetadataFields;\n private createCheckpointTuple;\n private applyTTL;\n private loadPendingWrites;\n private checkMetadataFilterMatch;\n private cleanupOldCheckpoint;\n private sanitizeMetadata;\n private ensureIndexes;\n}\n//# sourceMappingURL=shallow.d.ts.map"],"mappings":";;;;UAEiBQ,SAAAA;;EAAAA,aAAS,CAAA,EAAA,OAAA;AAa1B;;;;;;;;;;AAM4CN,cANvBO,iBAAAA,SAA0BT,mBAAAA,CAMHE;UAAsBE,MAAAA;UAAkCH,SAAAA;aAA0BM,CAAAA,MAAAA,EAAAA,GAAAA,EAAAA,SAAAA,CAAAA,EAHrFC,SAGqFD;SAARG,OAAAA,CAAAA,GAAAA,EAAAA,MAAAA,EAAAA,SAAAA,CAAAA,EAF1EF,SAE0EE,CAAAA,EAF9DA,OAE8DA,CAFtDD,iBAEsDC,CAAAA;KACjGH,CAAAA,MAAAA,EAFLA,cAEKA,CAAAA,EAFYG,OAEZH,CAFoBL,UAEpBK,GAAAA,SAAAA,CAAAA;KAAyBF,CAAAA,MAAAA,EAD9BE,cAC8BF,EAAAA,UAAAA,EADFH,UACEG,EAAAA,QAAAA,EADoBD,kBACpBC,EAAAA,YAAAA,EADsDJ,eACtDI,CAAAA,EADwEK,OACxEL,CADgFE,cAChFF,CAAAA;UAARK,CAAAA,MAAAA,EAAjBH,cAAiBG,CAAAA,EAAAA,OAAAA,CAAQL,eAARK,GAAAA,SAAAA,CAAAA;MACrBH,CAAAA,MAAAA,EAAAA,cAAAA,GAAAA,IAAAA,EAAAA,QAAAA,EAAiCJ,qBAAjCI,GAAAA;IAAiCJ,MAAAA,CAAAA,EACjCC,kBADiCD;MAE1CQ,cADSP,CACMC,eADND,CAAAA;WACMC,CAAAA,MAAAA,EACDE,cADCF,EAAAA,MAAAA,EACuBC,YADvBD,EAAAA,EAAAA,MAAAA,EAAAA,MAAAA,CAAAA,EACwDK,OADxDL,CAAAA,IAAAA,CAAAA;cAAfM,CAAAA,QAAAA,EAAAA,MAAAA,CAAAA,EAE4BD,OAF5BC,CAAAA,IAAAA,CAAAA;KACcJ,CAAAA,CAAAA,EAEXG,OAFWH,CAAAA,IAAAA,CAAAA;UAAwBD,2BAAAA;UAAiCI,qBAAAA;UAC3CA,QAAAA;UACzBA,iBAAAA;UAboCV,wBAAAA;EAAmB,QAAA,oBAAA"}
|
package/dist/shallow.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { escapeRediSearchTagValue } from "./utils.js";
|
|
1
2
|
import { BaseCheckpointSaver, uuid6 } from "@langchain/langgraph-checkpoint";
|
|
2
3
|
import { createClient } from "redis";
|
|
3
4
|
|
|
@@ -114,8 +115,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
|
|
|
114
115
|
return saver;
|
|
115
116
|
}
|
|
116
117
|
async get(config) {
|
|
117
|
-
|
|
118
|
-
return tuple?.checkpoint;
|
|
118
|
+
return (await this.getTuple(config))?.checkpoint;
|
|
119
119
|
}
|
|
120
120
|
async put(config, checkpoint, metadata, _newVersions) {
|
|
121
121
|
await this.ensureIndexes();
|
|
@@ -160,19 +160,16 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
|
|
|
160
160
|
const threadId = config.configurable?.thread_id;
|
|
161
161
|
const checkpointNs = config.configurable?.checkpoint_ns ?? "";
|
|
162
162
|
const checkpointId = config.configurable?.checkpoint_id;
|
|
163
|
-
if (!threadId) return
|
|
163
|
+
if (!threadId) return;
|
|
164
164
|
const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;
|
|
165
165
|
const jsonDoc = await this.client.json.get(key);
|
|
166
|
-
if (!jsonDoc) return
|
|
167
|
-
if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) return
|
|
166
|
+
if (!jsonDoc) return;
|
|
167
|
+
if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) return;
|
|
168
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
|
-
};
|
|
169
|
+
const checkpoint = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.checkpoint));
|
|
173
170
|
let pendingWrites;
|
|
174
171
|
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);
|
|
172
|
+
return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);
|
|
176
173
|
}
|
|
177
174
|
async *list(config, options) {
|
|
178
175
|
await this.ensureIndexes();
|
|
@@ -184,8 +181,14 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
|
|
|
184
181
|
} else {
|
|
185
182
|
const queryParts = [];
|
|
186
183
|
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")
|
|
188
|
-
|
|
184
|
+
for (const [key, value] of Object.entries(options.filter)) if (value === void 0) {} else if (value === null) {} else if (typeof value === "string") {
|
|
185
|
+
const escapedKey = escapeRediSearchTagValue(key);
|
|
186
|
+
const escapedValue = escapeRediSearchTagValue(value);
|
|
187
|
+
queryParts.push(`(@${escapedKey}:{${escapedValue}})`);
|
|
188
|
+
} else if (typeof value === "number") {
|
|
189
|
+
const escapedKey = escapeRediSearchTagValue(key);
|
|
190
|
+
queryParts.push(`(@${escapedKey}:[${value} ${value}])`);
|
|
191
|
+
}
|
|
189
192
|
}
|
|
190
193
|
if (queryParts.length === 0) queryParts.push("*");
|
|
191
194
|
const query = queryParts.join(" ");
|
|
@@ -212,24 +215,20 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
|
|
|
212
215
|
if (options?.filter) {
|
|
213
216
|
if (!this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)) continue;
|
|
214
217
|
}
|
|
215
|
-
const checkpoint =
|
|
216
|
-
|
|
217
|
-
channel_values: jsonDoc.checkpoint.channel_values || {}
|
|
218
|
-
};
|
|
219
|
-
yield this.createCheckpointTuple(jsonDoc, checkpoint);
|
|
218
|
+
const checkpoint = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.checkpoint));
|
|
219
|
+
yield await this.createCheckpointTuple(jsonDoc, checkpoint);
|
|
220
220
|
yieldCount++;
|
|
221
221
|
}
|
|
222
222
|
} catch (error) {
|
|
223
223
|
if (error.message?.includes("no such index")) {
|
|
224
|
-
const
|
|
225
|
-
const keys = await this.client.keys(pattern);
|
|
224
|
+
const keys = await this.client.keys(`checkpoint:*:*:shallow`);
|
|
226
225
|
if (keys.length === 0) return;
|
|
227
226
|
keys.sort().reverse();
|
|
228
227
|
const seenThreads = /* @__PURE__ */ new Set();
|
|
229
228
|
let yieldCount = 0;
|
|
230
|
-
const limit
|
|
229
|
+
const limit = options?.limit ?? 10;
|
|
231
230
|
for (const key of keys) {
|
|
232
|
-
if (yieldCount >= limit
|
|
231
|
+
if (yieldCount >= limit) break;
|
|
233
232
|
const jsonDoc = await this.client.json.get(key);
|
|
234
233
|
if (!jsonDoc) continue;
|
|
235
234
|
const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;
|
|
@@ -238,11 +237,8 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
|
|
|
238
237
|
if (options?.filter) {
|
|
239
238
|
if (!this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)) continue;
|
|
240
239
|
}
|
|
241
|
-
const checkpoint =
|
|
242
|
-
|
|
243
|
-
channel_values: jsonDoc.checkpoint.channel_values || {}
|
|
244
|
-
};
|
|
245
|
-
yield this.createCheckpointTuple(jsonDoc, checkpoint);
|
|
240
|
+
const checkpoint = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.checkpoint));
|
|
241
|
+
yield await this.createCheckpointTuple(jsonDoc, checkpoint);
|
|
246
242
|
yieldCount++;
|
|
247
243
|
}
|
|
248
244
|
return;
|
|
@@ -290,8 +286,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
|
|
|
290
286
|
if (this.ttlConfig?.defaultTTL) await this.applyTTL(...writeKeys, zsetKey);
|
|
291
287
|
}
|
|
292
288
|
const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;
|
|
293
|
-
|
|
294
|
-
if (checkpointExists) {
|
|
289
|
+
if (await this.client.exists(checkpointKey)) {
|
|
295
290
|
const currentDoc = await this.client.json.get(checkpointKey);
|
|
296
291
|
if (currentDoc) {
|
|
297
292
|
currentDoc.has_writes = "true";
|
|
@@ -320,7 +315,8 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
|
|
|
320
315
|
if ("writes" in metadata) jsonDoc.writes = typeof metadata.writes === "object" ? JSON.stringify(metadata.writes) : metadata.writes;
|
|
321
316
|
if ("score" in metadata) jsonDoc.score = metadata.score;
|
|
322
317
|
}
|
|
323
|
-
createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
|
|
318
|
+
async createCheckpointTuple(jsonDoc, checkpoint, pendingWrites) {
|
|
319
|
+
const metadata = await this.serde.loadsTyped("json", JSON.stringify(jsonDoc.metadata));
|
|
324
320
|
return {
|
|
325
321
|
config: { configurable: {
|
|
326
322
|
thread_id: jsonDoc.thread_id,
|
|
@@ -328,7 +324,7 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
|
|
|
328
324
|
checkpoint_id: jsonDoc.checkpoint_id
|
|
329
325
|
} },
|
|
330
326
|
checkpoint,
|
|
331
|
-
metadata
|
|
327
|
+
metadata,
|
|
332
328
|
parentConfig: jsonDoc.parent_checkpoint_id ? { configurable: {
|
|
333
329
|
thread_id: jsonDoc.thread_id,
|
|
334
330
|
checkpoint_ns: jsonDoc.checkpoint_ns,
|
|
@@ -346,15 +342,18 @@ var ShallowRedisSaver = class ShallowRedisSaver extends BaseCheckpointSaver {
|
|
|
346
342
|
async loadPendingWrites(threadId, checkpointNs, checkpointId) {
|
|
347
343
|
const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;
|
|
348
344
|
const writeKeys = await this.client.zRange(zsetKey, 0, -1);
|
|
349
|
-
if (writeKeys.length === 0) return
|
|
345
|
+
if (writeKeys.length === 0) return;
|
|
350
346
|
const pendingWrites = [];
|
|
351
347
|
for (const writeKey of writeKeys) {
|
|
352
348
|
const writeDoc = await this.client.json.get(writeKey);
|
|
353
|
-
if (writeDoc)
|
|
354
|
-
writeDoc.
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
349
|
+
if (writeDoc) {
|
|
350
|
+
const deserializedValue = await this.serde.loadsTyped("json", JSON.stringify(writeDoc.value));
|
|
351
|
+
pendingWrites.push([
|
|
352
|
+
writeDoc.task_id,
|
|
353
|
+
writeDoc.channel,
|
|
354
|
+
deserializedValue
|
|
355
|
+
]);
|
|
356
|
+
}
|
|
358
357
|
}
|
|
359
358
|
return pendingWrites;
|
|
360
359
|
}
|
package/dist/shallow.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shallow.js","names":["sortedObj: Record<string, any>","sorted: Record<string, any>","prevCheckpointData: any","prevCheckpointId: string | null","jsonDoc: any","pendingWrites: Array<[string, string, any]> | undefined","queryParts: string[]","error: any","limit","writeKeys: string[]","zaddArgs: Record<string, number>","pendingWrites: Array<[string, string, any]>","sanitized: any"],"sources":["../src/shallow.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient } from \"redis\";\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\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\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_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\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport class ShallowRedisSaver extends BaseCheckpointSaver {\n private client: any;\n private ttlConfig?: TTLConfig;\n\n constructor(client: any, 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<ShallowRedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new ShallowRedisSaver(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 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\n // In shallow mode, we use a single key per thread (no checkpoint_id in key)\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n\n // Get the previous checkpoint to know what to clean up\n let prevCheckpointData: any = null;\n let prevCheckpointId: string | null = null;\n try {\n prevCheckpointData = await this.client.json.get(key);\n if (prevCheckpointData && typeof prevCheckpointData === \"object\") {\n prevCheckpointId = prevCheckpointData.checkpoint_id;\n }\n } catch (error) {\n // Key doesn't exist yet, that's fine\n }\n\n // Clean up old checkpoint and related data if it exists\n if (prevCheckpointId && prevCheckpointId !== checkpointId) {\n await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);\n }\n\n // Store channel values inline - no blob storage in shallow mode\n const checkpointCopy = {\n ...checkpoint,\n channel_values: checkpoint.channel_values || {},\n // Remove channel_blobs if present\n channel_blobs: undefined,\n };\n\n // Structure matching Python implementation\n const jsonDoc: any = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: checkpointCopy,\n metadata: this.sanitizeMetadata(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);\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 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 // In shallow mode, we use a single key per thread\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const jsonDoc = await this.client.json.get(key);\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // If a specific checkpoint_id was requested, check if it matches\n if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) {\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 // Channel values are stored inline in shallow mode\n const checkpoint = {\n ...jsonDoc.checkpoint,\n channel_values: jsonDoc.checkpoint.channel_values || {},\n };\n\n // Load pending writes if they exist\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n jsonDoc.checkpoint_ns,\n jsonDoc.checkpoint_id\n );\n }\n\n return this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // In shallow mode, we only return the latest checkpoint per thread\n if (config?.configurable?.thread_id) {\n // Single thread case\n const tuple = await this.getTuple(config);\n if (tuple) {\n // Apply filter if provided\n if (options?.filter) {\n if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) {\n yield tuple;\n }\n } else {\n yield tuple;\n }\n }\n } else {\n // All threads case - use search\n const queryParts: string[] = [];\n\n // Add metadata filters\n if (options?.filter) {\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 queryParts.push(`(@${key}:{${value}})`);\n } else if (typeof value === \"number\") {\n queryParts.push(`(@${key}:[${value} ${value}])`);\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 const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: limit * 2 }, // Get more since we'll deduplicate\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n // In shallow mode, deduplicate by thread_id\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n\n for (const doc of results.documents) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = doc.value;\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check null filters manually if needed\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint = {\n ...jsonDoc.checkpoint,\n channel_values: jsonDoc.checkpoint.channel_values || {},\n };\n\n yield this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall back to scanning all shallow checkpoints\n const pattern = `checkpoint:*:*:shallow`;\n const keys = await this.client.keys(pattern);\n\n if (keys.length === 0) {\n return;\n }\n\n // Sort keys to have consistent ordering\n keys.sort().reverse();\n\n // Get unique threads\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const key of keys) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = await this.client.json.get(key);\n if (!jsonDoc) continue;\n\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check filter if provided\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint = {\n ...jsonDoc.checkpoint,\n channel_values: jsonDoc.checkpoint.channel_values || {},\n };\n\n yield this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n return;\n }\n throw error;\n }\n }\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 // In shallow mode, we overwrite all writes for the task\n // First, clean up old writes for this task\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Store new writes\n const writeKeys: string[] = [];\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 };\n\n await this.client.json.set(writeKey, \"$\", writeDoc);\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 // Clear existing entries for this task and add new ones\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = 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 await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n const currentDoc = await this.client.json.get(checkpointKey);\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete shallow checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*:shallow`;\n const checkpointKeys = await this.client.keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `checkpoint_write:${threadId}:*`;\n const writesKeys = await this.client.keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n\n // Delete write registries\n const zsetPattern = `write_keys_zset:${threadId}:*`;\n const zsetKeys = await this.client.keys(zsetPattern);\n\n if (zsetKeys.length > 0) {\n await this.client.del(zsetKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\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 create checkpoint tuple from json document\n private createCheckpointTuple(\n jsonDoc: any,\n checkpoint: Checkpoint,\n pendingWrites?: Array<[string, string, any]>\n ): CheckpointTuple {\n return {\n config: {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.checkpoint_id,\n },\n },\n checkpoint,\n metadata: jsonDoc.metadata,\n parentConfig: jsonDoc.parent_checkpoint_id\n ? {\n configurable: {\n thread_id: jsonDoc.thread_id,\n checkpoint_ns: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\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 // 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 const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n const writeKeys = await this.client.zRange(zsetKey, 0, -1);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeKey of writeKeys) {\n const writeDoc = await this.client.json.get(writeKey);\n if (writeDoc) {\n pendingWrites.push([\n writeDoc.task_id,\n writeDoc.channel,\n writeDoc.value,\n ]);\n }\n }\n\n return pendingWrites;\n }\n\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch(\n metadata: any,\n filter: CheckpointMetadata\n ): boolean {\n for (const [key, value] of Object.entries(filter)) {\n const metadataValue = metadata?.[key];\n if (value === null) {\n if (!(key in (metadata || {})) || metadataValue !== null) {\n return false;\n }\n } else if (typeof value === \"object\" && !Array.isArray(value)) {\n // Deep comparison for objects with deterministic key ordering\n if (typeof metadataValue !== \"object\" || metadataValue === null) {\n return false;\n }\n if (\n deterministicStringify(value) !==\n deterministicStringify(metadataValue)\n ) {\n return false;\n }\n } else if (metadataValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n private async cleanupOldCheckpoint(\n threadId: string,\n checkpointNs: string,\n oldCheckpointId: string\n ): Promise<void> {\n // Clean up old writes\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Clean up write registry\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${oldCheckpointId}`;\n await this.client.del(zsetKey);\n\n // Note: We don't clean up blob keys in shallow mode since we store inline\n // But for completeness, clean up any legacy blob keys if they exist\n const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldBlobKeys = await this.client.keys(blobPattern);\n if (oldBlobKeys.length > 0) {\n await this.client.del(oldBlobKeys);\n }\n }\n\n private sanitizeMetadata(metadata: CheckpointMetadata): CheckpointMetadata {\n if (!metadata) return {} as CheckpointMetadata;\n\n const sanitized: any = {};\n for (const [key, value] of Object.entries(metadata)) {\n // Remove null characters from keys and string values\n // eslint-disable-next-line no-control-regex\n const sanitizedKey = key.replace(/\\x00/g, \"\");\n sanitized[sanitizedKey] =\n // eslint-disable-next-line no-control-regex\n typeof value === \"string\" ? value.replace(/\\x00/g, \"\") : value;\n }\n return sanitized as CheckpointMetadata;\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, {\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"],"mappings":";;;;AAmBA,SAAS,uBAAuB,KAAkB;AAChD,KAAI,QAAQ,QAAQ,OAAO,QAAQ,SACjC,QAAO,KAAK,UAAU;AAExB,KAAI,MAAM,QAAQ,KAChB,QAAO,KAAK,UAAU,IAAI,KAAK,SAAS,uBAAuB;CAEjE,MAAMA,YAAiC;CACvC,MAAM,aAAa,OAAO,KAAK,KAAK;AACpC,MAAK,MAAM,OAAO,WAChB,WAAU,OAAO,IAAI;AAEvB,QAAO,KAAK,UAAU,YAAY,GAAG,UAAU;AAC7C,MAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,QAAQ;GACxE,MAAMC,SAA8B;GACpC,MAAM,OAAO,OAAO,KAAK,OAAO;AAChC,QAAK,MAAM,KAAK,KACd,QAAO,KAAK,MAAM;AAEpB,UAAO;;AAET,SAAO;;;AAIX,MAAM,UAAU,CACd;CACE,OAAO;CACP,QAAQ;CACR,QAAQ;EACN,eAAe;GAAE,MAAM;GAAO,IAAI;;EAClC,mBAAmB;GAAE,MAAM;GAAO,IAAI;;EACtC,mBAAmB;GAAE,MAAM;GAAO,IAAI;;EACtC,0BAA0B;GAAE,MAAM;GAAO,IAAI;;EAC7C,mBAAmB;GAAE,MAAM;GAAW,IAAI;;EAC1C,gBAAgB;GAAE,MAAM;GAAO,IAAI;;EACnC,YAAY;GAAE,MAAM;GAAO,IAAI;;EAC/B,UAAU;GAAE,MAAM;GAAW,IAAI;;;GAGrC;CACE,OAAO;CACP,QAAQ;CACR,QAAQ;EACN,eAAe;GAAE,MAAM;GAAO,IAAI;;EAClC,mBAAmB;GAAE,MAAM;GAAO,IAAI;;EACtC,mBAAmB;GAAE,MAAM;GAAO,IAAI;;EACtC,aAAa;GAAE,MAAM;GAAO,IAAI;;EAChC,SAAS;GAAE,MAAM;GAAW,IAAI;;EAChC,aAAa;GAAE,MAAM;GAAO,IAAI;;EAChC,UAAU;GAAE,MAAM;GAAO,IAAI;;;;;;;;;;;;;AAcnC,IAAa,oBAAb,MAAa,0BAA0B,oBAAoB;CACzD,AAAQ;CACR,AAAQ;CAER,YAAY,QAAa,WAAuB;AAC9C;AACA,OAAK,SAAS;AACd,OAAK,YAAY;;CAGnB,aAAa,QACX,KACA,WAC4B;EAC5B,MAAM,SAAS,aAAa,EAAE;AAC9B,QAAM,OAAO;EACb,MAAM,QAAQ,IAAI,kBAAkB,QAAQ;AAC5C,QAAM,MAAM;AACZ,SAAO;;CAGT,MAAM,IAAI,QAAyD;EACjE,MAAM,QAAQ,MAAM,KAAK,SAAS;AAClC,SAAO,OAAO;;CAGhB,MAAM,IACJ,QACA,YACA,UACA,cACyB;AACzB,QAAM,KAAK;EAEX,MAAM,WAAW,OAAO,cAAc;EACtC,MAAM,eAAe,OAAO,cAAc,iBAAiB;EAC3D,MAAM,qBAAqB,OAAO,cAAc;AAEhD,MAAI,CAAC,SACH,OAAM,IAAI,MAAM;EAGlB,MAAM,eAAe,WAAW,MAAM,MAAM;EAG5C,MAAM,MAAM,cAAc,SAAS,GAAG,aAAa;EAGnD,IAAIC,qBAA0B;EAC9B,IAAIC,mBAAkC;AACtC,MAAI;AACF,wBAAqB,MAAM,KAAK,OAAO,KAAK,IAAI;AAChD,OAAI,sBAAsB,OAAO,uBAAuB,SACtD,oBAAmB,mBAAmB;WAEjC,OAAO;AAKhB,MAAI,oBAAoB,qBAAqB,aAC3C,OAAM,KAAK,qBAAqB,UAAU,cAAc;EAI1D,MAAM,iBAAiB;GACrB,GAAG;GACH,gBAAgB,WAAW,kBAAkB;GAE7C,eAAe;;EAIjB,MAAMC,UAAe;GACnB,WAAW;GACX,eAAe;GACf,eAAe;GACf,sBAAsB,sBAAsB;GAC5C,YAAY;GACZ,UAAU,KAAK,iBAAiB;GAChC,eAAe,KAAK;GACpB,YAAY;;AAId,OAAK,4BAA4B,SAAS;AAG1C,QAAM,KAAK,OAAO,KAAK,IAAI,KAAK,KAAK;AAGrC,MAAI,KAAK,WAAW,WAClB,OAAM,KAAK,SAAS;AAGtB,SAAO,EACL,cAAc;GACZ,WAAW;GACX,eAAe;GACf,eAAe;;;CAKrB,MAAM,SAAS,QAA8D;EAC3E,MAAM,WAAW,OAAO,cAAc;EACtC,MAAM,eAAe,OAAO,cAAc,iBAAiB;EAC3D,MAAM,eAAe,OAAO,cAAc;AAE1C,MAAI,CAAC,SACH,QAAO;EAIT,MAAM,MAAM,cAAc,SAAS,GAAG,aAAa;EACnD,MAAM,UAAU,MAAM,KAAK,OAAO,KAAK,IAAI;AAE3C,MAAI,CAAC,QACH,QAAO;AAIT,MAAI,gBAAgB,QAAQ,kBAAkB,aAC5C,QAAO;AAIT,MAAI,KAAK,WAAW,iBAAiB,KAAK,WAAW,WACnD,OAAM,KAAK,SAAS;EAItB,MAAM,aAAa;GACjB,GAAG,QAAQ;GACX,gBAAgB,QAAQ,WAAW,kBAAkB;;EAIvD,IAAIC;AACJ,MAAI,QAAQ,eAAe,OACzB,iBAAgB,MAAM,KAAK,kBACzB,QAAQ,WACR,QAAQ,eACR,QAAQ;AAIZ,SAAO,KAAK,sBAAsB,SAAS,YAAY;;CAGzD,OAAO,KACL,QACA,SACiC;AACjC,QAAM,KAAK;AAGX,MAAI,QAAQ,cAAc,WAAW;GAEnC,MAAM,QAAQ,MAAM,KAAK,SAAS;AAClC,OAAI,MAEF,KAAI,SAAS,QACX;QAAI,KAAK,yBAAyB,MAAM,UAAU,QAAQ,QACxD,OAAM;SAGR,OAAM;SAGL;GAEL,MAAMC,aAAuB;AAG7B,OAAI,SAAS,QACX;SAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,QAAQ,QAChD,KAAI,UAAU,QAAW,YAEd,UAAU,MAAM,YAEhB,OAAO,UAAU,SAC1B,YAAW,KAAK,KAAK,IAAI,IAAI,MAAM;aAC1B,OAAO,UAAU,SAC1B,YAAW,KAAK,KAAK,IAAI,IAAI,MAAM,GAAG,MAAM;;AAKlD,OAAI,WAAW,WAAW,EACxB,YAAW,KAAK;GAGlB,MAAM,QAAQ,WAAW,KAAK;GAC9B,MAAM,QAAQ,SAAS,SAAS;AAEhC,OAAI;IACF,MAAM,UAAU,MAAM,KAAK,OAAO,GAAG,OAAO,eAAe,OAAO;KAChE,OAAO;MAAE,MAAM;MAAG,MAAM,QAAQ;;KAChC,QAAQ;MAAE,IAAI;MAAiB,WAAW;;;IAI5C,MAAM,8BAAc,IAAI;IACxB,IAAI,aAAa;AAEjB,SAAK,MAAM,OAAO,QAAQ,WAAW;AACnC,SAAI,cAAc,MAAO;KAEzB,MAAM,UAAU,IAAI;KACpB,MAAM,YAAY,GAAG,QAAQ,UAAU,GAAG,QAAQ;AAGlD,SAAI,YAAY,IAAI,WAClB;AAEF,iBAAY,IAAI;AAGhB,SAAI,SAAS,QACX;UACE,CAAC,KAAK,yBAAyB,QAAQ,UAAU,QAAQ,QAEzD;;KAKJ,MAAM,aAAa;MACjB,GAAG,QAAQ;MACX,gBAAgB,QAAQ,WAAW,kBAAkB;;AAGvD,WAAM,KAAK,sBAAsB,SAAS;AAC1C;;YAEKC,OAAY;AACnB,QAAI,MAAM,SAAS,SAAS,kBAAkB;KAE5C,MAAM,UAAU;KAChB,MAAM,OAAO,MAAM,KAAK,OAAO,KAAK;AAEpC,SAAI,KAAK,WAAW,EAClB;AAIF,UAAK,OAAO;KAGZ,MAAM,8BAAc,IAAI;KACxB,IAAI,aAAa;KACjB,MAAMC,UAAQ,SAAS,SAAS;AAEhC,UAAK,MAAM,OAAO,MAAM;AACtB,UAAI,cAAcA,QAAO;MAEzB,MAAM,UAAU,MAAM,KAAK,OAAO,KAAK,IAAI;AAC3C,UAAI,CAAC,QAAS;MAEd,MAAM,YAAY,GAAG,QAAQ,UAAU,GAAG,QAAQ;AAGlD,UAAI,YAAY,IAAI,WAClB;AAEF,kBAAY,IAAI;AAGhB,UAAI,SAAS,QACX;WACE,CAAC,KAAK,yBAAyB,QAAQ,UAAU,QAAQ,QAEzD;;MAKJ,MAAM,aAAa;OACjB,GAAG,QAAQ;OACX,gBAAgB,QAAQ,WAAW,kBAAkB;;AAGvD,YAAM,KAAK,sBAAsB,SAAS;AAC1C;;AAEF;;AAEF,UAAM;;;;CAKZ,MAAM,UACJ,QACA,QACA,QACe;AACf,QAAM,KAAK;EAEX,MAAM,WAAW,OAAO,cAAc;EACtC,MAAM,eAAe,OAAO,cAAc,iBAAiB;EAC3D,MAAM,eAAe,OAAO,cAAc;AAE1C,MAAI,CAAC,YAAY,CAAC,aAChB,OAAM,IAAI,MAAM;EAKlB,MAAM,eAAe,oBAAoB,SAAS,GAAG,aAAa,GAAG,aAAa,GAAG,OAAO;EAC5F,MAAM,eAAe,MAAM,KAAK,OAAO,KAAK;AAC5C,MAAI,aAAa,SAAS,EACxB,OAAM,KAAK,OAAO,IAAI;EAIxB,MAAMC,YAAsB;AAC5B,OAAK,IAAI,MAAM,GAAG,MAAM,OAAO,QAAQ,OAAO;GAC5C,MAAM,CAAC,SAAS,SAAS,OAAO;GAChC,MAAM,WAAW,oBAAoB,SAAS,GAAG,aAAa,GAAG,aAAa,GAAG,OAAO,GAAG;AAC3F,aAAU,KAAK;GAEf,MAAM,WAAW;IACf,WAAW;IACX,eAAe;IACf,eAAe;IACf,SAAS;IACJ;IACI;IACT,MAAM,OAAO,UAAU,WAAW,SAAS;IACpC;;AAGT,SAAM,KAAK,OAAO,KAAK,IAAI,UAAU,KAAK;;AAI5C,MAAI,UAAU,SAAS,GAAG;GACxB,MAAM,UAAU,mBAAmB,SAAS,GAAG,aAAa,GAAG;GAG/D,MAAMC,WAAmC;AACzC,aAAU,SAAS,KAAK,QAAQ;AAC9B,aAAS,OAAO;;AAElB,SAAM,KAAK,OAAO,KAChB,SACA,OAAO,QAAQ,UAAU,KAAK,CAAC,KAAK,YAAY;IAAE;IAAO,OAAO;;AAIlE,OAAI,KAAK,WAAW,WAClB,OAAM,KAAK,SAAS,GAAG,WAAW;;EAKtC,MAAM,gBAAgB,cAAc,SAAS,GAAG,aAAa;EAC7D,MAAM,mBAAmB,MAAM,KAAK,OAAO,OAAO;AAClD,MAAI,kBAAkB;GACpB,MAAM,aAAa,MAAM,KAAK,OAAO,KAAK,IAAI;AAC9C,OAAI,YAAY;AACd,eAAW,aAAa;AACxB,UAAM,KAAK,OAAO,KAAK,IAAI,eAAe,KAAK;;;;CAKrD,MAAM,aAAa,UAAiC;EAElD,MAAM,oBAAoB,cAAc,SAAS;EACjD,MAAM,iBAAiB,MAAM,KAAK,OAAO,KAAK;AAE9C,MAAI,eAAe,SAAS,EAC1B,OAAM,KAAK,OAAO,IAAI;EAIxB,MAAM,gBAAgB,oBAAoB,SAAS;EACnD,MAAM,aAAa,MAAM,KAAK,OAAO,KAAK;AAE1C,MAAI,WAAW,SAAS,EACtB,OAAM,KAAK,OAAO,IAAI;EAIxB,MAAM,cAAc,mBAAmB,SAAS;EAChD,MAAM,WAAW,MAAM,KAAK,OAAO,KAAK;AAExC,MAAI,SAAS,SAAS,EACpB,OAAM,KAAK,OAAO,IAAI;;CAI1B,MAAM,MAAqB;AACzB,QAAM,KAAK,OAAO;;CAIpB,AAAQ,4BACN,SACA,UACM;AACN,MAAI,CAAC,SAAU;AAGf,MAAI,YAAY,SACd,SAAQ,SAAS,SAAS;AAE5B,MAAI,UAAU,SACZ,SAAQ,OAAO,SAAS;AAE1B,MAAI,YAAY,SAEd,SAAQ,SACN,OAAO,SAAS,WAAW,WACvB,KAAK,UAAU,SAAS,UACxB,SAAS;AAEjB,MAAI,WAAW,SACb,SAAQ,QAAQ,SAAS;;CAK7B,AAAQ,sBACN,SACA,YACA,eACiB;AACjB,SAAO;GACL,QAAQ,EACN,cAAc;IACZ,WAAW,QAAQ;IACnB,eAAe,QAAQ;IACvB,eAAe,QAAQ;;GAG3B;GACA,UAAU,QAAQ;GAClB,cAAc,QAAQ,uBAClB,EACE,cAAc;IACZ,WAAW,QAAQ;IACnB,eAAe,QAAQ;IACvB,eAAe,QAAQ;SAG3B;GACJ;;;CAKJ,MAAc,SAAS,GAAG,MAA+B;AACvD,MAAI,CAAC,KAAK,WAAW,WAAY;EAEjC,MAAM,aAAa,KAAK,MAAM,KAAK,UAAU,aAAa;EAC1D,MAAM,UAAU,MAAM,QAAQ,WAC5B,KAAK,KAAK,QAAQ,KAAK,OAAO,OAAO,KAAK;AAI5C,OAAK,IAAI,IAAI,GAAG,IAAI,QAAQ,QAAQ,IAClC,KAAI,QAAQ,GAAG,WAAW,WACxB,SAAQ,KACN,6BAA6B,KAAK,GAAG,IACpC,QAAQ,GAA6B;;CAO9C,MAAc,kBACZ,UACA,cACA,cACmD;EACnD,MAAM,UAAU,mBAAmB,SAAS,GAAG,aAAa,GAAG;EAC/D,MAAM,YAAY,MAAM,KAAK,OAAO,OAAO,SAAS,GAAG;AAEvD,MAAI,UAAU,WAAW,EACvB,QAAO;EAGT,MAAMC,gBAA8C;AACpD,OAAK,MAAM,YAAY,WAAW;GAChC,MAAM,WAAW,MAAM,KAAK,OAAO,KAAK,IAAI;AAC5C,OAAI,SACF,eAAc,KAAK;IACjB,SAAS;IACT,SAAS;IACT,SAAS;;;AAKf,SAAO;;CAIT,AAAQ,yBACN,UACA,QACS;AACT,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,SAAS;GACjD,MAAM,gBAAgB,WAAW;AACjC,OAAI,UAAU,MACZ;QAAI,EAAE,QAAQ,YAAY,QAAQ,kBAAkB,KAClD,QAAO;cAEA,OAAO,UAAU,YAAY,CAAC,MAAM,QAAQ,QAAQ;AAE7D,QAAI,OAAO,kBAAkB,YAAY,kBAAkB,KACzD,QAAO;AAET,QACE,uBAAuB,WACvB,uBAAuB,eAEvB,QAAO;cAEA,kBAAkB,MAC3B,QAAO;;AAGX,SAAO;;CAGT,MAAc,qBACZ,UACA,cACA,iBACe;EAEf,MAAM,eAAe,oBAAoB,SAAS,GAAG,aAAa,GAAG,gBAAgB;EACrF,MAAM,eAAe,MAAM,KAAK,OAAO,KAAK;AAC5C,MAAI,aAAa,SAAS,EACxB,OAAM,KAAK,OAAO,IAAI;EAIxB,MAAM,UAAU,mBAAmB,SAAS,GAAG,aAAa,GAAG;AAC/D,QAAM,KAAK,OAAO,IAAI;EAItB,MAAM,cAAc,mBAAmB,SAAS,GAAG,aAAa,GAAG,gBAAgB;EACnF,MAAM,cAAc,MAAM,KAAK,OAAO,KAAK;AAC3C,MAAI,YAAY,SAAS,EACvB,OAAM,KAAK,OAAO,IAAI;;CAI1B,AAAQ,iBAAiB,UAAkD;AACzE,MAAI,CAAC,SAAU,QAAO;EAEtB,MAAMC,YAAiB;AACvB,OAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,WAAW;GAGnD,MAAM,eAAe,IAAI,QAAQ,SAAS;AAC1C,aAAU,gBAER,OAAO,UAAU,WAAW,MAAM,QAAQ,SAAS,MAAM;;AAE7D,SAAO;;CAGT,MAAc,gBAA+B;AAC3C,OAAK,MAAM,UAAU,QACnB,KAAI;AAEF,SAAM,KAAK,OAAO,GAAG,OAAO,OAAO,OAAO,OAAO,QAAQ;IACvD,IAAI;IACJ,QAAQ,OAAO;;WAEVL,OAAY;AAEnB,OAAI,CAAC,MAAM,SAAS,SAAS,wBAC3B,SAAQ,MACN,0BAA0B,OAAO,MAAM,IACvC,MAAM"}
|
|
1
|
+
{"version":3,"file":"shallow.js","names":[],"sources":["../src/shallow.ts"],"sourcesContent":["import {\n BaseCheckpointSaver,\n ChannelVersions,\n Checkpoint,\n CheckpointListOptions,\n CheckpointMetadata,\n CheckpointTuple,\n PendingWrite,\n uuid6,\n} from \"@langchain/langgraph-checkpoint\";\nimport { RunnableConfig } from \"@langchain/core/runnables\";\nimport { createClient } from \"redis\";\nimport { escapeRediSearchTagValue } from \"./utils.js\";\n\nexport interface TTLConfig {\n defaultTTL?: number; // TTL in minutes\n refreshOnRead?: boolean; // Whether to refresh TTL when reading\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\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_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\n/**\n * ShallowRedisSaver - A Redis checkpoint saver that only keeps the latest checkpoint per thread.\n *\n * This is a memory-optimized variant that:\n * - Only stores the most recent checkpoint for each thread\n * - Stores channel values inline (no separate blob storage)\n * - Automatically cleans up old checkpoints and writes when new ones are added\n * - Reduces storage usage for applications that don't need checkpoint history\n */\nexport class ShallowRedisSaver extends BaseCheckpointSaver {\n private client: any;\n private ttlConfig?: TTLConfig;\n\n constructor(client: any, 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<ShallowRedisSaver> {\n const client = createClient({ url });\n await client.connect();\n const saver = new ShallowRedisSaver(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 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\n // In shallow mode, we use a single key per thread (no checkpoint_id in key)\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n\n // Get the previous checkpoint to know what to clean up\n let prevCheckpointData: any = null;\n let prevCheckpointId: string | null = null;\n try {\n prevCheckpointData = await this.client.json.get(key);\n if (prevCheckpointData && typeof prevCheckpointData === \"object\") {\n prevCheckpointId = prevCheckpointData.checkpoint_id;\n }\n } catch (error) {\n // Key doesn't exist yet, that's fine\n }\n\n // Clean up old checkpoint and related data if it exists\n if (prevCheckpointId && prevCheckpointId !== checkpointId) {\n await this.cleanupOldCheckpoint(threadId, checkpointNs, prevCheckpointId);\n }\n\n // Store channel values inline - no blob storage in shallow mode\n const checkpointCopy = {\n ...checkpoint,\n channel_values: checkpoint.channel_values || {},\n // Remove channel_blobs if present\n channel_blobs: undefined,\n };\n\n // Structure matching Python implementation\n const jsonDoc: any = {\n thread_id: threadId,\n checkpoint_ns: checkpointNs,\n checkpoint_id: checkpointId,\n parent_checkpoint_id: parentCheckpointId || null,\n checkpoint: checkpointCopy,\n metadata: this.sanitizeMetadata(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);\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 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 // In shallow mode, we use a single key per thread\n const key = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const jsonDoc = await this.client.json.get(key);\n\n if (!jsonDoc) {\n return undefined;\n }\n\n // If a specific checkpoint_id was requested, check if it matches\n if (checkpointId && jsonDoc.checkpoint_id !== checkpointId) {\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 // 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 // Load pending writes if they exist\n let pendingWrites: Array<[string, string, any]> | undefined;\n if (jsonDoc.has_writes === \"true\") {\n pendingWrites = await this.loadPendingWrites(\n jsonDoc.thread_id,\n jsonDoc.checkpoint_ns,\n jsonDoc.checkpoint_id\n );\n }\n\n return await this.createCheckpointTuple(jsonDoc, checkpoint, pendingWrites);\n }\n\n async *list(\n config: RunnableConfig | null,\n options?: CheckpointListOptions & { filter?: CheckpointMetadata }\n ): AsyncGenerator<CheckpointTuple> {\n await this.ensureIndexes();\n\n // In shallow mode, we only return the latest checkpoint per thread\n if (config?.configurable?.thread_id) {\n // Single thread case\n const tuple = await this.getTuple(config);\n if (tuple) {\n // Apply filter if provided\n if (options?.filter) {\n if (this.checkMetadataFilterMatch(tuple.metadata, options.filter)) {\n yield tuple;\n }\n } else {\n yield tuple;\n }\n }\n } else {\n // All threads case - use search\n const queryParts: string[] = [];\n\n // Add metadata filters\n if (options?.filter) {\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 }\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 const results = await this.client.ft.search(\"checkpoints\", query, {\n LIMIT: { from: 0, size: limit * 2 }, // Get more since we'll deduplicate\n SORTBY: { BY: \"checkpoint_ts\", DIRECTION: \"DESC\" },\n });\n\n // In shallow mode, deduplicate by thread_id\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n\n for (const doc of results.documents) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = doc.value;\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check null filters manually if needed\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n } catch (error: any) {\n if (error.message?.includes(\"no such index\")) {\n // Index doesn't exist yet, fall back to scanning all shallow checkpoints\n const pattern = `checkpoint:*:*:shallow`;\n const keys = await this.client.keys(pattern);\n\n if (keys.length === 0) {\n return;\n }\n\n // Sort keys to have consistent ordering\n keys.sort().reverse();\n\n // Get unique threads\n const seenThreads = new Set<string>();\n let yieldCount = 0;\n const limit = options?.limit ?? 10;\n\n for (const key of keys) {\n if (yieldCount >= limit) break;\n\n const jsonDoc = await this.client.json.get(key);\n if (!jsonDoc) continue;\n\n const threadKey = `${jsonDoc.thread_id}:${jsonDoc.checkpoint_ns}`;\n\n // Skip if we've already seen this thread\n if (seenThreads.has(threadKey)) {\n continue;\n }\n seenThreads.add(threadKey);\n\n // Check filter if provided\n if (options?.filter) {\n if (\n !this.checkMetadataFilterMatch(jsonDoc.metadata, options.filter)\n ) {\n continue;\n }\n }\n\n // Channel values are inline in shallow mode\n const checkpoint: Checkpoint = await this.serde.loadsTyped(\n \"json\",\n JSON.stringify(jsonDoc.checkpoint)\n );\n\n yield await this.createCheckpointTuple(jsonDoc, checkpoint);\n yieldCount++;\n }\n return;\n }\n throw error;\n }\n }\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 // In shallow mode, we overwrite all writes for the task\n // First, clean up old writes for this task\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${checkpointId}:${taskId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Store new writes\n const writeKeys: string[] = [];\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 };\n\n await this.client.json.set(writeKey, \"$\", writeDoc);\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 // Clear existing entries for this task and add new ones\n const zaddArgs: Record<string, number> = {};\n writeKeys.forEach((key, idx) => {\n zaddArgs[key] = 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 await this.applyTTL(...writeKeys, zsetKey);\n }\n }\n\n // Update checkpoint to indicate it has writes\n const checkpointKey = `checkpoint:${threadId}:${checkpointNs}:shallow`;\n const checkpointExists = await this.client.exists(checkpointKey);\n if (checkpointExists) {\n const currentDoc = await this.client.json.get(checkpointKey);\n if (currentDoc) {\n currentDoc.has_writes = \"true\";\n await this.client.json.set(checkpointKey, \"$\", currentDoc);\n }\n }\n }\n\n async deleteThread(threadId: string): Promise<void> {\n // Delete shallow checkpoints\n const checkpointPattern = `checkpoint:${threadId}:*:shallow`;\n const checkpointKeys = await this.client.keys(checkpointPattern);\n\n if (checkpointKeys.length > 0) {\n await this.client.del(checkpointKeys);\n }\n\n // Delete writes\n const writesPattern = `checkpoint_write:${threadId}:*`;\n const writesKeys = await this.client.keys(writesPattern);\n\n if (writesKeys.length > 0) {\n await this.client.del(writesKeys);\n }\n\n // Delete write registries\n const zsetPattern = `write_keys_zset:${threadId}:*`;\n const zsetKeys = await this.client.keys(zsetPattern);\n\n if (zsetKeys.length > 0) {\n await this.client.del(zsetKeys);\n }\n }\n\n async end(): Promise<void> {\n await this.client.quit();\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 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 // 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: jsonDoc.checkpoint_ns,\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: jsonDoc.checkpoint_ns,\n checkpoint_id: jsonDoc.parent_checkpoint_id,\n },\n }\n : undefined,\n pendingWrites,\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 // 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 const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${checkpointId}`;\n const writeKeys = await this.client.zRange(zsetKey, 0, -1);\n\n if (writeKeys.length === 0) {\n return undefined;\n }\n\n const pendingWrites: Array<[string, string, any]> = [];\n for (const writeKey of writeKeys) {\n const writeDoc = await this.client.json.get(writeKey);\n if (writeDoc) {\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\n return pendingWrites;\n }\n\n // Helper method to check metadata filter matches\n private checkMetadataFilterMatch(\n metadata: any,\n filter: CheckpointMetadata\n ): boolean {\n for (const [key, value] of Object.entries(filter)) {\n const metadataValue = metadata?.[key];\n if (value === null) {\n if (!(key in (metadata || {})) || metadataValue !== null) {\n return false;\n }\n } else if (typeof value === \"object\" && !Array.isArray(value)) {\n // Deep comparison for objects with deterministic key ordering\n if (typeof metadataValue !== \"object\" || metadataValue === null) {\n return false;\n }\n if (\n deterministicStringify(value) !==\n deterministicStringify(metadataValue)\n ) {\n return false;\n }\n } else if (metadataValue !== value) {\n return false;\n }\n }\n return true;\n }\n\n private async cleanupOldCheckpoint(\n threadId: string,\n checkpointNs: string,\n oldCheckpointId: string\n ): Promise<void> {\n // Clean up old writes\n const writePattern = `checkpoint_write:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldWriteKeys = await this.client.keys(writePattern);\n if (oldWriteKeys.length > 0) {\n await this.client.del(oldWriteKeys);\n }\n\n // Clean up write registry\n const zsetKey = `write_keys_zset:${threadId}:${checkpointNs}:${oldCheckpointId}`;\n await this.client.del(zsetKey);\n\n // Note: We don't clean up blob keys in shallow mode since we store inline\n // But for completeness, clean up any legacy blob keys if they exist\n const blobPattern = `checkpoint_blob:${threadId}:${checkpointNs}:${oldCheckpointId}:*`;\n const oldBlobKeys = await this.client.keys(blobPattern);\n if (oldBlobKeys.length > 0) {\n await this.client.del(oldBlobKeys);\n }\n }\n\n private sanitizeMetadata(metadata: CheckpointMetadata): CheckpointMetadata {\n if (!metadata) return {} as CheckpointMetadata;\n\n const sanitized: any = {};\n for (const [key, value] of Object.entries(metadata)) {\n // Remove null characters from keys and string values\n // eslint-disable-next-line no-control-regex\n const sanitizedKey = key.replace(/\\x00/g, \"\");\n sanitized[sanitizedKey] =\n // eslint-disable-next-line no-control-regex\n typeof value === \"string\" ? value.replace(/\\x00/g, \"\") : value;\n }\n return sanitized as CheckpointMetadata;\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, {\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"],"mappings":"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"}
|