@librechat/agents 3.7.5 → 3.7.7
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/dist/cjs/eventActor/EventActorExecutor.cjs +305 -11
- package/dist/cjs/eventActor/EventActorExecutor.cjs.map +1 -1
- package/dist/cjs/tools/search/keenable-search.cjs +7 -1
- package/dist/cjs/tools/search/keenable-search.cjs.map +1 -1
- package/dist/esm/eventActor/EventActorExecutor.mjs +305 -11
- package/dist/esm/eventActor/EventActorExecutor.mjs.map +1 -1
- package/dist/esm/tools/search/keenable-search.mjs +7 -1
- package/dist/esm/tools/search/keenable-search.mjs.map +1 -1
- package/dist/types/eventActor/EventActorExecutor.d.ts +3 -1
- package/dist/types/eventActor/index.d.ts +1 -1
- package/dist/types/eventActor/types.d.ts +149 -5
- package/package.json +1 -1
- package/src/eventActor/EventActorExecutor.ts +545 -16
- package/src/eventActor/index.ts +17 -0
- package/src/eventActor/types.ts +182 -3
- package/src/tools/search/keenable-search.ts +18 -1
|
@@ -11,7 +11,11 @@ import type { RunnableConfig } from '@langchain/core/runnables';
|
|
|
11
11
|
import type {
|
|
12
12
|
EventActorAdapterPrepareRequest,
|
|
13
13
|
EventActorAdapterPreparation,
|
|
14
|
+
EventActorAdapterInvocationResult,
|
|
15
|
+
EventActorAdapterResumeResult,
|
|
14
16
|
EventActorAppliedResult,
|
|
17
|
+
EventActorCancelSuspensionRequest,
|
|
18
|
+
EventActorCancelSuspensionResult,
|
|
15
19
|
EventActorCheckpointFork,
|
|
16
20
|
EventActorCheckpointReference,
|
|
17
21
|
EventActorDiscardReason,
|
|
@@ -28,12 +32,17 @@ import type {
|
|
|
28
32
|
EventActorPreparedInvocation,
|
|
29
33
|
EventActorPrepareRequest,
|
|
30
34
|
EventActorPreparation,
|
|
35
|
+
EventActorResumeRequest,
|
|
36
|
+
EventActorSettlementAuthority,
|
|
31
37
|
EventActorSettlementResult,
|
|
38
|
+
EventActorSuspendedResult,
|
|
39
|
+
EventActorSuspension,
|
|
32
40
|
EventActorTerminalResult,
|
|
33
41
|
} from './types';
|
|
34
42
|
|
|
35
43
|
const DEFAULT_MAX_DEPTH = 1;
|
|
36
44
|
const DEFAULT_DORMANT_CHECKPOINT_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
45
|
+
const DEFAULT_MAX_SUSPENSION_PAYLOAD_BYTES = 64 * 1_024;
|
|
37
46
|
|
|
38
47
|
function createInvocationCheckpointNs(
|
|
39
48
|
request: EventActorPrepareRequest<EventActorEvent>,
|
|
@@ -265,6 +274,53 @@ function serializeUnavailablePreparation<TEvent extends EventActorEvent>(
|
|
|
265
274
|
});
|
|
266
275
|
}
|
|
267
276
|
|
|
277
|
+
function serializeSuspension(
|
|
278
|
+
suspension: Omit<EventActorSuspension, 'suspensionDigest'>
|
|
279
|
+
): string {
|
|
280
|
+
return JSON.stringify({
|
|
281
|
+
version: suspension.version,
|
|
282
|
+
suspensionId: suspension.suspensionId,
|
|
283
|
+
attempt: suspension.attempt,
|
|
284
|
+
issuedAt: suspension.issuedAt,
|
|
285
|
+
expiresAt: suspension.expiresAt,
|
|
286
|
+
invocation: {
|
|
287
|
+
actorThreadId: suspension.invocation.actorThreadId,
|
|
288
|
+
invocationId: suspension.invocation.invocationId,
|
|
289
|
+
depth: suspension.invocation.depth,
|
|
290
|
+
continuation: suspension.invocation.continuation,
|
|
291
|
+
base: canonicalHead(suspension.invocation.base),
|
|
292
|
+
fork: snapshotCheckpointFork(suspension.invocation.fork),
|
|
293
|
+
},
|
|
294
|
+
checkpoint: snapshotCheckpointFork(suspension.checkpoint),
|
|
295
|
+
interrupt: {
|
|
296
|
+
id: suspension.interrupt.id,
|
|
297
|
+
payload: snapshotEvent(suspension.interrupt.payload),
|
|
298
|
+
},
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function serializeSettlement<TResult extends EventActorEvent>(
|
|
303
|
+
authority: Omit<EventActorSettlementAuthority, 'settlementDigest'>,
|
|
304
|
+
invocation: EventActorInvocationReference,
|
|
305
|
+
checkpoint: EventActorCheckpointFork,
|
|
306
|
+
result: TResult
|
|
307
|
+
): string {
|
|
308
|
+
return JSON.stringify({
|
|
309
|
+
kind: 'resumed_settlement',
|
|
310
|
+
authority,
|
|
311
|
+
invocation: {
|
|
312
|
+
actorThreadId: invocation.actorThreadId,
|
|
313
|
+
invocationId: invocation.invocationId,
|
|
314
|
+
depth: invocation.depth,
|
|
315
|
+
continuation: invocation.continuation,
|
|
316
|
+
base: canonicalHead(invocation.base),
|
|
317
|
+
fork: snapshotCheckpointFork(invocation.fork),
|
|
318
|
+
},
|
|
319
|
+
checkpoint: snapshotCheckpointFork(checkpoint),
|
|
320
|
+
result: snapshotEvent(result),
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
268
324
|
function freezePrepareRequest<TEvent extends EventActorEvent>(
|
|
269
325
|
request: EventActorPrepareRequest<TEvent>
|
|
270
326
|
): EventActorPrepareRequest<TEvent> {
|
|
@@ -375,7 +431,8 @@ function validateInvocation<TEvent extends EventActorEvent>(
|
|
|
375
431
|
|
|
376
432
|
function validateInvocationReference(
|
|
377
433
|
invocation: EventActorInvocationReference,
|
|
378
|
-
maxDepth?: number
|
|
434
|
+
maxDepth?: number,
|
|
435
|
+
allowAdvancedFork = false
|
|
379
436
|
): void {
|
|
380
437
|
requireNonEmpty(invocation.actorThreadId, 'actorThreadId');
|
|
381
438
|
requireNonEmpty(invocation.invocationId, 'invocationId');
|
|
@@ -408,6 +465,7 @@ function validateInvocationReference(
|
|
|
408
465
|
'fork.checkpointId for resumed actor'
|
|
409
466
|
);
|
|
410
467
|
if (
|
|
468
|
+
!allowAdvancedFork &&
|
|
411
469
|
invocation.continuation === 'warm' &&
|
|
412
470
|
invocation.fork.checkpointId !== invocation.base.checkpoint.checkpointId
|
|
413
471
|
) {
|
|
@@ -555,6 +613,12 @@ type EventActorPreparationPhase =
|
|
|
555
613
|
expiresAt: number;
|
|
556
614
|
};
|
|
557
615
|
|
|
616
|
+
interface EventActorSettlementSuspension {
|
|
617
|
+
suspensionId: string;
|
|
618
|
+
attempt: number;
|
|
619
|
+
resumeAttemptId: string;
|
|
620
|
+
}
|
|
621
|
+
|
|
558
622
|
function createIndeterminateResult<TResult extends EventActorEvent>(
|
|
559
623
|
invocation: EventActorInvocationReference,
|
|
560
624
|
error: unknown,
|
|
@@ -667,6 +731,7 @@ export class EventActorExecutor<
|
|
|
667
731
|
readonly #adapter: EventActorHostAdapter<TEvent, TResult>;
|
|
668
732
|
readonly #maxDepth: number;
|
|
669
733
|
readonly #dormantCheckpointTtlMs: number;
|
|
734
|
+
readonly #maxSuspensionPayloadBytes: number;
|
|
670
735
|
readonly #preparationSigningKey: Uint8Array;
|
|
671
736
|
readonly #issuedSettlements = new WeakSet<object>();
|
|
672
737
|
readonly #preparationPhases = new Map<string, EventActorPreparationPhase>();
|
|
@@ -680,6 +745,14 @@ export class EventActorExecutor<
|
|
|
680
745
|
this.#maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
|
|
681
746
|
this.#dormantCheckpointTtlMs =
|
|
682
747
|
options.dormantCheckpointTtlMs ?? DEFAULT_DORMANT_CHECKPOINT_TTL_MS;
|
|
748
|
+
this.#maxSuspensionPayloadBytes =
|
|
749
|
+
options.maxSuspensionPayloadBytes ?? DEFAULT_MAX_SUSPENSION_PAYLOAD_BYTES;
|
|
750
|
+
if (
|
|
751
|
+
!Number.isSafeInteger(this.#maxSuspensionPayloadBytes) ||
|
|
752
|
+
this.#maxSuspensionPayloadBytes < 1
|
|
753
|
+
) {
|
|
754
|
+
throw new Error('maxSuspensionPayloadBytes must be a positive integer');
|
|
755
|
+
}
|
|
683
756
|
const signingKey = Buffer.from(
|
|
684
757
|
options.preparationSigningKey ?? randomBytes(32)
|
|
685
758
|
);
|
|
@@ -1037,7 +1110,7 @@ export class EventActorExecutor<
|
|
|
1037
1110
|
}
|
|
1038
1111
|
this.#preparationPhases.set(preparationDigest, { status: 'invoking' });
|
|
1039
1112
|
const settlementInvocation = snapshotInvocationReference(trustedInvocation);
|
|
1040
|
-
let terminal:
|
|
1113
|
+
let terminal: EventActorAdapterInvocationResult<TResult>;
|
|
1041
1114
|
try {
|
|
1042
1115
|
terminal = await this.#invokeWithConfig(
|
|
1043
1116
|
snapshotInvocation(trustedInvocation),
|
|
@@ -1075,6 +1148,19 @@ export class EventActorExecutor<
|
|
|
1075
1148
|
} catch (error) {
|
|
1076
1149
|
return createIndeterminateResult(settlementInvocation, error);
|
|
1077
1150
|
}
|
|
1151
|
+
if (status === 'suspended') {
|
|
1152
|
+
try {
|
|
1153
|
+
return await this.#createSuspensionResult(
|
|
1154
|
+
settlementInvocation,
|
|
1155
|
+
terminal as Extract<
|
|
1156
|
+
EventActorAdapterInvocationResult<TResult>,
|
|
1157
|
+
{ status: 'suspended' }
|
|
1158
|
+
>
|
|
1159
|
+
);
|
|
1160
|
+
} catch (error) {
|
|
1161
|
+
return createIndeterminateResult(settlementInvocation, error);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1078
1164
|
if (status === 'applied') {
|
|
1079
1165
|
const snapshot = snapshotAppliedTerminal<TResult>(
|
|
1080
1166
|
settlementInvocation,
|
|
@@ -1098,11 +1184,15 @@ export class EventActorExecutor<
|
|
|
1098
1184
|
{ status: 'completed_no_action' }
|
|
1099
1185
|
>;
|
|
1100
1186
|
try {
|
|
1187
|
+
const terminalCompleted = terminal as Extract<
|
|
1188
|
+
EventActorTerminalResult<TResult>,
|
|
1189
|
+
{ status: 'completed_no_action' }
|
|
1190
|
+
>;
|
|
1101
1191
|
completed = Object.freeze({
|
|
1102
1192
|
status: 'completed_no_action' as const,
|
|
1103
|
-
...(
|
|
1193
|
+
...(terminalCompleted.result === undefined
|
|
1104
1194
|
? {}
|
|
1105
|
-
: { result: snapshotEvent(
|
|
1195
|
+
: { result: snapshotEvent(terminalCompleted.result) }),
|
|
1106
1196
|
});
|
|
1107
1197
|
} catch (error) {
|
|
1108
1198
|
this.#setTerminalPreparationPhase(
|
|
@@ -1122,24 +1212,422 @@ export class EventActorExecutor<
|
|
|
1122
1212
|
return completed;
|
|
1123
1213
|
}
|
|
1124
1214
|
|
|
1215
|
+
async #createSuspensionResult(
|
|
1216
|
+
invocation: EventActorInvocationReference,
|
|
1217
|
+
paused: Extract<
|
|
1218
|
+
EventActorAdapterInvocationResult<TResult>,
|
|
1219
|
+
{ status: 'suspended' }
|
|
1220
|
+
>,
|
|
1221
|
+
previous?: {
|
|
1222
|
+
suspension: EventActorSuspension;
|
|
1223
|
+
resumeAttemptId: string;
|
|
1224
|
+
}
|
|
1225
|
+
): Promise<EventActorSuspendedResult> {
|
|
1226
|
+
const checkpoint = snapshotCheckpointFork(paused.checkpoint);
|
|
1227
|
+
const invocationStart =
|
|
1228
|
+
previous == null
|
|
1229
|
+
? invocation
|
|
1230
|
+
: freezeInvocationReference({
|
|
1231
|
+
...invocation,
|
|
1232
|
+
fork: previous.suspension.checkpoint,
|
|
1233
|
+
});
|
|
1234
|
+
validateTerminalCheckpoint(invocationStart, checkpoint);
|
|
1235
|
+
requireNonEmpty(paused.interrupt.id, 'interrupt.id');
|
|
1236
|
+
if (previous?.suspension.attempt === Number.MAX_SAFE_INTEGER) {
|
|
1237
|
+
throw new Error('Event actor suspension attempt is exhausted');
|
|
1238
|
+
}
|
|
1239
|
+
const issuedAt = Date.now();
|
|
1240
|
+
const unsigned: Omit<EventActorSuspension, 'suspensionDigest'> = {
|
|
1241
|
+
version: 1,
|
|
1242
|
+
suspensionId: randomUUID(),
|
|
1243
|
+
attempt: previous == null ? 0 : previous.suspension.attempt + 1,
|
|
1244
|
+
issuedAt,
|
|
1245
|
+
expiresAt: Math.min(
|
|
1246
|
+
Number.MAX_SAFE_INTEGER,
|
|
1247
|
+
issuedAt + this.#dormantCheckpointTtlMs
|
|
1248
|
+
),
|
|
1249
|
+
invocation: freezeInvocationReference(invocation),
|
|
1250
|
+
checkpoint: Object.freeze(checkpoint),
|
|
1251
|
+
interrupt: Object.freeze({
|
|
1252
|
+
id: paused.interrupt.id,
|
|
1253
|
+
payload: snapshotEvent(paused.interrupt.payload),
|
|
1254
|
+
}),
|
|
1255
|
+
};
|
|
1256
|
+
const serialized = serializeSuspension(unsigned);
|
|
1257
|
+
this.#validateSuspensionSize(serialized);
|
|
1258
|
+
const suspension = Object.freeze({
|
|
1259
|
+
...unsigned,
|
|
1260
|
+
suspensionDigest: this.#signPreparation(serialized),
|
|
1261
|
+
});
|
|
1262
|
+
if (this.#adapter.suspend == null) {
|
|
1263
|
+
throw new Error('Event actor suspension storage is unavailable');
|
|
1264
|
+
}
|
|
1265
|
+
const stored = await this.#adapter.suspend({
|
|
1266
|
+
suspension,
|
|
1267
|
+
...(previous == null
|
|
1268
|
+
? {}
|
|
1269
|
+
: {
|
|
1270
|
+
previous: {
|
|
1271
|
+
suspensionId: previous.suspension.suspensionId,
|
|
1272
|
+
attempt: previous.suspension.attempt,
|
|
1273
|
+
resumeAttemptId: previous.resumeAttemptId,
|
|
1274
|
+
},
|
|
1275
|
+
}),
|
|
1276
|
+
});
|
|
1277
|
+
if (stored.status !== 'stored') {
|
|
1278
|
+
throw new Error('Event actor suspension ownership is stale');
|
|
1279
|
+
}
|
|
1280
|
+
return Object.freeze({ status: 'suspended', suspension });
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1125
1283
|
#issueSettlement(
|
|
1126
|
-
snapshot: EventActorAppliedSnapshot<TResult
|
|
1284
|
+
snapshot: EventActorAppliedSnapshot<TResult>,
|
|
1285
|
+
suspension?: EventActorSettlementSuspension
|
|
1127
1286
|
): EventActorAppliedResult<TResult> {
|
|
1287
|
+
const invocation = freezeInvocationReference(snapshot.invocation);
|
|
1288
|
+
const checkpoint = Object.freeze(
|
|
1289
|
+
snapshotCheckpointFork(snapshot.checkpoint)
|
|
1290
|
+
);
|
|
1291
|
+
let settlementAuthority: EventActorSettlementAuthority | undefined;
|
|
1292
|
+
if (suspension != null) {
|
|
1293
|
+
const issuedAt = Date.now();
|
|
1294
|
+
const unsigned: Omit<EventActorSettlementAuthority, 'settlementDigest'> =
|
|
1295
|
+
{
|
|
1296
|
+
version: 1,
|
|
1297
|
+
...suspension,
|
|
1298
|
+
issuedAt,
|
|
1299
|
+
expiresAt: Math.min(
|
|
1300
|
+
Number.MAX_SAFE_INTEGER,
|
|
1301
|
+
issuedAt + this.#dormantCheckpointTtlMs
|
|
1302
|
+
),
|
|
1303
|
+
};
|
|
1304
|
+
settlementAuthority = Object.freeze({
|
|
1305
|
+
...unsigned,
|
|
1306
|
+
settlementDigest: this.#signPreparation(
|
|
1307
|
+
serializeSettlement(unsigned, invocation, checkpoint, snapshot.result)
|
|
1308
|
+
),
|
|
1309
|
+
});
|
|
1310
|
+
}
|
|
1128
1311
|
const settlement = Object.freeze({
|
|
1129
1312
|
status: 'applied' as const,
|
|
1130
1313
|
result: snapshot.result,
|
|
1131
|
-
checkpoint
|
|
1132
|
-
invocation
|
|
1314
|
+
checkpoint,
|
|
1315
|
+
invocation,
|
|
1316
|
+
...(settlementAuthority == null ? {} : { settlementAuthority }),
|
|
1133
1317
|
});
|
|
1134
1318
|
this.#issuedSettlements.add(settlement);
|
|
1135
1319
|
return settlement;
|
|
1136
1320
|
}
|
|
1137
1321
|
|
|
1322
|
+
#snapshotAndValidateSettlementAuthority(
|
|
1323
|
+
authority: EventActorSettlementAuthority | undefined,
|
|
1324
|
+
invocation: EventActorInvocationReference,
|
|
1325
|
+
checkpoint: EventActorCheckpointFork,
|
|
1326
|
+
result: TResult
|
|
1327
|
+
): EventActorSettlementAuthority | undefined {
|
|
1328
|
+
if (authority == null) {
|
|
1329
|
+
return undefined;
|
|
1330
|
+
}
|
|
1331
|
+
const trusted = Object.freeze({
|
|
1332
|
+
version: authority.version,
|
|
1333
|
+
suspensionId: authority.suspensionId,
|
|
1334
|
+
attempt: authority.attempt,
|
|
1335
|
+
resumeAttemptId: authority.resumeAttemptId,
|
|
1336
|
+
issuedAt: authority.issuedAt,
|
|
1337
|
+
expiresAt: authority.expiresAt,
|
|
1338
|
+
settlementDigest: authority.settlementDigest,
|
|
1339
|
+
});
|
|
1340
|
+
const version: unknown = trusted.version;
|
|
1341
|
+
if (version !== 1) {
|
|
1342
|
+
throw new Error('Event actor settlement authority version is invalid');
|
|
1343
|
+
}
|
|
1344
|
+
requireNonEmpty(trusted.suspensionId, 'suspensionId');
|
|
1345
|
+
requireNonEmpty(trusted.resumeAttemptId, 'resumeAttemptId');
|
|
1346
|
+
if (!Number.isSafeInteger(trusted.attempt) || trusted.attempt < 0) {
|
|
1347
|
+
throw new Error('Event actor settlement authority attempt is invalid');
|
|
1348
|
+
}
|
|
1349
|
+
if (
|
|
1350
|
+
!Number.isSafeInteger(trusted.issuedAt) ||
|
|
1351
|
+
!Number.isSafeInteger(trusted.expiresAt) ||
|
|
1352
|
+
trusted.issuedAt < 0 ||
|
|
1353
|
+
trusted.expiresAt <= trusted.issuedAt
|
|
1354
|
+
) {
|
|
1355
|
+
throw new Error('Event actor settlement authority lifetime is invalid');
|
|
1356
|
+
}
|
|
1357
|
+
const { settlementDigest: _digest, ...unsigned } = trusted;
|
|
1358
|
+
if (
|
|
1359
|
+
!this.#preparationSignatureMatches(
|
|
1360
|
+
trusted.settlementDigest,
|
|
1361
|
+
serializeSettlement(unsigned, invocation, checkpoint, result)
|
|
1362
|
+
)
|
|
1363
|
+
) {
|
|
1364
|
+
throw new Error('Event actor settlement authority binding is invalid');
|
|
1365
|
+
}
|
|
1366
|
+
if (trusted.expiresAt <= Date.now()) {
|
|
1367
|
+
throw new Error('Event actor settlement authority binding has expired');
|
|
1368
|
+
}
|
|
1369
|
+
return trusted;
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
#snapshotAndValidateSuspension(
|
|
1373
|
+
suspension: EventActorSuspension,
|
|
1374
|
+
allowExpired = false
|
|
1375
|
+
): EventActorSuspension {
|
|
1376
|
+
const trusted: EventActorSuspension = Object.freeze({
|
|
1377
|
+
version: suspension.version,
|
|
1378
|
+
suspensionId: suspension.suspensionId,
|
|
1379
|
+
attempt: suspension.attempt,
|
|
1380
|
+
issuedAt: suspension.issuedAt,
|
|
1381
|
+
expiresAt: suspension.expiresAt,
|
|
1382
|
+
invocation: freezeInvocationReference(suspension.invocation),
|
|
1383
|
+
checkpoint: Object.freeze(snapshotCheckpointFork(suspension.checkpoint)),
|
|
1384
|
+
interrupt: Object.freeze({
|
|
1385
|
+
id: suspension.interrupt.id,
|
|
1386
|
+
payload: snapshotEvent(suspension.interrupt.payload),
|
|
1387
|
+
}),
|
|
1388
|
+
suspensionDigest: suspension.suspensionDigest,
|
|
1389
|
+
});
|
|
1390
|
+
const version: unknown = trusted.version;
|
|
1391
|
+
if (version !== 1) {
|
|
1392
|
+
throw new Error('Event actor suspension version is invalid');
|
|
1393
|
+
}
|
|
1394
|
+
requireNonEmpty(trusted.suspensionId, 'suspensionId');
|
|
1395
|
+
requireNonEmpty(trusted.interrupt.id, 'interrupt.id');
|
|
1396
|
+
if (!Number.isSafeInteger(trusted.attempt) || trusted.attempt < 0) {
|
|
1397
|
+
throw new Error('Event actor suspension attempt is invalid');
|
|
1398
|
+
}
|
|
1399
|
+
if (
|
|
1400
|
+
!Number.isSafeInteger(trusted.issuedAt) ||
|
|
1401
|
+
!Number.isSafeInteger(trusted.expiresAt) ||
|
|
1402
|
+
trusted.issuedAt < 0 ||
|
|
1403
|
+
trusted.expiresAt <= trusted.issuedAt
|
|
1404
|
+
) {
|
|
1405
|
+
throw new Error('Event actor suspension lifetime is invalid');
|
|
1406
|
+
}
|
|
1407
|
+
validateInvocationReference(trusted.invocation, this.#maxDepth);
|
|
1408
|
+
validateTerminalCheckpoint(trusted.invocation, trusted.checkpoint);
|
|
1409
|
+
const { suspensionDigest: _digest, ...unsigned } = trusted;
|
|
1410
|
+
const serialized = serializeSuspension(unsigned);
|
|
1411
|
+
this.#validateSuspensionSize(serialized);
|
|
1412
|
+
if (
|
|
1413
|
+
!this.#preparationSignatureMatches(trusted.suspensionDigest, serialized)
|
|
1414
|
+
) {
|
|
1415
|
+
throw new Error('Event actor suspension binding is invalid');
|
|
1416
|
+
}
|
|
1417
|
+
if (!allowExpired && trusted.expiresAt <= Date.now()) {
|
|
1418
|
+
throw new Error('Event actor suspension binding has expired');
|
|
1419
|
+
}
|
|
1420
|
+
return trusted;
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
#validateSuspensionSize(serialized: string): void {
|
|
1424
|
+
if (
|
|
1425
|
+
Buffer.byteLength(serialized, 'utf8') > this.#maxSuspensionPayloadBytes
|
|
1426
|
+
) {
|
|
1427
|
+
throw new Error('Event actor suspension exceeds maximum payload size');
|
|
1428
|
+
}
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
async cancelSuspension(
|
|
1432
|
+
request: EventActorCancelSuspensionRequest
|
|
1433
|
+
): Promise<EventActorCancelSuspensionResult> {
|
|
1434
|
+
const suspension = this.#snapshotAndValidateSuspension(
|
|
1435
|
+
request.suspension,
|
|
1436
|
+
true
|
|
1437
|
+
);
|
|
1438
|
+
requireNonEmpty(request.cancelAttemptId, 'cancelAttemptId');
|
|
1439
|
+
const reason: unknown = request.reason ?? 'cancelled';
|
|
1440
|
+
if (reason !== 'cancelled' && reason !== 'expired') {
|
|
1441
|
+
throw new Error('Event actor suspension cancellation reason is invalid');
|
|
1442
|
+
}
|
|
1443
|
+
if (this.#adapter.cancelSuspension == null) {
|
|
1444
|
+
throw new Error('Event actor suspension cancellation is unavailable');
|
|
1445
|
+
}
|
|
1446
|
+
const controller = new AbortController();
|
|
1447
|
+
const abort = (): void => controller.abort(request.signal?.reason);
|
|
1448
|
+
if (isAborted(request.signal)) {
|
|
1449
|
+
abort();
|
|
1450
|
+
} else {
|
|
1451
|
+
request.signal?.addEventListener('abort', abort, { once: true });
|
|
1452
|
+
}
|
|
1453
|
+
try {
|
|
1454
|
+
const cancelled = await this.#adapter.cancelSuspension(
|
|
1455
|
+
{
|
|
1456
|
+
suspension,
|
|
1457
|
+
cancelAttemptId: request.cancelAttemptId,
|
|
1458
|
+
reason,
|
|
1459
|
+
},
|
|
1460
|
+
{ signal: controller.signal }
|
|
1461
|
+
);
|
|
1462
|
+
if (cancelled.status !== 'cancelled') {
|
|
1463
|
+
return createIndeterminateResult(
|
|
1464
|
+
{
|
|
1465
|
+
...suspension.invocation,
|
|
1466
|
+
fork: suspension.checkpoint,
|
|
1467
|
+
},
|
|
1468
|
+
new Error('Event actor suspension ownership is stale')
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
return Object.freeze({ status: 'cancelled' });
|
|
1472
|
+
} catch (error) {
|
|
1473
|
+
return createIndeterminateResult(
|
|
1474
|
+
{
|
|
1475
|
+
...suspension.invocation,
|
|
1476
|
+
fork: suspension.checkpoint,
|
|
1477
|
+
},
|
|
1478
|
+
error
|
|
1479
|
+
);
|
|
1480
|
+
} finally {
|
|
1481
|
+
request.signal?.removeEventListener('abort', abort);
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1485
|
+
async resume(
|
|
1486
|
+
request: EventActorResumeRequest
|
|
1487
|
+
): Promise<EventActorInvocationResult<TResult>> {
|
|
1488
|
+
const suspension = this.#snapshotAndValidateSuspension(request.suspension);
|
|
1489
|
+
requireNonEmpty(request.resumeAttemptId, 'resumeAttemptId');
|
|
1490
|
+
if (this.#adapter.resume == null) {
|
|
1491
|
+
throw new Error('Event actor suspension resume is unavailable');
|
|
1492
|
+
}
|
|
1493
|
+
const value = snapshotEvent(request.value);
|
|
1494
|
+
const resumedReference = freezeInvocationReference({
|
|
1495
|
+
...suspension.invocation,
|
|
1496
|
+
fork: suspension.checkpoint,
|
|
1497
|
+
});
|
|
1498
|
+
const controller = new AbortController();
|
|
1499
|
+
const abort = (): void => controller.abort(request.signal?.reason);
|
|
1500
|
+
if (isAborted(request.signal)) {
|
|
1501
|
+
abort();
|
|
1502
|
+
} else {
|
|
1503
|
+
request.signal?.addEventListener('abort', abort, { once: true });
|
|
1504
|
+
}
|
|
1505
|
+
const config = createRunnableConfig(
|
|
1506
|
+
resumedReference,
|
|
1507
|
+
controller.signal,
|
|
1508
|
+
AsyncLocalStorageProviderSingleton.getRunnableConfig()
|
|
1509
|
+
);
|
|
1510
|
+
let resumed: EventActorAdapterResumeResult<TResult>;
|
|
1511
|
+
try {
|
|
1512
|
+
resumed = await AsyncLocalStorageProviderSingleton.runWithConfig(
|
|
1513
|
+
config,
|
|
1514
|
+
() =>
|
|
1515
|
+
this.#adapter.resume!(
|
|
1516
|
+
{
|
|
1517
|
+
suspension,
|
|
1518
|
+
resumeAttemptId: request.resumeAttemptId,
|
|
1519
|
+
value,
|
|
1520
|
+
},
|
|
1521
|
+
{ signal: controller.signal, config }
|
|
1522
|
+
)
|
|
1523
|
+
);
|
|
1524
|
+
} catch (error) {
|
|
1525
|
+
return createIndeterminateResult(resumedReference, error);
|
|
1526
|
+
} finally {
|
|
1527
|
+
request.signal?.removeEventListener('abort', abort);
|
|
1528
|
+
}
|
|
1529
|
+
if (resumed.status === 'claimed_failed') {
|
|
1530
|
+
try {
|
|
1531
|
+
if (this.#adapter.settleSuspension == null) {
|
|
1532
|
+
throw new Error('Event actor suspension settlement is unavailable');
|
|
1533
|
+
}
|
|
1534
|
+
const settled = await this.#adapter.settleSuspension({
|
|
1535
|
+
suspensionId: suspension.suspensionId,
|
|
1536
|
+
attempt: suspension.attempt,
|
|
1537
|
+
resumeAttemptId: request.resumeAttemptId,
|
|
1538
|
+
status: 'failed',
|
|
1539
|
+
});
|
|
1540
|
+
if (settled.status !== 'settled') {
|
|
1541
|
+
throw new Error('Event actor suspension settlement is stale');
|
|
1542
|
+
}
|
|
1543
|
+
} catch (error) {
|
|
1544
|
+
return createIndeterminateResult(resumedReference, error);
|
|
1545
|
+
}
|
|
1546
|
+
throw resumed.error;
|
|
1547
|
+
}
|
|
1548
|
+
if (resumed.status !== 'claimed') {
|
|
1549
|
+
return createIndeterminateResult(
|
|
1550
|
+
resumedReference,
|
|
1551
|
+
new Error('Event actor suspension ownership is stale')
|
|
1552
|
+
);
|
|
1553
|
+
}
|
|
1554
|
+
const status: unknown = resumed.result.status;
|
|
1555
|
+
if (status === 'suspended') {
|
|
1556
|
+
try {
|
|
1557
|
+
return await this.#createSuspensionResult(
|
|
1558
|
+
suspension.invocation,
|
|
1559
|
+
resumed.result as Extract<
|
|
1560
|
+
EventActorAdapterInvocationResult<TResult>,
|
|
1561
|
+
{ status: 'suspended' }
|
|
1562
|
+
>,
|
|
1563
|
+
{
|
|
1564
|
+
suspension,
|
|
1565
|
+
resumeAttemptId: request.resumeAttemptId,
|
|
1566
|
+
}
|
|
1567
|
+
);
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
return createIndeterminateResult(resumedReference, error);
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
if (status === 'completed_no_action') {
|
|
1573
|
+
let result: TResult | undefined;
|
|
1574
|
+
try {
|
|
1575
|
+
const completed = resumed.result as Extract<
|
|
1576
|
+
EventActorTerminalResult<TResult>,
|
|
1577
|
+
{ status: 'completed_no_action' }
|
|
1578
|
+
>;
|
|
1579
|
+
result =
|
|
1580
|
+
completed.result === undefined
|
|
1581
|
+
? undefined
|
|
1582
|
+
: snapshotEvent(completed.result);
|
|
1583
|
+
if (this.#adapter.settleSuspension == null) {
|
|
1584
|
+
throw new Error('Event actor suspension settlement is unavailable');
|
|
1585
|
+
}
|
|
1586
|
+
const settled = await this.#adapter.settleSuspension({
|
|
1587
|
+
suspensionId: suspension.suspensionId,
|
|
1588
|
+
attempt: suspension.attempt,
|
|
1589
|
+
resumeAttemptId: request.resumeAttemptId,
|
|
1590
|
+
status: 'completed_no_action',
|
|
1591
|
+
});
|
|
1592
|
+
if (settled.status !== 'settled') {
|
|
1593
|
+
throw new Error('Event actor suspension settlement is stale');
|
|
1594
|
+
}
|
|
1595
|
+
return Object.freeze({
|
|
1596
|
+
status: 'completed_no_action' as const,
|
|
1597
|
+
...(result === undefined ? {} : { result }),
|
|
1598
|
+
});
|
|
1599
|
+
} catch (error) {
|
|
1600
|
+
return createIndeterminateResult(resumedReference, error, result);
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
if (status !== 'applied') {
|
|
1604
|
+
return createIndeterminateResult(
|
|
1605
|
+
resumedReference,
|
|
1606
|
+
new Error('Event actor resumed invocation returned an invalid status')
|
|
1607
|
+
);
|
|
1608
|
+
}
|
|
1609
|
+
const applied = snapshotAppliedTerminal<TResult>(
|
|
1610
|
+
resumedReference,
|
|
1611
|
+
resumed.result as Extract<
|
|
1612
|
+
EventActorTerminalResult<TResult>,
|
|
1613
|
+
{ status: 'applied' }
|
|
1614
|
+
>
|
|
1615
|
+
);
|
|
1616
|
+
if (applied.status === 'commit_indeterminate') {
|
|
1617
|
+
return applied;
|
|
1618
|
+
}
|
|
1619
|
+
return this.#issueSettlement(applied, {
|
|
1620
|
+
suspensionId: suspension.suspensionId,
|
|
1621
|
+
attempt: suspension.attempt,
|
|
1622
|
+
resumeAttemptId: request.resumeAttemptId,
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1138
1626
|
async #invokeWithConfig(
|
|
1139
1627
|
invocation: EventActorInvocation<TEvent>,
|
|
1140
1628
|
signal: AbortSignal | undefined,
|
|
1141
1629
|
ambientConfig: RunnableConfig | undefined
|
|
1142
|
-
): Promise<
|
|
1630
|
+
): Promise<EventActorAdapterInvocationResult<TResult>> {
|
|
1143
1631
|
resolveExecutionDepth(invocation.depth, ambientConfig);
|
|
1144
1632
|
validateInvocation(
|
|
1145
1633
|
{
|
|
@@ -1185,22 +1673,42 @@ export class EventActorExecutor<
|
|
|
1185
1673
|
async commit(
|
|
1186
1674
|
settlement: EventActorAppliedResult<TResult>
|
|
1187
1675
|
): Promise<EventActorSettlementResult<TResult>> {
|
|
1188
|
-
|
|
1189
|
-
throw new Error('Event actor settlement was not issued by this executor');
|
|
1190
|
-
}
|
|
1676
|
+
const locallyIssued = this.#issuedSettlements.has(settlement);
|
|
1191
1677
|
const trustedInvocation = snapshotInvocationReference(
|
|
1192
1678
|
settlement.invocation
|
|
1193
1679
|
);
|
|
1194
|
-
validateInvocationReference(trustedInvocation, this.#maxDepth);
|
|
1195
1680
|
const trustedCheckpoint = snapshotCheckpointFork(settlement.checkpoint);
|
|
1681
|
+
const trustedResult = snapshotEvent(settlement.result);
|
|
1682
|
+
const settlementAuthority = this.#snapshotAndValidateSettlementAuthority(
|
|
1683
|
+
settlement.settlementAuthority,
|
|
1684
|
+
trustedInvocation,
|
|
1685
|
+
trustedCheckpoint,
|
|
1686
|
+
trustedResult
|
|
1687
|
+
);
|
|
1688
|
+
if (settlementAuthority == null && !locallyIssued) {
|
|
1689
|
+
throw new Error('Event actor settlement was not issued by this executor');
|
|
1690
|
+
}
|
|
1691
|
+
validateInvocationReference(
|
|
1692
|
+
trustedInvocation,
|
|
1693
|
+
this.#maxDepth,
|
|
1694
|
+
settlementAuthority != null
|
|
1695
|
+
);
|
|
1196
1696
|
validateTerminalCheckpoint(trustedInvocation, trustedCheckpoint);
|
|
1197
1697
|
this.#issuedSettlements.delete(settlement);
|
|
1698
|
+
const trustedSettlement: EventActorAppliedResult<TResult> = {
|
|
1699
|
+
status: 'applied',
|
|
1700
|
+
result: trustedResult,
|
|
1701
|
+
checkpoint: trustedCheckpoint,
|
|
1702
|
+
invocation: trustedInvocation,
|
|
1703
|
+
...(settlementAuthority == null ? {} : { settlementAuthority }),
|
|
1704
|
+
};
|
|
1198
1705
|
try {
|
|
1199
1706
|
const committed = await this.#adapter.commit({
|
|
1200
1707
|
invocation: snapshotInvocationReference(trustedInvocation),
|
|
1201
1708
|
expectedHead: snapshotHead(trustedInvocation.base),
|
|
1202
1709
|
checkpoint: { ...trustedCheckpoint },
|
|
1203
|
-
result:
|
|
1710
|
+
result: trustedResult,
|
|
1711
|
+
...(settlementAuthority == null ? {} : { settlementAuthority }),
|
|
1204
1712
|
retention: {
|
|
1205
1713
|
committedCheckpoints: 2,
|
|
1206
1714
|
dormantCheckpointTtlMs: this.#dormantCheckpointTtlMs,
|
|
@@ -1258,7 +1766,7 @@ export class EventActorExecutor<
|
|
|
1258
1766
|
}
|
|
1259
1767
|
return { status: 'stale' };
|
|
1260
1768
|
} catch (error) {
|
|
1261
|
-
return createSettlementIndeterminateResult(
|
|
1769
|
+
return createSettlementIndeterminateResult(trustedSettlement, error);
|
|
1262
1770
|
}
|
|
1263
1771
|
}
|
|
1264
1772
|
|
|
@@ -1417,13 +1925,34 @@ export class EventActorExecutor<
|
|
|
1417
1925
|
continuation,
|
|
1418
1926
|
};
|
|
1419
1927
|
}
|
|
1928
|
+
if (terminalStatus === 'suspended') {
|
|
1929
|
+
try {
|
|
1930
|
+
const suspended = await this.#createSuspensionResult(
|
|
1931
|
+
invocationReference,
|
|
1932
|
+
terminal as Extract<
|
|
1933
|
+
EventActorAdapterInvocationResult<TResult>,
|
|
1934
|
+
{ status: 'suspended' }
|
|
1935
|
+
>
|
|
1936
|
+
);
|
|
1937
|
+
return { ...suspended, continuation };
|
|
1938
|
+
} catch (error) {
|
|
1939
|
+
return {
|
|
1940
|
+
...createIndeterminateResult(invocationReference, error),
|
|
1941
|
+
continuation,
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1420
1945
|
if (terminalStatus === 'completed_no_action') {
|
|
1421
1946
|
let result: TResult | undefined;
|
|
1422
1947
|
try {
|
|
1948
|
+
const completed = terminal as Extract<
|
|
1949
|
+
EventActorTerminalResult<TResult>,
|
|
1950
|
+
{ status: 'completed_no_action' }
|
|
1951
|
+
>;
|
|
1423
1952
|
result =
|
|
1424
|
-
|
|
1953
|
+
completed.result === undefined
|
|
1425
1954
|
? undefined
|
|
1426
|
-
: snapshotEvent(
|
|
1955
|
+
: snapshotEvent(completed.result);
|
|
1427
1956
|
} catch (error) {
|
|
1428
1957
|
await this.#discardInvocationReference(
|
|
1429
1958
|
invocationReference,
|