@librechat/agents 3.7.6 → 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.
@@ -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: EventActorTerminalResult<TResult>;
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
- ...(terminal.result === undefined
1193
+ ...(terminalCompleted.result === undefined
1104
1194
  ? {}
1105
- : { result: snapshotEvent(terminal.result) }),
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: Object.freeze(snapshotCheckpointFork(snapshot.checkpoint)),
1132
- invocation: freezeInvocationReference(snapshot.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<EventActorTerminalResult<TResult>> {
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
- if (!this.#issuedSettlements.has(settlement)) {
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: settlement.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(settlement, error);
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
- terminal.result === undefined
1953
+ completed.result === undefined
1425
1954
  ? undefined
1426
- : snapshotEvent(terminal.result);
1955
+ : snapshotEvent(completed.result);
1427
1956
  } catch (error) {
1428
1957
  await this.#discardInvocationReference(
1429
1958
  invocationReference,