@ferricstore/ferricstore 0.11.11 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -1
- package/dist/durability-DlDCsdlo.d.cts +16 -0
- package/dist/durability-DplL0SbW.d.ts +16 -0
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -162
- package/dist/index.d.ts +5 -162
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/internal-lEEDZpPH.d.cts +4 -0
- package/dist/internal-lEEDZpPH.d.ts +4 -0
- package/dist/langgraph.cjs +1278 -0
- package/dist/langgraph.cjs.map +1 -0
- package/dist/langgraph.d.cts +148 -0
- package/dist/langgraph.d.ts +148 -0
- package/dist/langgraph.js +1252 -0
- package/dist/langgraph.js.map +1 -0
- package/dist/openai-agents.cjs +580 -0
- package/dist/openai-agents.cjs.map +1 -0
- package/dist/openai-agents.d.cts +44 -0
- package/dist/openai-agents.d.ts +44 -0
- package/dist/openai-agents.js +555 -0
- package/dist/openai-agents.js.map +1 -0
- package/dist/outcomes-BbFDp3AH.d.ts +160 -0
- package/dist/outcomes-DmBwnq0Y.d.cts +160 -0
- package/docs/agent-frameworks.md +159 -0
- package/docs/api/assets/highlight.css +12 -12
- package/docs/api/classes/ClaimHydrationError.html +2 -2
- package/docs/api/classes/ConnectionClosedError.html +2 -2
- package/docs/api/classes/FerricStoreError.html +2 -2
- package/docs/api/classes/FlowAlreadyExistsError.html +2 -2
- package/docs/api/classes/FlowBatchError.html +2 -2
- package/docs/api/classes/FlowNotFoundError.html +2 -2
- package/docs/api/classes/FlowQueryError.html +2 -2
- package/docs/api/classes/FlowWrongStateError.html +2 -2
- package/docs/api/classes/HTTPTransportError.html +2 -2
- package/docs/api/classes/InvalidCommandError.html +2 -2
- package/docs/api/classes/LeaseRenewalError.html +2 -2
- package/docs/api/classes/LockHeldError.html +2 -2
- package/docs/api/classes/LockNotOwnedError.html +2 -2
- package/docs/api/classes/OverloadedError.html +2 -2
- package/docs/api/classes/QueueCompletionError.html +2 -2
- package/docs/api/classes/RequestTimeoutError.html +2 -2
- package/docs/api/classes/RerouteError.html +2 -2
- package/docs/api/classes/StaleLeaseError.html +2 -2
- package/docs/api/classes/StalePolicyGenerationError.html +2 -2
- package/docs/api/index.html +34 -24
- package/docs/api/media/agent-frameworks.md +159 -0
- package/docs/api/media/langgraph.ts +23 -0
- package/docs/api/media/openai-agents-session.ts +13 -0
- package/docs/api/variables/FERRICSTORE_SDK_VERSION.html +1 -1
- package/package.json +41 -2
|
@@ -0,0 +1,1252 @@
|
|
|
1
|
+
// src/langgraph/checkpoint.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
import {
|
|
4
|
+
BaseCheckpointSaver,
|
|
5
|
+
copyCheckpoint
|
|
6
|
+
} from "@langchain/langgraph";
|
|
7
|
+
|
|
8
|
+
// src/agent-persistence/durability.ts
|
|
9
|
+
import { randomUUID } from "crypto";
|
|
10
|
+
import { setTimeout as delay } from "timers/promises";
|
|
11
|
+
|
|
12
|
+
// src/errors.ts
|
|
13
|
+
var FerricStoreError = class extends Error {
|
|
14
|
+
code = "ferricstore_error";
|
|
15
|
+
raw;
|
|
16
|
+
retryable;
|
|
17
|
+
safeToRetry;
|
|
18
|
+
retryAfterMs;
|
|
19
|
+
constructor(message, options = {}) {
|
|
20
|
+
super(message, { cause: options.cause });
|
|
21
|
+
this.name = new.target.name;
|
|
22
|
+
this.raw = options.raw;
|
|
23
|
+
this.retryable = options.retryable ?? structuredBooleanField(options.raw, "retryable");
|
|
24
|
+
this.safeToRetry = options.safeToRetry ?? structuredBooleanField(options.raw, "safe_to_retry");
|
|
25
|
+
this.retryAfterMs = options.retryAfterMs ?? structuredIntegerField(options.raw, "retry_after_ms");
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
var LockHeldError = class extends FerricStoreError {
|
|
29
|
+
code = "lock_held";
|
|
30
|
+
};
|
|
31
|
+
function structuredIntegerField(raw, name) {
|
|
32
|
+
const value = structuredField(raw, name);
|
|
33
|
+
if (typeof value === "number") {
|
|
34
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "bigint") {
|
|
37
|
+
return value >= 0n && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : void 0;
|
|
38
|
+
}
|
|
39
|
+
const text = binaryText(value);
|
|
40
|
+
return text == null ? void 0 : nonNegativeSafeIntegerText(text);
|
|
41
|
+
}
|
|
42
|
+
function structuredBooleanField(raw, name) {
|
|
43
|
+
const value = structuredField(raw, name);
|
|
44
|
+
if (typeof value === "boolean") return value;
|
|
45
|
+
const text = binaryText(value)?.toLowerCase();
|
|
46
|
+
if (text === "true" || text === "1") return true;
|
|
47
|
+
if (text === "false" || text === "0") return false;
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
function nonNegativeSafeIntegerText(value) {
|
|
51
|
+
if (!/^[0-9]+$/u.test(value)) return void 0;
|
|
52
|
+
const parsed = Number.parseInt(value, 10);
|
|
53
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
54
|
+
}
|
|
55
|
+
function structuredField(raw, name) {
|
|
56
|
+
if (raw instanceof Map) {
|
|
57
|
+
if (raw.has(name)) return raw.get(name);
|
|
58
|
+
for (const [key, value] of raw.entries()) {
|
|
59
|
+
if (binaryText(key) === name) return value;
|
|
60
|
+
}
|
|
61
|
+
return void 0;
|
|
62
|
+
}
|
|
63
|
+
if (typeof raw === "object" && raw != null && Object.hasOwn(raw, name)) {
|
|
64
|
+
return raw[name];
|
|
65
|
+
}
|
|
66
|
+
return void 0;
|
|
67
|
+
}
|
|
68
|
+
function binaryText(value) {
|
|
69
|
+
if (typeof value === "string") return value;
|
|
70
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value).toString("utf8");
|
|
71
|
+
return void 0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/agent-persistence/durability.ts
|
|
75
|
+
var DEFAULT_LOCK_OPTIONS = {
|
|
76
|
+
lockRetryMs: 10,
|
|
77
|
+
lockTtlMs: 3e5,
|
|
78
|
+
lockWaitMs: 3e4
|
|
79
|
+
};
|
|
80
|
+
function normalizeKeyPrefix(value, defaultValue) {
|
|
81
|
+
const prefix = value.length === 0 ? defaultValue : value;
|
|
82
|
+
if (prefix.includes("\0")) throw new TypeError("keyPrefix must not contain NUL bytes");
|
|
83
|
+
const normalized = prefix.replace(/:+$/u, "");
|
|
84
|
+
if (normalized.length === 0) throw new TypeError("keyPrefix must contain a character other than ':'");
|
|
85
|
+
return normalized;
|
|
86
|
+
}
|
|
87
|
+
function positiveInteger(value, fallback, name) {
|
|
88
|
+
const normalized = value ?? fallback;
|
|
89
|
+
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
|
90
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
91
|
+
}
|
|
92
|
+
return normalized;
|
|
93
|
+
}
|
|
94
|
+
function nonNegativeInteger(value, fallback, name) {
|
|
95
|
+
const normalized = value ?? fallback;
|
|
96
|
+
if (!Number.isSafeInteger(normalized) || normalized < 0) {
|
|
97
|
+
throw new TypeError(`${name} must be a non-negative safe integer`);
|
|
98
|
+
}
|
|
99
|
+
return normalized;
|
|
100
|
+
}
|
|
101
|
+
function textResponse(value, name) {
|
|
102
|
+
if (typeof value === "string") return value;
|
|
103
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value).toString("utf8");
|
|
104
|
+
throw new TypeError(`FerricStore returned an invalid ${name}`);
|
|
105
|
+
}
|
|
106
|
+
function arrayResponse(value, name) {
|
|
107
|
+
if (!Array.isArray(value)) throw new TypeError(`FerricStore returned an invalid ${name}`);
|
|
108
|
+
return value;
|
|
109
|
+
}
|
|
110
|
+
function integerResponse(value, name) {
|
|
111
|
+
const parsed = typeof value === "number" ? value : Number(textResponse(value, name));
|
|
112
|
+
if (!Number.isSafeInteger(parsed)) throw new TypeError(`FerricStore returned an invalid ${name}`);
|
|
113
|
+
return parsed;
|
|
114
|
+
}
|
|
115
|
+
async function withMutationLocks(client, keys, operation, options = {}) {
|
|
116
|
+
const orderedKeys = [...new Set(keys)].sort();
|
|
117
|
+
if (orderedKeys.length === 0) return await operation();
|
|
118
|
+
const normalized = {
|
|
119
|
+
lockRetryMs: positiveInteger(options.lockRetryMs, DEFAULT_LOCK_OPTIONS.lockRetryMs, "lockRetryMs"),
|
|
120
|
+
lockTtlMs: positiveInteger(options.lockTtlMs, DEFAULT_LOCK_OPTIONS.lockTtlMs, "lockTtlMs"),
|
|
121
|
+
lockWaitMs: nonNegativeInteger(options.lockWaitMs, DEFAULT_LOCK_OPTIONS.lockWaitMs, "lockWaitMs")
|
|
122
|
+
};
|
|
123
|
+
const owner = randomUUID();
|
|
124
|
+
const acquired = [];
|
|
125
|
+
const deadline = performance.now() + normalized.lockWaitMs;
|
|
126
|
+
let primaryError;
|
|
127
|
+
let heartbeatError;
|
|
128
|
+
let releaseError;
|
|
129
|
+
let result;
|
|
130
|
+
let operationCompleted = false;
|
|
131
|
+
const heartbeatAbort = new AbortController();
|
|
132
|
+
try {
|
|
133
|
+
for (const key of orderedKeys) {
|
|
134
|
+
while (!await tryAcquireLock(client, key, owner, normalized.lockTtlMs)) {
|
|
135
|
+
if (performance.now() >= deadline) {
|
|
136
|
+
throw new Error(`timed out acquiring FerricStore lock ${JSON.stringify(key)}`);
|
|
137
|
+
}
|
|
138
|
+
await delay(normalized.lockRetryMs);
|
|
139
|
+
}
|
|
140
|
+
acquired.push(key);
|
|
141
|
+
}
|
|
142
|
+
const heartbeat = renewLocks(
|
|
143
|
+
client,
|
|
144
|
+
acquired,
|
|
145
|
+
owner,
|
|
146
|
+
normalized.lockTtlMs,
|
|
147
|
+
heartbeatAbort.signal,
|
|
148
|
+
(error) => {
|
|
149
|
+
heartbeatError ??= error;
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
try {
|
|
153
|
+
result = await operation();
|
|
154
|
+
operationCompleted = true;
|
|
155
|
+
} catch (error) {
|
|
156
|
+
primaryError = error;
|
|
157
|
+
} finally {
|
|
158
|
+
heartbeatAbort.abort();
|
|
159
|
+
await heartbeat;
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
primaryError ??= error;
|
|
163
|
+
} finally {
|
|
164
|
+
heartbeatAbort.abort();
|
|
165
|
+
for (const key of acquired.reverse()) {
|
|
166
|
+
try {
|
|
167
|
+
await client.command("UNLOCK", key, owner);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
releaseError ??= error;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (primaryError != null) throw errorObject(primaryError);
|
|
174
|
+
if (heartbeatError != null) throw errorObject(heartbeatError);
|
|
175
|
+
if (releaseError != null) throw errorObject(releaseError);
|
|
176
|
+
if (!operationCompleted) throw new Error("FerricStore mutation did not complete");
|
|
177
|
+
return result;
|
|
178
|
+
}
|
|
179
|
+
async function tryAcquireLock(client, key, owner, ttlMs) {
|
|
180
|
+
try {
|
|
181
|
+
const response = await client.command("LOCK", key, owner, ttlMs);
|
|
182
|
+
return response === true || response === "OK" || Buffer.isBuffer(response) && response.equals(Buffer.from("OK"));
|
|
183
|
+
} catch (error) {
|
|
184
|
+
if (error instanceof LockHeldError) return false;
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async function renewLocks(client, keys, owner, ttlMs, signal, onError) {
|
|
189
|
+
const intervalMs = Math.max(Math.floor(ttlMs / 3), 10);
|
|
190
|
+
const retryMs = Math.min(Math.max(Math.floor(intervalMs / 10), 10), 1e3);
|
|
191
|
+
const lastExtended = new Map(keys.map((key) => [key, performance.now()]));
|
|
192
|
+
let waitMs = intervalMs;
|
|
193
|
+
while (!signal.aborted) {
|
|
194
|
+
try {
|
|
195
|
+
await delay(waitMs, void 0, { signal });
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (signal.aborted) return;
|
|
198
|
+
onError(error);
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const now = performance.now();
|
|
202
|
+
let retry = false;
|
|
203
|
+
for (const key of keys) {
|
|
204
|
+
try {
|
|
205
|
+
const response = await client.command("EXTEND", key, owner, ttlMs);
|
|
206
|
+
if (integerResponse(response, "EXTEND response") !== 1) {
|
|
207
|
+
onError(new Error(`lost FerricStore lock ${JSON.stringify(key)} while mutating data`));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
lastExtended.set(key, now);
|
|
211
|
+
} catch (error) {
|
|
212
|
+
if (now - (lastExtended.get(key) ?? 0) >= ttlMs) {
|
|
213
|
+
onError(new Error(`lost FerricStore lock ${JSON.stringify(key)} while mutating data`, { cause: error }));
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
retry = true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
waitMs = retry ? retryMs : intervalMs;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function errorObject(value) {
|
|
223
|
+
return value instanceof Error ? value : new Error("FerricStore mutation failed", { cause: value });
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// src/langgraph/checkpoint.ts
|
|
227
|
+
var FORMAT_VERSION = 1;
|
|
228
|
+
var CHECKPOINT_FIELD_PREFIX = "checkpoint:";
|
|
229
|
+
var WRITE_FIELD_PREFIX = "write:";
|
|
230
|
+
var WRITES_INDEX = {
|
|
231
|
+
__error__: -1,
|
|
232
|
+
__interrupt__: -3,
|
|
233
|
+
__resume__: -4,
|
|
234
|
+
__scheduled__: -2
|
|
235
|
+
};
|
|
236
|
+
var FerricStoreSaver = class extends BaseCheckpointSaver {
|
|
237
|
+
client;
|
|
238
|
+
keyPrefix;
|
|
239
|
+
scanCount;
|
|
240
|
+
lockOptions;
|
|
241
|
+
constructor(client, options = {}) {
|
|
242
|
+
super(options.serde);
|
|
243
|
+
this.client = client;
|
|
244
|
+
this.keyPrefix = normalizeKeyPrefix(options.keyPrefix ?? "langgraph:checkpoint", "langgraph:checkpoint");
|
|
245
|
+
this.scanCount = positiveInteger(options.scanCount, 256, "scanCount");
|
|
246
|
+
this.lockOptions = {
|
|
247
|
+
lockRetryMs: options.lockRetryMs,
|
|
248
|
+
lockTtlMs: options.lockTtlMs,
|
|
249
|
+
lockWaitMs: options.lockWaitMs
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
async getTuple(config) {
|
|
253
|
+
const { checkpointNs, threadId } = identity(config);
|
|
254
|
+
const key = this.threadKey(threadId, checkpointNs);
|
|
255
|
+
let id = checkpointId(config);
|
|
256
|
+
let record;
|
|
257
|
+
if (id != null) {
|
|
258
|
+
record = await this.readRecord(key, id);
|
|
259
|
+
} else {
|
|
260
|
+
let offset = 0;
|
|
261
|
+
while (true) {
|
|
262
|
+
const values = arrayResponse(
|
|
263
|
+
await this.client.command("ZREVRANGE", this.checkpointIndexKey(key), offset, offset),
|
|
264
|
+
"ZREVRANGE checkpoint response"
|
|
265
|
+
);
|
|
266
|
+
if (values.length === 0) return void 0;
|
|
267
|
+
id = textResponse(values[0], "checkpoint ID");
|
|
268
|
+
record = await this.readRecord(key, id);
|
|
269
|
+
if (record != null) break;
|
|
270
|
+
offset += 1;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (id == null || record == null) return void 0;
|
|
274
|
+
return await this.tupleFromRecord(key, record);
|
|
275
|
+
}
|
|
276
|
+
async *list(config, options = {}) {
|
|
277
|
+
const limit = options.limit;
|
|
278
|
+
if (limit != null && (!Number.isSafeInteger(limit) || limit < 0)) {
|
|
279
|
+
throw new TypeError("limit must be a non-negative safe integer");
|
|
280
|
+
}
|
|
281
|
+
if (limit === 0) return;
|
|
282
|
+
const beforeId = options.before == null ? void 0 : checkpointId(options.before);
|
|
283
|
+
const configurable = config.configurable;
|
|
284
|
+
const threadId = optionalText(configurable?.thread_id, "thread_id");
|
|
285
|
+
const expectedId = optionalText(configurable?.checkpoint_id ?? configurable?.thread_ts, "checkpoint_id");
|
|
286
|
+
const namespaceSpecified = configurable != null && Object.hasOwn(configurable, "checkpoint_ns");
|
|
287
|
+
const checkpointNs = namespaceSpecified ? requiredText(configurable?.checkpoint_ns, "checkpoint_ns", true) : void 0;
|
|
288
|
+
const matches = [];
|
|
289
|
+
if (threadId == null) {
|
|
290
|
+
await this.collectGlobal(matches, beforeId, expectedId, options.filter, limit);
|
|
291
|
+
} else {
|
|
292
|
+
const keys = checkpointNs == null ? await this.threadKeys(threadId) : [this.threadKey(threadId, checkpointNs)];
|
|
293
|
+
for (const key of keys) {
|
|
294
|
+
const ids = expectedId == null ? await this.checkpointIds(key) : [expectedId];
|
|
295
|
+
for (const id of ids) {
|
|
296
|
+
if (beforeId != null && id >= beforeId) continue;
|
|
297
|
+
const record = await this.readRecord(key, id);
|
|
298
|
+
if (record == null || !metadataMatches(record.metadata, options.filter)) continue;
|
|
299
|
+
matches.push({ key, record });
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
matches.sort((left, right) => right.record.checkpointId.localeCompare(left.record.checkpointId));
|
|
303
|
+
if (limit != null) matches.splice(limit);
|
|
304
|
+
}
|
|
305
|
+
for (const match of matches) yield await this.tupleFromRecord(match.key, match.record);
|
|
306
|
+
}
|
|
307
|
+
async put(config, checkpoint, metadata, _newVersions) {
|
|
308
|
+
void _newVersions;
|
|
309
|
+
const { checkpointNs, threadId } = identity(config);
|
|
310
|
+
if (typeof checkpoint.id !== "string" || checkpoint.id.length === 0) {
|
|
311
|
+
throw new TypeError("checkpoint.id must be a non-empty string");
|
|
312
|
+
}
|
|
313
|
+
const parentCheckpointId = checkpointId(config);
|
|
314
|
+
const record = {
|
|
315
|
+
checkpoint: copyCheckpoint(checkpoint),
|
|
316
|
+
checkpointId: checkpoint.id,
|
|
317
|
+
checkpointNs,
|
|
318
|
+
formatVersion: FORMAT_VERSION,
|
|
319
|
+
metadata,
|
|
320
|
+
...parentCheckpointId == null ? {} : { parentCheckpointId },
|
|
321
|
+
threadId
|
|
322
|
+
};
|
|
323
|
+
const key = this.threadKey(threadId, checkpointNs);
|
|
324
|
+
await withMutationLocks(this.client, [this.threadLockKey(threadId)], async () => {
|
|
325
|
+
const locator = checkpointLocator(checkpoint.id, key);
|
|
326
|
+
await this.client.command("SADD", this.threadCatalogKey(threadId), key);
|
|
327
|
+
await this.client.command("ZADD", this.threadLocatorCatalogKey(threadId), 0, locator);
|
|
328
|
+
await this.client.command("ZADD", this.catalogKey(), 0, locator);
|
|
329
|
+
await this.client.command("ZADD", this.checkpointIndexKey(key), 0, checkpoint.id);
|
|
330
|
+
await this.client.command("HSET", key, checkpointField(checkpoint.id), await this.serialize(record));
|
|
331
|
+
}, this.lockOptions);
|
|
332
|
+
return checkpointConfig(threadId, checkpointNs, checkpoint.id);
|
|
333
|
+
}
|
|
334
|
+
async putWrites(config, writes, taskId) {
|
|
335
|
+
const { checkpointNs, threadId } = identity(config);
|
|
336
|
+
const id = checkpointId(config);
|
|
337
|
+
if (id == null) throw new TypeError("putWrites requires configurable.checkpoint_id");
|
|
338
|
+
if (typeof taskId !== "string" || taskId.length === 0) throw new TypeError("taskId must be non-empty text");
|
|
339
|
+
const snapshots = await Promise.all(writes.map(async ([channel, value], fallbackIndex) => {
|
|
340
|
+
if (typeof channel !== "string") throw new TypeError("pending-write channel must be text");
|
|
341
|
+
const index = WRITES_INDEX[channel] ?? fallbackIndex;
|
|
342
|
+
const record = {
|
|
343
|
+
channel,
|
|
344
|
+
formatVersion: FORMAT_VERSION,
|
|
345
|
+
index,
|
|
346
|
+
taskId,
|
|
347
|
+
value
|
|
348
|
+
};
|
|
349
|
+
return { index, value: await this.serialize(record) };
|
|
350
|
+
}));
|
|
351
|
+
const key = this.threadKey(threadId, checkpointNs);
|
|
352
|
+
await withMutationLocks(this.client, [this.threadLockKey(threadId)], async () => {
|
|
353
|
+
await this.client.command("SADD", this.threadCatalogKey(threadId), key);
|
|
354
|
+
for (const snapshot of snapshots) {
|
|
355
|
+
await this.client.command(
|
|
356
|
+
snapshot.index < 0 ? "HSET" : "HSETNX",
|
|
357
|
+
key,
|
|
358
|
+
writeField(id, taskId, snapshot.index),
|
|
359
|
+
snapshot.value
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}, this.lockOptions);
|
|
363
|
+
}
|
|
364
|
+
async deleteThread(threadId) {
|
|
365
|
+
const normalized = requiredText(threadId, "threadId");
|
|
366
|
+
await withMutationLocks(this.client, [this.threadLockKey(normalized)], async () => {
|
|
367
|
+
for (const key of await this.threadKeys(normalized)) {
|
|
368
|
+
await this.client.command("DEL", key, this.checkpointIndexKey(key));
|
|
369
|
+
}
|
|
370
|
+
const locatorKey = this.threadLocatorCatalogKey(normalized);
|
|
371
|
+
while (true) {
|
|
372
|
+
const locators = arrayResponse(
|
|
373
|
+
await this.client.command("ZRANGE", locatorKey, 0, this.scanCount - 1),
|
|
374
|
+
"thread checkpoint locator response"
|
|
375
|
+
);
|
|
376
|
+
if (locators.length === 0) break;
|
|
377
|
+
await this.client.command("ZREM", this.catalogKey(), ...asArguments(locators));
|
|
378
|
+
await this.client.command("ZREM", locatorKey, ...asArguments(locators));
|
|
379
|
+
}
|
|
380
|
+
await this.client.command("DEL", this.threadCatalogKey(normalized), locatorKey);
|
|
381
|
+
}, this.lockOptions);
|
|
382
|
+
}
|
|
383
|
+
catalogKey() {
|
|
384
|
+
return `${this.keyPrefix}:checkpoints`;
|
|
385
|
+
}
|
|
386
|
+
threadCatalogKey(threadId) {
|
|
387
|
+
const digest = sha256(threadId);
|
|
388
|
+
return `${this.keyPrefix}:{lgt:${digest}}:namespaces`;
|
|
389
|
+
}
|
|
390
|
+
threadLocatorCatalogKey(threadId) {
|
|
391
|
+
return `${this.threadCatalogKey(threadId)}:checkpoint-locators`;
|
|
392
|
+
}
|
|
393
|
+
threadLockKey(threadId) {
|
|
394
|
+
return `${this.keyPrefix}:{lgt:${sha256(threadId)}}:mutation-lock`;
|
|
395
|
+
}
|
|
396
|
+
threadKey(threadId, checkpointNs) {
|
|
397
|
+
return `${this.keyPrefix}:{lg:${sha256(lengthPrefixed([threadId, checkpointNs]))}}:thread`;
|
|
398
|
+
}
|
|
399
|
+
checkpointIndexKey(threadKey) {
|
|
400
|
+
return `${threadKey}:checkpoint-index`;
|
|
401
|
+
}
|
|
402
|
+
async serialize(value) {
|
|
403
|
+
const [type, data] = await this.serde.dumpsTyped(value);
|
|
404
|
+
const typeBytes = Buffer.from(type, "utf8");
|
|
405
|
+
if (typeBytes.length > 65535) throw new TypeError("serialized LangGraph type name is too long");
|
|
406
|
+
const header = Buffer.allocUnsafe(2);
|
|
407
|
+
header.writeUInt16BE(typeBytes.length);
|
|
408
|
+
return Buffer.concat([header, typeBytes, Buffer.from(data)]);
|
|
409
|
+
}
|
|
410
|
+
async deserialize(value, name) {
|
|
411
|
+
if (!(typeof value === "string" || Buffer.isBuffer(value) || value instanceof Uint8Array)) {
|
|
412
|
+
throw new TypeError(`FerricStore returned a non-binary ${name}`);
|
|
413
|
+
}
|
|
414
|
+
const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value);
|
|
415
|
+
if (bytes.length < 2) throw new Error(`invalid FerricStore ${name}`);
|
|
416
|
+
const typeLength = bytes.readUInt16BE(0);
|
|
417
|
+
if (bytes.length < typeLength + 2) throw new Error(`truncated FerricStore ${name}`);
|
|
418
|
+
const type = bytes.subarray(2, typeLength + 2).toString("utf8");
|
|
419
|
+
return await this.serde.loadsTyped(type, bytes.subarray(typeLength + 2));
|
|
420
|
+
}
|
|
421
|
+
async readRecord(key, id) {
|
|
422
|
+
const value = await this.client.command("HGET", key, checkpointField(id));
|
|
423
|
+
if (value == null) return void 0;
|
|
424
|
+
const record = await this.deserialize(value, "LangGraph checkpoint record");
|
|
425
|
+
if (record.formatVersion !== FORMAT_VERSION || record.checkpointId !== id) {
|
|
426
|
+
throw new Error("unsupported or corrupt FerricStore LangGraph checkpoint record");
|
|
427
|
+
}
|
|
428
|
+
return record;
|
|
429
|
+
}
|
|
430
|
+
async pendingWrites(key, id) {
|
|
431
|
+
const fields = await this.scanHash(key, `${WRITE_FIELD_PREFIX}${encodeComponent(id)}:*`);
|
|
432
|
+
const records = await Promise.all(fields.map(async ([, value]) => await this.deserialize(value, "LangGraph pending write")));
|
|
433
|
+
for (const record of records) {
|
|
434
|
+
if (record.formatVersion !== FORMAT_VERSION) {
|
|
435
|
+
throw new Error("unsupported FerricStore LangGraph pending-write format");
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
records.sort((left, right) => left.taskId.localeCompare(right.taskId) || left.index - right.index);
|
|
439
|
+
return records.map((record) => [record.taskId, record.channel, record.value]);
|
|
440
|
+
}
|
|
441
|
+
async tupleFromRecord(key, record) {
|
|
442
|
+
return {
|
|
443
|
+
checkpoint: record.checkpoint,
|
|
444
|
+
config: checkpointConfig(record.threadId, record.checkpointNs, record.checkpointId),
|
|
445
|
+
metadata: record.metadata,
|
|
446
|
+
pendingWrites: await this.pendingWrites(key, record.checkpointId),
|
|
447
|
+
...record.parentCheckpointId == null ? {} : { parentConfig: checkpointConfig(record.threadId, record.checkpointNs, record.parentCheckpointId) }
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
async scanHash(key, pattern) {
|
|
451
|
+
let cursor = 0;
|
|
452
|
+
const result = [];
|
|
453
|
+
do {
|
|
454
|
+
const response = arrayResponse(
|
|
455
|
+
await this.client.command("HSCAN", key, cursor, "MATCH", pattern, "COUNT", this.scanCount),
|
|
456
|
+
"HSCAN response"
|
|
457
|
+
);
|
|
458
|
+
if (response.length !== 2) throw new Error("FerricStore HSCAN response must contain cursor and items");
|
|
459
|
+
cursor = integerResponse(response[0], "HSCAN cursor");
|
|
460
|
+
const values = response[1];
|
|
461
|
+
if (values instanceof Map) {
|
|
462
|
+
result.push(...values.entries());
|
|
463
|
+
} else {
|
|
464
|
+
const flat = arrayResponse(values, "HSCAN items");
|
|
465
|
+
if (flat.length % 2 !== 0) throw new Error("FerricStore HSCAN returned an odd item count");
|
|
466
|
+
for (let index = 0; index < flat.length; index += 2) result.push([flat[index], flat[index + 1]]);
|
|
467
|
+
}
|
|
468
|
+
} while (cursor !== 0);
|
|
469
|
+
return result;
|
|
470
|
+
}
|
|
471
|
+
async threadKeys(threadId) {
|
|
472
|
+
const values = arrayResponse(
|
|
473
|
+
await this.client.command("SMEMBERS", this.threadCatalogKey(threadId)),
|
|
474
|
+
"thread checkpoint catalog response"
|
|
475
|
+
);
|
|
476
|
+
return values.map((value) => textResponse(value, "thread checkpoint key")).sort();
|
|
477
|
+
}
|
|
478
|
+
async checkpointIds(key) {
|
|
479
|
+
const values = arrayResponse(
|
|
480
|
+
await this.client.command("ZREVRANGE", this.checkpointIndexKey(key), 0, -1),
|
|
481
|
+
"checkpoint index response"
|
|
482
|
+
);
|
|
483
|
+
return values.map((value) => textResponse(value, "checkpoint ID"));
|
|
484
|
+
}
|
|
485
|
+
async collectGlobal(result, beforeId, expectedId, filter, limit) {
|
|
486
|
+
let offset = 0;
|
|
487
|
+
while (limit == null || result.length < limit) {
|
|
488
|
+
const values = arrayResponse(
|
|
489
|
+
await this.client.command("ZREVRANGE", this.catalogKey(), offset, offset + this.scanCount - 1),
|
|
490
|
+
"global checkpoint catalog response"
|
|
491
|
+
);
|
|
492
|
+
if (values.length === 0) break;
|
|
493
|
+
for (const value of values) {
|
|
494
|
+
const { checkpointId: id, threadKey: key } = decodeCheckpointLocator(value);
|
|
495
|
+
if (expectedId != null && id !== expectedId) continue;
|
|
496
|
+
if (beforeId != null && id >= beforeId) continue;
|
|
497
|
+
const record = await this.readRecord(key, id);
|
|
498
|
+
if (record == null || !metadataMatches(record.metadata, filter)) continue;
|
|
499
|
+
result.push({ key, record });
|
|
500
|
+
if (limit != null && result.length >= limit) break;
|
|
501
|
+
}
|
|
502
|
+
offset += values.length;
|
|
503
|
+
if (values.length < this.scanCount) break;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
function identity(config) {
|
|
508
|
+
if (config.configurable == null || typeof config.configurable !== "object") {
|
|
509
|
+
throw new TypeError("LangGraph config must contain configurable.thread_id");
|
|
510
|
+
}
|
|
511
|
+
return {
|
|
512
|
+
checkpointNs: requiredText(config.configurable.checkpoint_ns ?? "", "checkpoint_ns", true),
|
|
513
|
+
threadId: requiredText(config.configurable.thread_id, "thread_id")
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function checkpointId(config) {
|
|
517
|
+
return optionalText(config.configurable?.checkpoint_id ?? config.configurable?.thread_ts, "checkpoint_id");
|
|
518
|
+
}
|
|
519
|
+
function checkpointConfig(threadId, checkpointNs, id) {
|
|
520
|
+
return { configurable: { checkpoint_id: id, checkpoint_ns: checkpointNs, thread_id: threadId } };
|
|
521
|
+
}
|
|
522
|
+
function requiredText(value, name, allowEmpty = false) {
|
|
523
|
+
if (typeof value !== "string" || !allowEmpty && value.length === 0) {
|
|
524
|
+
throw new TypeError(`${name} must be ${allowEmpty ? "text" : "non-empty text"}`);
|
|
525
|
+
}
|
|
526
|
+
return value;
|
|
527
|
+
}
|
|
528
|
+
function optionalText(value, name) {
|
|
529
|
+
if (value == null || value === "") return void 0;
|
|
530
|
+
return requiredText(value, name);
|
|
531
|
+
}
|
|
532
|
+
function metadataMatches(metadata, filter) {
|
|
533
|
+
return filter == null || Object.entries(filter).every(([key, value]) => {
|
|
534
|
+
const actual = Object.getOwnPropertyDescriptor(metadata, key)?.value;
|
|
535
|
+
return Object.is(actual, value);
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
function checkpointField(id) {
|
|
539
|
+
return `${CHECKPOINT_FIELD_PREFIX}${encodeComponent(id)}`;
|
|
540
|
+
}
|
|
541
|
+
function writeField(id, taskId, index) {
|
|
542
|
+
return `${WRITE_FIELD_PREFIX}${encodeComponent(id)}:${encodeComponent(taskId)}:${index}`;
|
|
543
|
+
}
|
|
544
|
+
function encodeComponent(value) {
|
|
545
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
546
|
+
}
|
|
547
|
+
function checkpointLocator(id, threadKey) {
|
|
548
|
+
return Buffer.concat([orderedText(id), Buffer.from(threadKey, "utf8")]);
|
|
549
|
+
}
|
|
550
|
+
function decodeCheckpointLocator(value) {
|
|
551
|
+
if (!(typeof value === "string" || Buffer.isBuffer(value) || value instanceof Uint8Array)) {
|
|
552
|
+
throw new TypeError("FerricStore returned a non-binary checkpoint locator");
|
|
553
|
+
}
|
|
554
|
+
const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value);
|
|
555
|
+
const decoded = decodeOrderedText(bytes, 0);
|
|
556
|
+
const threadKey = bytes.subarray(decoded.offset).toString("utf8");
|
|
557
|
+
if (threadKey.length === 0) throw new Error("FerricStore checkpoint locator has an empty thread key");
|
|
558
|
+
return { checkpointId: decoded.value, threadKey };
|
|
559
|
+
}
|
|
560
|
+
function orderedText(value) {
|
|
561
|
+
const output = [];
|
|
562
|
+
for (const byte of Buffer.from(value, "utf8")) {
|
|
563
|
+
if (byte === 0) output.push(0, 255);
|
|
564
|
+
else output.push(byte);
|
|
565
|
+
}
|
|
566
|
+
output.push(0, 0);
|
|
567
|
+
return Buffer.from(output);
|
|
568
|
+
}
|
|
569
|
+
function decodeOrderedText(bytes, start) {
|
|
570
|
+
const output = [];
|
|
571
|
+
let offset = start;
|
|
572
|
+
while (offset < bytes.length) {
|
|
573
|
+
const byte = bytes[offset];
|
|
574
|
+
if (byte == null) throw new Error("truncated FerricStore checkpoint locator");
|
|
575
|
+
offset += 1;
|
|
576
|
+
if (byte !== 0) {
|
|
577
|
+
output.push(byte);
|
|
578
|
+
continue;
|
|
579
|
+
}
|
|
580
|
+
const escaped = bytes[offset];
|
|
581
|
+
offset += 1;
|
|
582
|
+
if (escaped === 0) return { offset, value: Buffer.from(output).toString("utf8") };
|
|
583
|
+
if (escaped === 255) output.push(0);
|
|
584
|
+
else throw new Error("invalid FerricStore checkpoint locator escape");
|
|
585
|
+
}
|
|
586
|
+
throw new Error("unterminated FerricStore checkpoint locator");
|
|
587
|
+
}
|
|
588
|
+
function sha256(value) {
|
|
589
|
+
return createHash("sha256").update(value).digest("hex");
|
|
590
|
+
}
|
|
591
|
+
function lengthPrefixed(values) {
|
|
592
|
+
const parts = [];
|
|
593
|
+
for (const value of values) {
|
|
594
|
+
const payload = Buffer.from(value, "utf8");
|
|
595
|
+
const length = Buffer.allocUnsafe(8);
|
|
596
|
+
length.writeBigUInt64BE(BigInt(payload.length));
|
|
597
|
+
parts.push(length, payload);
|
|
598
|
+
}
|
|
599
|
+
return Buffer.concat(parts);
|
|
600
|
+
}
|
|
601
|
+
function asArguments(values) {
|
|
602
|
+
return values.map((value) => {
|
|
603
|
+
if (typeof value === "string" || typeof value === "number" || Buffer.isBuffer(value)) return value;
|
|
604
|
+
if (value instanceof Uint8Array) return Buffer.from(value);
|
|
605
|
+
throw new TypeError("FerricStore returned an invalid command argument");
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/langgraph/store.ts
|
|
610
|
+
import { createHash as createHash2 } from "crypto";
|
|
611
|
+
import {
|
|
612
|
+
BaseStore
|
|
613
|
+
} from "@langchain/langgraph";
|
|
614
|
+
var FORMAT_VERSION2 = 1;
|
|
615
|
+
var ITEM_FIELD_PREFIX = "item:";
|
|
616
|
+
var FerricStoreStore = class extends BaseStore {
|
|
617
|
+
client;
|
|
618
|
+
keyPrefix;
|
|
619
|
+
scanCount;
|
|
620
|
+
lockOptions;
|
|
621
|
+
constructor(client, options = {}) {
|
|
622
|
+
super();
|
|
623
|
+
this.client = client;
|
|
624
|
+
this.keyPrefix = normalizeKeyPrefix(options.keyPrefix ?? "langgraph:store", "langgraph:store");
|
|
625
|
+
this.scanCount = positiveInteger(options.scanCount, 256, "scanCount");
|
|
626
|
+
this.lockOptions = {
|
|
627
|
+
lockRetryMs: options.lockRetryMs,
|
|
628
|
+
lockTtlMs: options.lockTtlMs,
|
|
629
|
+
lockWaitMs: options.lockWaitMs
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
async batch(operations) {
|
|
633
|
+
if (!Array.isArray(operations)) throw new TypeError("operations must be an array");
|
|
634
|
+
const lockKeys = operations.filter(isPutOperation).map((operation) => {
|
|
635
|
+
validatePut(operation);
|
|
636
|
+
return this.itemLockKey(operation.namespace, operation.key);
|
|
637
|
+
});
|
|
638
|
+
return await withMutationLocks(this.client, lockKeys, async () => {
|
|
639
|
+
const results = [];
|
|
640
|
+
const puts = /* @__PURE__ */ new Map();
|
|
641
|
+
for (const operation of operations) {
|
|
642
|
+
if (isPutOperation(operation)) {
|
|
643
|
+
puts.set(JSON.stringify([operation.namespace, operation.key]), operation);
|
|
644
|
+
results.push(void 0);
|
|
645
|
+
} else if (isSearchOperation(operation)) {
|
|
646
|
+
results.push(await this.searchOperation(operation));
|
|
647
|
+
} else if (isGetOperation(operation)) {
|
|
648
|
+
results.push(await this.getOperation(operation));
|
|
649
|
+
} else if (isListNamespacesOperation(operation)) {
|
|
650
|
+
results.push(await this.listNamespacesOperation(operation));
|
|
651
|
+
} else {
|
|
652
|
+
throw new TypeError("unsupported LangGraph store operation");
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
for (const operation of puts.values()) await this.putOperation(operation);
|
|
656
|
+
return results;
|
|
657
|
+
}, this.lockOptions);
|
|
658
|
+
}
|
|
659
|
+
catalogKey() {
|
|
660
|
+
return `${this.keyPrefix}:namespaces`;
|
|
661
|
+
}
|
|
662
|
+
namespaceKey(namespace) {
|
|
663
|
+
return `${this.keyPrefix}:{lgs:${sha2562(namespaceIdentity(namespace))}}:namespace`;
|
|
664
|
+
}
|
|
665
|
+
itemLockKey(namespace, key) {
|
|
666
|
+
const keyBytes = Buffer.from(key, "utf8");
|
|
667
|
+
return `${this.keyPrefix}:{lgsi:${sha2562(Buffer.concat([
|
|
668
|
+
namespaceIdentity(namespace),
|
|
669
|
+
uint64(keyBytes.length),
|
|
670
|
+
keyBytes
|
|
671
|
+
]))}}:mutation-lock`;
|
|
672
|
+
}
|
|
673
|
+
async getOperation(operation) {
|
|
674
|
+
validateNamespace(operation.namespace);
|
|
675
|
+
if (typeof operation.key !== "string") throw new TypeError("store key must be text");
|
|
676
|
+
const value = await this.client.command(
|
|
677
|
+
"HGET",
|
|
678
|
+
this.namespaceKey(operation.namespace),
|
|
679
|
+
itemField(operation.key)
|
|
680
|
+
);
|
|
681
|
+
return value == null ? null : decodeItem(value);
|
|
682
|
+
}
|
|
683
|
+
async putOperation(operation) {
|
|
684
|
+
validatePut(operation);
|
|
685
|
+
const namespaceKey = this.namespaceKey(operation.namespace);
|
|
686
|
+
const field = itemField(operation.key);
|
|
687
|
+
const locator = catalogMember(operation.namespace, operation.key);
|
|
688
|
+
if (operation.value == null) {
|
|
689
|
+
await this.client.command("HDEL", namespaceKey, field);
|
|
690
|
+
await this.client.command("ZREM", this.catalogKey(), locator);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
const storedValue = snapshotJsonValue(operation.value);
|
|
694
|
+
const existing = await this.client.command("HGET", namespaceKey, field);
|
|
695
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
696
|
+
const createdAt = existing == null ? now : decodeItemRecord(existing).createdAt;
|
|
697
|
+
const record = {
|
|
698
|
+
createdAt,
|
|
699
|
+
formatVersion: FORMAT_VERSION2,
|
|
700
|
+
key: operation.key,
|
|
701
|
+
namespace: [...operation.namespace],
|
|
702
|
+
updatedAt: now,
|
|
703
|
+
value: storedValue
|
|
704
|
+
};
|
|
705
|
+
await this.client.command("ZADD", this.catalogKey(), 0, locator);
|
|
706
|
+
await this.client.command("HSET", namespaceKey, field, encodeItem(record));
|
|
707
|
+
}
|
|
708
|
+
async searchOperation(operation) {
|
|
709
|
+
validateNamespacePrefix(operation.namespacePrefix);
|
|
710
|
+
if (operation.query != null) {
|
|
711
|
+
throw new Error(
|
|
712
|
+
"FerricStoreStore semantic query search is not configured; use metadata filters or a vector-enabled store"
|
|
713
|
+
);
|
|
714
|
+
}
|
|
715
|
+
const limit = normalizePageNumber(operation.limit, 10, "search limit");
|
|
716
|
+
const offset = normalizePageNumber(operation.offset, 0, "search offset");
|
|
717
|
+
if (limit === 0) return [];
|
|
718
|
+
const matches = [];
|
|
719
|
+
for await (const locator of this.catalogLocators()) {
|
|
720
|
+
if (!startsWithNamespace(locator.namespace, operation.namespacePrefix)) continue;
|
|
721
|
+
const item = await this.readCatalogItem(locator.namespace, locator.key);
|
|
722
|
+
if (item == null || !matchesFilter(item.value, operation.filter)) continue;
|
|
723
|
+
matches.push(item);
|
|
724
|
+
if (matches.length >= offset + limit) break;
|
|
725
|
+
}
|
|
726
|
+
return matches.slice(offset, offset + limit);
|
|
727
|
+
}
|
|
728
|
+
async listNamespacesOperation(operation) {
|
|
729
|
+
const limit = normalizePageNumber(operation.limit, 100, "namespace limit");
|
|
730
|
+
const offset = normalizePageNumber(operation.offset, 0, "namespace offset");
|
|
731
|
+
const maxDepth = operation.maxDepth == null ? void 0 : positiveInteger(operation.maxDepth, operation.maxDepth, "maxDepth");
|
|
732
|
+
if (limit === 0) return [];
|
|
733
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
734
|
+
for await (const locator of this.catalogLocators()) {
|
|
735
|
+
const item = await this.readCatalogItem(locator.namespace, locator.key);
|
|
736
|
+
if (item == null) continue;
|
|
737
|
+
if (!matchesConditions(item.namespace, operation.matchConditions)) continue;
|
|
738
|
+
const namespace = maxDepth == null ? item.namespace : item.namespace.slice(0, maxDepth);
|
|
739
|
+
namespaces.set(JSON.stringify(namespace), namespace);
|
|
740
|
+
}
|
|
741
|
+
return [...namespaces.values()].sort(compareNamespaces).slice(offset, offset + limit);
|
|
742
|
+
}
|
|
743
|
+
async readCatalogItem(namespace, key) {
|
|
744
|
+
const value = await this.client.command("HGET", this.namespaceKey(namespace), itemField(key));
|
|
745
|
+
return value == null ? null : decodeItem(value);
|
|
746
|
+
}
|
|
747
|
+
async *catalogLocators() {
|
|
748
|
+
let offset = 0;
|
|
749
|
+
while (true) {
|
|
750
|
+
const values = arrayResponse(
|
|
751
|
+
await this.client.command("ZRANGE", this.catalogKey(), offset, offset + this.scanCount - 1),
|
|
752
|
+
"LangGraph store catalog response"
|
|
753
|
+
);
|
|
754
|
+
if (values.length === 0) return;
|
|
755
|
+
for (const value of values) yield decodeCatalogMember(value);
|
|
756
|
+
offset += values.length;
|
|
757
|
+
if (values.length < this.scanCount) return;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
};
|
|
761
|
+
function isPutOperation(operation) {
|
|
762
|
+
return "namespace" in operation && "key" in operation && "value" in operation;
|
|
763
|
+
}
|
|
764
|
+
function isGetOperation(operation) {
|
|
765
|
+
return "namespace" in operation && "key" in operation && !("value" in operation);
|
|
766
|
+
}
|
|
767
|
+
function isSearchOperation(operation) {
|
|
768
|
+
return "namespacePrefix" in operation;
|
|
769
|
+
}
|
|
770
|
+
function isListNamespacesOperation(operation) {
|
|
771
|
+
return !("namespace" in operation) && !("namespacePrefix" in operation);
|
|
772
|
+
}
|
|
773
|
+
function validatePut(operation) {
|
|
774
|
+
validateNamespace(operation.namespace);
|
|
775
|
+
if (typeof operation.key !== "string") throw new TypeError("store key must be text");
|
|
776
|
+
if (operation.value == null) return;
|
|
777
|
+
if (typeof operation.value !== "object" || Array.isArray(operation.value)) {
|
|
778
|
+
throw new TypeError("LangGraph store values must be JSON objects");
|
|
779
|
+
}
|
|
780
|
+
validateJson(operation.value, /* @__PURE__ */ new WeakSet());
|
|
781
|
+
}
|
|
782
|
+
function validateNamespace(namespace) {
|
|
783
|
+
if (!Array.isArray(namespace) || namespace.length === 0) {
|
|
784
|
+
throw new Error("namespace cannot be empty");
|
|
785
|
+
}
|
|
786
|
+
for (const label of namespace) {
|
|
787
|
+
if (typeof label !== "string" || label.length === 0 || label.includes(".")) {
|
|
788
|
+
throw new Error("namespace labels must be non-empty strings without periods");
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (namespace[0] === "langgraph") throw new Error('root namespace label cannot be "langgraph"');
|
|
792
|
+
}
|
|
793
|
+
function validateNamespacePrefix(namespace) {
|
|
794
|
+
if (!Array.isArray(namespace)) throw new TypeError("namespacePrefix must be an array");
|
|
795
|
+
if (namespace.length > 0) validateNamespace(namespace);
|
|
796
|
+
}
|
|
797
|
+
function validateJson(value, ancestors) {
|
|
798
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
799
|
+
if (value === void 0) throw new TypeError("LangGraph store values must not contain undefined");
|
|
800
|
+
if (typeof value === "number") {
|
|
801
|
+
if (!Number.isFinite(value)) throw new TypeError("LangGraph store numbers must be finite");
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
if (typeof value !== "object") throw new TypeError("LangGraph store values must be JSON serializable");
|
|
805
|
+
if (ancestors.has(value)) throw new TypeError("LangGraph store values must not be cyclic");
|
|
806
|
+
ancestors.add(value);
|
|
807
|
+
try {
|
|
808
|
+
if (Array.isArray(value)) {
|
|
809
|
+
const keys = Reflect.ownKeys(value);
|
|
810
|
+
if (keys.length !== value.length + 1 || keys.some((key) => typeof key !== "string" || key !== "length" && !isArrayIndex(key, value.length))) {
|
|
811
|
+
throw new TypeError("LangGraph store arrays must not be sparse or customized");
|
|
812
|
+
}
|
|
813
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
814
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
815
|
+
if (descriptor == null || !descriptor.enumerable || !("value" in descriptor)) {
|
|
816
|
+
throw new TypeError("LangGraph store arrays contain an unsupported item");
|
|
817
|
+
}
|
|
818
|
+
validateJson(descriptor.value, ancestors);
|
|
819
|
+
}
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const prototype = Object.getPrototypeOf(value);
|
|
823
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
824
|
+
throw new TypeError("LangGraph store values must contain only JSON objects");
|
|
825
|
+
}
|
|
826
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
827
|
+
if (typeof key !== "string") throw new TypeError("LangGraph store objects must not have symbol keys");
|
|
828
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
829
|
+
if (descriptor == null || !descriptor.enumerable || !("value" in descriptor)) {
|
|
830
|
+
throw new TypeError("LangGraph store objects contain an unsupported property");
|
|
831
|
+
}
|
|
832
|
+
validateJson(descriptor.value, ancestors);
|
|
833
|
+
}
|
|
834
|
+
} finally {
|
|
835
|
+
ancestors.delete(value);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function snapshotJsonValue(value) {
|
|
839
|
+
validateJson(value, /* @__PURE__ */ new WeakSet());
|
|
840
|
+
return JSON.parse(JSON.stringify(value));
|
|
841
|
+
}
|
|
842
|
+
function encodeItem(record) {
|
|
843
|
+
return Buffer.from(JSON.stringify(record), "utf8");
|
|
844
|
+
}
|
|
845
|
+
function decodeItem(value) {
|
|
846
|
+
const record = decodeItemRecord(value);
|
|
847
|
+
return {
|
|
848
|
+
createdAt: new Date(record.createdAt),
|
|
849
|
+
key: record.key,
|
|
850
|
+
namespace: [...record.namespace],
|
|
851
|
+
updatedAt: new Date(record.updatedAt),
|
|
852
|
+
value: record.value
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
function decodeItemRecord(value) {
|
|
856
|
+
if (!(typeof value === "string" || Buffer.isBuffer(value) || value instanceof Uint8Array)) {
|
|
857
|
+
throw new TypeError("FerricStore returned a non-binary LangGraph store item");
|
|
858
|
+
}
|
|
859
|
+
const text = typeof value === "string" ? value : Buffer.from(value).toString("utf8");
|
|
860
|
+
let record;
|
|
861
|
+
try {
|
|
862
|
+
record = JSON.parse(text);
|
|
863
|
+
} catch (error) {
|
|
864
|
+
throw new Error("invalid FerricStore LangGraph store item", { cause: error });
|
|
865
|
+
}
|
|
866
|
+
if (record == null || typeof record !== "object" || record.formatVersion !== FORMAT_VERSION2 || typeof record.key !== "string" || !Array.isArray(record.namespace) || typeof record.createdAt !== "string" || typeof record.updatedAt !== "string" || record.value == null || typeof record.value !== "object") {
|
|
867
|
+
throw new Error("unsupported or corrupt FerricStore LangGraph store item");
|
|
868
|
+
}
|
|
869
|
+
return record;
|
|
870
|
+
}
|
|
871
|
+
function matchesFilter(value, filter) {
|
|
872
|
+
return filter == null || Object.entries(filter).every(([key, expected]) => compareValue(value[key], expected));
|
|
873
|
+
}
|
|
874
|
+
function compareValue(value, expected) {
|
|
875
|
+
if (expected != null && typeof expected === "object" && !Array.isArray(expected)) {
|
|
876
|
+
const entries = Object.entries(expected);
|
|
877
|
+
if (entries.length > 0 && entries.every(([key]) => FILTER_OPERATORS.has(key))) {
|
|
878
|
+
return entries.every(([operator, operand]) => applyOperator(value, operator, operand));
|
|
879
|
+
}
|
|
880
|
+
return value != null && typeof value === "object" && !Array.isArray(value) && entries.every(([key, nested]) => compareValue(value[key], nested));
|
|
881
|
+
}
|
|
882
|
+
if (Array.isArray(expected)) {
|
|
883
|
+
return Array.isArray(value) && value.length === expected.length && value.every((item, index) => compareValue(item, expected[index]));
|
|
884
|
+
}
|
|
885
|
+
return Object.is(value, expected);
|
|
886
|
+
}
|
|
887
|
+
function applyOperator(value, operator, expected) {
|
|
888
|
+
switch (operator) {
|
|
889
|
+
case "$eq":
|
|
890
|
+
return Object.is(value, expected);
|
|
891
|
+
case "$ne":
|
|
892
|
+
return !Object.is(value, expected);
|
|
893
|
+
case "$gt":
|
|
894
|
+
return comparable(value, expected, (left, right) => left > right);
|
|
895
|
+
case "$gte":
|
|
896
|
+
return comparable(value, expected, (left, right) => left >= right);
|
|
897
|
+
case "$in":
|
|
898
|
+
return Array.isArray(expected) && expected.some((item) => Object.is(item, value));
|
|
899
|
+
case "$lt":
|
|
900
|
+
return comparable(value, expected, (left, right) => left < right);
|
|
901
|
+
case "$lte":
|
|
902
|
+
return comparable(value, expected, (left, right) => left <= right);
|
|
903
|
+
case "$nin":
|
|
904
|
+
return !Array.isArray(expected) || expected.every((item) => !Object.is(item, value));
|
|
905
|
+
default:
|
|
906
|
+
throw new Error(`unsupported filter operator: ${operator}`);
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
function comparable(value, expected, compare) {
|
|
910
|
+
try {
|
|
911
|
+
return compare(Number(value), Number(expected));
|
|
912
|
+
} catch {
|
|
913
|
+
return false;
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
var FILTER_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$gt", "$gte", "$in", "$lt", "$lte", "$ne", "$nin"]);
|
|
917
|
+
function matchesConditions(namespace, conditions) {
|
|
918
|
+
return conditions == null || conditions.every((condition) => {
|
|
919
|
+
if (namespace.length < condition.path.length) return false;
|
|
920
|
+
const actual = condition.matchType === "prefix" ? namespace : [...namespace].reverse();
|
|
921
|
+
const pattern = condition.matchType === "prefix" ? condition.path : [...condition.path].reverse();
|
|
922
|
+
return pattern.every((component, index) => component === "*" || actual[index] === component);
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
function startsWithNamespace(namespace, prefix) {
|
|
926
|
+
return prefix.length <= namespace.length && prefix.every((value, index) => namespace[index] === value);
|
|
927
|
+
}
|
|
928
|
+
function normalizePageNumber(value, fallback, name) {
|
|
929
|
+
const normalized = value ?? fallback;
|
|
930
|
+
if (!Number.isSafeInteger(normalized) || normalized < 0) {
|
|
931
|
+
throw new TypeError(`${name} must be a non-negative safe integer`);
|
|
932
|
+
}
|
|
933
|
+
return normalized;
|
|
934
|
+
}
|
|
935
|
+
function compareNamespaces(left, right) {
|
|
936
|
+
const count = Math.min(left.length, right.length);
|
|
937
|
+
for (let index = 0; index < count; index += 1) {
|
|
938
|
+
const compared = (left[index] ?? "").localeCompare(right[index] ?? "");
|
|
939
|
+
if (compared !== 0) return compared;
|
|
940
|
+
}
|
|
941
|
+
return left.length - right.length;
|
|
942
|
+
}
|
|
943
|
+
function itemField(key) {
|
|
944
|
+
return `${ITEM_FIELD_PREFIX}${Buffer.from(key, "utf8").toString("base64url")}`;
|
|
945
|
+
}
|
|
946
|
+
function namespaceIdentity(namespace) {
|
|
947
|
+
return Buffer.concat(namespace.flatMap((component) => {
|
|
948
|
+
const payload = Buffer.from(component, "utf8");
|
|
949
|
+
return [uint64(payload.length), payload];
|
|
950
|
+
}));
|
|
951
|
+
}
|
|
952
|
+
function catalogMember(namespace, key) {
|
|
953
|
+
return Buffer.concat([
|
|
954
|
+
...namespace.map(orderedText2),
|
|
955
|
+
Buffer.from([0, 0]),
|
|
956
|
+
orderedText2(key)
|
|
957
|
+
]);
|
|
958
|
+
}
|
|
959
|
+
function decodeCatalogMember(value) {
|
|
960
|
+
if (!(typeof value === "string" || Buffer.isBuffer(value) || value instanceof Uint8Array)) {
|
|
961
|
+
throw new TypeError("FerricStore returned a non-binary LangGraph store locator");
|
|
962
|
+
}
|
|
963
|
+
const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value);
|
|
964
|
+
const namespace = [];
|
|
965
|
+
let offset = 0;
|
|
966
|
+
while (true) {
|
|
967
|
+
if (bytes[offset] === 0 && bytes[offset + 1] === 0) {
|
|
968
|
+
offset += 2;
|
|
969
|
+
break;
|
|
970
|
+
}
|
|
971
|
+
const decoded = decodeOrderedText2(bytes, offset);
|
|
972
|
+
namespace.push(decoded.value);
|
|
973
|
+
offset = decoded.offset;
|
|
974
|
+
}
|
|
975
|
+
const key = decodeOrderedText2(bytes, offset);
|
|
976
|
+
if (key.offset !== bytes.length || namespace.length === 0) {
|
|
977
|
+
throw new Error("invalid FerricStore LangGraph store locator");
|
|
978
|
+
}
|
|
979
|
+
return { key: key.value, namespace };
|
|
980
|
+
}
|
|
981
|
+
function orderedText2(value) {
|
|
982
|
+
const output = [];
|
|
983
|
+
for (const byte of Buffer.from(value, "utf8")) {
|
|
984
|
+
if (byte === 0) output.push(0, 255);
|
|
985
|
+
else output.push(byte);
|
|
986
|
+
}
|
|
987
|
+
output.push(0, 0);
|
|
988
|
+
return Buffer.from(output);
|
|
989
|
+
}
|
|
990
|
+
function decodeOrderedText2(bytes, start) {
|
|
991
|
+
const output = [];
|
|
992
|
+
let offset = start;
|
|
993
|
+
while (offset < bytes.length) {
|
|
994
|
+
const byte = bytes[offset];
|
|
995
|
+
if (byte == null) throw new Error("truncated FerricStore LangGraph store locator");
|
|
996
|
+
offset += 1;
|
|
997
|
+
if (byte !== 0) {
|
|
998
|
+
output.push(byte);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const escaped = bytes[offset];
|
|
1002
|
+
offset += 1;
|
|
1003
|
+
if (escaped === 0) return { offset, value: Buffer.from(output).toString("utf8") };
|
|
1004
|
+
if (escaped === 255) output.push(0);
|
|
1005
|
+
else throw new Error("invalid FerricStore LangGraph store locator escape");
|
|
1006
|
+
}
|
|
1007
|
+
throw new Error("unterminated FerricStore LangGraph store locator");
|
|
1008
|
+
}
|
|
1009
|
+
function uint64(value) {
|
|
1010
|
+
const result = Buffer.allocUnsafe(8);
|
|
1011
|
+
result.writeBigUInt64BE(BigInt(value));
|
|
1012
|
+
return result;
|
|
1013
|
+
}
|
|
1014
|
+
function sha2562(value) {
|
|
1015
|
+
return createHash2("sha256").update(value).digest("hex");
|
|
1016
|
+
}
|
|
1017
|
+
function isArrayIndex(key, length) {
|
|
1018
|
+
if (!/^(?:0|[1-9]\d*)$/u.test(key)) return false;
|
|
1019
|
+
const index = Number(key);
|
|
1020
|
+
return Number.isSafeInteger(index) && index >= 0 && index < length;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// src/langgraph/flow.ts
|
|
1024
|
+
import { createHash as createHash3 } from "crypto";
|
|
1025
|
+
import { Command } from "@langchain/langgraph";
|
|
1026
|
+
|
|
1027
|
+
// src/outcomes.ts
|
|
1028
|
+
var OUTCOME_BRAND = /* @__PURE__ */ Symbol("ferricstore.outcome");
|
|
1029
|
+
function transition(toState, options = {}) {
|
|
1030
|
+
return brandOutcome({ ...options, kind: "transition", toState });
|
|
1031
|
+
}
|
|
1032
|
+
function complete(options = {}) {
|
|
1033
|
+
return brandOutcome({ ...options, kind: "complete" });
|
|
1034
|
+
}
|
|
1035
|
+
function fail(options = {}) {
|
|
1036
|
+
return brandOutcome({ ...options, kind: "fail" });
|
|
1037
|
+
}
|
|
1038
|
+
function brandOutcome(outcome) {
|
|
1039
|
+
Object.defineProperty(outcome, "kind", {
|
|
1040
|
+
configurable: false,
|
|
1041
|
+
enumerable: true,
|
|
1042
|
+
value: outcome.kind,
|
|
1043
|
+
writable: false
|
|
1044
|
+
});
|
|
1045
|
+
if (outcome.kind === "transition") {
|
|
1046
|
+
Object.defineProperty(outcome, "toState", {
|
|
1047
|
+
configurable: false,
|
|
1048
|
+
enumerable: true,
|
|
1049
|
+
value: outcome.toState,
|
|
1050
|
+
writable: false
|
|
1051
|
+
});
|
|
1052
|
+
}
|
|
1053
|
+
Object.defineProperty(outcome, OUTCOME_BRAND, {
|
|
1054
|
+
configurable: false,
|
|
1055
|
+
enumerable: false,
|
|
1056
|
+
value: true,
|
|
1057
|
+
writable: false
|
|
1058
|
+
});
|
|
1059
|
+
return outcome;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
// src/langgraph/flow.ts
|
|
1063
|
+
var LangGraphFlowContext = class {
|
|
1064
|
+
flow;
|
|
1065
|
+
threadId;
|
|
1066
|
+
checkpointNs;
|
|
1067
|
+
constructor(flow, threadId, checkpointNs) {
|
|
1068
|
+
this.flow = flow;
|
|
1069
|
+
this.threadId = threadId;
|
|
1070
|
+
this.checkpointNs = checkpointNs;
|
|
1071
|
+
}
|
|
1072
|
+
get id() {
|
|
1073
|
+
return this.flow.id;
|
|
1074
|
+
}
|
|
1075
|
+
get type() {
|
|
1076
|
+
return this.flow.type;
|
|
1077
|
+
}
|
|
1078
|
+
get state() {
|
|
1079
|
+
return this.flow.state;
|
|
1080
|
+
}
|
|
1081
|
+
get partitionKey() {
|
|
1082
|
+
return this.flow.partitionKey;
|
|
1083
|
+
}
|
|
1084
|
+
get payload() {
|
|
1085
|
+
return this.flow.payload;
|
|
1086
|
+
}
|
|
1087
|
+
get values() {
|
|
1088
|
+
return this.flow.values;
|
|
1089
|
+
}
|
|
1090
|
+
};
|
|
1091
|
+
var LangGraphFlowRun = class {
|
|
1092
|
+
value;
|
|
1093
|
+
threadId;
|
|
1094
|
+
checkpointNs;
|
|
1095
|
+
interrupts;
|
|
1096
|
+
constructor(value, threadId, checkpointNs, interrupts2) {
|
|
1097
|
+
this.value = value;
|
|
1098
|
+
this.threadId = threadId;
|
|
1099
|
+
this.checkpointNs = checkpointNs;
|
|
1100
|
+
this.interrupts = interrupts2;
|
|
1101
|
+
}
|
|
1102
|
+
get interrupted() {
|
|
1103
|
+
return this.interrupts.length > 0;
|
|
1104
|
+
}
|
|
1105
|
+
get interruptValues() {
|
|
1106
|
+
return this.interrupts.map((item) => item != null && typeof item === "object" && "value" in item ? item.value : item);
|
|
1107
|
+
}
|
|
1108
|
+
};
|
|
1109
|
+
var LangGraphFlow = class {
|
|
1110
|
+
graph;
|
|
1111
|
+
options;
|
|
1112
|
+
constructor(graph, options = {}) {
|
|
1113
|
+
if (options.onInterrupt != null && options.interruptState != null) {
|
|
1114
|
+
throw new TypeError("onInterrupt and interruptState are mutually exclusive");
|
|
1115
|
+
}
|
|
1116
|
+
if (options.interruptState != null) requireText(options.interruptState, "interruptState");
|
|
1117
|
+
this.graph = graph;
|
|
1118
|
+
this.options = { ...options };
|
|
1119
|
+
}
|
|
1120
|
+
async config(flow, graphContext) {
|
|
1121
|
+
const additional = await this.options.config?.(flow) ?? {};
|
|
1122
|
+
const rawConfigurable = additional.configurable ?? {};
|
|
1123
|
+
if (rawConfigurable == null || typeof rawConfigurable !== "object" || Array.isArray(rawConfigurable)) {
|
|
1124
|
+
throw new TypeError("LangGraph config configurable must be an object");
|
|
1125
|
+
}
|
|
1126
|
+
const rawMetadata = additional.metadata ?? {};
|
|
1127
|
+
if (rawMetadata == null || typeof rawMetadata !== "object" || Array.isArray(rawMetadata)) {
|
|
1128
|
+
throw new TypeError("LangGraph config metadata must be an object");
|
|
1129
|
+
}
|
|
1130
|
+
const threadId = requireText(
|
|
1131
|
+
await (this.options.threadId?.(flow) ?? defaultThreadId(flow)),
|
|
1132
|
+
"threadId"
|
|
1133
|
+
);
|
|
1134
|
+
const checkpointNsOption = this.options.checkpointNs ?? "";
|
|
1135
|
+
const checkpointNs = requireText(
|
|
1136
|
+
await (typeof checkpointNsOption === "function" ? checkpointNsOption(flow) : checkpointNsOption),
|
|
1137
|
+
"checkpointNs",
|
|
1138
|
+
true
|
|
1139
|
+
);
|
|
1140
|
+
const context = graphContext === void 0 ? this.options.context == null ? new LangGraphFlowContext(flow, threadId, checkpointNs) : await this.options.context(flow) : graphContext;
|
|
1141
|
+
return {
|
|
1142
|
+
...this.options.invokeOptions,
|
|
1143
|
+
...additional,
|
|
1144
|
+
configurable: { ...rawConfigurable, checkpoint_ns: checkpointNs, thread_id: threadId },
|
|
1145
|
+
context,
|
|
1146
|
+
metadata: {
|
|
1147
|
+
...rawMetadata,
|
|
1148
|
+
ferricflow_id: flow.id,
|
|
1149
|
+
ferricflow_state: flow.logicalState,
|
|
1150
|
+
ferricflow_type: flow.type
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
}
|
|
1154
|
+
async invoke(flow, graphInput, graphContext) {
|
|
1155
|
+
const config = await this.config(flow, graphContext);
|
|
1156
|
+
let input = graphInput;
|
|
1157
|
+
if (input === void 0) {
|
|
1158
|
+
let hasCheckpoint = false;
|
|
1159
|
+
if ((this.options.recoverExisting ?? true) && this.graph.getState != null) {
|
|
1160
|
+
hasCheckpoint = snapshotHasCheckpoint(await this.graph.getState(config));
|
|
1161
|
+
}
|
|
1162
|
+
input = hasCheckpoint ? null : await (this.options.input?.(flow) ?? flow.payload);
|
|
1163
|
+
}
|
|
1164
|
+
const value = await this.graph.invoke(input, config);
|
|
1165
|
+
const configurable = config.configurable;
|
|
1166
|
+
return new LangGraphFlowRun(
|
|
1167
|
+
value,
|
|
1168
|
+
requireText(configurable?.thread_id, "thread_id"),
|
|
1169
|
+
requireText(configurable?.checkpoint_ns ?? "", "checkpoint_ns", true),
|
|
1170
|
+
interrupts(value)
|
|
1171
|
+
);
|
|
1172
|
+
}
|
|
1173
|
+
async outcome(run, flow) {
|
|
1174
|
+
const mapper = run.interrupted ? this.options.onInterrupt : this.options.onComplete;
|
|
1175
|
+
if (mapper != null) return await mapper(run, flow);
|
|
1176
|
+
const stateMeta = {
|
|
1177
|
+
langgraph_checkpoint_ns: run.checkpointNs,
|
|
1178
|
+
langgraph_interrupt_count: run.interrupts.length,
|
|
1179
|
+
langgraph_interrupted: run.interrupted,
|
|
1180
|
+
langgraph_thread_id: run.threadId
|
|
1181
|
+
};
|
|
1182
|
+
if (!run.interrupted) return complete({ result: run.value, stateMeta });
|
|
1183
|
+
if (this.options.interruptState != null) {
|
|
1184
|
+
return transition(this.options.interruptState, { stateMeta });
|
|
1185
|
+
}
|
|
1186
|
+
return fail({
|
|
1187
|
+
error: {
|
|
1188
|
+
checkpointNs: run.checkpointNs,
|
|
1189
|
+
interruptCount: run.interrupts.length,
|
|
1190
|
+
threadId: run.threadId,
|
|
1191
|
+
type: "unhandled_langgraph_interrupt"
|
|
1192
|
+
},
|
|
1193
|
+
stateMeta
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
async handle(flow, graphInput, graphContext) {
|
|
1197
|
+
return await this.outcome(await this.invoke(flow, graphInput, graphContext), flow);
|
|
1198
|
+
}
|
|
1199
|
+
async resume(flow, value, graphContext) {
|
|
1200
|
+
return await this.handle(flow, new Command({ resume: value }), graphContext);
|
|
1201
|
+
}
|
|
1202
|
+
async handler(flow) {
|
|
1203
|
+
return await this.handle(flow);
|
|
1204
|
+
}
|
|
1205
|
+
};
|
|
1206
|
+
function interrupts(value) {
|
|
1207
|
+
if (value == null || typeof value !== "object" || !("__interrupt__" in value)) return [];
|
|
1208
|
+
const raw = value.__interrupt__;
|
|
1209
|
+
if (raw == null) return [];
|
|
1210
|
+
return Array.isArray(raw) ? raw : [raw];
|
|
1211
|
+
}
|
|
1212
|
+
function snapshotHasCheckpoint(snapshot) {
|
|
1213
|
+
if (snapshot == null || typeof snapshot !== "object") return false;
|
|
1214
|
+
const value = snapshot;
|
|
1215
|
+
if (value.config != null && typeof value.config === "object") {
|
|
1216
|
+
const configurable = value.config.configurable;
|
|
1217
|
+
if (configurable != null && typeof configurable === "object" && typeof configurable.checkpoint_id === "string") return true;
|
|
1218
|
+
}
|
|
1219
|
+
return value.createdAt != null || value.metadata != null;
|
|
1220
|
+
}
|
|
1221
|
+
function defaultThreadId(flow) {
|
|
1222
|
+
const identity2 = Buffer.concat([
|
|
1223
|
+
identityComponent(flow.type),
|
|
1224
|
+
identityComponent(flow.partitionKey),
|
|
1225
|
+
identityComponent(flow.id)
|
|
1226
|
+
]);
|
|
1227
|
+
return `ferricflow:${createHash3("sha256").update(identity2).digest("hex")}`;
|
|
1228
|
+
}
|
|
1229
|
+
function identityComponent(value) {
|
|
1230
|
+
if (value == null) return Buffer.concat([Buffer.from("n"), uint642(0)]);
|
|
1231
|
+
const payload = Buffer.from(value, "utf8");
|
|
1232
|
+
return Buffer.concat([Buffer.from("s"), uint642(payload.length), payload]);
|
|
1233
|
+
}
|
|
1234
|
+
function uint642(value) {
|
|
1235
|
+
const result = Buffer.allocUnsafe(8);
|
|
1236
|
+
result.writeBigUInt64BE(BigInt(value));
|
|
1237
|
+
return result;
|
|
1238
|
+
}
|
|
1239
|
+
function requireText(value, name, allowEmpty = false) {
|
|
1240
|
+
if (typeof value !== "string" || !allowEmpty && value.length === 0 || value.includes("\0")) {
|
|
1241
|
+
throw new TypeError(`${name} must be ${allowEmpty ? "text" : "non-empty text"} without NUL bytes`);
|
|
1242
|
+
}
|
|
1243
|
+
return value;
|
|
1244
|
+
}
|
|
1245
|
+
export {
|
|
1246
|
+
FerricStoreSaver,
|
|
1247
|
+
FerricStoreStore,
|
|
1248
|
+
LangGraphFlow,
|
|
1249
|
+
LangGraphFlowContext,
|
|
1250
|
+
LangGraphFlowRun
|
|
1251
|
+
};
|
|
1252
|
+
//# sourceMappingURL=langgraph.js.map
|