@ferricstore/ferricstore 0.12.0 → 0.12.1
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 +3 -0
- package/dist/langgraph.cjs +395 -125
- package/dist/langgraph.cjs.map +1 -1
- package/dist/langgraph.d.cts +17 -7
- package/dist/langgraph.d.ts +17 -7
- package/dist/langgraph.js +396 -126
- package/dist/langgraph.js.map +1 -1
- package/dist/openai-agents.cjs +161 -24
- package/dist/openai-agents.cjs.map +1 -1
- package/dist/openai-agents.d.cts +10 -4
- package/dist/openai-agents.d.ts +10 -4
- package/dist/openai-agents.js +161 -24
- package/dist/openai-agents.js.map +1 -1
- package/docs/agent-api/assets/hierarchy.js +1 -0
- package/docs/agent-api/assets/highlight.css +92 -0
- package/docs/agent-api/assets/icons.js +18 -0
- package/docs/agent-api/assets/icons.svg +1 -0
- package/docs/agent-api/assets/main.js +60 -0
- package/docs/agent-api/assets/navigation.js +1 -0
- package/docs/agent-api/assets/search.js +1 -0
- package/docs/agent-api/assets/style.css +1648 -0
- package/docs/agent-api/classes/langgraph.FerricStoreSaver.html +297 -0
- package/docs/agent-api/classes/langgraph.FerricStoreStore.html +298 -0
- package/docs/agent-api/classes/langgraph.LangGraphFlow.html +190 -0
- package/docs/agent-api/classes/langgraph.LangGraphFlowContext.html +158 -0
- package/docs/agent-api/classes/langgraph.LangGraphFlowRun.html +133 -0
- package/docs/agent-api/classes/openai-agents.FerricStoreSession.html +241 -0
- package/docs/agent-api/hierarchy.html +44 -0
- package/docs/agent-api/index.html +142 -0
- package/docs/agent-api/interfaces/langgraph.FerricFlowHandlerContext.html +94 -0
- package/docs/agent-api/interfaces/langgraph.FerricStoreCommandClient.html +77 -0
- package/docs/agent-api/interfaces/langgraph.FerricStoreLockOptions.html +81 -0
- package/docs/agent-api/interfaces/langgraph.FerricStoreSaverOptions.html +106 -0
- package/docs/agent-api/interfaces/langgraph.FerricStoreStoreOptions.html +98 -0
- package/docs/agent-api/interfaces/langgraph.InvokableLangGraph.html +83 -0
- package/docs/agent-api/interfaces/langgraph.LangGraphFlowOptions.html +116 -0
- package/docs/agent-api/interfaces/openai-agents.FerricStoreSessionOptions.html +114 -0
- package/docs/agent-api/interfaces/openai-agents.Session.html +208 -0
- package/docs/agent-api/interfaces/openai-agents.SessionHistoryRewriteAwareSession.html +230 -0
- package/docs/agent-api/interfaces/openai-agents.SessionHistoryTransactionAwareSession.html +237 -0
- package/docs/agent-api/modules/langgraph.html +44 -0
- package/docs/agent-api/modules/openai-agents.html +44 -0
- package/docs/agent-api/modules.html +35 -0
- package/docs/agent-api/types/langgraph.LangGraphChannelVersions.html +33 -0
- package/docs/agent-api/types/langgraph.LangGraphInvocationConfig.html +33 -0
- package/docs/agent-api/types/langgraph.LangGraphOutcomeMapper.html +50 -0
- package/docs/agent-api/types/langgraph.LangGraphPendingWrite.html +33 -0
- package/docs/agent-api/types/openai-agents.AgentInputItem.html +35 -0
- package/docs/agent-frameworks.md +25 -8
- package/docs/api/index.html +4 -1
- package/docs/api/media/agent-frameworks.md +25 -8
- package/package.json +3 -3
package/dist/langgraph.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/langgraph/checkpoint.ts
|
|
2
|
-
import { createHash } from "crypto";
|
|
2
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
3
3
|
import {
|
|
4
4
|
BaseCheckpointSaver,
|
|
5
5
|
copyCheckpoint
|
|
@@ -112,14 +112,44 @@ function integerResponse(value, name) {
|
|
|
112
112
|
if (!Number.isSafeInteger(parsed)) throw new TypeError(`FerricStore returned an invalid ${name}`);
|
|
113
113
|
return parsed;
|
|
114
114
|
}
|
|
115
|
+
async function readAtomicValue(client, key, name) {
|
|
116
|
+
const value = await client.command("GET", key);
|
|
117
|
+
if (value == null) return void 0;
|
|
118
|
+
if (typeof value === "string") return Buffer.from(value, "utf8");
|
|
119
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value);
|
|
120
|
+
throw new TypeError(`FerricStore returned a non-binary ${name}`);
|
|
121
|
+
}
|
|
122
|
+
async function compareAndSetAtomicValue(client, key, expected, value) {
|
|
123
|
+
if (expected == null) {
|
|
124
|
+
const response2 = await client.command("SET", key, value, "NX");
|
|
125
|
+
if (response2 == null || response2 === false) return false;
|
|
126
|
+
if (response2 === true) return true;
|
|
127
|
+
return textResponse(response2, "SET NX response").toUpperCase() === "OK";
|
|
128
|
+
}
|
|
129
|
+
const response = await client.command("CAS", key, expected, value);
|
|
130
|
+
if (response == null || response === false) return false;
|
|
131
|
+
if (response === true) return true;
|
|
132
|
+
return integerResponse(response, "CAS response") === 1;
|
|
133
|
+
}
|
|
115
134
|
async function withMutationLocks(client, keys, operation, options = {}) {
|
|
116
135
|
const orderedKeys = [...new Set(keys)].sort();
|
|
117
|
-
if (orderedKeys.length === 0)
|
|
136
|
+
if (orderedKeys.length === 0) {
|
|
137
|
+
const signal = new AbortController().signal;
|
|
138
|
+
return await operation({
|
|
139
|
+
signal,
|
|
140
|
+
assertOwned: () => void 0,
|
|
141
|
+
publish: async (...args) => await additiveCommand(client, args),
|
|
142
|
+
compareAndSet: async (key, expected, value) => await compareAndSetAtomicValue(client, key, expected, value)
|
|
143
|
+
});
|
|
144
|
+
}
|
|
118
145
|
const normalized = {
|
|
119
146
|
lockRetryMs: positiveInteger(options.lockRetryMs, DEFAULT_LOCK_OPTIONS.lockRetryMs, "lockRetryMs"),
|
|
120
147
|
lockTtlMs: positiveInteger(options.lockTtlMs, DEFAULT_LOCK_OPTIONS.lockTtlMs, "lockTtlMs"),
|
|
121
148
|
lockWaitMs: nonNegativeInteger(options.lockWaitMs, DEFAULT_LOCK_OPTIONS.lockWaitMs, "lockWaitMs")
|
|
122
149
|
};
|
|
150
|
+
if (normalized.lockRetryMs >= normalized.lockTtlMs) {
|
|
151
|
+
throw new TypeError("lockRetryMs must be less than lockTtlMs");
|
|
152
|
+
}
|
|
123
153
|
const owner = randomUUID();
|
|
124
154
|
const acquired = [];
|
|
125
155
|
const deadline = performance.now() + normalized.lockWaitMs;
|
|
@@ -128,29 +158,81 @@ async function withMutationLocks(client, keys, operation, options = {}) {
|
|
|
128
158
|
let releaseError;
|
|
129
159
|
let result;
|
|
130
160
|
let operationCompleted = false;
|
|
161
|
+
let conditionalCommitCompleted = false;
|
|
131
162
|
const heartbeatAbort = new AbortController();
|
|
163
|
+
const ownershipAbort = new AbortController();
|
|
164
|
+
const lastExtended = /* @__PURE__ */ new Map();
|
|
165
|
+
const loseOwnership = (error) => {
|
|
166
|
+
const normalizedError = errorObject(error);
|
|
167
|
+
heartbeatError ??= normalizedError;
|
|
168
|
+
if (!ownershipAbort.signal.aborted) ownershipAbort.abort(normalizedError);
|
|
169
|
+
return normalizedError;
|
|
170
|
+
};
|
|
171
|
+
const assertOwned = () => {
|
|
172
|
+
if (heartbeatError != null) throw errorObject(heartbeatError);
|
|
173
|
+
if (ownershipAbort.signal.aborted) throw errorObject(ownershipAbort.signal.reason);
|
|
174
|
+
};
|
|
175
|
+
const renewOwned = async () => {
|
|
176
|
+
assertOwned();
|
|
177
|
+
for (const key of acquired) {
|
|
178
|
+
try {
|
|
179
|
+
const response = await client.command("EXTEND", key, owner, normalized.lockTtlMs);
|
|
180
|
+
if (integerResponse(response, "EXTEND response") !== 1) {
|
|
181
|
+
throw new Error(`lost FerricStore lock ${JSON.stringify(key)} while mutating data`);
|
|
182
|
+
}
|
|
183
|
+
lastExtended.set(key, performance.now());
|
|
184
|
+
} catch (error) {
|
|
185
|
+
throw loseOwnership(new Error(
|
|
186
|
+
`could not validate FerricStore lock ${JSON.stringify(key)} before mutating data`,
|
|
187
|
+
{ cause: error }
|
|
188
|
+
));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
assertOwned();
|
|
192
|
+
};
|
|
193
|
+
const lease = {
|
|
194
|
+
signal: ownershipAbort.signal,
|
|
195
|
+
assertOwned,
|
|
196
|
+
publish: async (...args) => {
|
|
197
|
+
await renewOwned();
|
|
198
|
+
const response = await additiveCommand(client, args);
|
|
199
|
+
assertOwned();
|
|
200
|
+
return response;
|
|
201
|
+
},
|
|
202
|
+
compareAndSet: async (key, expected, value) => {
|
|
203
|
+
await renewOwned();
|
|
204
|
+
const committed = await compareAndSetAtomicValue(client, key, expected, value);
|
|
205
|
+
if (committed) conditionalCommitCompleted = true;
|
|
206
|
+
else assertOwned();
|
|
207
|
+
return committed;
|
|
208
|
+
}
|
|
209
|
+
};
|
|
132
210
|
try {
|
|
133
211
|
for (const key of orderedKeys) {
|
|
134
212
|
while (!await tryAcquireLock(client, key, owner, normalized.lockTtlMs)) {
|
|
135
213
|
if (performance.now() >= deadline) {
|
|
136
214
|
throw new Error(`timed out acquiring FerricStore lock ${JSON.stringify(key)}`);
|
|
137
215
|
}
|
|
216
|
+
await extendAcquiredLocks(client, acquired, owner, normalized.lockTtlMs);
|
|
138
217
|
await delay(normalized.lockRetryMs);
|
|
139
218
|
}
|
|
140
219
|
acquired.push(key);
|
|
141
220
|
}
|
|
221
|
+
await extendAcquiredLocks(client, acquired, owner, normalized.lockTtlMs);
|
|
222
|
+
for (const key of acquired) lastExtended.set(key, performance.now());
|
|
142
223
|
const heartbeat = renewLocks(
|
|
143
224
|
client,
|
|
144
225
|
acquired,
|
|
145
226
|
owner,
|
|
146
227
|
normalized.lockTtlMs,
|
|
228
|
+
lastExtended,
|
|
147
229
|
heartbeatAbort.signal,
|
|
148
230
|
(error) => {
|
|
149
|
-
|
|
231
|
+
loseOwnership(error);
|
|
150
232
|
}
|
|
151
233
|
);
|
|
152
234
|
try {
|
|
153
|
-
result = await operation();
|
|
235
|
+
result = await operation(lease);
|
|
154
236
|
operationCompleted = true;
|
|
155
237
|
} catch (error) {
|
|
156
238
|
primaryError = error;
|
|
@@ -171,11 +253,32 @@ async function withMutationLocks(client, keys, operation, options = {}) {
|
|
|
171
253
|
}
|
|
172
254
|
}
|
|
173
255
|
if (primaryError != null) throw errorObject(primaryError);
|
|
174
|
-
if (heartbeatError != null) throw errorObject(heartbeatError);
|
|
175
|
-
if (releaseError != null
|
|
256
|
+
if (heartbeatError != null && !conditionalCommitCompleted) throw errorObject(heartbeatError);
|
|
257
|
+
if (releaseError != null && !(heartbeatError != null && conditionalCommitCompleted)) {
|
|
258
|
+
throw errorObject(releaseError);
|
|
259
|
+
}
|
|
176
260
|
if (!operationCompleted) throw new Error("FerricStore mutation did not complete");
|
|
177
261
|
return result;
|
|
178
262
|
}
|
|
263
|
+
async function additiveCommand(client, args) {
|
|
264
|
+
const rawName = args[0];
|
|
265
|
+
const name = typeof rawName === "string" ? rawName.toUpperCase() : Buffer.isBuffer(rawName) || rawName instanceof Uint8Array ? Buffer.from(rawName).toString("utf8").toUpperCase() : "";
|
|
266
|
+
if (name !== "SADD" && name !== "ZADD") {
|
|
267
|
+
throw new TypeError("FerricStore mutation leases only publish add-only SADD or ZADD indexes");
|
|
268
|
+
}
|
|
269
|
+
if (name === "ZADD" && (args.length < 4 || args.length % 2 !== 0 || args.slice(2).some((value, index) => index % 2 === 0 && Number(value) !== 0))) {
|
|
270
|
+
throw new TypeError("FerricStore mutation leases only publish zero-score ZADD indexes");
|
|
271
|
+
}
|
|
272
|
+
return await client.command(...args);
|
|
273
|
+
}
|
|
274
|
+
async function extendAcquiredLocks(client, keys, owner, ttlMs) {
|
|
275
|
+
for (const key of keys) {
|
|
276
|
+
const response = await client.command("EXTEND", key, owner, ttlMs);
|
|
277
|
+
if (integerResponse(response, "EXTEND response") !== 1) {
|
|
278
|
+
throw new Error(`lost FerricStore lock ${JSON.stringify(key)} before mutating data`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
179
282
|
async function tryAcquireLock(client, key, owner, ttlMs) {
|
|
180
283
|
try {
|
|
181
284
|
const response = await client.command("LOCK", key, owner, ttlMs);
|
|
@@ -185,10 +288,9 @@ async function tryAcquireLock(client, key, owner, ttlMs) {
|
|
|
185
288
|
throw error;
|
|
186
289
|
}
|
|
187
290
|
}
|
|
188
|
-
async function renewLocks(client, keys, owner, ttlMs, signal, onError) {
|
|
189
|
-
const intervalMs = Math.max(Math.floor(ttlMs / 3),
|
|
190
|
-
const retryMs = Math.min(Math.max(Math.floor(intervalMs / 10),
|
|
191
|
-
const lastExtended = new Map(keys.map((key) => [key, performance.now()]));
|
|
291
|
+
async function renewLocks(client, keys, owner, ttlMs, lastExtended, signal, onError) {
|
|
292
|
+
const intervalMs = Math.max(Math.floor(ttlMs / 3), 1);
|
|
293
|
+
const retryMs = Math.min(Math.max(Math.floor(intervalMs / 10), 1), 1e3);
|
|
192
294
|
let waitMs = intervalMs;
|
|
193
295
|
while (!signal.aborted) {
|
|
194
296
|
try {
|
|
@@ -225,6 +327,8 @@ function errorObject(value) {
|
|
|
225
327
|
|
|
226
328
|
// src/langgraph/checkpoint.ts
|
|
227
329
|
var FORMAT_VERSION = 1;
|
|
330
|
+
var LEGACY_EPOCH_PREFIX = "legacy:";
|
|
331
|
+
var CURRENT_EPOCH_PREFIX = "v2:";
|
|
228
332
|
var CHECKPOINT_FIELD_PREFIX = "checkpoint:";
|
|
229
333
|
var WRITE_FIELD_PREFIX = "write:";
|
|
230
334
|
var WRITES_INDEX = {
|
|
@@ -252,22 +356,18 @@ var FerricStoreSaver = class extends BaseCheckpointSaver {
|
|
|
252
356
|
async getTuple(config) {
|
|
253
357
|
const { checkpointNs, threadId } = identity(config);
|
|
254
358
|
const key = this.threadKey(threadId, checkpointNs);
|
|
359
|
+
const epoch = await this.readThreadEpoch(threadId);
|
|
255
360
|
let id = checkpointId(config);
|
|
256
361
|
let record;
|
|
257
362
|
if (id != null) {
|
|
258
|
-
record = await this.
|
|
363
|
+
record = await this.readRecordAtEpoch(key, id, epoch);
|
|
259
364
|
} else {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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;
|
|
365
|
+
for (const candidate of await this.checkpointIdsAtEpoch(key, epoch)) {
|
|
366
|
+
record = await this.readRecordAtEpoch(key, candidate, epoch);
|
|
367
|
+
if (record != null) {
|
|
368
|
+
id = candidate;
|
|
369
|
+
break;
|
|
370
|
+
}
|
|
271
371
|
}
|
|
272
372
|
}
|
|
273
373
|
if (id == null || record == null) return void 0;
|
|
@@ -287,14 +387,15 @@ var FerricStoreSaver = class extends BaseCheckpointSaver {
|
|
|
287
387
|
const checkpointNs = namespaceSpecified ? requiredText(configurable?.checkpoint_ns, "checkpoint_ns", true) : void 0;
|
|
288
388
|
const matches = [];
|
|
289
389
|
if (threadId == null) {
|
|
290
|
-
await this.collectGlobal(matches, beforeId, expectedId, options.filter, limit);
|
|
390
|
+
await this.collectGlobal(matches, beforeId, expectedId, checkpointNs, options.filter, limit);
|
|
291
391
|
} else {
|
|
292
392
|
const keys = checkpointNs == null ? await this.threadKeys(threadId) : [this.threadKey(threadId, checkpointNs)];
|
|
293
393
|
for (const key of keys) {
|
|
294
|
-
const
|
|
394
|
+
const epoch = await this.readThreadEpoch(threadId);
|
|
395
|
+
const ids = expectedId == null ? await this.checkpointIdsAtEpoch(key, epoch) : [expectedId];
|
|
295
396
|
for (const id of ids) {
|
|
296
397
|
if (beforeId != null && id >= beforeId) continue;
|
|
297
|
-
const record = await this.
|
|
398
|
+
const record = await this.readRecordAtEpoch(key, id, epoch);
|
|
298
399
|
if (record == null || !metadataMatches(record.metadata, options.filter)) continue;
|
|
299
400
|
matches.push({ key, record });
|
|
300
401
|
}
|
|
@@ -321,13 +422,24 @@ var FerricStoreSaver = class extends BaseCheckpointSaver {
|
|
|
321
422
|
threadId
|
|
322
423
|
};
|
|
323
424
|
const key = this.threadKey(threadId, checkpointNs);
|
|
324
|
-
await withMutationLocks(this.client, [this.threadLockKey(threadId)], async () => {
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
await
|
|
328
|
-
await
|
|
329
|
-
await
|
|
330
|
-
await
|
|
425
|
+
await withMutationLocks(this.client, [this.threadLockKey(threadId)], async (lease) => {
|
|
426
|
+
const epoch = await this.ensureThreadEpoch(threadId, lease);
|
|
427
|
+
const locator = checkpointLocator(checkpoint.id, key, threadId);
|
|
428
|
+
await lease.publish("SADD", this.threadCatalogKey(threadId), key);
|
|
429
|
+
await lease.publish("ZADD", this.threadLocatorCatalogKey(threadId), 0, locator);
|
|
430
|
+
await lease.publish("ZADD", this.catalogKey(), 0, locator);
|
|
431
|
+
await lease.publish("ZADD", this.checkpointIndexKey(key), 0, checkpoint.id);
|
|
432
|
+
await lease.publish("ZADD", this.atomicCheckpointIndexKey(key, requireEpoch(epoch)), 0, checkpoint.id);
|
|
433
|
+
const dataKey = this.checkpointRecordKey(key, requireEpoch(epoch), checkpoint.id);
|
|
434
|
+
const value = await this.serialize(record);
|
|
435
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
436
|
+
const expected = await readAtomicValue(this.client, dataKey, "LangGraph atomic checkpoint record");
|
|
437
|
+
if (await lease.compareAndSet(dataKey, expected, value)) {
|
|
438
|
+
await this.assertCurrentEpoch(threadId, epoch);
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
throw new Error("concurrent FerricStore LangGraph checkpoint mutation did not converge");
|
|
331
443
|
}, this.lockOptions);
|
|
332
444
|
return checkpointConfig(threadId, checkpointNs, checkpoint.id);
|
|
333
445
|
}
|
|
@@ -349,35 +461,39 @@ var FerricStoreSaver = class extends BaseCheckpointSaver {
|
|
|
349
461
|
return { index, value: await this.serialize(record) };
|
|
350
462
|
}));
|
|
351
463
|
const key = this.threadKey(threadId, checkpointNs);
|
|
352
|
-
await withMutationLocks(this.client, [this.threadLockKey(threadId)], async () => {
|
|
353
|
-
await this.
|
|
464
|
+
await withMutationLocks(this.client, [this.threadLockKey(threadId)], async (lease) => {
|
|
465
|
+
const epoch = await this.ensureThreadEpoch(threadId, lease);
|
|
466
|
+
const epochValue = requireEpoch(epoch);
|
|
467
|
+
await lease.publish("SADD", this.threadCatalogKey(threadId), key);
|
|
354
468
|
for (const snapshot of snapshots) {
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
469
|
+
const dataKey = this.pendingWriteKey(key, epochValue, id, taskId, snapshot.index);
|
|
470
|
+
const indexKey = this.atomicPendingWriteIndexKey(key, epochValue, id);
|
|
471
|
+
await lease.publish("ZADD", indexKey, 0, dataKey);
|
|
472
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
473
|
+
const expected = await readAtomicValue(this.client, dataKey, "LangGraph atomic pending write");
|
|
474
|
+
if (snapshot.index >= 0 && expected != null) break;
|
|
475
|
+
if (snapshot.index >= 0 && epoch.legacyFallback) {
|
|
476
|
+
const legacy = await this.client.command("HGET", key, writeField(id, taskId, snapshot.index));
|
|
477
|
+
if (legacy != null) break;
|
|
478
|
+
}
|
|
479
|
+
if (await lease.compareAndSet(dataKey, expected, snapshot.value)) break;
|
|
480
|
+
if (attempt === 7) {
|
|
481
|
+
throw new Error("concurrent FerricStore LangGraph pending-write mutation did not converge");
|
|
482
|
+
}
|
|
483
|
+
}
|
|
361
484
|
}
|
|
485
|
+
await this.assertCurrentEpoch(threadId, epoch);
|
|
362
486
|
}, this.lockOptions);
|
|
363
487
|
}
|
|
364
488
|
async deleteThread(threadId) {
|
|
365
489
|
const normalized = requiredText(threadId, "threadId");
|
|
366
|
-
await withMutationLocks(this.client, [this.threadLockKey(normalized)], async () => {
|
|
367
|
-
for (
|
|
368
|
-
await this.
|
|
369
|
-
|
|
370
|
-
|
|
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));
|
|
490
|
+
await withMutationLocks(this.client, [this.threadLockKey(normalized)], async (lease) => {
|
|
491
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
492
|
+
const epoch = await this.ensureThreadEpoch(normalized, lease);
|
|
493
|
+
const next = Buffer.from(`${CURRENT_EPOCH_PREFIX}${randomUUID2()}`, "utf8");
|
|
494
|
+
if (await lease.compareAndSet(this.threadEpochKey(normalized), epoch.raw, next)) return;
|
|
379
495
|
}
|
|
380
|
-
|
|
496
|
+
throw new Error("concurrent FerricStore LangGraph thread deletion did not converge");
|
|
381
497
|
}, this.lockOptions);
|
|
382
498
|
}
|
|
383
499
|
catalogKey() {
|
|
@@ -399,6 +515,47 @@ var FerricStoreSaver = class extends BaseCheckpointSaver {
|
|
|
399
515
|
checkpointIndexKey(threadKey) {
|
|
400
516
|
return `${threadKey}:checkpoint-index`;
|
|
401
517
|
}
|
|
518
|
+
threadEpochKey(threadId) {
|
|
519
|
+
return `${this.threadCatalogKey(threadId)}:atomic-epoch`;
|
|
520
|
+
}
|
|
521
|
+
atomicCheckpointIndexKey(threadKey, epoch) {
|
|
522
|
+
return `${threadKey}:atomic:${sha256(epoch)}:checkpoint-index`;
|
|
523
|
+
}
|
|
524
|
+
atomicPendingWriteIndexKey(threadKey, epoch, checkpointId2) {
|
|
525
|
+
return `${threadKey}:atomic:${sha256(epoch)}:writes:${encodeComponent(checkpointId2)}`;
|
|
526
|
+
}
|
|
527
|
+
checkpointRecordKey(threadKey, epoch, checkpointId2) {
|
|
528
|
+
return `${threadKey}:atomic:${sha256(epoch)}:checkpoint:${encodeComponent(checkpointId2)}`;
|
|
529
|
+
}
|
|
530
|
+
pendingWriteKey(threadKey, epoch, checkpointId2, taskId, index) {
|
|
531
|
+
return `${threadKey}:atomic:${sha256(epoch)}:write:${encodeComponent(checkpointId2)}:${encodeComponent(taskId)}:${index}`;
|
|
532
|
+
}
|
|
533
|
+
async readThreadEpoch(threadId) {
|
|
534
|
+
const raw = await readAtomicValue(this.client, this.threadEpochKey(threadId), "LangGraph thread epoch");
|
|
535
|
+
if (raw == null) return { legacyFallback: true, raw, value: void 0 };
|
|
536
|
+
const value = raw.toString("utf8");
|
|
537
|
+
if (!value.startsWith(LEGACY_EPOCH_PREFIX) && !value.startsWith(CURRENT_EPOCH_PREFIX)) {
|
|
538
|
+
throw new Error("unsupported or corrupt FerricStore LangGraph thread epoch");
|
|
539
|
+
}
|
|
540
|
+
return { legacyFallback: value.startsWith(LEGACY_EPOCH_PREFIX), raw, value };
|
|
541
|
+
}
|
|
542
|
+
async ensureThreadEpoch(threadId, lease) {
|
|
543
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
544
|
+
const epoch = await this.readThreadEpoch(threadId);
|
|
545
|
+
if (epoch.value != null) return epoch;
|
|
546
|
+
const raw = Buffer.from(`${LEGACY_EPOCH_PREFIX}${randomUUID2()}`, "utf8");
|
|
547
|
+
if (await lease.compareAndSet(this.threadEpochKey(threadId), void 0, raw)) {
|
|
548
|
+
return { legacyFallback: true, raw, value: raw.toString("utf8") };
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
throw new Error("concurrent FerricStore LangGraph epoch initialization did not converge");
|
|
552
|
+
}
|
|
553
|
+
async assertCurrentEpoch(threadId, expected) {
|
|
554
|
+
const current = await this.readThreadEpoch(threadId);
|
|
555
|
+
if (expected.raw == null || current.raw?.equals(expected.raw) !== true) {
|
|
556
|
+
throw new Error("FerricStore LangGraph thread epoch changed while committing data");
|
|
557
|
+
}
|
|
558
|
+
}
|
|
402
559
|
async serialize(value) {
|
|
403
560
|
const [type, data] = await this.serde.dumpsTyped(value);
|
|
404
561
|
const typeBytes = Buffer.from(type, "utf8");
|
|
@@ -418,8 +575,16 @@ var FerricStoreSaver = class extends BaseCheckpointSaver {
|
|
|
418
575
|
const type = bytes.subarray(2, typeLength + 2).toString("utf8");
|
|
419
576
|
return await this.serde.loadsTyped(type, bytes.subarray(typeLength + 2));
|
|
420
577
|
}
|
|
421
|
-
async
|
|
422
|
-
|
|
578
|
+
async readRecordAtEpoch(key, id, epoch) {
|
|
579
|
+
let value;
|
|
580
|
+
if (epoch.value != null) {
|
|
581
|
+
value = await this.client.command("GET", this.checkpointRecordKey(key, epoch.value, id));
|
|
582
|
+
if (value == null && epoch.legacyFallback) {
|
|
583
|
+
value = await this.client.command("HGET", key, checkpointField(id));
|
|
584
|
+
}
|
|
585
|
+
} else {
|
|
586
|
+
value = await this.client.command("HGET", key, checkpointField(id));
|
|
587
|
+
}
|
|
423
588
|
if (value == null) return void 0;
|
|
424
589
|
const record = await this.deserialize(value, "LangGraph checkpoint record");
|
|
425
590
|
if (record.formatVersion !== FORMAT_VERSION || record.checkpointId !== id) {
|
|
@@ -427,23 +592,45 @@ var FerricStoreSaver = class extends BaseCheckpointSaver {
|
|
|
427
592
|
}
|
|
428
593
|
return record;
|
|
429
594
|
}
|
|
430
|
-
async
|
|
431
|
-
const
|
|
432
|
-
|
|
433
|
-
|
|
595
|
+
async pendingWritesAtEpoch(key, id, epoch) {
|
|
596
|
+
const records = /* @__PURE__ */ new Map();
|
|
597
|
+
if (epoch.value != null) {
|
|
598
|
+
const keys = arrayResponse(
|
|
599
|
+
await this.client.command("ZRANGE", this.atomicPendingWriteIndexKey(key, epoch.value, id), 0, -1),
|
|
600
|
+
"LangGraph atomic pending-write index response"
|
|
601
|
+
);
|
|
602
|
+
for (const rawKey of keys) {
|
|
603
|
+
const dataKey = textResponse(rawKey, "LangGraph pending-write key");
|
|
604
|
+
const value = await this.client.command("GET", dataKey);
|
|
605
|
+
if (value == null) continue;
|
|
606
|
+
const record = await this.deserialize(value, "LangGraph pending write");
|
|
607
|
+
records.set(pendingWriteIdentity(record), record);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
if (epoch.legacyFallback) {
|
|
611
|
+
const fields = await this.scanHash(key, `${WRITE_FIELD_PREFIX}${encodeComponent(id)}:*`);
|
|
612
|
+
for (const [, value] of fields) {
|
|
613
|
+
const record = await this.deserialize(value, "LangGraph pending write");
|
|
614
|
+
const identity2 = pendingWriteIdentity(record);
|
|
615
|
+
if (!records.has(identity2)) records.set(identity2, record);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
const ordered = [...records.values()];
|
|
619
|
+
for (const record of ordered) {
|
|
434
620
|
if (record.formatVersion !== FORMAT_VERSION) {
|
|
435
621
|
throw new Error("unsupported FerricStore LangGraph pending-write format");
|
|
436
622
|
}
|
|
437
623
|
}
|
|
438
|
-
|
|
439
|
-
return
|
|
624
|
+
ordered.sort((left, right) => left.taskId.localeCompare(right.taskId) || left.index - right.index);
|
|
625
|
+
return ordered.map((record) => [record.taskId, record.channel, record.value]);
|
|
440
626
|
}
|
|
441
627
|
async tupleFromRecord(key, record) {
|
|
628
|
+
const epoch = await this.readThreadEpoch(record.threadId);
|
|
442
629
|
return {
|
|
443
630
|
checkpoint: record.checkpoint,
|
|
444
631
|
config: checkpointConfig(record.threadId, record.checkpointNs, record.checkpointId),
|
|
445
632
|
metadata: record.metadata,
|
|
446
|
-
pendingWrites: await this.
|
|
633
|
+
pendingWrites: await this.pendingWritesAtEpoch(key, record.checkpointId, epoch),
|
|
447
634
|
...record.parentCheckpointId == null ? {} : { parentConfig: checkpointConfig(record.threadId, record.checkpointNs, record.parentCheckpointId) }
|
|
448
635
|
};
|
|
449
636
|
}
|
|
@@ -475,15 +662,27 @@ var FerricStoreSaver = class extends BaseCheckpointSaver {
|
|
|
475
662
|
);
|
|
476
663
|
return values.map((value) => textResponse(value, "thread checkpoint key")).sort();
|
|
477
664
|
}
|
|
478
|
-
async
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
665
|
+
async checkpointIdsAtEpoch(key, epoch) {
|
|
666
|
+
const ids = /* @__PURE__ */ new Set();
|
|
667
|
+
if (epoch.value != null) {
|
|
668
|
+
const values = arrayResponse(
|
|
669
|
+
await this.client.command("ZREVRANGE", this.atomicCheckpointIndexKey(key, epoch.value), 0, -1),
|
|
670
|
+
"atomic checkpoint index response"
|
|
671
|
+
);
|
|
672
|
+
for (const value of values) ids.add(textResponse(value, "checkpoint ID"));
|
|
673
|
+
}
|
|
674
|
+
if (epoch.legacyFallback) {
|
|
675
|
+
const values = arrayResponse(
|
|
676
|
+
await this.client.command("ZREVRANGE", this.checkpointIndexKey(key), 0, -1),
|
|
677
|
+
"checkpoint index response"
|
|
678
|
+
);
|
|
679
|
+
for (const value of values) ids.add(textResponse(value, "checkpoint ID"));
|
|
680
|
+
}
|
|
681
|
+
return [...ids].sort((left, right) => right.localeCompare(left));
|
|
484
682
|
}
|
|
485
|
-
async collectGlobal(result, beforeId, expectedId, filter, limit) {
|
|
683
|
+
async collectGlobal(result, beforeId, expectedId, checkpointNs, filter, limit) {
|
|
486
684
|
let offset = 0;
|
|
685
|
+
const seen = /* @__PURE__ */ new Set();
|
|
487
686
|
while (limit == null || result.length < limit) {
|
|
488
687
|
const values = arrayResponse(
|
|
489
688
|
await this.client.command("ZREVRANGE", this.catalogKey(), offset, offset + this.scanCount - 1),
|
|
@@ -491,11 +690,24 @@ var FerricStoreSaver = class extends BaseCheckpointSaver {
|
|
|
491
690
|
);
|
|
492
691
|
if (values.length === 0) break;
|
|
493
692
|
for (const value of values) {
|
|
494
|
-
const
|
|
693
|
+
const locator = decodeCheckpointLocator(value);
|
|
694
|
+
const { checkpointId: id, threadKey: key } = locator;
|
|
495
695
|
if (expectedId != null && id !== expectedId) continue;
|
|
496
696
|
if (beforeId != null && id >= beforeId) continue;
|
|
497
|
-
|
|
498
|
-
if (
|
|
697
|
+
let record;
|
|
698
|
+
if (locator.threadId != null) {
|
|
699
|
+
record = await this.readRecordAtEpoch(key, id, await this.readThreadEpoch(locator.threadId));
|
|
700
|
+
} else {
|
|
701
|
+
const legacy = await this.client.command("HGET", key, checkpointField(id));
|
|
702
|
+
if (legacy != null) {
|
|
703
|
+
const candidate = await this.deserialize(legacy, "LangGraph checkpoint record");
|
|
704
|
+
record = await this.readRecordAtEpoch(key, id, await this.readThreadEpoch(candidate.threadId));
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
if (record == null || checkpointNs != null && record.checkpointNs !== checkpointNs || !metadataMatches(record.metadata, filter)) continue;
|
|
708
|
+
const identity2 = lengthPrefixed([record.threadId, record.checkpointNs, record.checkpointId]).toString("base64url");
|
|
709
|
+
if (seen.has(identity2)) continue;
|
|
710
|
+
seen.add(identity2);
|
|
499
711
|
result.push({ key, record });
|
|
500
712
|
if (limit != null && result.length >= limit) break;
|
|
501
713
|
}
|
|
@@ -544,8 +756,18 @@ function writeField(id, taskId, index) {
|
|
|
544
756
|
function encodeComponent(value) {
|
|
545
757
|
return Buffer.from(value, "utf8").toString("base64url");
|
|
546
758
|
}
|
|
547
|
-
function checkpointLocator(id, threadKey) {
|
|
548
|
-
return Buffer.concat([orderedText(id), Buffer.from(threadKey, "utf8")]);
|
|
759
|
+
function checkpointLocator(id, threadKey, threadId) {
|
|
760
|
+
if (threadId == null) return Buffer.concat([orderedText(id), Buffer.from(threadKey, "utf8")]);
|
|
761
|
+
const threadIdBytes = Buffer.from(threadId, "utf8");
|
|
762
|
+
const length = Buffer.allocUnsafe(4);
|
|
763
|
+
length.writeUInt32BE(threadIdBytes.length);
|
|
764
|
+
return Buffer.concat([
|
|
765
|
+
orderedText(id),
|
|
766
|
+
Buffer.from([0, 1]),
|
|
767
|
+
length,
|
|
768
|
+
threadIdBytes,
|
|
769
|
+
Buffer.from(threadKey, "utf8")
|
|
770
|
+
]);
|
|
549
771
|
}
|
|
550
772
|
function decodeCheckpointLocator(value) {
|
|
551
773
|
if (!(typeof value === "string" || Buffer.isBuffer(value) || value instanceof Uint8Array)) {
|
|
@@ -553,9 +775,21 @@ function decodeCheckpointLocator(value) {
|
|
|
553
775
|
}
|
|
554
776
|
const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value);
|
|
555
777
|
const decoded = decodeOrderedText(bytes, 0);
|
|
556
|
-
|
|
778
|
+
let offset = decoded.offset;
|
|
779
|
+
let threadId;
|
|
780
|
+
if (bytes[offset] === 0 && bytes[offset + 1] === 1) {
|
|
781
|
+
offset += 2;
|
|
782
|
+
if (bytes.length < offset + 4) throw new Error("truncated FerricStore checkpoint locator");
|
|
783
|
+
const length = bytes.readUInt32BE(offset);
|
|
784
|
+
offset += 4;
|
|
785
|
+
if (bytes.length < offset + length) throw new Error("truncated FerricStore checkpoint locator thread ID");
|
|
786
|
+
threadId = bytes.subarray(offset, offset + length).toString("utf8");
|
|
787
|
+
offset += length;
|
|
788
|
+
if (threadId.length === 0) throw new Error("FerricStore checkpoint locator has an empty thread ID");
|
|
789
|
+
}
|
|
790
|
+
const threadKey = bytes.subarray(offset).toString("utf8");
|
|
557
791
|
if (threadKey.length === 0) throw new Error("FerricStore checkpoint locator has an empty thread key");
|
|
558
|
-
return { checkpointId: decoded.value, threadKey };
|
|
792
|
+
return { checkpointId: decoded.value, ...threadId == null ? {} : { threadId }, threadKey };
|
|
559
793
|
}
|
|
560
794
|
function orderedText(value) {
|
|
561
795
|
const output = [];
|
|
@@ -598,12 +832,12 @@ function lengthPrefixed(values) {
|
|
|
598
832
|
}
|
|
599
833
|
return Buffer.concat(parts);
|
|
600
834
|
}
|
|
601
|
-
function
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
835
|
+
function requireEpoch(epoch) {
|
|
836
|
+
if (epoch.value == null) throw new Error("FerricStore LangGraph thread epoch is not initialized");
|
|
837
|
+
return epoch.value;
|
|
838
|
+
}
|
|
839
|
+
function pendingWriteIdentity(record) {
|
|
840
|
+
return lengthPrefixed([record.taskId, String(record.index)]).toString("base64url");
|
|
607
841
|
}
|
|
608
842
|
|
|
609
843
|
// src/langgraph/store.ts
|
|
@@ -613,6 +847,7 @@ import {
|
|
|
613
847
|
} from "@langchain/langgraph";
|
|
614
848
|
var FORMAT_VERSION2 = 1;
|
|
615
849
|
var ITEM_FIELD_PREFIX = "item:";
|
|
850
|
+
var DELETED_ITEM = Buffer.from("ferricstore:langgraph:item:deleted:v2", "utf8");
|
|
616
851
|
var FerricStoreStore = class extends BaseStore {
|
|
617
852
|
client;
|
|
618
853
|
keyPrefix;
|
|
@@ -635,7 +870,7 @@ var FerricStoreStore = class extends BaseStore {
|
|
|
635
870
|
validatePut(operation);
|
|
636
871
|
return this.itemLockKey(operation.namespace, operation.key);
|
|
637
872
|
});
|
|
638
|
-
return await withMutationLocks(this.client, lockKeys, async () => {
|
|
873
|
+
return await withMutationLocks(this.client, lockKeys, async (lease) => {
|
|
639
874
|
const results = [];
|
|
640
875
|
const puts = /* @__PURE__ */ new Map();
|
|
641
876
|
for (const operation of operations) {
|
|
@@ -652,7 +887,7 @@ var FerricStoreStore = class extends BaseStore {
|
|
|
652
887
|
throw new TypeError("unsupported LangGraph store operation");
|
|
653
888
|
}
|
|
654
889
|
}
|
|
655
|
-
for (const operation of puts.values()) await this.putOperation(operation);
|
|
890
|
+
for (const operation of puts.values()) await this.putOperation(operation, lease);
|
|
656
891
|
return results;
|
|
657
892
|
}, this.lockOptions);
|
|
658
893
|
}
|
|
@@ -663,47 +898,45 @@ var FerricStoreStore = class extends BaseStore {
|
|
|
663
898
|
return `${this.keyPrefix}:{lgs:${sha2562(namespaceIdentity(namespace))}}:namespace`;
|
|
664
899
|
}
|
|
665
900
|
itemLockKey(namespace, key) {
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
keyBytes
|
|
671
|
-
]))}}:mutation-lock`;
|
|
901
|
+
return `${this.keyPrefix}:{lgsi:${itemIdentityDigest(namespace, key)}}:mutation-lock`;
|
|
902
|
+
}
|
|
903
|
+
itemDataKey(namespace, key) {
|
|
904
|
+
return `${this.keyPrefix}:{lgsi:${itemIdentityDigest(namespace, key)}}:atomic-item`;
|
|
672
905
|
}
|
|
673
906
|
async getOperation(operation) {
|
|
674
907
|
validateNamespace(operation.namespace);
|
|
675
908
|
if (typeof operation.key !== "string") throw new TypeError("store key must be text");
|
|
676
|
-
const
|
|
677
|
-
|
|
678
|
-
this.namespaceKey(operation.namespace),
|
|
679
|
-
itemField(operation.key)
|
|
680
|
-
);
|
|
681
|
-
return value == null ? null : decodeItem(value);
|
|
909
|
+
const snapshot = await this.readItemSnapshot(operation.namespace, operation.key);
|
|
910
|
+
return snapshot.record == null ? null : itemFromRecord(snapshot.record);
|
|
682
911
|
}
|
|
683
|
-
async putOperation(operation) {
|
|
912
|
+
async putOperation(operation, lease) {
|
|
684
913
|
validatePut(operation);
|
|
685
|
-
const
|
|
686
|
-
const field = itemField(operation.key);
|
|
914
|
+
const dataKey = this.itemDataKey(operation.namespace, operation.key);
|
|
687
915
|
const locator = catalogMember(operation.namespace, operation.key);
|
|
688
916
|
if (operation.value == null) {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
917
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
918
|
+
const snapshot = await this.readItemSnapshot(operation.namespace, operation.key);
|
|
919
|
+
if (snapshot.expected?.equals(DELETED_ITEM) === true) return;
|
|
920
|
+
if (await lease.compareAndSet(dataKey, snapshot.expected, DELETED_ITEM)) return;
|
|
921
|
+
}
|
|
922
|
+
throw new Error("concurrent FerricStore LangGraph store deletion did not converge");
|
|
692
923
|
}
|
|
693
924
|
const storedValue = snapshotJsonValue(operation.value);
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
925
|
+
await lease.publish("ZADD", this.catalogKey(), 0, locator);
|
|
926
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
927
|
+
const snapshot = await this.readItemSnapshot(operation.namespace, operation.key);
|
|
928
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
929
|
+
const record = {
|
|
930
|
+
createdAt: snapshot.record?.createdAt ?? now,
|
|
931
|
+
formatVersion: FORMAT_VERSION2,
|
|
932
|
+
key: operation.key,
|
|
933
|
+
namespace: [...operation.namespace],
|
|
934
|
+
updatedAt: now,
|
|
935
|
+
value: storedValue
|
|
936
|
+
};
|
|
937
|
+
if (await lease.compareAndSet(dataKey, snapshot.expected, encodeItem(record))) return;
|
|
938
|
+
}
|
|
939
|
+
throw new Error("concurrent FerricStore LangGraph store mutation did not converge");
|
|
707
940
|
}
|
|
708
941
|
async searchOperation(operation) {
|
|
709
942
|
validateNamespacePrefix(operation.namespacePrefix);
|
|
@@ -741,8 +974,23 @@ var FerricStoreStore = class extends BaseStore {
|
|
|
741
974
|
return [...namespaces.values()].sort(compareNamespaces).slice(offset, offset + limit);
|
|
742
975
|
}
|
|
743
976
|
async readCatalogItem(namespace, key) {
|
|
744
|
-
const
|
|
745
|
-
return
|
|
977
|
+
const snapshot = await this.readItemSnapshot(namespace, key);
|
|
978
|
+
return snapshot.record == null ? null : itemFromRecord(snapshot.record);
|
|
979
|
+
}
|
|
980
|
+
async readItemSnapshot(namespace, key) {
|
|
981
|
+
const expected = await readAtomicValue(
|
|
982
|
+
this.client,
|
|
983
|
+
this.itemDataKey(namespace, key),
|
|
984
|
+
"LangGraph atomic store item"
|
|
985
|
+
);
|
|
986
|
+
if (expected != null) {
|
|
987
|
+
return {
|
|
988
|
+
expected,
|
|
989
|
+
record: expected.equals(DELETED_ITEM) ? null : decodeItemRecord(expected)
|
|
990
|
+
};
|
|
991
|
+
}
|
|
992
|
+
const legacy = await this.client.command("HGET", this.namespaceKey(namespace), itemField(key));
|
|
993
|
+
return { expected, record: legacy == null ? null : decodeItemRecord(legacy) };
|
|
746
994
|
}
|
|
747
995
|
async *catalogLocators() {
|
|
748
996
|
let offset = 0;
|
|
@@ -842,8 +1090,7 @@ function snapshotJsonValue(value) {
|
|
|
842
1090
|
function encodeItem(record) {
|
|
843
1091
|
return Buffer.from(JSON.stringify(record), "utf8");
|
|
844
1092
|
}
|
|
845
|
-
function
|
|
846
|
-
const record = decodeItemRecord(value);
|
|
1093
|
+
function itemFromRecord(record) {
|
|
847
1094
|
return {
|
|
848
1095
|
createdAt: new Date(record.createdAt),
|
|
849
1096
|
key: record.key,
|
|
@@ -852,6 +1099,14 @@ function decodeItem(value) {
|
|
|
852
1099
|
value: record.value
|
|
853
1100
|
};
|
|
854
1101
|
}
|
|
1102
|
+
function itemIdentityDigest(namespace, key) {
|
|
1103
|
+
const keyBytes = Buffer.from(key, "utf8");
|
|
1104
|
+
return sha2562(Buffer.concat([
|
|
1105
|
+
namespaceIdentity(namespace),
|
|
1106
|
+
uint64(keyBytes.length),
|
|
1107
|
+
keyBytes
|
|
1108
|
+
]));
|
|
1109
|
+
}
|
|
855
1110
|
function decodeItemRecord(value) {
|
|
856
1111
|
if (!(typeof value === "string" || Buffer.isBuffer(value) || value instanceof Uint8Array)) {
|
|
857
1112
|
throw new TypeError("FerricStore returned a non-binary LangGraph store item");
|
|
@@ -1118,13 +1373,22 @@ var LangGraphFlow = class {
|
|
|
1118
1373
|
this.options = { ...options };
|
|
1119
1374
|
}
|
|
1120
1375
|
async config(flow, graphContext) {
|
|
1376
|
+
const invokeOptions = this.options.invokeOptions ?? {};
|
|
1121
1377
|
const additional = await this.options.config?.(flow) ?? {};
|
|
1378
|
+
const baseConfigurable = invokeOptions.configurable ?? {};
|
|
1379
|
+
if (typeof baseConfigurable !== "object" || Array.isArray(baseConfigurable)) {
|
|
1380
|
+
throw new TypeError("LangGraph invokeOptions configurable must be an object");
|
|
1381
|
+
}
|
|
1122
1382
|
const rawConfigurable = additional.configurable ?? {};
|
|
1123
|
-
if (
|
|
1383
|
+
if (typeof rawConfigurable !== "object" || Array.isArray(rawConfigurable)) {
|
|
1124
1384
|
throw new TypeError("LangGraph config configurable must be an object");
|
|
1125
1385
|
}
|
|
1386
|
+
const baseMetadata = invokeOptions.metadata ?? {};
|
|
1387
|
+
if (typeof baseMetadata !== "object" || Array.isArray(baseMetadata)) {
|
|
1388
|
+
throw new TypeError("LangGraph invokeOptions metadata must be an object");
|
|
1389
|
+
}
|
|
1126
1390
|
const rawMetadata = additional.metadata ?? {};
|
|
1127
|
-
if (
|
|
1391
|
+
if (typeof rawMetadata !== "object" || Array.isArray(rawMetadata)) {
|
|
1128
1392
|
throw new TypeError("LangGraph config metadata must be an object");
|
|
1129
1393
|
}
|
|
1130
1394
|
const threadId = requireText(
|
|
@@ -1137,13 +1401,19 @@ var LangGraphFlow = class {
|
|
|
1137
1401
|
"checkpointNs",
|
|
1138
1402
|
true
|
|
1139
1403
|
);
|
|
1140
|
-
const context = graphContext === void 0 ? this.options.context
|
|
1404
|
+
const context = graphContext === void 0 ? this.options.context != null ? await this.options.context(flow) : Object.hasOwn(additional, "context") ? additional.context : Object.hasOwn(invokeOptions, "context") ? invokeOptions.context : new LangGraphFlowContext(flow, threadId, checkpointNs) : graphContext;
|
|
1141
1405
|
return {
|
|
1142
|
-
...
|
|
1406
|
+
...invokeOptions,
|
|
1143
1407
|
...additional,
|
|
1144
|
-
configurable: {
|
|
1408
|
+
configurable: {
|
|
1409
|
+
...baseConfigurable,
|
|
1410
|
+
...rawConfigurable,
|
|
1411
|
+
checkpoint_ns: checkpointNs,
|
|
1412
|
+
thread_id: threadId
|
|
1413
|
+
},
|
|
1145
1414
|
context,
|
|
1146
1415
|
metadata: {
|
|
1416
|
+
...baseMetadata,
|
|
1147
1417
|
...rawMetadata,
|
|
1148
1418
|
ferricflow_id: flow.id,
|
|
1149
1419
|
ferricflow_state: flow.logicalState,
|
|
@@ -1159,7 +1429,7 @@ var LangGraphFlow = class {
|
|
|
1159
1429
|
if ((this.options.recoverExisting ?? true) && this.graph.getState != null) {
|
|
1160
1430
|
hasCheckpoint = snapshotHasCheckpoint(await this.graph.getState(config));
|
|
1161
1431
|
}
|
|
1162
|
-
input = hasCheckpoint ? null :
|
|
1432
|
+
input = hasCheckpoint ? null : this.options.input == null ? flow.payload : await this.options.input(flow);
|
|
1163
1433
|
}
|
|
1164
1434
|
const value = await this.graph.invoke(input, config);
|
|
1165
1435
|
const configurable = config.configurable;
|