@ferricstore/ferricstore 0.11.11 → 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 +19 -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 +1548 -0
- package/dist/langgraph.cjs.map +1 -0
- package/dist/langgraph.d.cts +158 -0
- package/dist/langgraph.d.ts +158 -0
- package/dist/langgraph.js +1522 -0
- package/dist/langgraph.js.map +1 -0
- package/dist/openai-agents.cjs +717 -0
- package/dist/openai-agents.cjs.map +1 -0
- package/dist/openai-agents.d.cts +50 -0
- package/dist/openai-agents.d.ts +50 -0
- package/dist/openai-agents.js +692 -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-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 +176 -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 +37 -24
- package/docs/api/media/agent-frameworks.md +176 -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 +43 -4
|
@@ -0,0 +1,1548 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/langgraph.ts
|
|
21
|
+
var langgraph_exports = {};
|
|
22
|
+
__export(langgraph_exports, {
|
|
23
|
+
FerricStoreSaver: () => FerricStoreSaver,
|
|
24
|
+
FerricStoreStore: () => FerricStoreStore,
|
|
25
|
+
LangGraphFlow: () => LangGraphFlow,
|
|
26
|
+
LangGraphFlowContext: () => LangGraphFlowContext,
|
|
27
|
+
LangGraphFlowRun: () => LangGraphFlowRun
|
|
28
|
+
});
|
|
29
|
+
module.exports = __toCommonJS(langgraph_exports);
|
|
30
|
+
|
|
31
|
+
// src/langgraph/checkpoint.ts
|
|
32
|
+
var import_node_crypto2 = require("crypto");
|
|
33
|
+
var import_langgraph = require("@langchain/langgraph");
|
|
34
|
+
|
|
35
|
+
// src/agent-persistence/durability.ts
|
|
36
|
+
var import_node_crypto = require("crypto");
|
|
37
|
+
var import_promises = require("timers/promises");
|
|
38
|
+
|
|
39
|
+
// src/errors.ts
|
|
40
|
+
var FerricStoreError = class extends Error {
|
|
41
|
+
code = "ferricstore_error";
|
|
42
|
+
raw;
|
|
43
|
+
retryable;
|
|
44
|
+
safeToRetry;
|
|
45
|
+
retryAfterMs;
|
|
46
|
+
constructor(message, options = {}) {
|
|
47
|
+
super(message, { cause: options.cause });
|
|
48
|
+
this.name = new.target.name;
|
|
49
|
+
this.raw = options.raw;
|
|
50
|
+
this.retryable = options.retryable ?? structuredBooleanField(options.raw, "retryable");
|
|
51
|
+
this.safeToRetry = options.safeToRetry ?? structuredBooleanField(options.raw, "safe_to_retry");
|
|
52
|
+
this.retryAfterMs = options.retryAfterMs ?? structuredIntegerField(options.raw, "retry_after_ms");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
var LockHeldError = class extends FerricStoreError {
|
|
56
|
+
code = "lock_held";
|
|
57
|
+
};
|
|
58
|
+
function structuredIntegerField(raw, name) {
|
|
59
|
+
const value = structuredField(raw, name);
|
|
60
|
+
if (typeof value === "number") {
|
|
61
|
+
return Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
62
|
+
}
|
|
63
|
+
if (typeof value === "bigint") {
|
|
64
|
+
return value >= 0n && value <= BigInt(Number.MAX_SAFE_INTEGER) ? Number(value) : void 0;
|
|
65
|
+
}
|
|
66
|
+
const text = binaryText(value);
|
|
67
|
+
return text == null ? void 0 : nonNegativeSafeIntegerText(text);
|
|
68
|
+
}
|
|
69
|
+
function structuredBooleanField(raw, name) {
|
|
70
|
+
const value = structuredField(raw, name);
|
|
71
|
+
if (typeof value === "boolean") return value;
|
|
72
|
+
const text = binaryText(value)?.toLowerCase();
|
|
73
|
+
if (text === "true" || text === "1") return true;
|
|
74
|
+
if (text === "false" || text === "0") return false;
|
|
75
|
+
return void 0;
|
|
76
|
+
}
|
|
77
|
+
function nonNegativeSafeIntegerText(value) {
|
|
78
|
+
if (!/^[0-9]+$/u.test(value)) return void 0;
|
|
79
|
+
const parsed = Number.parseInt(value, 10);
|
|
80
|
+
return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : void 0;
|
|
81
|
+
}
|
|
82
|
+
function structuredField(raw, name) {
|
|
83
|
+
if (raw instanceof Map) {
|
|
84
|
+
if (raw.has(name)) return raw.get(name);
|
|
85
|
+
for (const [key, value] of raw.entries()) {
|
|
86
|
+
if (binaryText(key) === name) return value;
|
|
87
|
+
}
|
|
88
|
+
return void 0;
|
|
89
|
+
}
|
|
90
|
+
if (typeof raw === "object" && raw != null && Object.hasOwn(raw, name)) {
|
|
91
|
+
return raw[name];
|
|
92
|
+
}
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
function binaryText(value) {
|
|
96
|
+
if (typeof value === "string") return value;
|
|
97
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value).toString("utf8");
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/agent-persistence/durability.ts
|
|
102
|
+
var DEFAULT_LOCK_OPTIONS = {
|
|
103
|
+
lockRetryMs: 10,
|
|
104
|
+
lockTtlMs: 3e5,
|
|
105
|
+
lockWaitMs: 3e4
|
|
106
|
+
};
|
|
107
|
+
function normalizeKeyPrefix(value, defaultValue) {
|
|
108
|
+
const prefix = value.length === 0 ? defaultValue : value;
|
|
109
|
+
if (prefix.includes("\0")) throw new TypeError("keyPrefix must not contain NUL bytes");
|
|
110
|
+
const normalized = prefix.replace(/:+$/u, "");
|
|
111
|
+
if (normalized.length === 0) throw new TypeError("keyPrefix must contain a character other than ':'");
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
function positiveInteger(value, fallback, name) {
|
|
115
|
+
const normalized = value ?? fallback;
|
|
116
|
+
if (!Number.isSafeInteger(normalized) || normalized <= 0) {
|
|
117
|
+
throw new TypeError(`${name} must be a positive safe integer`);
|
|
118
|
+
}
|
|
119
|
+
return normalized;
|
|
120
|
+
}
|
|
121
|
+
function nonNegativeInteger(value, fallback, name) {
|
|
122
|
+
const normalized = value ?? fallback;
|
|
123
|
+
if (!Number.isSafeInteger(normalized) || normalized < 0) {
|
|
124
|
+
throw new TypeError(`${name} must be a non-negative safe integer`);
|
|
125
|
+
}
|
|
126
|
+
return normalized;
|
|
127
|
+
}
|
|
128
|
+
function textResponse(value, name) {
|
|
129
|
+
if (typeof value === "string") return value;
|
|
130
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value).toString("utf8");
|
|
131
|
+
throw new TypeError(`FerricStore returned an invalid ${name}`);
|
|
132
|
+
}
|
|
133
|
+
function arrayResponse(value, name) {
|
|
134
|
+
if (!Array.isArray(value)) throw new TypeError(`FerricStore returned an invalid ${name}`);
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
function integerResponse(value, name) {
|
|
138
|
+
const parsed = typeof value === "number" ? value : Number(textResponse(value, name));
|
|
139
|
+
if (!Number.isSafeInteger(parsed)) throw new TypeError(`FerricStore returned an invalid ${name}`);
|
|
140
|
+
return parsed;
|
|
141
|
+
}
|
|
142
|
+
async function readAtomicValue(client, key, name) {
|
|
143
|
+
const value = await client.command("GET", key);
|
|
144
|
+
if (value == null) return void 0;
|
|
145
|
+
if (typeof value === "string") return Buffer.from(value, "utf8");
|
|
146
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value);
|
|
147
|
+
throw new TypeError(`FerricStore returned a non-binary ${name}`);
|
|
148
|
+
}
|
|
149
|
+
async function compareAndSetAtomicValue(client, key, expected, value) {
|
|
150
|
+
if (expected == null) {
|
|
151
|
+
const response2 = await client.command("SET", key, value, "NX");
|
|
152
|
+
if (response2 == null || response2 === false) return false;
|
|
153
|
+
if (response2 === true) return true;
|
|
154
|
+
return textResponse(response2, "SET NX response").toUpperCase() === "OK";
|
|
155
|
+
}
|
|
156
|
+
const response = await client.command("CAS", key, expected, value);
|
|
157
|
+
if (response == null || response === false) return false;
|
|
158
|
+
if (response === true) return true;
|
|
159
|
+
return integerResponse(response, "CAS response") === 1;
|
|
160
|
+
}
|
|
161
|
+
async function withMutationLocks(client, keys, operation, options = {}) {
|
|
162
|
+
const orderedKeys = [...new Set(keys)].sort();
|
|
163
|
+
if (orderedKeys.length === 0) {
|
|
164
|
+
const signal = new AbortController().signal;
|
|
165
|
+
return await operation({
|
|
166
|
+
signal,
|
|
167
|
+
assertOwned: () => void 0,
|
|
168
|
+
publish: async (...args) => await additiveCommand(client, args),
|
|
169
|
+
compareAndSet: async (key, expected, value) => await compareAndSetAtomicValue(client, key, expected, value)
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
const normalized = {
|
|
173
|
+
lockRetryMs: positiveInteger(options.lockRetryMs, DEFAULT_LOCK_OPTIONS.lockRetryMs, "lockRetryMs"),
|
|
174
|
+
lockTtlMs: positiveInteger(options.lockTtlMs, DEFAULT_LOCK_OPTIONS.lockTtlMs, "lockTtlMs"),
|
|
175
|
+
lockWaitMs: nonNegativeInteger(options.lockWaitMs, DEFAULT_LOCK_OPTIONS.lockWaitMs, "lockWaitMs")
|
|
176
|
+
};
|
|
177
|
+
if (normalized.lockRetryMs >= normalized.lockTtlMs) {
|
|
178
|
+
throw new TypeError("lockRetryMs must be less than lockTtlMs");
|
|
179
|
+
}
|
|
180
|
+
const owner = (0, import_node_crypto.randomUUID)();
|
|
181
|
+
const acquired = [];
|
|
182
|
+
const deadline = performance.now() + normalized.lockWaitMs;
|
|
183
|
+
let primaryError;
|
|
184
|
+
let heartbeatError;
|
|
185
|
+
let releaseError;
|
|
186
|
+
let result;
|
|
187
|
+
let operationCompleted = false;
|
|
188
|
+
let conditionalCommitCompleted = false;
|
|
189
|
+
const heartbeatAbort = new AbortController();
|
|
190
|
+
const ownershipAbort = new AbortController();
|
|
191
|
+
const lastExtended = /* @__PURE__ */ new Map();
|
|
192
|
+
const loseOwnership = (error) => {
|
|
193
|
+
const normalizedError = errorObject(error);
|
|
194
|
+
heartbeatError ??= normalizedError;
|
|
195
|
+
if (!ownershipAbort.signal.aborted) ownershipAbort.abort(normalizedError);
|
|
196
|
+
return normalizedError;
|
|
197
|
+
};
|
|
198
|
+
const assertOwned = () => {
|
|
199
|
+
if (heartbeatError != null) throw errorObject(heartbeatError);
|
|
200
|
+
if (ownershipAbort.signal.aborted) throw errorObject(ownershipAbort.signal.reason);
|
|
201
|
+
};
|
|
202
|
+
const renewOwned = async () => {
|
|
203
|
+
assertOwned();
|
|
204
|
+
for (const key of acquired) {
|
|
205
|
+
try {
|
|
206
|
+
const response = await client.command("EXTEND", key, owner, normalized.lockTtlMs);
|
|
207
|
+
if (integerResponse(response, "EXTEND response") !== 1) {
|
|
208
|
+
throw new Error(`lost FerricStore lock ${JSON.stringify(key)} while mutating data`);
|
|
209
|
+
}
|
|
210
|
+
lastExtended.set(key, performance.now());
|
|
211
|
+
} catch (error) {
|
|
212
|
+
throw loseOwnership(new Error(
|
|
213
|
+
`could not validate FerricStore lock ${JSON.stringify(key)} before mutating data`,
|
|
214
|
+
{ cause: error }
|
|
215
|
+
));
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
assertOwned();
|
|
219
|
+
};
|
|
220
|
+
const lease = {
|
|
221
|
+
signal: ownershipAbort.signal,
|
|
222
|
+
assertOwned,
|
|
223
|
+
publish: async (...args) => {
|
|
224
|
+
await renewOwned();
|
|
225
|
+
const response = await additiveCommand(client, args);
|
|
226
|
+
assertOwned();
|
|
227
|
+
return response;
|
|
228
|
+
},
|
|
229
|
+
compareAndSet: async (key, expected, value) => {
|
|
230
|
+
await renewOwned();
|
|
231
|
+
const committed = await compareAndSetAtomicValue(client, key, expected, value);
|
|
232
|
+
if (committed) conditionalCommitCompleted = true;
|
|
233
|
+
else assertOwned();
|
|
234
|
+
return committed;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
try {
|
|
238
|
+
for (const key of orderedKeys) {
|
|
239
|
+
while (!await tryAcquireLock(client, key, owner, normalized.lockTtlMs)) {
|
|
240
|
+
if (performance.now() >= deadline) {
|
|
241
|
+
throw new Error(`timed out acquiring FerricStore lock ${JSON.stringify(key)}`);
|
|
242
|
+
}
|
|
243
|
+
await extendAcquiredLocks(client, acquired, owner, normalized.lockTtlMs);
|
|
244
|
+
await (0, import_promises.setTimeout)(normalized.lockRetryMs);
|
|
245
|
+
}
|
|
246
|
+
acquired.push(key);
|
|
247
|
+
}
|
|
248
|
+
await extendAcquiredLocks(client, acquired, owner, normalized.lockTtlMs);
|
|
249
|
+
for (const key of acquired) lastExtended.set(key, performance.now());
|
|
250
|
+
const heartbeat = renewLocks(
|
|
251
|
+
client,
|
|
252
|
+
acquired,
|
|
253
|
+
owner,
|
|
254
|
+
normalized.lockTtlMs,
|
|
255
|
+
lastExtended,
|
|
256
|
+
heartbeatAbort.signal,
|
|
257
|
+
(error) => {
|
|
258
|
+
loseOwnership(error);
|
|
259
|
+
}
|
|
260
|
+
);
|
|
261
|
+
try {
|
|
262
|
+
result = await operation(lease);
|
|
263
|
+
operationCompleted = true;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
primaryError = error;
|
|
266
|
+
} finally {
|
|
267
|
+
heartbeatAbort.abort();
|
|
268
|
+
await heartbeat;
|
|
269
|
+
}
|
|
270
|
+
} catch (error) {
|
|
271
|
+
primaryError ??= error;
|
|
272
|
+
} finally {
|
|
273
|
+
heartbeatAbort.abort();
|
|
274
|
+
for (const key of acquired.reverse()) {
|
|
275
|
+
try {
|
|
276
|
+
await client.command("UNLOCK", key, owner);
|
|
277
|
+
} catch (error) {
|
|
278
|
+
releaseError ??= error;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (primaryError != null) throw errorObject(primaryError);
|
|
283
|
+
if (heartbeatError != null && !conditionalCommitCompleted) throw errorObject(heartbeatError);
|
|
284
|
+
if (releaseError != null && !(heartbeatError != null && conditionalCommitCompleted)) {
|
|
285
|
+
throw errorObject(releaseError);
|
|
286
|
+
}
|
|
287
|
+
if (!operationCompleted) throw new Error("FerricStore mutation did not complete");
|
|
288
|
+
return result;
|
|
289
|
+
}
|
|
290
|
+
async function additiveCommand(client, args) {
|
|
291
|
+
const rawName = args[0];
|
|
292
|
+
const name = typeof rawName === "string" ? rawName.toUpperCase() : Buffer.isBuffer(rawName) || rawName instanceof Uint8Array ? Buffer.from(rawName).toString("utf8").toUpperCase() : "";
|
|
293
|
+
if (name !== "SADD" && name !== "ZADD") {
|
|
294
|
+
throw new TypeError("FerricStore mutation leases only publish add-only SADD or ZADD indexes");
|
|
295
|
+
}
|
|
296
|
+
if (name === "ZADD" && (args.length < 4 || args.length % 2 !== 0 || args.slice(2).some((value, index) => index % 2 === 0 && Number(value) !== 0))) {
|
|
297
|
+
throw new TypeError("FerricStore mutation leases only publish zero-score ZADD indexes");
|
|
298
|
+
}
|
|
299
|
+
return await client.command(...args);
|
|
300
|
+
}
|
|
301
|
+
async function extendAcquiredLocks(client, keys, owner, ttlMs) {
|
|
302
|
+
for (const key of keys) {
|
|
303
|
+
const response = await client.command("EXTEND", key, owner, ttlMs);
|
|
304
|
+
if (integerResponse(response, "EXTEND response") !== 1) {
|
|
305
|
+
throw new Error(`lost FerricStore lock ${JSON.stringify(key)} before mutating data`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
async function tryAcquireLock(client, key, owner, ttlMs) {
|
|
310
|
+
try {
|
|
311
|
+
const response = await client.command("LOCK", key, owner, ttlMs);
|
|
312
|
+
return response === true || response === "OK" || Buffer.isBuffer(response) && response.equals(Buffer.from("OK"));
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (error instanceof LockHeldError) return false;
|
|
315
|
+
throw error;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
async function renewLocks(client, keys, owner, ttlMs, lastExtended, signal, onError) {
|
|
319
|
+
const intervalMs = Math.max(Math.floor(ttlMs / 3), 1);
|
|
320
|
+
const retryMs = Math.min(Math.max(Math.floor(intervalMs / 10), 1), 1e3);
|
|
321
|
+
let waitMs = intervalMs;
|
|
322
|
+
while (!signal.aborted) {
|
|
323
|
+
try {
|
|
324
|
+
await (0, import_promises.setTimeout)(waitMs, void 0, { signal });
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (signal.aborted) return;
|
|
327
|
+
onError(error);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
const now = performance.now();
|
|
331
|
+
let retry = false;
|
|
332
|
+
for (const key of keys) {
|
|
333
|
+
try {
|
|
334
|
+
const response = await client.command("EXTEND", key, owner, ttlMs);
|
|
335
|
+
if (integerResponse(response, "EXTEND response") !== 1) {
|
|
336
|
+
onError(new Error(`lost FerricStore lock ${JSON.stringify(key)} while mutating data`));
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
lastExtended.set(key, now);
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (now - (lastExtended.get(key) ?? 0) >= ttlMs) {
|
|
342
|
+
onError(new Error(`lost FerricStore lock ${JSON.stringify(key)} while mutating data`, { cause: error }));
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
retry = true;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
waitMs = retry ? retryMs : intervalMs;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function errorObject(value) {
|
|
352
|
+
return value instanceof Error ? value : new Error("FerricStore mutation failed", { cause: value });
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// src/langgraph/checkpoint.ts
|
|
356
|
+
var FORMAT_VERSION = 1;
|
|
357
|
+
var LEGACY_EPOCH_PREFIX = "legacy:";
|
|
358
|
+
var CURRENT_EPOCH_PREFIX = "v2:";
|
|
359
|
+
var CHECKPOINT_FIELD_PREFIX = "checkpoint:";
|
|
360
|
+
var WRITE_FIELD_PREFIX = "write:";
|
|
361
|
+
var WRITES_INDEX = {
|
|
362
|
+
__error__: -1,
|
|
363
|
+
__interrupt__: -3,
|
|
364
|
+
__resume__: -4,
|
|
365
|
+
__scheduled__: -2
|
|
366
|
+
};
|
|
367
|
+
var FerricStoreSaver = class extends import_langgraph.BaseCheckpointSaver {
|
|
368
|
+
client;
|
|
369
|
+
keyPrefix;
|
|
370
|
+
scanCount;
|
|
371
|
+
lockOptions;
|
|
372
|
+
constructor(client, options = {}) {
|
|
373
|
+
super(options.serde);
|
|
374
|
+
this.client = client;
|
|
375
|
+
this.keyPrefix = normalizeKeyPrefix(options.keyPrefix ?? "langgraph:checkpoint", "langgraph:checkpoint");
|
|
376
|
+
this.scanCount = positiveInteger(options.scanCount, 256, "scanCount");
|
|
377
|
+
this.lockOptions = {
|
|
378
|
+
lockRetryMs: options.lockRetryMs,
|
|
379
|
+
lockTtlMs: options.lockTtlMs,
|
|
380
|
+
lockWaitMs: options.lockWaitMs
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
async getTuple(config) {
|
|
384
|
+
const { checkpointNs, threadId } = identity(config);
|
|
385
|
+
const key = this.threadKey(threadId, checkpointNs);
|
|
386
|
+
const epoch = await this.readThreadEpoch(threadId);
|
|
387
|
+
let id = checkpointId(config);
|
|
388
|
+
let record;
|
|
389
|
+
if (id != null) {
|
|
390
|
+
record = await this.readRecordAtEpoch(key, id, epoch);
|
|
391
|
+
} else {
|
|
392
|
+
for (const candidate of await this.checkpointIdsAtEpoch(key, epoch)) {
|
|
393
|
+
record = await this.readRecordAtEpoch(key, candidate, epoch);
|
|
394
|
+
if (record != null) {
|
|
395
|
+
id = candidate;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (id == null || record == null) return void 0;
|
|
401
|
+
return await this.tupleFromRecord(key, record);
|
|
402
|
+
}
|
|
403
|
+
async *list(config, options = {}) {
|
|
404
|
+
const limit = options.limit;
|
|
405
|
+
if (limit != null && (!Number.isSafeInteger(limit) || limit < 0)) {
|
|
406
|
+
throw new TypeError("limit must be a non-negative safe integer");
|
|
407
|
+
}
|
|
408
|
+
if (limit === 0) return;
|
|
409
|
+
const beforeId = options.before == null ? void 0 : checkpointId(options.before);
|
|
410
|
+
const configurable = config.configurable;
|
|
411
|
+
const threadId = optionalText(configurable?.thread_id, "thread_id");
|
|
412
|
+
const expectedId = optionalText(configurable?.checkpoint_id ?? configurable?.thread_ts, "checkpoint_id");
|
|
413
|
+
const namespaceSpecified = configurable != null && Object.hasOwn(configurable, "checkpoint_ns");
|
|
414
|
+
const checkpointNs = namespaceSpecified ? requiredText(configurable?.checkpoint_ns, "checkpoint_ns", true) : void 0;
|
|
415
|
+
const matches = [];
|
|
416
|
+
if (threadId == null) {
|
|
417
|
+
await this.collectGlobal(matches, beforeId, expectedId, checkpointNs, options.filter, limit);
|
|
418
|
+
} else {
|
|
419
|
+
const keys = checkpointNs == null ? await this.threadKeys(threadId) : [this.threadKey(threadId, checkpointNs)];
|
|
420
|
+
for (const key of keys) {
|
|
421
|
+
const epoch = await this.readThreadEpoch(threadId);
|
|
422
|
+
const ids = expectedId == null ? await this.checkpointIdsAtEpoch(key, epoch) : [expectedId];
|
|
423
|
+
for (const id of ids) {
|
|
424
|
+
if (beforeId != null && id >= beforeId) continue;
|
|
425
|
+
const record = await this.readRecordAtEpoch(key, id, epoch);
|
|
426
|
+
if (record == null || !metadataMatches(record.metadata, options.filter)) continue;
|
|
427
|
+
matches.push({ key, record });
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
matches.sort((left, right) => right.record.checkpointId.localeCompare(left.record.checkpointId));
|
|
431
|
+
if (limit != null) matches.splice(limit);
|
|
432
|
+
}
|
|
433
|
+
for (const match of matches) yield await this.tupleFromRecord(match.key, match.record);
|
|
434
|
+
}
|
|
435
|
+
async put(config, checkpoint, metadata, _newVersions) {
|
|
436
|
+
void _newVersions;
|
|
437
|
+
const { checkpointNs, threadId } = identity(config);
|
|
438
|
+
if (typeof checkpoint.id !== "string" || checkpoint.id.length === 0) {
|
|
439
|
+
throw new TypeError("checkpoint.id must be a non-empty string");
|
|
440
|
+
}
|
|
441
|
+
const parentCheckpointId = checkpointId(config);
|
|
442
|
+
const record = {
|
|
443
|
+
checkpoint: (0, import_langgraph.copyCheckpoint)(checkpoint),
|
|
444
|
+
checkpointId: checkpoint.id,
|
|
445
|
+
checkpointNs,
|
|
446
|
+
formatVersion: FORMAT_VERSION,
|
|
447
|
+
metadata,
|
|
448
|
+
...parentCheckpointId == null ? {} : { parentCheckpointId },
|
|
449
|
+
threadId
|
|
450
|
+
};
|
|
451
|
+
const key = this.threadKey(threadId, checkpointNs);
|
|
452
|
+
await withMutationLocks(this.client, [this.threadLockKey(threadId)], async (lease) => {
|
|
453
|
+
const epoch = await this.ensureThreadEpoch(threadId, lease);
|
|
454
|
+
const locator = checkpointLocator(checkpoint.id, key, threadId);
|
|
455
|
+
await lease.publish("SADD", this.threadCatalogKey(threadId), key);
|
|
456
|
+
await lease.publish("ZADD", this.threadLocatorCatalogKey(threadId), 0, locator);
|
|
457
|
+
await lease.publish("ZADD", this.catalogKey(), 0, locator);
|
|
458
|
+
await lease.publish("ZADD", this.checkpointIndexKey(key), 0, checkpoint.id);
|
|
459
|
+
await lease.publish("ZADD", this.atomicCheckpointIndexKey(key, requireEpoch(epoch)), 0, checkpoint.id);
|
|
460
|
+
const dataKey = this.checkpointRecordKey(key, requireEpoch(epoch), checkpoint.id);
|
|
461
|
+
const value = await this.serialize(record);
|
|
462
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
463
|
+
const expected = await readAtomicValue(this.client, dataKey, "LangGraph atomic checkpoint record");
|
|
464
|
+
if (await lease.compareAndSet(dataKey, expected, value)) {
|
|
465
|
+
await this.assertCurrentEpoch(threadId, epoch);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
throw new Error("concurrent FerricStore LangGraph checkpoint mutation did not converge");
|
|
470
|
+
}, this.lockOptions);
|
|
471
|
+
return checkpointConfig(threadId, checkpointNs, checkpoint.id);
|
|
472
|
+
}
|
|
473
|
+
async putWrites(config, writes, taskId) {
|
|
474
|
+
const { checkpointNs, threadId } = identity(config);
|
|
475
|
+
const id = checkpointId(config);
|
|
476
|
+
if (id == null) throw new TypeError("putWrites requires configurable.checkpoint_id");
|
|
477
|
+
if (typeof taskId !== "string" || taskId.length === 0) throw new TypeError("taskId must be non-empty text");
|
|
478
|
+
const snapshots = await Promise.all(writes.map(async ([channel, value], fallbackIndex) => {
|
|
479
|
+
if (typeof channel !== "string") throw new TypeError("pending-write channel must be text");
|
|
480
|
+
const index = WRITES_INDEX[channel] ?? fallbackIndex;
|
|
481
|
+
const record = {
|
|
482
|
+
channel,
|
|
483
|
+
formatVersion: FORMAT_VERSION,
|
|
484
|
+
index,
|
|
485
|
+
taskId,
|
|
486
|
+
value
|
|
487
|
+
};
|
|
488
|
+
return { index, value: await this.serialize(record) };
|
|
489
|
+
}));
|
|
490
|
+
const key = this.threadKey(threadId, checkpointNs);
|
|
491
|
+
await withMutationLocks(this.client, [this.threadLockKey(threadId)], async (lease) => {
|
|
492
|
+
const epoch = await this.ensureThreadEpoch(threadId, lease);
|
|
493
|
+
const epochValue = requireEpoch(epoch);
|
|
494
|
+
await lease.publish("SADD", this.threadCatalogKey(threadId), key);
|
|
495
|
+
for (const snapshot of snapshots) {
|
|
496
|
+
const dataKey = this.pendingWriteKey(key, epochValue, id, taskId, snapshot.index);
|
|
497
|
+
const indexKey = this.atomicPendingWriteIndexKey(key, epochValue, id);
|
|
498
|
+
await lease.publish("ZADD", indexKey, 0, dataKey);
|
|
499
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
500
|
+
const expected = await readAtomicValue(this.client, dataKey, "LangGraph atomic pending write");
|
|
501
|
+
if (snapshot.index >= 0 && expected != null) break;
|
|
502
|
+
if (snapshot.index >= 0 && epoch.legacyFallback) {
|
|
503
|
+
const legacy = await this.client.command("HGET", key, writeField(id, taskId, snapshot.index));
|
|
504
|
+
if (legacy != null) break;
|
|
505
|
+
}
|
|
506
|
+
if (await lease.compareAndSet(dataKey, expected, snapshot.value)) break;
|
|
507
|
+
if (attempt === 7) {
|
|
508
|
+
throw new Error("concurrent FerricStore LangGraph pending-write mutation did not converge");
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
await this.assertCurrentEpoch(threadId, epoch);
|
|
513
|
+
}, this.lockOptions);
|
|
514
|
+
}
|
|
515
|
+
async deleteThread(threadId) {
|
|
516
|
+
const normalized = requiredText(threadId, "threadId");
|
|
517
|
+
await withMutationLocks(this.client, [this.threadLockKey(normalized)], async (lease) => {
|
|
518
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
519
|
+
const epoch = await this.ensureThreadEpoch(normalized, lease);
|
|
520
|
+
const next = Buffer.from(`${CURRENT_EPOCH_PREFIX}${(0, import_node_crypto2.randomUUID)()}`, "utf8");
|
|
521
|
+
if (await lease.compareAndSet(this.threadEpochKey(normalized), epoch.raw, next)) return;
|
|
522
|
+
}
|
|
523
|
+
throw new Error("concurrent FerricStore LangGraph thread deletion did not converge");
|
|
524
|
+
}, this.lockOptions);
|
|
525
|
+
}
|
|
526
|
+
catalogKey() {
|
|
527
|
+
return `${this.keyPrefix}:checkpoints`;
|
|
528
|
+
}
|
|
529
|
+
threadCatalogKey(threadId) {
|
|
530
|
+
const digest = sha256(threadId);
|
|
531
|
+
return `${this.keyPrefix}:{lgt:${digest}}:namespaces`;
|
|
532
|
+
}
|
|
533
|
+
threadLocatorCatalogKey(threadId) {
|
|
534
|
+
return `${this.threadCatalogKey(threadId)}:checkpoint-locators`;
|
|
535
|
+
}
|
|
536
|
+
threadLockKey(threadId) {
|
|
537
|
+
return `${this.keyPrefix}:{lgt:${sha256(threadId)}}:mutation-lock`;
|
|
538
|
+
}
|
|
539
|
+
threadKey(threadId, checkpointNs) {
|
|
540
|
+
return `${this.keyPrefix}:{lg:${sha256(lengthPrefixed([threadId, checkpointNs]))}}:thread`;
|
|
541
|
+
}
|
|
542
|
+
checkpointIndexKey(threadKey) {
|
|
543
|
+
return `${threadKey}:checkpoint-index`;
|
|
544
|
+
}
|
|
545
|
+
threadEpochKey(threadId) {
|
|
546
|
+
return `${this.threadCatalogKey(threadId)}:atomic-epoch`;
|
|
547
|
+
}
|
|
548
|
+
atomicCheckpointIndexKey(threadKey, epoch) {
|
|
549
|
+
return `${threadKey}:atomic:${sha256(epoch)}:checkpoint-index`;
|
|
550
|
+
}
|
|
551
|
+
atomicPendingWriteIndexKey(threadKey, epoch, checkpointId2) {
|
|
552
|
+
return `${threadKey}:atomic:${sha256(epoch)}:writes:${encodeComponent(checkpointId2)}`;
|
|
553
|
+
}
|
|
554
|
+
checkpointRecordKey(threadKey, epoch, checkpointId2) {
|
|
555
|
+
return `${threadKey}:atomic:${sha256(epoch)}:checkpoint:${encodeComponent(checkpointId2)}`;
|
|
556
|
+
}
|
|
557
|
+
pendingWriteKey(threadKey, epoch, checkpointId2, taskId, index) {
|
|
558
|
+
return `${threadKey}:atomic:${sha256(epoch)}:write:${encodeComponent(checkpointId2)}:${encodeComponent(taskId)}:${index}`;
|
|
559
|
+
}
|
|
560
|
+
async readThreadEpoch(threadId) {
|
|
561
|
+
const raw = await readAtomicValue(this.client, this.threadEpochKey(threadId), "LangGraph thread epoch");
|
|
562
|
+
if (raw == null) return { legacyFallback: true, raw, value: void 0 };
|
|
563
|
+
const value = raw.toString("utf8");
|
|
564
|
+
if (!value.startsWith(LEGACY_EPOCH_PREFIX) && !value.startsWith(CURRENT_EPOCH_PREFIX)) {
|
|
565
|
+
throw new Error("unsupported or corrupt FerricStore LangGraph thread epoch");
|
|
566
|
+
}
|
|
567
|
+
return { legacyFallback: value.startsWith(LEGACY_EPOCH_PREFIX), raw, value };
|
|
568
|
+
}
|
|
569
|
+
async ensureThreadEpoch(threadId, lease) {
|
|
570
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
571
|
+
const epoch = await this.readThreadEpoch(threadId);
|
|
572
|
+
if (epoch.value != null) return epoch;
|
|
573
|
+
const raw = Buffer.from(`${LEGACY_EPOCH_PREFIX}${(0, import_node_crypto2.randomUUID)()}`, "utf8");
|
|
574
|
+
if (await lease.compareAndSet(this.threadEpochKey(threadId), void 0, raw)) {
|
|
575
|
+
return { legacyFallback: true, raw, value: raw.toString("utf8") };
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
throw new Error("concurrent FerricStore LangGraph epoch initialization did not converge");
|
|
579
|
+
}
|
|
580
|
+
async assertCurrentEpoch(threadId, expected) {
|
|
581
|
+
const current = await this.readThreadEpoch(threadId);
|
|
582
|
+
if (expected.raw == null || current.raw?.equals(expected.raw) !== true) {
|
|
583
|
+
throw new Error("FerricStore LangGraph thread epoch changed while committing data");
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
async serialize(value) {
|
|
587
|
+
const [type, data] = await this.serde.dumpsTyped(value);
|
|
588
|
+
const typeBytes = Buffer.from(type, "utf8");
|
|
589
|
+
if (typeBytes.length > 65535) throw new TypeError("serialized LangGraph type name is too long");
|
|
590
|
+
const header = Buffer.allocUnsafe(2);
|
|
591
|
+
header.writeUInt16BE(typeBytes.length);
|
|
592
|
+
return Buffer.concat([header, typeBytes, Buffer.from(data)]);
|
|
593
|
+
}
|
|
594
|
+
async deserialize(value, name) {
|
|
595
|
+
if (!(typeof value === "string" || Buffer.isBuffer(value) || value instanceof Uint8Array)) {
|
|
596
|
+
throw new TypeError(`FerricStore returned a non-binary ${name}`);
|
|
597
|
+
}
|
|
598
|
+
const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value);
|
|
599
|
+
if (bytes.length < 2) throw new Error(`invalid FerricStore ${name}`);
|
|
600
|
+
const typeLength = bytes.readUInt16BE(0);
|
|
601
|
+
if (bytes.length < typeLength + 2) throw new Error(`truncated FerricStore ${name}`);
|
|
602
|
+
const type = bytes.subarray(2, typeLength + 2).toString("utf8");
|
|
603
|
+
return await this.serde.loadsTyped(type, bytes.subarray(typeLength + 2));
|
|
604
|
+
}
|
|
605
|
+
async readRecordAtEpoch(key, id, epoch) {
|
|
606
|
+
let value;
|
|
607
|
+
if (epoch.value != null) {
|
|
608
|
+
value = await this.client.command("GET", this.checkpointRecordKey(key, epoch.value, id));
|
|
609
|
+
if (value == null && epoch.legacyFallback) {
|
|
610
|
+
value = await this.client.command("HGET", key, checkpointField(id));
|
|
611
|
+
}
|
|
612
|
+
} else {
|
|
613
|
+
value = await this.client.command("HGET", key, checkpointField(id));
|
|
614
|
+
}
|
|
615
|
+
if (value == null) return void 0;
|
|
616
|
+
const record = await this.deserialize(value, "LangGraph checkpoint record");
|
|
617
|
+
if (record.formatVersion !== FORMAT_VERSION || record.checkpointId !== id) {
|
|
618
|
+
throw new Error("unsupported or corrupt FerricStore LangGraph checkpoint record");
|
|
619
|
+
}
|
|
620
|
+
return record;
|
|
621
|
+
}
|
|
622
|
+
async pendingWritesAtEpoch(key, id, epoch) {
|
|
623
|
+
const records = /* @__PURE__ */ new Map();
|
|
624
|
+
if (epoch.value != null) {
|
|
625
|
+
const keys = arrayResponse(
|
|
626
|
+
await this.client.command("ZRANGE", this.atomicPendingWriteIndexKey(key, epoch.value, id), 0, -1),
|
|
627
|
+
"LangGraph atomic pending-write index response"
|
|
628
|
+
);
|
|
629
|
+
for (const rawKey of keys) {
|
|
630
|
+
const dataKey = textResponse(rawKey, "LangGraph pending-write key");
|
|
631
|
+
const value = await this.client.command("GET", dataKey);
|
|
632
|
+
if (value == null) continue;
|
|
633
|
+
const record = await this.deserialize(value, "LangGraph pending write");
|
|
634
|
+
records.set(pendingWriteIdentity(record), record);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
if (epoch.legacyFallback) {
|
|
638
|
+
const fields = await this.scanHash(key, `${WRITE_FIELD_PREFIX}${encodeComponent(id)}:*`);
|
|
639
|
+
for (const [, value] of fields) {
|
|
640
|
+
const record = await this.deserialize(value, "LangGraph pending write");
|
|
641
|
+
const identity2 = pendingWriteIdentity(record);
|
|
642
|
+
if (!records.has(identity2)) records.set(identity2, record);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
const ordered = [...records.values()];
|
|
646
|
+
for (const record of ordered) {
|
|
647
|
+
if (record.formatVersion !== FORMAT_VERSION) {
|
|
648
|
+
throw new Error("unsupported FerricStore LangGraph pending-write format");
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
ordered.sort((left, right) => left.taskId.localeCompare(right.taskId) || left.index - right.index);
|
|
652
|
+
return ordered.map((record) => [record.taskId, record.channel, record.value]);
|
|
653
|
+
}
|
|
654
|
+
async tupleFromRecord(key, record) {
|
|
655
|
+
const epoch = await this.readThreadEpoch(record.threadId);
|
|
656
|
+
return {
|
|
657
|
+
checkpoint: record.checkpoint,
|
|
658
|
+
config: checkpointConfig(record.threadId, record.checkpointNs, record.checkpointId),
|
|
659
|
+
metadata: record.metadata,
|
|
660
|
+
pendingWrites: await this.pendingWritesAtEpoch(key, record.checkpointId, epoch),
|
|
661
|
+
...record.parentCheckpointId == null ? {} : { parentConfig: checkpointConfig(record.threadId, record.checkpointNs, record.parentCheckpointId) }
|
|
662
|
+
};
|
|
663
|
+
}
|
|
664
|
+
async scanHash(key, pattern) {
|
|
665
|
+
let cursor = 0;
|
|
666
|
+
const result = [];
|
|
667
|
+
do {
|
|
668
|
+
const response = arrayResponse(
|
|
669
|
+
await this.client.command("HSCAN", key, cursor, "MATCH", pattern, "COUNT", this.scanCount),
|
|
670
|
+
"HSCAN response"
|
|
671
|
+
);
|
|
672
|
+
if (response.length !== 2) throw new Error("FerricStore HSCAN response must contain cursor and items");
|
|
673
|
+
cursor = integerResponse(response[0], "HSCAN cursor");
|
|
674
|
+
const values = response[1];
|
|
675
|
+
if (values instanceof Map) {
|
|
676
|
+
result.push(...values.entries());
|
|
677
|
+
} else {
|
|
678
|
+
const flat = arrayResponse(values, "HSCAN items");
|
|
679
|
+
if (flat.length % 2 !== 0) throw new Error("FerricStore HSCAN returned an odd item count");
|
|
680
|
+
for (let index = 0; index < flat.length; index += 2) result.push([flat[index], flat[index + 1]]);
|
|
681
|
+
}
|
|
682
|
+
} while (cursor !== 0);
|
|
683
|
+
return result;
|
|
684
|
+
}
|
|
685
|
+
async threadKeys(threadId) {
|
|
686
|
+
const values = arrayResponse(
|
|
687
|
+
await this.client.command("SMEMBERS", this.threadCatalogKey(threadId)),
|
|
688
|
+
"thread checkpoint catalog response"
|
|
689
|
+
);
|
|
690
|
+
return values.map((value) => textResponse(value, "thread checkpoint key")).sort();
|
|
691
|
+
}
|
|
692
|
+
async checkpointIdsAtEpoch(key, epoch) {
|
|
693
|
+
const ids = /* @__PURE__ */ new Set();
|
|
694
|
+
if (epoch.value != null) {
|
|
695
|
+
const values = arrayResponse(
|
|
696
|
+
await this.client.command("ZREVRANGE", this.atomicCheckpointIndexKey(key, epoch.value), 0, -1),
|
|
697
|
+
"atomic checkpoint index response"
|
|
698
|
+
);
|
|
699
|
+
for (const value of values) ids.add(textResponse(value, "checkpoint ID"));
|
|
700
|
+
}
|
|
701
|
+
if (epoch.legacyFallback) {
|
|
702
|
+
const values = arrayResponse(
|
|
703
|
+
await this.client.command("ZREVRANGE", this.checkpointIndexKey(key), 0, -1),
|
|
704
|
+
"checkpoint index response"
|
|
705
|
+
);
|
|
706
|
+
for (const value of values) ids.add(textResponse(value, "checkpoint ID"));
|
|
707
|
+
}
|
|
708
|
+
return [...ids].sort((left, right) => right.localeCompare(left));
|
|
709
|
+
}
|
|
710
|
+
async collectGlobal(result, beforeId, expectedId, checkpointNs, filter, limit) {
|
|
711
|
+
let offset = 0;
|
|
712
|
+
const seen = /* @__PURE__ */ new Set();
|
|
713
|
+
while (limit == null || result.length < limit) {
|
|
714
|
+
const values = arrayResponse(
|
|
715
|
+
await this.client.command("ZREVRANGE", this.catalogKey(), offset, offset + this.scanCount - 1),
|
|
716
|
+
"global checkpoint catalog response"
|
|
717
|
+
);
|
|
718
|
+
if (values.length === 0) break;
|
|
719
|
+
for (const value of values) {
|
|
720
|
+
const locator = decodeCheckpointLocator(value);
|
|
721
|
+
const { checkpointId: id, threadKey: key } = locator;
|
|
722
|
+
if (expectedId != null && id !== expectedId) continue;
|
|
723
|
+
if (beforeId != null && id >= beforeId) continue;
|
|
724
|
+
let record;
|
|
725
|
+
if (locator.threadId != null) {
|
|
726
|
+
record = await this.readRecordAtEpoch(key, id, await this.readThreadEpoch(locator.threadId));
|
|
727
|
+
} else {
|
|
728
|
+
const legacy = await this.client.command("HGET", key, checkpointField(id));
|
|
729
|
+
if (legacy != null) {
|
|
730
|
+
const candidate = await this.deserialize(legacy, "LangGraph checkpoint record");
|
|
731
|
+
record = await this.readRecordAtEpoch(key, id, await this.readThreadEpoch(candidate.threadId));
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (record == null || checkpointNs != null && record.checkpointNs !== checkpointNs || !metadataMatches(record.metadata, filter)) continue;
|
|
735
|
+
const identity2 = lengthPrefixed([record.threadId, record.checkpointNs, record.checkpointId]).toString("base64url");
|
|
736
|
+
if (seen.has(identity2)) continue;
|
|
737
|
+
seen.add(identity2);
|
|
738
|
+
result.push({ key, record });
|
|
739
|
+
if (limit != null && result.length >= limit) break;
|
|
740
|
+
}
|
|
741
|
+
offset += values.length;
|
|
742
|
+
if (values.length < this.scanCount) break;
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
function identity(config) {
|
|
747
|
+
if (config.configurable == null || typeof config.configurable !== "object") {
|
|
748
|
+
throw new TypeError("LangGraph config must contain configurable.thread_id");
|
|
749
|
+
}
|
|
750
|
+
return {
|
|
751
|
+
checkpointNs: requiredText(config.configurable.checkpoint_ns ?? "", "checkpoint_ns", true),
|
|
752
|
+
threadId: requiredText(config.configurable.thread_id, "thread_id")
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
function checkpointId(config) {
|
|
756
|
+
return optionalText(config.configurable?.checkpoint_id ?? config.configurable?.thread_ts, "checkpoint_id");
|
|
757
|
+
}
|
|
758
|
+
function checkpointConfig(threadId, checkpointNs, id) {
|
|
759
|
+
return { configurable: { checkpoint_id: id, checkpoint_ns: checkpointNs, thread_id: threadId } };
|
|
760
|
+
}
|
|
761
|
+
function requiredText(value, name, allowEmpty = false) {
|
|
762
|
+
if (typeof value !== "string" || !allowEmpty && value.length === 0) {
|
|
763
|
+
throw new TypeError(`${name} must be ${allowEmpty ? "text" : "non-empty text"}`);
|
|
764
|
+
}
|
|
765
|
+
return value;
|
|
766
|
+
}
|
|
767
|
+
function optionalText(value, name) {
|
|
768
|
+
if (value == null || value === "") return void 0;
|
|
769
|
+
return requiredText(value, name);
|
|
770
|
+
}
|
|
771
|
+
function metadataMatches(metadata, filter) {
|
|
772
|
+
return filter == null || Object.entries(filter).every(([key, value]) => {
|
|
773
|
+
const actual = Object.getOwnPropertyDescriptor(metadata, key)?.value;
|
|
774
|
+
return Object.is(actual, value);
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
function checkpointField(id) {
|
|
778
|
+
return `${CHECKPOINT_FIELD_PREFIX}${encodeComponent(id)}`;
|
|
779
|
+
}
|
|
780
|
+
function writeField(id, taskId, index) {
|
|
781
|
+
return `${WRITE_FIELD_PREFIX}${encodeComponent(id)}:${encodeComponent(taskId)}:${index}`;
|
|
782
|
+
}
|
|
783
|
+
function encodeComponent(value) {
|
|
784
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
785
|
+
}
|
|
786
|
+
function checkpointLocator(id, threadKey, threadId) {
|
|
787
|
+
if (threadId == null) return Buffer.concat([orderedText(id), Buffer.from(threadKey, "utf8")]);
|
|
788
|
+
const threadIdBytes = Buffer.from(threadId, "utf8");
|
|
789
|
+
const length = Buffer.allocUnsafe(4);
|
|
790
|
+
length.writeUInt32BE(threadIdBytes.length);
|
|
791
|
+
return Buffer.concat([
|
|
792
|
+
orderedText(id),
|
|
793
|
+
Buffer.from([0, 1]),
|
|
794
|
+
length,
|
|
795
|
+
threadIdBytes,
|
|
796
|
+
Buffer.from(threadKey, "utf8")
|
|
797
|
+
]);
|
|
798
|
+
}
|
|
799
|
+
function decodeCheckpointLocator(value) {
|
|
800
|
+
if (!(typeof value === "string" || Buffer.isBuffer(value) || value instanceof Uint8Array)) {
|
|
801
|
+
throw new TypeError("FerricStore returned a non-binary checkpoint locator");
|
|
802
|
+
}
|
|
803
|
+
const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value);
|
|
804
|
+
const decoded = decodeOrderedText(bytes, 0);
|
|
805
|
+
let offset = decoded.offset;
|
|
806
|
+
let threadId;
|
|
807
|
+
if (bytes[offset] === 0 && bytes[offset + 1] === 1) {
|
|
808
|
+
offset += 2;
|
|
809
|
+
if (bytes.length < offset + 4) throw new Error("truncated FerricStore checkpoint locator");
|
|
810
|
+
const length = bytes.readUInt32BE(offset);
|
|
811
|
+
offset += 4;
|
|
812
|
+
if (bytes.length < offset + length) throw new Error("truncated FerricStore checkpoint locator thread ID");
|
|
813
|
+
threadId = bytes.subarray(offset, offset + length).toString("utf8");
|
|
814
|
+
offset += length;
|
|
815
|
+
if (threadId.length === 0) throw new Error("FerricStore checkpoint locator has an empty thread ID");
|
|
816
|
+
}
|
|
817
|
+
const threadKey = bytes.subarray(offset).toString("utf8");
|
|
818
|
+
if (threadKey.length === 0) throw new Error("FerricStore checkpoint locator has an empty thread key");
|
|
819
|
+
return { checkpointId: decoded.value, ...threadId == null ? {} : { threadId }, threadKey };
|
|
820
|
+
}
|
|
821
|
+
function orderedText(value) {
|
|
822
|
+
const output = [];
|
|
823
|
+
for (const byte of Buffer.from(value, "utf8")) {
|
|
824
|
+
if (byte === 0) output.push(0, 255);
|
|
825
|
+
else output.push(byte);
|
|
826
|
+
}
|
|
827
|
+
output.push(0, 0);
|
|
828
|
+
return Buffer.from(output);
|
|
829
|
+
}
|
|
830
|
+
function decodeOrderedText(bytes, start) {
|
|
831
|
+
const output = [];
|
|
832
|
+
let offset = start;
|
|
833
|
+
while (offset < bytes.length) {
|
|
834
|
+
const byte = bytes[offset];
|
|
835
|
+
if (byte == null) throw new Error("truncated FerricStore checkpoint locator");
|
|
836
|
+
offset += 1;
|
|
837
|
+
if (byte !== 0) {
|
|
838
|
+
output.push(byte);
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
const escaped = bytes[offset];
|
|
842
|
+
offset += 1;
|
|
843
|
+
if (escaped === 0) return { offset, value: Buffer.from(output).toString("utf8") };
|
|
844
|
+
if (escaped === 255) output.push(0);
|
|
845
|
+
else throw new Error("invalid FerricStore checkpoint locator escape");
|
|
846
|
+
}
|
|
847
|
+
throw new Error("unterminated FerricStore checkpoint locator");
|
|
848
|
+
}
|
|
849
|
+
function sha256(value) {
|
|
850
|
+
return (0, import_node_crypto2.createHash)("sha256").update(value).digest("hex");
|
|
851
|
+
}
|
|
852
|
+
function lengthPrefixed(values) {
|
|
853
|
+
const parts = [];
|
|
854
|
+
for (const value of values) {
|
|
855
|
+
const payload = Buffer.from(value, "utf8");
|
|
856
|
+
const length = Buffer.allocUnsafe(8);
|
|
857
|
+
length.writeBigUInt64BE(BigInt(payload.length));
|
|
858
|
+
parts.push(length, payload);
|
|
859
|
+
}
|
|
860
|
+
return Buffer.concat(parts);
|
|
861
|
+
}
|
|
862
|
+
function requireEpoch(epoch) {
|
|
863
|
+
if (epoch.value == null) throw new Error("FerricStore LangGraph thread epoch is not initialized");
|
|
864
|
+
return epoch.value;
|
|
865
|
+
}
|
|
866
|
+
function pendingWriteIdentity(record) {
|
|
867
|
+
return lengthPrefixed([record.taskId, String(record.index)]).toString("base64url");
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// src/langgraph/store.ts
|
|
871
|
+
var import_node_crypto3 = require("crypto");
|
|
872
|
+
var import_langgraph2 = require("@langchain/langgraph");
|
|
873
|
+
var FORMAT_VERSION2 = 1;
|
|
874
|
+
var ITEM_FIELD_PREFIX = "item:";
|
|
875
|
+
var DELETED_ITEM = Buffer.from("ferricstore:langgraph:item:deleted:v2", "utf8");
|
|
876
|
+
var FerricStoreStore = class extends import_langgraph2.BaseStore {
|
|
877
|
+
client;
|
|
878
|
+
keyPrefix;
|
|
879
|
+
scanCount;
|
|
880
|
+
lockOptions;
|
|
881
|
+
constructor(client, options = {}) {
|
|
882
|
+
super();
|
|
883
|
+
this.client = client;
|
|
884
|
+
this.keyPrefix = normalizeKeyPrefix(options.keyPrefix ?? "langgraph:store", "langgraph:store");
|
|
885
|
+
this.scanCount = positiveInteger(options.scanCount, 256, "scanCount");
|
|
886
|
+
this.lockOptions = {
|
|
887
|
+
lockRetryMs: options.lockRetryMs,
|
|
888
|
+
lockTtlMs: options.lockTtlMs,
|
|
889
|
+
lockWaitMs: options.lockWaitMs
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
async batch(operations) {
|
|
893
|
+
if (!Array.isArray(operations)) throw new TypeError("operations must be an array");
|
|
894
|
+
const lockKeys = operations.filter(isPutOperation).map((operation) => {
|
|
895
|
+
validatePut(operation);
|
|
896
|
+
return this.itemLockKey(operation.namespace, operation.key);
|
|
897
|
+
});
|
|
898
|
+
return await withMutationLocks(this.client, lockKeys, async (lease) => {
|
|
899
|
+
const results = [];
|
|
900
|
+
const puts = /* @__PURE__ */ new Map();
|
|
901
|
+
for (const operation of operations) {
|
|
902
|
+
if (isPutOperation(operation)) {
|
|
903
|
+
puts.set(JSON.stringify([operation.namespace, operation.key]), operation);
|
|
904
|
+
results.push(void 0);
|
|
905
|
+
} else if (isSearchOperation(operation)) {
|
|
906
|
+
results.push(await this.searchOperation(operation));
|
|
907
|
+
} else if (isGetOperation(operation)) {
|
|
908
|
+
results.push(await this.getOperation(operation));
|
|
909
|
+
} else if (isListNamespacesOperation(operation)) {
|
|
910
|
+
results.push(await this.listNamespacesOperation(operation));
|
|
911
|
+
} else {
|
|
912
|
+
throw new TypeError("unsupported LangGraph store operation");
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
for (const operation of puts.values()) await this.putOperation(operation, lease);
|
|
916
|
+
return results;
|
|
917
|
+
}, this.lockOptions);
|
|
918
|
+
}
|
|
919
|
+
catalogKey() {
|
|
920
|
+
return `${this.keyPrefix}:namespaces`;
|
|
921
|
+
}
|
|
922
|
+
namespaceKey(namespace) {
|
|
923
|
+
return `${this.keyPrefix}:{lgs:${sha2562(namespaceIdentity(namespace))}}:namespace`;
|
|
924
|
+
}
|
|
925
|
+
itemLockKey(namespace, key) {
|
|
926
|
+
return `${this.keyPrefix}:{lgsi:${itemIdentityDigest(namespace, key)}}:mutation-lock`;
|
|
927
|
+
}
|
|
928
|
+
itemDataKey(namespace, key) {
|
|
929
|
+
return `${this.keyPrefix}:{lgsi:${itemIdentityDigest(namespace, key)}}:atomic-item`;
|
|
930
|
+
}
|
|
931
|
+
async getOperation(operation) {
|
|
932
|
+
validateNamespace(operation.namespace);
|
|
933
|
+
if (typeof operation.key !== "string") throw new TypeError("store key must be text");
|
|
934
|
+
const snapshot = await this.readItemSnapshot(operation.namespace, operation.key);
|
|
935
|
+
return snapshot.record == null ? null : itemFromRecord(snapshot.record);
|
|
936
|
+
}
|
|
937
|
+
async putOperation(operation, lease) {
|
|
938
|
+
validatePut(operation);
|
|
939
|
+
const dataKey = this.itemDataKey(operation.namespace, operation.key);
|
|
940
|
+
const locator = catalogMember(operation.namespace, operation.key);
|
|
941
|
+
if (operation.value == null) {
|
|
942
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
943
|
+
const snapshot = await this.readItemSnapshot(operation.namespace, operation.key);
|
|
944
|
+
if (snapshot.expected?.equals(DELETED_ITEM) === true) return;
|
|
945
|
+
if (await lease.compareAndSet(dataKey, snapshot.expected, DELETED_ITEM)) return;
|
|
946
|
+
}
|
|
947
|
+
throw new Error("concurrent FerricStore LangGraph store deletion did not converge");
|
|
948
|
+
}
|
|
949
|
+
const storedValue = snapshotJsonValue(operation.value);
|
|
950
|
+
await lease.publish("ZADD", this.catalogKey(), 0, locator);
|
|
951
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
952
|
+
const snapshot = await this.readItemSnapshot(operation.namespace, operation.key);
|
|
953
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
954
|
+
const record = {
|
|
955
|
+
createdAt: snapshot.record?.createdAt ?? now,
|
|
956
|
+
formatVersion: FORMAT_VERSION2,
|
|
957
|
+
key: operation.key,
|
|
958
|
+
namespace: [...operation.namespace],
|
|
959
|
+
updatedAt: now,
|
|
960
|
+
value: storedValue
|
|
961
|
+
};
|
|
962
|
+
if (await lease.compareAndSet(dataKey, snapshot.expected, encodeItem(record))) return;
|
|
963
|
+
}
|
|
964
|
+
throw new Error("concurrent FerricStore LangGraph store mutation did not converge");
|
|
965
|
+
}
|
|
966
|
+
async searchOperation(operation) {
|
|
967
|
+
validateNamespacePrefix(operation.namespacePrefix);
|
|
968
|
+
if (operation.query != null) {
|
|
969
|
+
throw new Error(
|
|
970
|
+
"FerricStoreStore semantic query search is not configured; use metadata filters or a vector-enabled store"
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
const limit = normalizePageNumber(operation.limit, 10, "search limit");
|
|
974
|
+
const offset = normalizePageNumber(operation.offset, 0, "search offset");
|
|
975
|
+
if (limit === 0) return [];
|
|
976
|
+
const matches = [];
|
|
977
|
+
for await (const locator of this.catalogLocators()) {
|
|
978
|
+
if (!startsWithNamespace(locator.namespace, operation.namespacePrefix)) continue;
|
|
979
|
+
const item = await this.readCatalogItem(locator.namespace, locator.key);
|
|
980
|
+
if (item == null || !matchesFilter(item.value, operation.filter)) continue;
|
|
981
|
+
matches.push(item);
|
|
982
|
+
if (matches.length >= offset + limit) break;
|
|
983
|
+
}
|
|
984
|
+
return matches.slice(offset, offset + limit);
|
|
985
|
+
}
|
|
986
|
+
async listNamespacesOperation(operation) {
|
|
987
|
+
const limit = normalizePageNumber(operation.limit, 100, "namespace limit");
|
|
988
|
+
const offset = normalizePageNumber(operation.offset, 0, "namespace offset");
|
|
989
|
+
const maxDepth = operation.maxDepth == null ? void 0 : positiveInteger(operation.maxDepth, operation.maxDepth, "maxDepth");
|
|
990
|
+
if (limit === 0) return [];
|
|
991
|
+
const namespaces = /* @__PURE__ */ new Map();
|
|
992
|
+
for await (const locator of this.catalogLocators()) {
|
|
993
|
+
const item = await this.readCatalogItem(locator.namespace, locator.key);
|
|
994
|
+
if (item == null) continue;
|
|
995
|
+
if (!matchesConditions(item.namespace, operation.matchConditions)) continue;
|
|
996
|
+
const namespace = maxDepth == null ? item.namespace : item.namespace.slice(0, maxDepth);
|
|
997
|
+
namespaces.set(JSON.stringify(namespace), namespace);
|
|
998
|
+
}
|
|
999
|
+
return [...namespaces.values()].sort(compareNamespaces).slice(offset, offset + limit);
|
|
1000
|
+
}
|
|
1001
|
+
async readCatalogItem(namespace, key) {
|
|
1002
|
+
const snapshot = await this.readItemSnapshot(namespace, key);
|
|
1003
|
+
return snapshot.record == null ? null : itemFromRecord(snapshot.record);
|
|
1004
|
+
}
|
|
1005
|
+
async readItemSnapshot(namespace, key) {
|
|
1006
|
+
const expected = await readAtomicValue(
|
|
1007
|
+
this.client,
|
|
1008
|
+
this.itemDataKey(namespace, key),
|
|
1009
|
+
"LangGraph atomic store item"
|
|
1010
|
+
);
|
|
1011
|
+
if (expected != null) {
|
|
1012
|
+
return {
|
|
1013
|
+
expected,
|
|
1014
|
+
record: expected.equals(DELETED_ITEM) ? null : decodeItemRecord(expected)
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
const legacy = await this.client.command("HGET", this.namespaceKey(namespace), itemField(key));
|
|
1018
|
+
return { expected, record: legacy == null ? null : decodeItemRecord(legacy) };
|
|
1019
|
+
}
|
|
1020
|
+
async *catalogLocators() {
|
|
1021
|
+
let offset = 0;
|
|
1022
|
+
while (true) {
|
|
1023
|
+
const values = arrayResponse(
|
|
1024
|
+
await this.client.command("ZRANGE", this.catalogKey(), offset, offset + this.scanCount - 1),
|
|
1025
|
+
"LangGraph store catalog response"
|
|
1026
|
+
);
|
|
1027
|
+
if (values.length === 0) return;
|
|
1028
|
+
for (const value of values) yield decodeCatalogMember(value);
|
|
1029
|
+
offset += values.length;
|
|
1030
|
+
if (values.length < this.scanCount) return;
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
};
|
|
1034
|
+
function isPutOperation(operation) {
|
|
1035
|
+
return "namespace" in operation && "key" in operation && "value" in operation;
|
|
1036
|
+
}
|
|
1037
|
+
function isGetOperation(operation) {
|
|
1038
|
+
return "namespace" in operation && "key" in operation && !("value" in operation);
|
|
1039
|
+
}
|
|
1040
|
+
function isSearchOperation(operation) {
|
|
1041
|
+
return "namespacePrefix" in operation;
|
|
1042
|
+
}
|
|
1043
|
+
function isListNamespacesOperation(operation) {
|
|
1044
|
+
return !("namespace" in operation) && !("namespacePrefix" in operation);
|
|
1045
|
+
}
|
|
1046
|
+
function validatePut(operation) {
|
|
1047
|
+
validateNamespace(operation.namespace);
|
|
1048
|
+
if (typeof operation.key !== "string") throw new TypeError("store key must be text");
|
|
1049
|
+
if (operation.value == null) return;
|
|
1050
|
+
if (typeof operation.value !== "object" || Array.isArray(operation.value)) {
|
|
1051
|
+
throw new TypeError("LangGraph store values must be JSON objects");
|
|
1052
|
+
}
|
|
1053
|
+
validateJson(operation.value, /* @__PURE__ */ new WeakSet());
|
|
1054
|
+
}
|
|
1055
|
+
function validateNamespace(namespace) {
|
|
1056
|
+
if (!Array.isArray(namespace) || namespace.length === 0) {
|
|
1057
|
+
throw new Error("namespace cannot be empty");
|
|
1058
|
+
}
|
|
1059
|
+
for (const label of namespace) {
|
|
1060
|
+
if (typeof label !== "string" || label.length === 0 || label.includes(".")) {
|
|
1061
|
+
throw new Error("namespace labels must be non-empty strings without periods");
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
if (namespace[0] === "langgraph") throw new Error('root namespace label cannot be "langgraph"');
|
|
1065
|
+
}
|
|
1066
|
+
function validateNamespacePrefix(namespace) {
|
|
1067
|
+
if (!Array.isArray(namespace)) throw new TypeError("namespacePrefix must be an array");
|
|
1068
|
+
if (namespace.length > 0) validateNamespace(namespace);
|
|
1069
|
+
}
|
|
1070
|
+
function validateJson(value, ancestors) {
|
|
1071
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return;
|
|
1072
|
+
if (value === void 0) throw new TypeError("LangGraph store values must not contain undefined");
|
|
1073
|
+
if (typeof value === "number") {
|
|
1074
|
+
if (!Number.isFinite(value)) throw new TypeError("LangGraph store numbers must be finite");
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (typeof value !== "object") throw new TypeError("LangGraph store values must be JSON serializable");
|
|
1078
|
+
if (ancestors.has(value)) throw new TypeError("LangGraph store values must not be cyclic");
|
|
1079
|
+
ancestors.add(value);
|
|
1080
|
+
try {
|
|
1081
|
+
if (Array.isArray(value)) {
|
|
1082
|
+
const keys = Reflect.ownKeys(value);
|
|
1083
|
+
if (keys.length !== value.length + 1 || keys.some((key) => typeof key !== "string" || key !== "length" && !isArrayIndex(key, value.length))) {
|
|
1084
|
+
throw new TypeError("LangGraph store arrays must not be sparse or customized");
|
|
1085
|
+
}
|
|
1086
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1087
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, String(index));
|
|
1088
|
+
if (descriptor == null || !descriptor.enumerable || !("value" in descriptor)) {
|
|
1089
|
+
throw new TypeError("LangGraph store arrays contain an unsupported item");
|
|
1090
|
+
}
|
|
1091
|
+
validateJson(descriptor.value, ancestors);
|
|
1092
|
+
}
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1095
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1096
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
1097
|
+
throw new TypeError("LangGraph store values must contain only JSON objects");
|
|
1098
|
+
}
|
|
1099
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
1100
|
+
if (typeof key !== "string") throw new TypeError("LangGraph store objects must not have symbol keys");
|
|
1101
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1102
|
+
if (descriptor == null || !descriptor.enumerable || !("value" in descriptor)) {
|
|
1103
|
+
throw new TypeError("LangGraph store objects contain an unsupported property");
|
|
1104
|
+
}
|
|
1105
|
+
validateJson(descriptor.value, ancestors);
|
|
1106
|
+
}
|
|
1107
|
+
} finally {
|
|
1108
|
+
ancestors.delete(value);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function snapshotJsonValue(value) {
|
|
1112
|
+
validateJson(value, /* @__PURE__ */ new WeakSet());
|
|
1113
|
+
return JSON.parse(JSON.stringify(value));
|
|
1114
|
+
}
|
|
1115
|
+
function encodeItem(record) {
|
|
1116
|
+
return Buffer.from(JSON.stringify(record), "utf8");
|
|
1117
|
+
}
|
|
1118
|
+
function itemFromRecord(record) {
|
|
1119
|
+
return {
|
|
1120
|
+
createdAt: new Date(record.createdAt),
|
|
1121
|
+
key: record.key,
|
|
1122
|
+
namespace: [...record.namespace],
|
|
1123
|
+
updatedAt: new Date(record.updatedAt),
|
|
1124
|
+
value: record.value
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
function itemIdentityDigest(namespace, key) {
|
|
1128
|
+
const keyBytes = Buffer.from(key, "utf8");
|
|
1129
|
+
return sha2562(Buffer.concat([
|
|
1130
|
+
namespaceIdentity(namespace),
|
|
1131
|
+
uint64(keyBytes.length),
|
|
1132
|
+
keyBytes
|
|
1133
|
+
]));
|
|
1134
|
+
}
|
|
1135
|
+
function decodeItemRecord(value) {
|
|
1136
|
+
if (!(typeof value === "string" || Buffer.isBuffer(value) || value instanceof Uint8Array)) {
|
|
1137
|
+
throw new TypeError("FerricStore returned a non-binary LangGraph store item");
|
|
1138
|
+
}
|
|
1139
|
+
const text = typeof value === "string" ? value : Buffer.from(value).toString("utf8");
|
|
1140
|
+
let record;
|
|
1141
|
+
try {
|
|
1142
|
+
record = JSON.parse(text);
|
|
1143
|
+
} catch (error) {
|
|
1144
|
+
throw new Error("invalid FerricStore LangGraph store item", { cause: error });
|
|
1145
|
+
}
|
|
1146
|
+
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") {
|
|
1147
|
+
throw new Error("unsupported or corrupt FerricStore LangGraph store item");
|
|
1148
|
+
}
|
|
1149
|
+
return record;
|
|
1150
|
+
}
|
|
1151
|
+
function matchesFilter(value, filter) {
|
|
1152
|
+
return filter == null || Object.entries(filter).every(([key, expected]) => compareValue(value[key], expected));
|
|
1153
|
+
}
|
|
1154
|
+
function compareValue(value, expected) {
|
|
1155
|
+
if (expected != null && typeof expected === "object" && !Array.isArray(expected)) {
|
|
1156
|
+
const entries = Object.entries(expected);
|
|
1157
|
+
if (entries.length > 0 && entries.every(([key]) => FILTER_OPERATORS.has(key))) {
|
|
1158
|
+
return entries.every(([operator, operand]) => applyOperator(value, operator, operand));
|
|
1159
|
+
}
|
|
1160
|
+
return value != null && typeof value === "object" && !Array.isArray(value) && entries.every(([key, nested]) => compareValue(value[key], nested));
|
|
1161
|
+
}
|
|
1162
|
+
if (Array.isArray(expected)) {
|
|
1163
|
+
return Array.isArray(value) && value.length === expected.length && value.every((item, index) => compareValue(item, expected[index]));
|
|
1164
|
+
}
|
|
1165
|
+
return Object.is(value, expected);
|
|
1166
|
+
}
|
|
1167
|
+
function applyOperator(value, operator, expected) {
|
|
1168
|
+
switch (operator) {
|
|
1169
|
+
case "$eq":
|
|
1170
|
+
return Object.is(value, expected);
|
|
1171
|
+
case "$ne":
|
|
1172
|
+
return !Object.is(value, expected);
|
|
1173
|
+
case "$gt":
|
|
1174
|
+
return comparable(value, expected, (left, right) => left > right);
|
|
1175
|
+
case "$gte":
|
|
1176
|
+
return comparable(value, expected, (left, right) => left >= right);
|
|
1177
|
+
case "$in":
|
|
1178
|
+
return Array.isArray(expected) && expected.some((item) => Object.is(item, value));
|
|
1179
|
+
case "$lt":
|
|
1180
|
+
return comparable(value, expected, (left, right) => left < right);
|
|
1181
|
+
case "$lte":
|
|
1182
|
+
return comparable(value, expected, (left, right) => left <= right);
|
|
1183
|
+
case "$nin":
|
|
1184
|
+
return !Array.isArray(expected) || expected.every((item) => !Object.is(item, value));
|
|
1185
|
+
default:
|
|
1186
|
+
throw new Error(`unsupported filter operator: ${operator}`);
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
function comparable(value, expected, compare) {
|
|
1190
|
+
try {
|
|
1191
|
+
return compare(Number(value), Number(expected));
|
|
1192
|
+
} catch {
|
|
1193
|
+
return false;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
var FILTER_OPERATORS = /* @__PURE__ */ new Set(["$eq", "$gt", "$gte", "$in", "$lt", "$lte", "$ne", "$nin"]);
|
|
1197
|
+
function matchesConditions(namespace, conditions) {
|
|
1198
|
+
return conditions == null || conditions.every((condition) => {
|
|
1199
|
+
if (namespace.length < condition.path.length) return false;
|
|
1200
|
+
const actual = condition.matchType === "prefix" ? namespace : [...namespace].reverse();
|
|
1201
|
+
const pattern = condition.matchType === "prefix" ? condition.path : [...condition.path].reverse();
|
|
1202
|
+
return pattern.every((component, index) => component === "*" || actual[index] === component);
|
|
1203
|
+
});
|
|
1204
|
+
}
|
|
1205
|
+
function startsWithNamespace(namespace, prefix) {
|
|
1206
|
+
return prefix.length <= namespace.length && prefix.every((value, index) => namespace[index] === value);
|
|
1207
|
+
}
|
|
1208
|
+
function normalizePageNumber(value, fallback, name) {
|
|
1209
|
+
const normalized = value ?? fallback;
|
|
1210
|
+
if (!Number.isSafeInteger(normalized) || normalized < 0) {
|
|
1211
|
+
throw new TypeError(`${name} must be a non-negative safe integer`);
|
|
1212
|
+
}
|
|
1213
|
+
return normalized;
|
|
1214
|
+
}
|
|
1215
|
+
function compareNamespaces(left, right) {
|
|
1216
|
+
const count = Math.min(left.length, right.length);
|
|
1217
|
+
for (let index = 0; index < count; index += 1) {
|
|
1218
|
+
const compared = (left[index] ?? "").localeCompare(right[index] ?? "");
|
|
1219
|
+
if (compared !== 0) return compared;
|
|
1220
|
+
}
|
|
1221
|
+
return left.length - right.length;
|
|
1222
|
+
}
|
|
1223
|
+
function itemField(key) {
|
|
1224
|
+
return `${ITEM_FIELD_PREFIX}${Buffer.from(key, "utf8").toString("base64url")}`;
|
|
1225
|
+
}
|
|
1226
|
+
function namespaceIdentity(namespace) {
|
|
1227
|
+
return Buffer.concat(namespace.flatMap((component) => {
|
|
1228
|
+
const payload = Buffer.from(component, "utf8");
|
|
1229
|
+
return [uint64(payload.length), payload];
|
|
1230
|
+
}));
|
|
1231
|
+
}
|
|
1232
|
+
function catalogMember(namespace, key) {
|
|
1233
|
+
return Buffer.concat([
|
|
1234
|
+
...namespace.map(orderedText2),
|
|
1235
|
+
Buffer.from([0, 0]),
|
|
1236
|
+
orderedText2(key)
|
|
1237
|
+
]);
|
|
1238
|
+
}
|
|
1239
|
+
function decodeCatalogMember(value) {
|
|
1240
|
+
if (!(typeof value === "string" || Buffer.isBuffer(value) || value instanceof Uint8Array)) {
|
|
1241
|
+
throw new TypeError("FerricStore returned a non-binary LangGraph store locator");
|
|
1242
|
+
}
|
|
1243
|
+
const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(value);
|
|
1244
|
+
const namespace = [];
|
|
1245
|
+
let offset = 0;
|
|
1246
|
+
while (true) {
|
|
1247
|
+
if (bytes[offset] === 0 && bytes[offset + 1] === 0) {
|
|
1248
|
+
offset += 2;
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
const decoded = decodeOrderedText2(bytes, offset);
|
|
1252
|
+
namespace.push(decoded.value);
|
|
1253
|
+
offset = decoded.offset;
|
|
1254
|
+
}
|
|
1255
|
+
const key = decodeOrderedText2(bytes, offset);
|
|
1256
|
+
if (key.offset !== bytes.length || namespace.length === 0) {
|
|
1257
|
+
throw new Error("invalid FerricStore LangGraph store locator");
|
|
1258
|
+
}
|
|
1259
|
+
return { key: key.value, namespace };
|
|
1260
|
+
}
|
|
1261
|
+
function orderedText2(value) {
|
|
1262
|
+
const output = [];
|
|
1263
|
+
for (const byte of Buffer.from(value, "utf8")) {
|
|
1264
|
+
if (byte === 0) output.push(0, 255);
|
|
1265
|
+
else output.push(byte);
|
|
1266
|
+
}
|
|
1267
|
+
output.push(0, 0);
|
|
1268
|
+
return Buffer.from(output);
|
|
1269
|
+
}
|
|
1270
|
+
function decodeOrderedText2(bytes, start) {
|
|
1271
|
+
const output = [];
|
|
1272
|
+
let offset = start;
|
|
1273
|
+
while (offset < bytes.length) {
|
|
1274
|
+
const byte = bytes[offset];
|
|
1275
|
+
if (byte == null) throw new Error("truncated FerricStore LangGraph store locator");
|
|
1276
|
+
offset += 1;
|
|
1277
|
+
if (byte !== 0) {
|
|
1278
|
+
output.push(byte);
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
const escaped = bytes[offset];
|
|
1282
|
+
offset += 1;
|
|
1283
|
+
if (escaped === 0) return { offset, value: Buffer.from(output).toString("utf8") };
|
|
1284
|
+
if (escaped === 255) output.push(0);
|
|
1285
|
+
else throw new Error("invalid FerricStore LangGraph store locator escape");
|
|
1286
|
+
}
|
|
1287
|
+
throw new Error("unterminated FerricStore LangGraph store locator");
|
|
1288
|
+
}
|
|
1289
|
+
function uint64(value) {
|
|
1290
|
+
const result = Buffer.allocUnsafe(8);
|
|
1291
|
+
result.writeBigUInt64BE(BigInt(value));
|
|
1292
|
+
return result;
|
|
1293
|
+
}
|
|
1294
|
+
function sha2562(value) {
|
|
1295
|
+
return (0, import_node_crypto3.createHash)("sha256").update(value).digest("hex");
|
|
1296
|
+
}
|
|
1297
|
+
function isArrayIndex(key, length) {
|
|
1298
|
+
if (!/^(?:0|[1-9]\d*)$/u.test(key)) return false;
|
|
1299
|
+
const index = Number(key);
|
|
1300
|
+
return Number.isSafeInteger(index) && index >= 0 && index < length;
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
// src/langgraph/flow.ts
|
|
1304
|
+
var import_node_crypto4 = require("crypto");
|
|
1305
|
+
var import_langgraph3 = require("@langchain/langgraph");
|
|
1306
|
+
|
|
1307
|
+
// src/outcomes.ts
|
|
1308
|
+
var OUTCOME_BRAND = /* @__PURE__ */ Symbol("ferricstore.outcome");
|
|
1309
|
+
function transition(toState, options = {}) {
|
|
1310
|
+
return brandOutcome({ ...options, kind: "transition", toState });
|
|
1311
|
+
}
|
|
1312
|
+
function complete(options = {}) {
|
|
1313
|
+
return brandOutcome({ ...options, kind: "complete" });
|
|
1314
|
+
}
|
|
1315
|
+
function fail(options = {}) {
|
|
1316
|
+
return brandOutcome({ ...options, kind: "fail" });
|
|
1317
|
+
}
|
|
1318
|
+
function brandOutcome(outcome) {
|
|
1319
|
+
Object.defineProperty(outcome, "kind", {
|
|
1320
|
+
configurable: false,
|
|
1321
|
+
enumerable: true,
|
|
1322
|
+
value: outcome.kind,
|
|
1323
|
+
writable: false
|
|
1324
|
+
});
|
|
1325
|
+
if (outcome.kind === "transition") {
|
|
1326
|
+
Object.defineProperty(outcome, "toState", {
|
|
1327
|
+
configurable: false,
|
|
1328
|
+
enumerable: true,
|
|
1329
|
+
value: outcome.toState,
|
|
1330
|
+
writable: false
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
Object.defineProperty(outcome, OUTCOME_BRAND, {
|
|
1334
|
+
configurable: false,
|
|
1335
|
+
enumerable: false,
|
|
1336
|
+
value: true,
|
|
1337
|
+
writable: false
|
|
1338
|
+
});
|
|
1339
|
+
return outcome;
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// src/langgraph/flow.ts
|
|
1343
|
+
var LangGraphFlowContext = class {
|
|
1344
|
+
flow;
|
|
1345
|
+
threadId;
|
|
1346
|
+
checkpointNs;
|
|
1347
|
+
constructor(flow, threadId, checkpointNs) {
|
|
1348
|
+
this.flow = flow;
|
|
1349
|
+
this.threadId = threadId;
|
|
1350
|
+
this.checkpointNs = checkpointNs;
|
|
1351
|
+
}
|
|
1352
|
+
get id() {
|
|
1353
|
+
return this.flow.id;
|
|
1354
|
+
}
|
|
1355
|
+
get type() {
|
|
1356
|
+
return this.flow.type;
|
|
1357
|
+
}
|
|
1358
|
+
get state() {
|
|
1359
|
+
return this.flow.state;
|
|
1360
|
+
}
|
|
1361
|
+
get partitionKey() {
|
|
1362
|
+
return this.flow.partitionKey;
|
|
1363
|
+
}
|
|
1364
|
+
get payload() {
|
|
1365
|
+
return this.flow.payload;
|
|
1366
|
+
}
|
|
1367
|
+
get values() {
|
|
1368
|
+
return this.flow.values;
|
|
1369
|
+
}
|
|
1370
|
+
};
|
|
1371
|
+
var LangGraphFlowRun = class {
|
|
1372
|
+
value;
|
|
1373
|
+
threadId;
|
|
1374
|
+
checkpointNs;
|
|
1375
|
+
interrupts;
|
|
1376
|
+
constructor(value, threadId, checkpointNs, interrupts2) {
|
|
1377
|
+
this.value = value;
|
|
1378
|
+
this.threadId = threadId;
|
|
1379
|
+
this.checkpointNs = checkpointNs;
|
|
1380
|
+
this.interrupts = interrupts2;
|
|
1381
|
+
}
|
|
1382
|
+
get interrupted() {
|
|
1383
|
+
return this.interrupts.length > 0;
|
|
1384
|
+
}
|
|
1385
|
+
get interruptValues() {
|
|
1386
|
+
return this.interrupts.map((item) => item != null && typeof item === "object" && "value" in item ? item.value : item);
|
|
1387
|
+
}
|
|
1388
|
+
};
|
|
1389
|
+
var LangGraphFlow = class {
|
|
1390
|
+
graph;
|
|
1391
|
+
options;
|
|
1392
|
+
constructor(graph, options = {}) {
|
|
1393
|
+
if (options.onInterrupt != null && options.interruptState != null) {
|
|
1394
|
+
throw new TypeError("onInterrupt and interruptState are mutually exclusive");
|
|
1395
|
+
}
|
|
1396
|
+
if (options.interruptState != null) requireText(options.interruptState, "interruptState");
|
|
1397
|
+
this.graph = graph;
|
|
1398
|
+
this.options = { ...options };
|
|
1399
|
+
}
|
|
1400
|
+
async config(flow, graphContext) {
|
|
1401
|
+
const invokeOptions = this.options.invokeOptions ?? {};
|
|
1402
|
+
const additional = await this.options.config?.(flow) ?? {};
|
|
1403
|
+
const baseConfigurable = invokeOptions.configurable ?? {};
|
|
1404
|
+
if (typeof baseConfigurable !== "object" || Array.isArray(baseConfigurable)) {
|
|
1405
|
+
throw new TypeError("LangGraph invokeOptions configurable must be an object");
|
|
1406
|
+
}
|
|
1407
|
+
const rawConfigurable = additional.configurable ?? {};
|
|
1408
|
+
if (typeof rawConfigurable !== "object" || Array.isArray(rawConfigurable)) {
|
|
1409
|
+
throw new TypeError("LangGraph config configurable must be an object");
|
|
1410
|
+
}
|
|
1411
|
+
const baseMetadata = invokeOptions.metadata ?? {};
|
|
1412
|
+
if (typeof baseMetadata !== "object" || Array.isArray(baseMetadata)) {
|
|
1413
|
+
throw new TypeError("LangGraph invokeOptions metadata must be an object");
|
|
1414
|
+
}
|
|
1415
|
+
const rawMetadata = additional.metadata ?? {};
|
|
1416
|
+
if (typeof rawMetadata !== "object" || Array.isArray(rawMetadata)) {
|
|
1417
|
+
throw new TypeError("LangGraph config metadata must be an object");
|
|
1418
|
+
}
|
|
1419
|
+
const threadId = requireText(
|
|
1420
|
+
await (this.options.threadId?.(flow) ?? defaultThreadId(flow)),
|
|
1421
|
+
"threadId"
|
|
1422
|
+
);
|
|
1423
|
+
const checkpointNsOption = this.options.checkpointNs ?? "";
|
|
1424
|
+
const checkpointNs = requireText(
|
|
1425
|
+
await (typeof checkpointNsOption === "function" ? checkpointNsOption(flow) : checkpointNsOption),
|
|
1426
|
+
"checkpointNs",
|
|
1427
|
+
true
|
|
1428
|
+
);
|
|
1429
|
+
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;
|
|
1430
|
+
return {
|
|
1431
|
+
...invokeOptions,
|
|
1432
|
+
...additional,
|
|
1433
|
+
configurable: {
|
|
1434
|
+
...baseConfigurable,
|
|
1435
|
+
...rawConfigurable,
|
|
1436
|
+
checkpoint_ns: checkpointNs,
|
|
1437
|
+
thread_id: threadId
|
|
1438
|
+
},
|
|
1439
|
+
context,
|
|
1440
|
+
metadata: {
|
|
1441
|
+
...baseMetadata,
|
|
1442
|
+
...rawMetadata,
|
|
1443
|
+
ferricflow_id: flow.id,
|
|
1444
|
+
ferricflow_state: flow.logicalState,
|
|
1445
|
+
ferricflow_type: flow.type
|
|
1446
|
+
}
|
|
1447
|
+
};
|
|
1448
|
+
}
|
|
1449
|
+
async invoke(flow, graphInput, graphContext) {
|
|
1450
|
+
const config = await this.config(flow, graphContext);
|
|
1451
|
+
let input = graphInput;
|
|
1452
|
+
if (input === void 0) {
|
|
1453
|
+
let hasCheckpoint = false;
|
|
1454
|
+
if ((this.options.recoverExisting ?? true) && this.graph.getState != null) {
|
|
1455
|
+
hasCheckpoint = snapshotHasCheckpoint(await this.graph.getState(config));
|
|
1456
|
+
}
|
|
1457
|
+
input = hasCheckpoint ? null : this.options.input == null ? flow.payload : await this.options.input(flow);
|
|
1458
|
+
}
|
|
1459
|
+
const value = await this.graph.invoke(input, config);
|
|
1460
|
+
const configurable = config.configurable;
|
|
1461
|
+
return new LangGraphFlowRun(
|
|
1462
|
+
value,
|
|
1463
|
+
requireText(configurable?.thread_id, "thread_id"),
|
|
1464
|
+
requireText(configurable?.checkpoint_ns ?? "", "checkpoint_ns", true),
|
|
1465
|
+
interrupts(value)
|
|
1466
|
+
);
|
|
1467
|
+
}
|
|
1468
|
+
async outcome(run, flow) {
|
|
1469
|
+
const mapper = run.interrupted ? this.options.onInterrupt : this.options.onComplete;
|
|
1470
|
+
if (mapper != null) return await mapper(run, flow);
|
|
1471
|
+
const stateMeta = {
|
|
1472
|
+
langgraph_checkpoint_ns: run.checkpointNs,
|
|
1473
|
+
langgraph_interrupt_count: run.interrupts.length,
|
|
1474
|
+
langgraph_interrupted: run.interrupted,
|
|
1475
|
+
langgraph_thread_id: run.threadId
|
|
1476
|
+
};
|
|
1477
|
+
if (!run.interrupted) return complete({ result: run.value, stateMeta });
|
|
1478
|
+
if (this.options.interruptState != null) {
|
|
1479
|
+
return transition(this.options.interruptState, { stateMeta });
|
|
1480
|
+
}
|
|
1481
|
+
return fail({
|
|
1482
|
+
error: {
|
|
1483
|
+
checkpointNs: run.checkpointNs,
|
|
1484
|
+
interruptCount: run.interrupts.length,
|
|
1485
|
+
threadId: run.threadId,
|
|
1486
|
+
type: "unhandled_langgraph_interrupt"
|
|
1487
|
+
},
|
|
1488
|
+
stateMeta
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
async handle(flow, graphInput, graphContext) {
|
|
1492
|
+
return await this.outcome(await this.invoke(flow, graphInput, graphContext), flow);
|
|
1493
|
+
}
|
|
1494
|
+
async resume(flow, value, graphContext) {
|
|
1495
|
+
return await this.handle(flow, new import_langgraph3.Command({ resume: value }), graphContext);
|
|
1496
|
+
}
|
|
1497
|
+
async handler(flow) {
|
|
1498
|
+
return await this.handle(flow);
|
|
1499
|
+
}
|
|
1500
|
+
};
|
|
1501
|
+
function interrupts(value) {
|
|
1502
|
+
if (value == null || typeof value !== "object" || !("__interrupt__" in value)) return [];
|
|
1503
|
+
const raw = value.__interrupt__;
|
|
1504
|
+
if (raw == null) return [];
|
|
1505
|
+
return Array.isArray(raw) ? raw : [raw];
|
|
1506
|
+
}
|
|
1507
|
+
function snapshotHasCheckpoint(snapshot) {
|
|
1508
|
+
if (snapshot == null || typeof snapshot !== "object") return false;
|
|
1509
|
+
const value = snapshot;
|
|
1510
|
+
if (value.config != null && typeof value.config === "object") {
|
|
1511
|
+
const configurable = value.config.configurable;
|
|
1512
|
+
if (configurable != null && typeof configurable === "object" && typeof configurable.checkpoint_id === "string") return true;
|
|
1513
|
+
}
|
|
1514
|
+
return value.createdAt != null || value.metadata != null;
|
|
1515
|
+
}
|
|
1516
|
+
function defaultThreadId(flow) {
|
|
1517
|
+
const identity2 = Buffer.concat([
|
|
1518
|
+
identityComponent(flow.type),
|
|
1519
|
+
identityComponent(flow.partitionKey),
|
|
1520
|
+
identityComponent(flow.id)
|
|
1521
|
+
]);
|
|
1522
|
+
return `ferricflow:${(0, import_node_crypto4.createHash)("sha256").update(identity2).digest("hex")}`;
|
|
1523
|
+
}
|
|
1524
|
+
function identityComponent(value) {
|
|
1525
|
+
if (value == null) return Buffer.concat([Buffer.from("n"), uint642(0)]);
|
|
1526
|
+
const payload = Buffer.from(value, "utf8");
|
|
1527
|
+
return Buffer.concat([Buffer.from("s"), uint642(payload.length), payload]);
|
|
1528
|
+
}
|
|
1529
|
+
function uint642(value) {
|
|
1530
|
+
const result = Buffer.allocUnsafe(8);
|
|
1531
|
+
result.writeBigUInt64BE(BigInt(value));
|
|
1532
|
+
return result;
|
|
1533
|
+
}
|
|
1534
|
+
function requireText(value, name, allowEmpty = false) {
|
|
1535
|
+
if (typeof value !== "string" || !allowEmpty && value.length === 0 || value.includes("\0")) {
|
|
1536
|
+
throw new TypeError(`${name} must be ${allowEmpty ? "text" : "non-empty text"} without NUL bytes`);
|
|
1537
|
+
}
|
|
1538
|
+
return value;
|
|
1539
|
+
}
|
|
1540
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1541
|
+
0 && (module.exports = {
|
|
1542
|
+
FerricStoreSaver,
|
|
1543
|
+
FerricStoreStore,
|
|
1544
|
+
LangGraphFlow,
|
|
1545
|
+
LangGraphFlowContext,
|
|
1546
|
+
LangGraphFlowRun
|
|
1547
|
+
});
|
|
1548
|
+
//# sourceMappingURL=langgraph.cjs.map
|