@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/openai-agents.d.cts
CHANGED
|
@@ -10,14 +10,17 @@ interface FerricStoreSessionOptions extends FerricStoreLockOptions {
|
|
|
10
10
|
initialItems?: AgentInputItem[];
|
|
11
11
|
/** FerricStore key prefix. Defaults to `openai:agents:session`. */
|
|
12
12
|
keyPrefix?: string;
|
|
13
|
+
/** Previous worker locales to accept when migrating unversioned operation receipts. */
|
|
14
|
+
legacyReceiptLocales?: string[];
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Durable OpenAI Agents SDK conversation history backed by FerricStore.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* capability in addition to its base Session
|
|
19
|
+
* Renewable locks reduce contention, while compare-and-swap makes every state
|
|
20
|
+
* commit safe even if a writer's lease expires in flight. History transactions
|
|
21
|
+
* and their operation receipts are persisted in one atomic value, implementing
|
|
22
|
+
* the SDK's retry-safe transaction capability in addition to its base Session
|
|
23
|
+
* contract.
|
|
21
24
|
*/
|
|
22
25
|
declare class FerricStoreSession implements Session, SessionHistoryRewriteAwareSession, SessionHistoryTransactionAwareSession {
|
|
23
26
|
readonly client: FerricStoreCommandClient;
|
|
@@ -25,7 +28,9 @@ declare class FerricStoreSession implements Session, SessionHistoryRewriteAwareS
|
|
|
25
28
|
readonly keyPrefix: string;
|
|
26
29
|
private readonly initialItems;
|
|
27
30
|
private readonly lockOptions;
|
|
31
|
+
private readonly legacyReceiptLocales;
|
|
28
32
|
private readonly sessionKey;
|
|
33
|
+
private readonly stateKey;
|
|
29
34
|
private readonly lockKey;
|
|
30
35
|
constructor(client: FerricStoreCommandClient, options?: FerricStoreSessionOptions);
|
|
31
36
|
getSessionId(): Promise<string>;
|
|
@@ -38,6 +43,7 @@ declare class FerricStoreSession implements Session, SessionHistoryRewriteAwareS
|
|
|
38
43
|
applyHistoryTransaction(args: SessionHistoryTransactionArgs): Promise<void>;
|
|
39
44
|
private mutate;
|
|
40
45
|
private readState;
|
|
46
|
+
private readMutationState;
|
|
41
47
|
private emptyState;
|
|
42
48
|
}
|
|
43
49
|
|
package/dist/openai-agents.d.ts
CHANGED
|
@@ -10,14 +10,17 @@ interface FerricStoreSessionOptions extends FerricStoreLockOptions {
|
|
|
10
10
|
initialItems?: AgentInputItem[];
|
|
11
11
|
/** FerricStore key prefix. Defaults to `openai:agents:session`. */
|
|
12
12
|
keyPrefix?: string;
|
|
13
|
+
/** Previous worker locales to accept when migrating unversioned operation receipts. */
|
|
14
|
+
legacyReceiptLocales?: string[];
|
|
13
15
|
}
|
|
14
16
|
/**
|
|
15
17
|
* Durable OpenAI Agents SDK conversation history backed by FerricStore.
|
|
16
18
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* capability in addition to its base Session
|
|
19
|
+
* Renewable locks reduce contention, while compare-and-swap makes every state
|
|
20
|
+
* commit safe even if a writer's lease expires in flight. History transactions
|
|
21
|
+
* and their operation receipts are persisted in one atomic value, implementing
|
|
22
|
+
* the SDK's retry-safe transaction capability in addition to its base Session
|
|
23
|
+
* contract.
|
|
21
24
|
*/
|
|
22
25
|
declare class FerricStoreSession implements Session, SessionHistoryRewriteAwareSession, SessionHistoryTransactionAwareSession {
|
|
23
26
|
readonly client: FerricStoreCommandClient;
|
|
@@ -25,7 +28,9 @@ declare class FerricStoreSession implements Session, SessionHistoryRewriteAwareS
|
|
|
25
28
|
readonly keyPrefix: string;
|
|
26
29
|
private readonly initialItems;
|
|
27
30
|
private readonly lockOptions;
|
|
31
|
+
private readonly legacyReceiptLocales;
|
|
28
32
|
private readonly sessionKey;
|
|
33
|
+
private readonly stateKey;
|
|
29
34
|
private readonly lockKey;
|
|
30
35
|
constructor(client: FerricStoreCommandClient, options?: FerricStoreSessionOptions);
|
|
31
36
|
getSessionId(): Promise<string>;
|
|
@@ -38,6 +43,7 @@ declare class FerricStoreSession implements Session, SessionHistoryRewriteAwareS
|
|
|
38
43
|
applyHistoryTransaction(args: SessionHistoryTransactionArgs): Promise<void>;
|
|
39
44
|
private mutate;
|
|
40
45
|
private readState;
|
|
46
|
+
private readMutationState;
|
|
41
47
|
private emptyState;
|
|
42
48
|
}
|
|
43
49
|
|
package/dist/openai-agents.js
CHANGED
|
@@ -104,14 +104,44 @@ function integerResponse(value, name) {
|
|
|
104
104
|
if (!Number.isSafeInteger(parsed)) throw new TypeError(`FerricStore returned an invalid ${name}`);
|
|
105
105
|
return parsed;
|
|
106
106
|
}
|
|
107
|
+
async function readAtomicValue(client, key, name) {
|
|
108
|
+
const value = await client.command("GET", key);
|
|
109
|
+
if (value == null) return void 0;
|
|
110
|
+
if (typeof value === "string") return Buffer.from(value, "utf8");
|
|
111
|
+
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return Buffer.from(value);
|
|
112
|
+
throw new TypeError(`FerricStore returned a non-binary ${name}`);
|
|
113
|
+
}
|
|
114
|
+
async function compareAndSetAtomicValue(client, key, expected, value) {
|
|
115
|
+
if (expected == null) {
|
|
116
|
+
const response2 = await client.command("SET", key, value, "NX");
|
|
117
|
+
if (response2 == null || response2 === false) return false;
|
|
118
|
+
if (response2 === true) return true;
|
|
119
|
+
return textResponse(response2, "SET NX response").toUpperCase() === "OK";
|
|
120
|
+
}
|
|
121
|
+
const response = await client.command("CAS", key, expected, value);
|
|
122
|
+
if (response == null || response === false) return false;
|
|
123
|
+
if (response === true) return true;
|
|
124
|
+
return integerResponse(response, "CAS response") === 1;
|
|
125
|
+
}
|
|
107
126
|
async function withMutationLocks(client, keys, operation, options = {}) {
|
|
108
127
|
const orderedKeys = [...new Set(keys)].sort();
|
|
109
|
-
if (orderedKeys.length === 0)
|
|
128
|
+
if (orderedKeys.length === 0) {
|
|
129
|
+
const signal = new AbortController().signal;
|
|
130
|
+
return await operation({
|
|
131
|
+
signal,
|
|
132
|
+
assertOwned: () => void 0,
|
|
133
|
+
publish: async (...args) => await additiveCommand(client, args),
|
|
134
|
+
compareAndSet: async (key, expected, value) => await compareAndSetAtomicValue(client, key, expected, value)
|
|
135
|
+
});
|
|
136
|
+
}
|
|
110
137
|
const normalized = {
|
|
111
138
|
lockRetryMs: positiveInteger(options.lockRetryMs, DEFAULT_LOCK_OPTIONS.lockRetryMs, "lockRetryMs"),
|
|
112
139
|
lockTtlMs: positiveInteger(options.lockTtlMs, DEFAULT_LOCK_OPTIONS.lockTtlMs, "lockTtlMs"),
|
|
113
140
|
lockWaitMs: nonNegativeInteger(options.lockWaitMs, DEFAULT_LOCK_OPTIONS.lockWaitMs, "lockWaitMs")
|
|
114
141
|
};
|
|
142
|
+
if (normalized.lockRetryMs >= normalized.lockTtlMs) {
|
|
143
|
+
throw new TypeError("lockRetryMs must be less than lockTtlMs");
|
|
144
|
+
}
|
|
115
145
|
const owner = randomUUID();
|
|
116
146
|
const acquired = [];
|
|
117
147
|
const deadline = performance.now() + normalized.lockWaitMs;
|
|
@@ -120,29 +150,81 @@ async function withMutationLocks(client, keys, operation, options = {}) {
|
|
|
120
150
|
let releaseError;
|
|
121
151
|
let result;
|
|
122
152
|
let operationCompleted = false;
|
|
153
|
+
let conditionalCommitCompleted = false;
|
|
123
154
|
const heartbeatAbort = new AbortController();
|
|
155
|
+
const ownershipAbort = new AbortController();
|
|
156
|
+
const lastExtended = /* @__PURE__ */ new Map();
|
|
157
|
+
const loseOwnership = (error) => {
|
|
158
|
+
const normalizedError = errorObject(error);
|
|
159
|
+
heartbeatError ??= normalizedError;
|
|
160
|
+
if (!ownershipAbort.signal.aborted) ownershipAbort.abort(normalizedError);
|
|
161
|
+
return normalizedError;
|
|
162
|
+
};
|
|
163
|
+
const assertOwned = () => {
|
|
164
|
+
if (heartbeatError != null) throw errorObject(heartbeatError);
|
|
165
|
+
if (ownershipAbort.signal.aborted) throw errorObject(ownershipAbort.signal.reason);
|
|
166
|
+
};
|
|
167
|
+
const renewOwned = async () => {
|
|
168
|
+
assertOwned();
|
|
169
|
+
for (const key of acquired) {
|
|
170
|
+
try {
|
|
171
|
+
const response = await client.command("EXTEND", key, owner, normalized.lockTtlMs);
|
|
172
|
+
if (integerResponse(response, "EXTEND response") !== 1) {
|
|
173
|
+
throw new Error(`lost FerricStore lock ${JSON.stringify(key)} while mutating data`);
|
|
174
|
+
}
|
|
175
|
+
lastExtended.set(key, performance.now());
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw loseOwnership(new Error(
|
|
178
|
+
`could not validate FerricStore lock ${JSON.stringify(key)} before mutating data`,
|
|
179
|
+
{ cause: error }
|
|
180
|
+
));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
assertOwned();
|
|
184
|
+
};
|
|
185
|
+
const lease = {
|
|
186
|
+
signal: ownershipAbort.signal,
|
|
187
|
+
assertOwned,
|
|
188
|
+
publish: async (...args) => {
|
|
189
|
+
await renewOwned();
|
|
190
|
+
const response = await additiveCommand(client, args);
|
|
191
|
+
assertOwned();
|
|
192
|
+
return response;
|
|
193
|
+
},
|
|
194
|
+
compareAndSet: async (key, expected, value) => {
|
|
195
|
+
await renewOwned();
|
|
196
|
+
const committed = await compareAndSetAtomicValue(client, key, expected, value);
|
|
197
|
+
if (committed) conditionalCommitCompleted = true;
|
|
198
|
+
else assertOwned();
|
|
199
|
+
return committed;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
124
202
|
try {
|
|
125
203
|
for (const key of orderedKeys) {
|
|
126
204
|
while (!await tryAcquireLock(client, key, owner, normalized.lockTtlMs)) {
|
|
127
205
|
if (performance.now() >= deadline) {
|
|
128
206
|
throw new Error(`timed out acquiring FerricStore lock ${JSON.stringify(key)}`);
|
|
129
207
|
}
|
|
208
|
+
await extendAcquiredLocks(client, acquired, owner, normalized.lockTtlMs);
|
|
130
209
|
await delay(normalized.lockRetryMs);
|
|
131
210
|
}
|
|
132
211
|
acquired.push(key);
|
|
133
212
|
}
|
|
213
|
+
await extendAcquiredLocks(client, acquired, owner, normalized.lockTtlMs);
|
|
214
|
+
for (const key of acquired) lastExtended.set(key, performance.now());
|
|
134
215
|
const heartbeat = renewLocks(
|
|
135
216
|
client,
|
|
136
217
|
acquired,
|
|
137
218
|
owner,
|
|
138
219
|
normalized.lockTtlMs,
|
|
220
|
+
lastExtended,
|
|
139
221
|
heartbeatAbort.signal,
|
|
140
222
|
(error) => {
|
|
141
|
-
|
|
223
|
+
loseOwnership(error);
|
|
142
224
|
}
|
|
143
225
|
);
|
|
144
226
|
try {
|
|
145
|
-
result = await operation();
|
|
227
|
+
result = await operation(lease);
|
|
146
228
|
operationCompleted = true;
|
|
147
229
|
} catch (error) {
|
|
148
230
|
primaryError = error;
|
|
@@ -163,11 +245,32 @@ async function withMutationLocks(client, keys, operation, options = {}) {
|
|
|
163
245
|
}
|
|
164
246
|
}
|
|
165
247
|
if (primaryError != null) throw errorObject(primaryError);
|
|
166
|
-
if (heartbeatError != null) throw errorObject(heartbeatError);
|
|
167
|
-
if (releaseError != null
|
|
248
|
+
if (heartbeatError != null && !conditionalCommitCompleted) throw errorObject(heartbeatError);
|
|
249
|
+
if (releaseError != null && !(heartbeatError != null && conditionalCommitCompleted)) {
|
|
250
|
+
throw errorObject(releaseError);
|
|
251
|
+
}
|
|
168
252
|
if (!operationCompleted) throw new Error("FerricStore mutation did not complete");
|
|
169
253
|
return result;
|
|
170
254
|
}
|
|
255
|
+
async function additiveCommand(client, args) {
|
|
256
|
+
const rawName = args[0];
|
|
257
|
+
const name = typeof rawName === "string" ? rawName.toUpperCase() : Buffer.isBuffer(rawName) || rawName instanceof Uint8Array ? Buffer.from(rawName).toString("utf8").toUpperCase() : "";
|
|
258
|
+
if (name !== "SADD" && name !== "ZADD") {
|
|
259
|
+
throw new TypeError("FerricStore mutation leases only publish add-only SADD or ZADD indexes");
|
|
260
|
+
}
|
|
261
|
+
if (name === "ZADD" && (args.length < 4 || args.length % 2 !== 0 || args.slice(2).some((value, index) => index % 2 === 0 && Number(value) !== 0))) {
|
|
262
|
+
throw new TypeError("FerricStore mutation leases only publish zero-score ZADD indexes");
|
|
263
|
+
}
|
|
264
|
+
return await client.command(...args);
|
|
265
|
+
}
|
|
266
|
+
async function extendAcquiredLocks(client, keys, owner, ttlMs) {
|
|
267
|
+
for (const key of keys) {
|
|
268
|
+
const response = await client.command("EXTEND", key, owner, ttlMs);
|
|
269
|
+
if (integerResponse(response, "EXTEND response") !== 1) {
|
|
270
|
+
throw new Error(`lost FerricStore lock ${JSON.stringify(key)} before mutating data`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
171
274
|
async function tryAcquireLock(client, key, owner, ttlMs) {
|
|
172
275
|
try {
|
|
173
276
|
const response = await client.command("LOCK", key, owner, ttlMs);
|
|
@@ -177,10 +280,9 @@ async function tryAcquireLock(client, key, owner, ttlMs) {
|
|
|
177
280
|
throw error;
|
|
178
281
|
}
|
|
179
282
|
}
|
|
180
|
-
async function renewLocks(client, keys, owner, ttlMs, signal, onError) {
|
|
181
|
-
const intervalMs = Math.max(Math.floor(ttlMs / 3),
|
|
182
|
-
const retryMs = Math.min(Math.max(Math.floor(intervalMs / 10),
|
|
183
|
-
const lastExtended = new Map(keys.map((key) => [key, performance.now()]));
|
|
283
|
+
async function renewLocks(client, keys, owner, ttlMs, lastExtended, signal, onError) {
|
|
284
|
+
const intervalMs = Math.max(Math.floor(ttlMs / 3), 1);
|
|
285
|
+
const retryMs = Math.min(Math.max(Math.floor(intervalMs / 10), 1), 1e3);
|
|
184
286
|
let waitMs = intervalMs;
|
|
185
287
|
while (!signal.aborted) {
|
|
186
288
|
try {
|
|
@@ -218,7 +320,7 @@ function errorObject(value) {
|
|
|
218
320
|
// src/agent-persistence/snapshot.ts
|
|
219
321
|
import { createHash } from "crypto";
|
|
220
322
|
function encodeSnapshot(value) {
|
|
221
|
-
return
|
|
323
|
+
return encodeSnapshotWith(value, defaultKeyComparator);
|
|
222
324
|
}
|
|
223
325
|
function decodeSnapshot(value, name) {
|
|
224
326
|
const bytes = typeof value === "string" ? Buffer.from(value, "utf8") : Buffer.from(asBytes(value, name));
|
|
@@ -233,13 +335,22 @@ function decodeSnapshot(value, name) {
|
|
|
233
335
|
function snapshotDigest(value) {
|
|
234
336
|
return createHash("sha256").update(encodeSnapshot(value)).digest("hex");
|
|
235
337
|
}
|
|
338
|
+
function legacySnapshotDigest(value, locale) {
|
|
339
|
+
return createHash("sha256").update(encodeSnapshotWith(value, (left, right) => left.localeCompare(right, locale))).digest("hex");
|
|
340
|
+
}
|
|
236
341
|
function cloneSnapshot(value) {
|
|
237
342
|
return decodeSnapshot(encodeSnapshot(value), "snapshot");
|
|
238
343
|
}
|
|
239
344
|
function snapshotsEqual(left, right) {
|
|
240
345
|
return encodeSnapshot(left).equals(encodeSnapshot(right));
|
|
241
346
|
}
|
|
242
|
-
function
|
|
347
|
+
function encodeSnapshotWith(value, compareKeys) {
|
|
348
|
+
return Buffer.from(JSON.stringify(snapshot(value, /* @__PURE__ */ new WeakSet(), compareKeys)), "utf8");
|
|
349
|
+
}
|
|
350
|
+
function defaultKeyComparator(left, right) {
|
|
351
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
352
|
+
}
|
|
353
|
+
function snapshot(value, ancestors, compareKeys) {
|
|
243
354
|
if (value === null) return ["null"];
|
|
244
355
|
if (value === void 0) return ["undefined"];
|
|
245
356
|
if (typeof value === "string") return ["string", value];
|
|
@@ -273,7 +384,7 @@ function snapshot(value, ancestors) {
|
|
|
273
384
|
if (descriptor == null || !descriptor.enumerable || !("value" in descriptor)) {
|
|
274
385
|
throw new TypeError("session history contains an unsupported array item");
|
|
275
386
|
}
|
|
276
|
-
items.push(snapshot(descriptor.value, ancestors));
|
|
387
|
+
items.push(snapshot(descriptor.value, ancestors, compareKeys));
|
|
277
388
|
}
|
|
278
389
|
return ["array", items];
|
|
279
390
|
}
|
|
@@ -286,12 +397,12 @@ function snapshot(value, ancestors) {
|
|
|
286
397
|
throw new TypeError("session history contains a symbol property");
|
|
287
398
|
}
|
|
288
399
|
const entries = [];
|
|
289
|
-
for (const key of keys.sort(
|
|
400
|
+
for (const key of keys.sort(compareKeys)) {
|
|
290
401
|
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
291
402
|
if (descriptor == null || !descriptor.enumerable || !("value" in descriptor)) {
|
|
292
403
|
throw new TypeError("session history contains an unsupported property");
|
|
293
404
|
}
|
|
294
|
-
entries.push([key, snapshot(descriptor.value, ancestors)]);
|
|
405
|
+
entries.push([key, snapshot(descriptor.value, ancestors, compareKeys)]);
|
|
295
406
|
}
|
|
296
407
|
return ["object", entries];
|
|
297
408
|
} finally {
|
|
@@ -362,13 +473,16 @@ function isArrayIndex(key, length) {
|
|
|
362
473
|
// src/openai-agents.ts
|
|
363
474
|
var SESSION_FORMAT_VERSION = 1;
|
|
364
475
|
var SESSION_STATE_FIELD = "state";
|
|
476
|
+
var RECEIPT_DIGEST_VERSION = "v2:";
|
|
365
477
|
var FerricStoreSession = class {
|
|
366
478
|
client;
|
|
367
479
|
sessionId;
|
|
368
480
|
keyPrefix;
|
|
369
481
|
initialItems;
|
|
370
482
|
lockOptions;
|
|
483
|
+
legacyReceiptLocales;
|
|
371
484
|
sessionKey;
|
|
485
|
+
stateKey;
|
|
372
486
|
lockKey;
|
|
373
487
|
constructor(client, options = {}) {
|
|
374
488
|
this.client = client;
|
|
@@ -383,8 +497,17 @@ var FerricStoreSession = class {
|
|
|
383
497
|
lockTtlMs: options.lockTtlMs,
|
|
384
498
|
lockWaitMs: options.lockWaitMs
|
|
385
499
|
};
|
|
500
|
+
if (options.legacyReceiptLocales != null && !Array.isArray(options.legacyReceiptLocales)) {
|
|
501
|
+
throw new TypeError("legacyReceiptLocales must be an array");
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
this.legacyReceiptLocales = Intl.getCanonicalLocales(options.legacyReceiptLocales ?? []);
|
|
505
|
+
} catch (error) {
|
|
506
|
+
throw new TypeError("legacyReceiptLocales contains an invalid locale", { cause: error });
|
|
507
|
+
}
|
|
386
508
|
const digest = createHash2("sha256").update(this.sessionId, "utf8").digest("hex");
|
|
387
509
|
this.sessionKey = `${this.keyPrefix}:{oais:${digest}}:session`;
|
|
510
|
+
this.stateKey = `${this.sessionKey}:atomic-state`;
|
|
388
511
|
this.lockKey = `${this.keyPrefix}:{oais:${digest}}:mutation-lock`;
|
|
389
512
|
}
|
|
390
513
|
async getSessionId() {
|
|
@@ -455,15 +578,21 @@ var FerricStoreSession = class {
|
|
|
455
578
|
}
|
|
456
579
|
async applyHistoryTransaction(args) {
|
|
457
580
|
const { operationId, transaction } = snapshotTransactionArgs(args);
|
|
458
|
-
const digest = snapshotDigest(transaction)
|
|
581
|
+
const digest = `${RECEIPT_DIGEST_VERSION}${snapshotDigest(transaction)}`;
|
|
582
|
+
const legacyDigests = /* @__PURE__ */ new Set([
|
|
583
|
+
snapshotDigest(transaction),
|
|
584
|
+
legacySnapshotDigest(transaction),
|
|
585
|
+
...this.legacyReceiptLocales.map((locale) => legacySnapshotDigest(transaction, locale))
|
|
586
|
+
]);
|
|
459
587
|
await this.mutate(async (state) => {
|
|
460
588
|
const existing = Object.getOwnPropertyDescriptor(state.operations, operationId)?.value;
|
|
461
589
|
if (existing != null) {
|
|
462
590
|
if (typeof existing !== "string") throw new Error("corrupt session history operation receipt");
|
|
463
|
-
if (existing
|
|
591
|
+
if (existing === digest) return state;
|
|
592
|
+
if (!legacyDigests.has(existing)) {
|
|
464
593
|
throw new Error("session history operation was already applied with a different transaction");
|
|
465
594
|
}
|
|
466
|
-
return state;
|
|
595
|
+
return { ...state, operations: { ...state.operations, [operationId]: digest } };
|
|
467
596
|
}
|
|
468
597
|
let items;
|
|
469
598
|
if (transaction.type === "append_items") {
|
|
@@ -484,20 +613,28 @@ var FerricStoreSession = class {
|
|
|
484
613
|
});
|
|
485
614
|
}
|
|
486
615
|
async mutate(operation) {
|
|
487
|
-
await withMutationLocks(this.client, [this.lockKey], async () => {
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
616
|
+
await withMutationLocks(this.client, [this.lockKey], async (lease) => {
|
|
617
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
618
|
+
lease.assertOwned();
|
|
619
|
+
const snapshot2 = await this.readMutationState();
|
|
620
|
+
const next = await operation(snapshot2.state);
|
|
621
|
+
if (await lease.compareAndSet(this.stateKey, snapshot2.expected, encodeSnapshot(next))) return;
|
|
622
|
+
}
|
|
623
|
+
throw new Error("concurrent FerricStore OpenAI Agents session mutation did not converge");
|
|
491
624
|
}, this.lockOptions);
|
|
492
625
|
}
|
|
493
626
|
async readState() {
|
|
494
|
-
|
|
495
|
-
|
|
627
|
+
return (await this.readMutationState()).state;
|
|
628
|
+
}
|
|
629
|
+
async readMutationState() {
|
|
630
|
+
const expected = await readAtomicValue(this.client, this.stateKey, "OpenAI Agents atomic session state");
|
|
631
|
+
const value = expected ?? await this.client.command("HGET", this.sessionKey, SESSION_STATE_FIELD);
|
|
632
|
+
if (value == null) return { expected, state: this.emptyState() };
|
|
496
633
|
const state = decodeSnapshot(value, "OpenAI Agents session state");
|
|
497
634
|
if (state == null || typeof state !== "object" || state.formatVersion !== SESSION_FORMAT_VERSION || state.sessionId !== this.sessionId || !Array.isArray(state.items) || state.operations == null || typeof state.operations !== "object" || Array.isArray(state.operations) || Object.values(state.operations).some((digest) => typeof digest !== "string")) {
|
|
498
635
|
throw new Error("unsupported or corrupt FerricStore OpenAI Agents session state");
|
|
499
636
|
}
|
|
500
|
-
return state;
|
|
637
|
+
return { expected, state };
|
|
501
638
|
}
|
|
502
639
|
emptyState() {
|
|
503
640
|
return {
|