@librechat/agents 3.7.1 → 3.7.3

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.
@@ -0,0 +1,1499 @@
1
+ import { isGraphInterrupt, isParentCommand } from '@langchain/langgraph';
2
+ import { AsyncLocalStorageProviderSingleton } from '@langchain/core/singletons';
3
+ import {
4
+ createHash,
5
+ createHmac,
6
+ randomBytes,
7
+ randomUUID,
8
+ timingSafeEqual,
9
+ } from 'node:crypto';
10
+ import type { RunnableConfig } from '@langchain/core/runnables';
11
+ import type {
12
+ EventActorAdapterPrepareRequest,
13
+ EventActorAdapterPreparation,
14
+ EventActorAppliedResult,
15
+ EventActorCheckpointFork,
16
+ EventActorCheckpointReference,
17
+ EventActorDiscardReason,
18
+ EventActorEvent,
19
+ EventActorExecutionRequest,
20
+ EventActorExecutionResult,
21
+ EventActorExecutorOptions,
22
+ EventActorHead,
23
+ EventActorHostAdapter,
24
+ EventActorIndeterminateResult,
25
+ EventActorInvocation,
26
+ EventActorInvocationResult,
27
+ EventActorInvocationReference,
28
+ EventActorPreparedInvocation,
29
+ EventActorPrepareRequest,
30
+ EventActorPreparation,
31
+ EventActorSettlementResult,
32
+ EventActorTerminalResult,
33
+ } from './types';
34
+
35
+ const DEFAULT_MAX_DEPTH = 1;
36
+ const DEFAULT_DORMANT_CHECKPOINT_TTL_MS = 24 * 60 * 60 * 1_000;
37
+
38
+ function createInvocationCheckpointNs(
39
+ request: EventActorPrepareRequest<EventActorEvent>,
40
+ attemptId = randomUUID()
41
+ ): string {
42
+ return `event-actor/${createHash('sha256')
43
+ .update(request.actorThreadId)
44
+ .update('\0')
45
+ .update(request.invocationId)
46
+ .update('\0')
47
+ .update(attemptId)
48
+ .digest('hex')
49
+ .slice(0, 32)}`;
50
+ }
51
+
52
+ function snapshotEvent<TEvent extends EventActorEvent>(event: TEvent): TEvent {
53
+ const ancestors = new WeakSet<object>();
54
+ const clone = (value: unknown): EventActorEvent => {
55
+ if (
56
+ value === null ||
57
+ typeof value === 'string' ||
58
+ typeof value === 'boolean'
59
+ ) {
60
+ return value;
61
+ }
62
+ if (typeof value === 'number') {
63
+ if (!Number.isFinite(value)) {
64
+ throw new Error('Event actor event numbers must be finite');
65
+ }
66
+ return Object.is(value, -0) ? 0 : value;
67
+ }
68
+ if (typeof value !== 'object') {
69
+ throw new Error('Event actor events must contain only JSON values');
70
+ }
71
+ if (ancestors.has(value)) {
72
+ throw new Error('Event actor events must not contain cycles');
73
+ }
74
+ ancestors.add(value);
75
+ try {
76
+ if (Array.isArray(value)) {
77
+ if (Object.getOwnPropertySymbols(value).length > 0) {
78
+ throw new Error('Event actor event arrays must not contain symbols');
79
+ }
80
+ const snapshot: EventActorEvent[] = [];
81
+ for (let index = 0; index < value.length; index += 1) {
82
+ if (!Object.hasOwn(value, index)) {
83
+ throw new Error('Event actor event arrays must not contain holes');
84
+ }
85
+ snapshot.push(clone(value[index]));
86
+ }
87
+ if (Object.keys(value).length !== value.length) {
88
+ throw new Error(
89
+ 'Event actor event arrays must not contain named properties'
90
+ );
91
+ }
92
+ return Object.freeze(snapshot);
93
+ }
94
+ const prototype = Object.getPrototypeOf(value);
95
+ if (prototype !== Object.prototype && prototype !== null) {
96
+ throw new Error('Event actor events must contain only JSON objects');
97
+ }
98
+ if (Object.getOwnPropertySymbols(value).length > 0) {
99
+ throw new Error('Event actor events must not contain symbol keys');
100
+ }
101
+ const snapshot: Record<string, EventActorEvent> = {};
102
+ for (const key of Object.keys(value).sort()) {
103
+ const item = value[key as keyof typeof value];
104
+ Object.defineProperty(snapshot, key, {
105
+ configurable: false,
106
+ enumerable: true,
107
+ writable: false,
108
+ value: clone(item),
109
+ });
110
+ }
111
+ return Object.freeze(snapshot);
112
+ } finally {
113
+ ancestors.delete(value);
114
+ }
115
+ };
116
+ return clone(event) as TEvent;
117
+ }
118
+
119
+ function snapshotCheckpointReference(
120
+ checkpoint: EventActorCheckpointReference
121
+ ): EventActorCheckpointReference {
122
+ return {
123
+ threadId: checkpoint.threadId,
124
+ ...(checkpoint.checkpointId == null
125
+ ? {}
126
+ : { checkpointId: checkpoint.checkpointId }),
127
+ checkpointNs: checkpoint.checkpointNs,
128
+ };
129
+ }
130
+
131
+ function snapshotCheckpointFork(
132
+ checkpoint: EventActorCheckpointFork
133
+ ): EventActorCheckpointFork {
134
+ return {
135
+ ...snapshotCheckpointReference(checkpoint),
136
+ invocationId: checkpoint.invocationId,
137
+ };
138
+ }
139
+
140
+ function snapshotHead(head: EventActorHead): EventActorHead {
141
+ return {
142
+ actorThreadId: head.actorThreadId,
143
+ generation: Object.is(head.generation, -0) ? 0 : head.generation,
144
+ ...(head.checkpoint == null
145
+ ? {}
146
+ : { checkpoint: snapshotCheckpointReference(head.checkpoint) }),
147
+ };
148
+ }
149
+
150
+ function snapshotInvocationReference(
151
+ invocation: EventActorInvocationReference
152
+ ): EventActorInvocationReference {
153
+ return {
154
+ actorThreadId: invocation.actorThreadId,
155
+ invocationId: invocation.invocationId,
156
+ depth: invocation.depth,
157
+ continuation: invocation.continuation,
158
+ base: snapshotHead(invocation.base),
159
+ fork: snapshotCheckpointFork(invocation.fork),
160
+ };
161
+ }
162
+
163
+ function snapshotInvocation<TEvent extends EventActorEvent>(
164
+ invocation: EventActorInvocation<TEvent>
165
+ ): EventActorInvocation<TEvent> {
166
+ return {
167
+ ...snapshotInvocationReference(invocation),
168
+ event: snapshotEvent(invocation.event),
169
+ };
170
+ }
171
+
172
+ function snapshotPrepareRequest<TEvent extends EventActorEvent>(
173
+ request: EventActorPrepareRequest<TEvent>
174
+ ): EventActorPrepareRequest<TEvent> {
175
+ return {
176
+ actorThreadId: request.actorThreadId,
177
+ invocationId: request.invocationId,
178
+ depth: request.depth,
179
+ event: snapshotEvent(request.event),
180
+ };
181
+ }
182
+
183
+ function freezeInvocationReference(
184
+ invocation: EventActorInvocationReference
185
+ ): EventActorInvocationReference {
186
+ const snapshot = snapshotInvocationReference(invocation);
187
+ if (snapshot.base.checkpoint != null) {
188
+ Object.freeze(snapshot.base.checkpoint);
189
+ }
190
+ Object.freeze(snapshot.base);
191
+ Object.freeze(snapshot.fork);
192
+ return Object.freeze(snapshot);
193
+ }
194
+
195
+ function freezeInvocation<TEvent extends EventActorEvent>(
196
+ invocation: EventActorInvocation<TEvent>
197
+ ): EventActorInvocation<TEvent> {
198
+ return Object.freeze({
199
+ ...freezeInvocationReference(invocation),
200
+ event: snapshotEvent(invocation.event),
201
+ });
202
+ }
203
+
204
+ function snapshotPreparedInvocation<TEvent extends EventActorEvent>(
205
+ invocation: EventActorPreparedInvocation<TEvent>
206
+ ): EventActorPreparedInvocation<TEvent> {
207
+ return Object.freeze({
208
+ ...freezeInvocation(invocation),
209
+ preparationDigest: invocation.preparationDigest,
210
+ });
211
+ }
212
+
213
+ function canonicalHead(head: EventActorHead): object {
214
+ if (head.checkpoint == null) {
215
+ return {
216
+ actorThreadId: head.actorThreadId,
217
+ generation: head.generation,
218
+ checkpoint: null,
219
+ };
220
+ }
221
+ return {
222
+ actorThreadId: head.actorThreadId,
223
+ generation: head.generation,
224
+ checkpoint: {
225
+ threadId: head.checkpoint.threadId,
226
+ checkpointId: head.checkpoint.checkpointId ?? null,
227
+ checkpointNs: head.checkpoint.checkpointNs,
228
+ },
229
+ };
230
+ }
231
+
232
+ function serializeInvocationPreparation<TEvent extends EventActorEvent>(
233
+ invocation: EventActorInvocation<TEvent>
234
+ ): string {
235
+ return JSON.stringify({
236
+ kind: 'invocation',
237
+ actorThreadId: invocation.actorThreadId,
238
+ invocationId: invocation.invocationId,
239
+ depth: invocation.depth,
240
+ continuation: invocation.continuation,
241
+ base: canonicalHead(invocation.base),
242
+ fork: {
243
+ invocationId: invocation.fork.invocationId,
244
+ threadId: invocation.fork.threadId,
245
+ checkpointId: invocation.fork.checkpointId ?? null,
246
+ checkpointNs: invocation.fork.checkpointNs,
247
+ },
248
+ event: snapshotEvent(invocation.event),
249
+ });
250
+ }
251
+
252
+ function serializeUnavailablePreparation<TEvent extends EventActorEvent>(
253
+ request: EventActorPrepareRequest<TEvent>,
254
+ head: EventActorHead
255
+ ): string {
256
+ return JSON.stringify({
257
+ kind: 'checkpoint_unavailable',
258
+ request: {
259
+ actorThreadId: request.actorThreadId,
260
+ invocationId: request.invocationId,
261
+ depth: request.depth,
262
+ event: snapshotEvent(request.event),
263
+ },
264
+ head: canonicalHead(head),
265
+ });
266
+ }
267
+
268
+ function freezePrepareRequest<TEvent extends EventActorEvent>(
269
+ request: EventActorPrepareRequest<TEvent>
270
+ ): EventActorPrepareRequest<TEvent> {
271
+ return Object.freeze(snapshotPrepareRequest(request));
272
+ }
273
+
274
+ function freezeHead(head: EventActorHead): EventActorHead {
275
+ const snapshot = snapshotHead(head);
276
+ if (snapshot.checkpoint != null) {
277
+ Object.freeze(snapshot.checkpoint);
278
+ }
279
+ return Object.freeze(snapshot);
280
+ }
281
+
282
+ function snapshotAmbientConfig(
283
+ config: RunnableConfig | undefined
284
+ ): RunnableConfig | undefined {
285
+ if (config == null) {
286
+ return undefined;
287
+ }
288
+ return {
289
+ ...config,
290
+ ...(config.tags == null ? {} : { tags: [...config.tags] }),
291
+ ...(config.metadata == null ? {} : { metadata: { ...config.metadata } }),
292
+ ...(config.configurable == null
293
+ ? {}
294
+ : { configurable: { ...config.configurable } }),
295
+ };
296
+ }
297
+
298
+ function requireNonEmpty(value: string, name: string): void {
299
+ if (value.trim() === '') {
300
+ throw new Error(`${name} must not be empty`);
301
+ }
302
+ }
303
+
304
+ function validateHead(
305
+ head: EventActorHead,
306
+ actorThreadId: string,
307
+ checkpointRequired = false
308
+ ): void {
309
+ if (
310
+ head.actorThreadId !== actorThreadId ||
311
+ !Number.isSafeInteger(head.generation) ||
312
+ head.generation < 0
313
+ ) {
314
+ throw new Error('Event actor head is invalid');
315
+ }
316
+ if (head.checkpoint == null) {
317
+ if (checkpointRequired) {
318
+ throw new Error('Committed event actor head has no checkpoint');
319
+ }
320
+ if (head.generation > 0) {
321
+ throw new Error('Advanced event actor head has no checkpoint');
322
+ }
323
+ return;
324
+ }
325
+ requireNonEmpty(head.checkpoint.threadId, 'head.checkpoint.threadId');
326
+ if (typeof head.checkpoint.checkpointNs !== 'string') {
327
+ throw new Error('head.checkpoint.checkpointNs must be a string');
328
+ }
329
+ requireNonEmpty(
330
+ head.checkpoint.checkpointId ?? '',
331
+ 'head.checkpoint.checkpointId'
332
+ );
333
+ }
334
+
335
+ function validateInvocation<TEvent extends EventActorEvent>(
336
+ request: EventActorPrepareRequest<TEvent>,
337
+ invocation: EventActorInvocation<TEvent>,
338
+ continuation: 'warm' | 'cold',
339
+ checkpointNs: string,
340
+ maxDepth: number,
341
+ expectedHead?: EventActorInvocation<TEvent>['base']
342
+ ): void {
343
+ validateInvocationReference(invocation, maxDepth);
344
+ if (
345
+ invocation.actorThreadId !== request.actorThreadId ||
346
+ invocation.invocationId !== request.invocationId ||
347
+ invocation.depth !== request.depth ||
348
+ invocation.continuation !== continuation
349
+ ) {
350
+ throw new Error('Event actor preparation returned a mismatched invocation');
351
+ }
352
+ if (
353
+ invocation.base.actorThreadId !== request.actorThreadId ||
354
+ invocation.fork.invocationId !== request.invocationId ||
355
+ invocation.fork.checkpointNs !== checkpointNs
356
+ ) {
357
+ throw new Error(
358
+ 'Event actor preparation returned mismatched checkpoint ownership'
359
+ );
360
+ }
361
+ if (
362
+ expectedHead != null &&
363
+ (expectedHead.actorThreadId !== request.actorThreadId ||
364
+ invocation.base.generation !== expectedHead.generation ||
365
+ invocation.base.checkpoint?.threadId !==
366
+ expectedHead.checkpoint?.threadId ||
367
+ invocation.base.checkpoint?.checkpointId !==
368
+ expectedHead.checkpoint?.checkpointId ||
369
+ invocation.base.checkpoint?.checkpointNs !==
370
+ expectedHead.checkpoint?.checkpointNs)
371
+ ) {
372
+ throw new Error('Cold continuation did not use the prepared actor head');
373
+ }
374
+ }
375
+
376
+ function validateInvocationReference(
377
+ invocation: EventActorInvocationReference,
378
+ maxDepth?: number
379
+ ): void {
380
+ requireNonEmpty(invocation.actorThreadId, 'actorThreadId');
381
+ requireNonEmpty(invocation.invocationId, 'invocationId');
382
+ if (!Number.isSafeInteger(invocation.depth) || invocation.depth < 1) {
383
+ throw new Error('Event actor invocation depth is invalid');
384
+ }
385
+ if (maxDepth != null && invocation.depth > maxDepth) {
386
+ throw new Error(
387
+ `Event actor depth ${invocation.depth} exceeds maximum ${maxDepth}`
388
+ );
389
+ }
390
+ const continuation: unknown = invocation.continuation;
391
+ if (continuation !== 'warm' && continuation !== 'cold') {
392
+ throw new Error('Event actor invocation continuation is invalid');
393
+ }
394
+ validateHead(invocation.base, invocation.actorThreadId);
395
+ if (invocation.fork.invocationId !== invocation.invocationId) {
396
+ throw new Error(
397
+ 'Event actor invocation has mismatched checkpoint ownership'
398
+ );
399
+ }
400
+ requireNonEmpty(invocation.fork.threadId, 'fork.threadId');
401
+ requireNonEmpty(invocation.fork.checkpointNs, 'fork.checkpointNs');
402
+ if (invocation.base.checkpoint != null) {
403
+ if (invocation.fork.threadId !== invocation.base.checkpoint.threadId) {
404
+ throw new Error('Event actor fork changed its logical checkpoint thread');
405
+ }
406
+ requireNonEmpty(
407
+ invocation.fork.checkpointId ?? '',
408
+ 'fork.checkpointId for resumed actor'
409
+ );
410
+ if (
411
+ invocation.continuation === 'warm' &&
412
+ invocation.fork.checkpointId !== invocation.base.checkpoint.checkpointId
413
+ ) {
414
+ throw new Error(
415
+ 'Warm event actor fork did not start from the committed checkpoint'
416
+ );
417
+ }
418
+ }
419
+ }
420
+
421
+ function checkpointIdsMatch(
422
+ left: EventActorCheckpointFork | EventActorHead['checkpoint'],
423
+ right: EventActorCheckpointFork | EventActorHead['checkpoint']
424
+ ): boolean {
425
+ return (
426
+ left != null && right != null && left.checkpointId === right.checkpointId
427
+ );
428
+ }
429
+
430
+ function checkpointsMatch(
431
+ left: EventActorCheckpointFork | EventActorHead['checkpoint'],
432
+ right: EventActorCheckpointFork | EventActorHead['checkpoint']
433
+ ): boolean {
434
+ return (
435
+ checkpointIdsMatch(left, right) &&
436
+ left?.threadId === right?.threadId &&
437
+ left?.checkpointNs === right?.checkpointNs
438
+ );
439
+ }
440
+
441
+ function validateTerminalCheckpoint(
442
+ invocation: EventActorInvocationReference,
443
+ checkpoint: EventActorCheckpointFork
444
+ ): void {
445
+ if (
446
+ checkpoint.invocationId !== invocation.invocationId ||
447
+ checkpoint.threadId !== invocation.fork.threadId ||
448
+ checkpoint.checkpointNs !== invocation.fork.checkpointNs ||
449
+ checkpoint.checkpointId == null ||
450
+ checkpoint.checkpointId.trim() === '' ||
451
+ checkpoint.checkpointId === invocation.fork.checkpointId ||
452
+ checkpointIdsMatch(checkpoint, invocation.base.checkpoint)
453
+ ) {
454
+ throw new Error(
455
+ 'Event actor result escaped its invocation checkpoint fork'
456
+ );
457
+ }
458
+ }
459
+
460
+ function createRunnableConfig(
461
+ invocation: EventActorInvocationReference,
462
+ signal: AbortSignal,
463
+ ambient?: RunnableConfig
464
+ ): RunnableConfig {
465
+ const {
466
+ signal: _ambientSignal,
467
+ runId: _ambientRunId,
468
+ runName: _ambientRunName,
469
+ callbacks: ambientCallbacks,
470
+ tags: ambientTags,
471
+ metadata: ambientMetadata,
472
+ configurable: ambientConfigurable,
473
+ ...ambientRuntime
474
+ } = ambient ?? {};
475
+ const configurable = Object.fromEntries(
476
+ Object.entries(ambientConfigurable ?? {}).filter(
477
+ ([key]) =>
478
+ !key.startsWith('__pregel_') &&
479
+ !key.startsWith('__librechat_') &&
480
+ key !== 'lc_run_breaker_scope'
481
+ )
482
+ );
483
+ delete configurable.run_id;
484
+ delete configurable.thread_id;
485
+ delete configurable.checkpoint_ns;
486
+ delete configurable.checkpoint_id;
487
+ delete configurable.checkpoint_map;
488
+ delete configurable.event_actor_thread_id;
489
+ delete configurable.event_actor_invocation_id;
490
+ delete configurable.event_actor_generation;
491
+ delete configurable.event_actor_depth;
492
+ delete configurable.event_actor_continuation;
493
+ const metadata = Object.fromEntries(
494
+ Object.entries(ambientMetadata ?? {}).filter(
495
+ ([key]) =>
496
+ !key.startsWith('langgraph_') &&
497
+ !key.startsWith('__pregel_') &&
498
+ key !== 'run_id' &&
499
+ key !== 'thread_id' &&
500
+ key !== 'checkpoint_ns' &&
501
+ key !== 'checkpoint_id' &&
502
+ key !== 'checkpoint_map'
503
+ )
504
+ );
505
+ return {
506
+ ...ambientRuntime,
507
+ signal,
508
+ ...(ambientCallbacks == null ? {} : { callbacks: ambientCallbacks }),
509
+ ...(ambientTags == null ? {} : { tags: ambientTags }),
510
+ metadata: {
511
+ ...metadata,
512
+ thread_id: invocation.fork.threadId,
513
+ checkpoint_ns: invocation.fork.checkpointNs,
514
+ eventActorThreadId: invocation.actorThreadId,
515
+ eventActorInvocationId: invocation.invocationId,
516
+ eventActorGeneration: invocation.base.generation,
517
+ eventActorDepth: invocation.depth,
518
+ eventActorContinuation: invocation.continuation,
519
+ },
520
+ configurable: {
521
+ ...configurable,
522
+ thread_id: invocation.fork.threadId,
523
+ checkpoint_ns: invocation.fork.checkpointNs,
524
+ ...(invocation.fork.checkpointId == null
525
+ ? {}
526
+ : { checkpoint_id: invocation.fork.checkpointId }),
527
+ event_actor_thread_id: invocation.actorThreadId,
528
+ event_actor_invocation_id: invocation.invocationId,
529
+ event_actor_generation: invocation.base.generation,
530
+ event_actor_depth: invocation.depth,
531
+ event_actor_continuation: invocation.continuation,
532
+ },
533
+ };
534
+ }
535
+
536
+ function asError(error: unknown): Error {
537
+ try {
538
+ return error instanceof Error ? error : new Error(String(error));
539
+ } catch {
540
+ return new Error('Unknown event actor error');
541
+ }
542
+ }
543
+
544
+ type EventActorAppliedSnapshot<TResult extends EventActorEvent> = {
545
+ status: 'snapshot_ready';
546
+ result: TResult;
547
+ checkpoint: EventActorCheckpointFork;
548
+ invocation: EventActorInvocationReference;
549
+ };
550
+
551
+ type EventActorPreparationPhase =
552
+ | { status: 'invoking' | 'discarding' }
553
+ | {
554
+ status: 'discardable' | 'retained' | 'discarded';
555
+ expiresAt: number;
556
+ };
557
+
558
+ function createIndeterminateResult<TResult extends EventActorEvent>(
559
+ invocation: EventActorInvocationReference,
560
+ error: unknown,
561
+ result?: TResult
562
+ ): EventActorIndeterminateResult<TResult> {
563
+ return Object.freeze({
564
+ status: 'commit_indeterminate',
565
+ ...(result === undefined ? {} : { result }),
566
+ checkpoint: Object.freeze({
567
+ invocationId: invocation.fork.invocationId,
568
+ threadId: invocation.fork.threadId,
569
+ checkpointNs: invocation.fork.checkpointNs,
570
+ }),
571
+ error: asError(error),
572
+ });
573
+ }
574
+
575
+ function createSettlementIndeterminateResult<TResult extends EventActorEvent>(
576
+ settlement: EventActorAppliedResult<TResult>,
577
+ error: unknown
578
+ ): EventActorIndeterminateResult<TResult> {
579
+ return Object.freeze({
580
+ status: 'commit_indeterminate',
581
+ result: settlement.result,
582
+ checkpoint: Object.freeze(snapshotCheckpointFork(settlement.checkpoint)),
583
+ error: asError(error),
584
+ });
585
+ }
586
+
587
+ function snapshotAppliedTerminal<TResult extends EventActorEvent>(
588
+ invocation: EventActorInvocationReference,
589
+ terminal: Extract<EventActorTerminalResult<TResult>, { status: 'applied' }>
590
+ ): EventActorAppliedSnapshot<TResult> | EventActorIndeterminateResult<TResult> {
591
+ let result: TResult | undefined;
592
+ try {
593
+ result = snapshotEvent(terminal.result);
594
+ const snapshot: EventActorAppliedSnapshot<TResult> = {
595
+ status: 'snapshot_ready',
596
+ result,
597
+ checkpoint: snapshotCheckpointFork(terminal.checkpoint),
598
+ invocation: freezeInvocationReference(invocation),
599
+ };
600
+ validateTerminalCheckpoint(snapshot.invocation, snapshot.checkpoint);
601
+ return snapshot;
602
+ } catch (error) {
603
+ return createIndeterminateResult(invocation, error, result);
604
+ }
605
+ }
606
+
607
+ function isAborted(signal?: AbortSignal): boolean {
608
+ return signal?.aborted === true;
609
+ }
610
+
611
+ class EventActorPreparationCancelledError extends Error {
612
+ constructor(
613
+ readonly continuation: 'warm' | 'cold',
614
+ reason: unknown
615
+ ) {
616
+ super(`Event actor ${continuation} preparation was cancelled`, {
617
+ cause: reason,
618
+ });
619
+ this.name = 'EventActorPreparationCancelledError';
620
+ }
621
+ }
622
+
623
+ function resolveExecutionDepth(
624
+ requestedDepth: number | undefined,
625
+ ambientConfig: RunnableConfig | undefined
626
+ ): number {
627
+ const ambientDepth = ambientConfig?.configurable?.event_actor_depth;
628
+ if (ambientDepth == null) {
629
+ return requestedDepth ?? 1;
630
+ }
631
+ if (!Number.isSafeInteger(ambientDepth) || Number(ambientDepth) < 1) {
632
+ throw new Error('Ambient event actor depth is invalid');
633
+ }
634
+ const nestedDepth = Number(ambientDepth) + 1;
635
+ if (requestedDepth != null && requestedDepth !== nestedDepth) {
636
+ throw new Error(
637
+ `Nested event actor depth ${requestedDepth} must advance parent depth ${ambientDepth}`
638
+ );
639
+ }
640
+ return nestedDepth;
641
+ }
642
+
643
+ function validateCommittedHead(
644
+ invocation: EventActorInvocationReference,
645
+ checkpoint: EventActorCheckpointFork,
646
+ head: EventActorInvocationReference['base']
647
+ ): void {
648
+ validateHead(head, invocation.actorThreadId, true);
649
+ if (
650
+ head.generation !== invocation.base.generation + 1 ||
651
+ head.checkpoint?.threadId !== checkpoint.threadId ||
652
+ head.checkpoint.checkpointNs !== checkpoint.checkpointNs ||
653
+ head.checkpoint.checkpointId !== checkpoint.checkpointId
654
+ ) {
655
+ throw new Error('Event actor commit returned an invalid logical head');
656
+ }
657
+ }
658
+
659
+ /**
660
+ * Runs one event against an isolated checkpoint fork and advances the stable
661
+ * actor head only through the host's atomic commit interface.
662
+ */
663
+ export class EventActorExecutor<
664
+ TEvent extends EventActorEvent,
665
+ TResult extends EventActorEvent,
666
+ > {
667
+ readonly #adapter: EventActorHostAdapter<TEvent, TResult>;
668
+ readonly #maxDepth: number;
669
+ readonly #dormantCheckpointTtlMs: number;
670
+ readonly #preparationSigningKey: Uint8Array;
671
+ readonly #issuedSettlements = new WeakSet<object>();
672
+ readonly #preparationPhases = new Map<string, EventActorPreparationPhase>();
673
+ #nextPhaseExpiry = Number.POSITIVE_INFINITY;
674
+
675
+ constructor(
676
+ adapter: EventActorHostAdapter<TEvent, TResult>,
677
+ options: EventActorExecutorOptions = {}
678
+ ) {
679
+ this.#adapter = adapter;
680
+ this.#maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
681
+ this.#dormantCheckpointTtlMs =
682
+ options.dormantCheckpointTtlMs ?? DEFAULT_DORMANT_CHECKPOINT_TTL_MS;
683
+ const signingKey = Buffer.from(
684
+ options.preparationSigningKey ?? randomBytes(32)
685
+ );
686
+ if (signingKey.byteLength < 32) {
687
+ throw new Error('preparationSigningKey must contain at least 32 bytes');
688
+ }
689
+ this.#preparationSigningKey = signingKey;
690
+ if (!Number.isSafeInteger(this.#maxDepth) || this.#maxDepth < 1) {
691
+ throw new Error('maxDepth must be a positive safe integer');
692
+ }
693
+ if (
694
+ !Number.isSafeInteger(this.#dormantCheckpointTtlMs) ||
695
+ this.#dormantCheckpointTtlMs < 1
696
+ ) {
697
+ throw new Error('dormantCheckpointTtlMs must be a positive safe integer');
698
+ }
699
+ }
700
+
701
+ #signPreparation(payload: string): string {
702
+ return createHmac('sha256', this.#preparationSigningKey)
703
+ .update(payload)
704
+ .digest('hex');
705
+ }
706
+
707
+ #preparationSignatureMatches(signature: string, payload: string): boolean {
708
+ if (!/^[a-f0-9]{64}$/.test(signature)) {
709
+ return false;
710
+ }
711
+ return timingSafeEqual(
712
+ Buffer.from(signature, 'hex'),
713
+ Buffer.from(this.#signPreparation(payload), 'hex')
714
+ );
715
+ }
716
+
717
+ #createPreparedInvocation(
718
+ invocation: EventActorInvocation<TEvent>
719
+ ): EventActorPreparedInvocation<TEvent> {
720
+ const trustedInvocation = freezeInvocation(invocation);
721
+ const payload = serializeInvocationPreparation(trustedInvocation);
722
+ return Object.freeze({
723
+ ...trustedInvocation,
724
+ preparationDigest: this.#createTimedPreparationDigest(payload),
725
+ });
726
+ }
727
+
728
+ #createTimedPreparationDigest(payload: string): string {
729
+ const expiresAt = Math.min(
730
+ Number.MAX_SAFE_INTEGER,
731
+ Date.now() + this.#dormantCheckpointTtlMs
732
+ );
733
+ return `${expiresAt}.${this.#signPreparation(`${expiresAt}\0${payload}`)}`;
734
+ }
735
+
736
+ #validateTimedPreparationDigest(
737
+ preparationDigest: string,
738
+ payload: string,
739
+ subject: 'prepared invocation' | 'unavailable preparation',
740
+ allowExpired = false
741
+ ): number {
742
+ requireNonEmpty(preparationDigest, 'preparationDigest');
743
+ const match = /^(\d+)\.([a-f0-9]{64})$/.exec(preparationDigest);
744
+ const expiresAt = Number(match?.[1]);
745
+ if (
746
+ match == null ||
747
+ !Number.isSafeInteger(expiresAt) ||
748
+ expiresAt < 1 ||
749
+ !this.#preparationSignatureMatches(match[2], `${expiresAt}\0${payload}`)
750
+ ) {
751
+ throw new Error(`Event actor ${subject} binding is invalid`);
752
+ }
753
+ if (!allowExpired && expiresAt <= Date.now()) {
754
+ throw new Error(`Event actor ${subject} binding has expired`);
755
+ }
756
+ return expiresAt;
757
+ }
758
+
759
+ #validatePreparedInvocation(
760
+ invocation: EventActorPreparedInvocation<TEvent>,
761
+ allowExpired = false
762
+ ): number {
763
+ return this.#validateTimedPreparationDigest(
764
+ invocation.preparationDigest,
765
+ serializeInvocationPreparation(invocation),
766
+ 'prepared invocation',
767
+ allowExpired
768
+ );
769
+ }
770
+
771
+ #prunePreparationPhases(now = Date.now()): void {
772
+ this.#nextPhaseExpiry = Number.POSITIVE_INFINITY;
773
+ for (const [digest, phase] of this.#preparationPhases) {
774
+ if ('expiresAt' in phase && phase.expiresAt <= now) {
775
+ this.#preparationPhases.delete(digest);
776
+ } else if ('expiresAt' in phase) {
777
+ this.#nextPhaseExpiry = Math.min(
778
+ this.#nextPhaseExpiry,
779
+ phase.expiresAt
780
+ );
781
+ }
782
+ }
783
+ }
784
+
785
+ #getPreparationPhase(
786
+ preparationDigest: string
787
+ ): EventActorPreparationPhase | undefined {
788
+ if (Date.now() >= this.#nextPhaseExpiry) {
789
+ this.#prunePreparationPhases();
790
+ }
791
+ return this.#preparationPhases.get(preparationDigest);
792
+ }
793
+
794
+ #setTerminalPreparationPhase(
795
+ preparationDigest: string,
796
+ status: 'discardable' | 'retained' | 'discarded',
797
+ authorityExpiresAt: number
798
+ ): void {
799
+ const now = Date.now();
800
+ if (now >= this.#nextPhaseExpiry) {
801
+ this.#prunePreparationPhases(now);
802
+ }
803
+ const phase = {
804
+ status,
805
+ expiresAt: Math.max(
806
+ authorityExpiresAt,
807
+ Math.min(Number.MAX_SAFE_INTEGER, now + this.#dormantCheckpointTtlMs)
808
+ ),
809
+ } as const;
810
+ this.#preparationPhases.set(preparationDigest, phase);
811
+ this.#nextPhaseExpiry = Math.min(this.#nextPhaseExpiry, phase.expiresAt);
812
+ }
813
+
814
+ async prepare(
815
+ request: EventActorPrepareRequest<TEvent>,
816
+ signal?: AbortSignal
817
+ ): Promise<EventActorPreparation<TEvent>> {
818
+ const trustedRequest = snapshotPrepareRequest(request);
819
+ resolveExecutionDepth(
820
+ trustedRequest.depth,
821
+ AsyncLocalStorageProviderSingleton.getRunnableConfig()
822
+ );
823
+ this.#validatePrepareRequest(trustedRequest);
824
+ const checkpointNs = createInvocationCheckpointNs(trustedRequest);
825
+ const adapterRequest: EventActorAdapterPrepareRequest<TEvent> = {
826
+ ...snapshotPrepareRequest(trustedRequest),
827
+ checkpointNs,
828
+ };
829
+ const controller = new AbortController();
830
+ const abort = (): void => controller.abort(signal?.reason);
831
+ if (isAborted(signal)) {
832
+ abort();
833
+ } else {
834
+ signal?.addEventListener('abort', abort, { once: true });
835
+ }
836
+ if (isAborted(controller.signal)) {
837
+ signal?.removeEventListener('abort', abort);
838
+ throw new EventActorPreparationCancelledError(
839
+ 'warm',
840
+ controller.signal.reason
841
+ );
842
+ }
843
+ let preparation;
844
+ try {
845
+ preparation = await this.#adapter.prepare(
846
+ { ...adapterRequest },
847
+ { signal: controller.signal }
848
+ );
849
+ } catch (error) {
850
+ if (isAborted(controller.signal) && error === controller.signal.reason) {
851
+ throw new EventActorPreparationCancelledError('warm', error);
852
+ }
853
+ throw error;
854
+ } finally {
855
+ signal?.removeEventListener('abort', abort);
856
+ }
857
+ const preparationStatus: unknown = preparation.status;
858
+ if (preparationStatus === 'ready') {
859
+ const readyPreparation = preparation as Extract<
860
+ EventActorAdapterPreparation<TEvent>,
861
+ { status: 'ready' }
862
+ >;
863
+ const adapterInvocation = snapshotInvocation(readyPreparation.invocation);
864
+ validateInvocation(
865
+ trustedRequest,
866
+ adapterInvocation,
867
+ 'warm',
868
+ checkpointNs,
869
+ this.#maxDepth
870
+ );
871
+ const preparedInvocation = this.#createPreparedInvocation({
872
+ ...adapterInvocation,
873
+ event: snapshotEvent(trustedRequest.event),
874
+ });
875
+ if (isAborted(controller.signal)) {
876
+ await this.#discardInvocationReference(
877
+ snapshotInvocationReference(preparedInvocation),
878
+ 'cancelled'
879
+ );
880
+ throw new EventActorPreparationCancelledError(
881
+ 'warm',
882
+ controller.signal.reason
883
+ );
884
+ }
885
+ return Object.freeze({
886
+ status: 'ready',
887
+ invocation: preparedInvocation,
888
+ });
889
+ } else {
890
+ if (preparationStatus !== 'checkpoint_unavailable') {
891
+ throw new Error('Event actor preparation returned an invalid status');
892
+ }
893
+ const unavailablePreparation = preparation as Extract<
894
+ EventActorAdapterPreparation<TEvent>,
895
+ { status: 'checkpoint_unavailable' }
896
+ >;
897
+ const preparedHead = freezeHead(unavailablePreparation.head);
898
+ validateHead(preparedHead, trustedRequest.actorThreadId);
899
+ if (isAborted(controller.signal)) {
900
+ throw new EventActorPreparationCancelledError(
901
+ 'warm',
902
+ controller.signal.reason
903
+ );
904
+ }
905
+ const preparedRequest = freezePrepareRequest(trustedRequest);
906
+ return Object.freeze({
907
+ status: 'checkpoint_unavailable',
908
+ request: preparedRequest,
909
+ head: preparedHead,
910
+ preparationDigest: this.#createTimedPreparationDigest(
911
+ serializeUnavailablePreparation(preparedRequest, preparedHead)
912
+ ),
913
+ });
914
+ }
915
+ }
916
+
917
+ async coldContinue(
918
+ preparation: Extract<
919
+ EventActorPreparation<TEvent>,
920
+ { status: 'checkpoint_unavailable' }
921
+ >,
922
+ signal?: AbortSignal
923
+ ): Promise<EventActorPreparedInvocation<TEvent>> {
924
+ const request = snapshotPrepareRequest(preparation.request);
925
+ const trustedHead = snapshotHead(preparation.head);
926
+ const preparationDigest = preparation.preparationDigest;
927
+ const authorityExpiresAt = this.#validateTimedPreparationDigest(
928
+ preparationDigest,
929
+ serializeUnavailablePreparation(request, trustedHead),
930
+ 'unavailable preparation'
931
+ );
932
+ resolveExecutionDepth(
933
+ request.depth,
934
+ AsyncLocalStorageProviderSingleton.getRunnableConfig()
935
+ );
936
+ this.#validatePrepareRequest(request);
937
+ validateHead(trustedHead, request.actorThreadId);
938
+ const checkpointNs = createInvocationCheckpointNs(request);
939
+ const adapterRequest: EventActorAdapterPrepareRequest<TEvent> = {
940
+ ...snapshotPrepareRequest(request),
941
+ checkpointNs,
942
+ };
943
+ const controller = new AbortController();
944
+ const abort = (): void => controller.abort(signal?.reason);
945
+ if (isAborted(signal)) {
946
+ abort();
947
+ } else {
948
+ signal?.addEventListener('abort', abort, { once: true });
949
+ }
950
+ if (isAborted(controller.signal)) {
951
+ signal?.removeEventListener('abort', abort);
952
+ throw new EventActorPreparationCancelledError(
953
+ 'cold',
954
+ controller.signal.reason
955
+ );
956
+ }
957
+ if (this.#getPreparationPhase(preparationDigest) != null) {
958
+ signal?.removeEventListener('abort', abort);
959
+ throw new Error(
960
+ 'Event actor unavailable preparation was already consumed'
961
+ );
962
+ }
963
+ this.#preparationPhases.set(preparationDigest, { status: 'invoking' });
964
+ let invocation;
965
+ try {
966
+ invocation = await this.#adapter.coldContinue(
967
+ { ...adapterRequest },
968
+ snapshotHead(trustedHead),
969
+ { signal: controller.signal }
970
+ );
971
+ } catch (error) {
972
+ this.#setTerminalPreparationPhase(
973
+ preparationDigest,
974
+ 'discarded',
975
+ authorityExpiresAt
976
+ );
977
+ if (isAborted(controller.signal) && error === controller.signal.reason) {
978
+ throw new EventActorPreparationCancelledError('cold', error);
979
+ }
980
+ throw error;
981
+ } finally {
982
+ signal?.removeEventListener('abort', abort);
983
+ }
984
+ this.#setTerminalPreparationPhase(
985
+ preparationDigest,
986
+ 'retained',
987
+ authorityExpiresAt
988
+ );
989
+ const adapterInvocation = snapshotInvocation(invocation);
990
+ validateInvocation(
991
+ request,
992
+ adapterInvocation,
993
+ 'cold',
994
+ checkpointNs,
995
+ this.#maxDepth,
996
+ trustedHead
997
+ );
998
+ const trustedInvocation: EventActorInvocation<TEvent> = {
999
+ ...adapterInvocation,
1000
+ event: snapshotEvent(request.event),
1001
+ };
1002
+ if (isAborted(controller.signal)) {
1003
+ await this.#discardInvocationReference(
1004
+ snapshotInvocationReference(trustedInvocation),
1005
+ 'cancelled'
1006
+ );
1007
+ this.#setTerminalPreparationPhase(
1008
+ preparationDigest,
1009
+ 'discarded',
1010
+ authorityExpiresAt
1011
+ );
1012
+ throw new EventActorPreparationCancelledError(
1013
+ 'cold',
1014
+ controller.signal.reason
1015
+ );
1016
+ }
1017
+ const preparedInvocation =
1018
+ this.#createPreparedInvocation(trustedInvocation);
1019
+ this.#setTerminalPreparationPhase(
1020
+ preparationDigest,
1021
+ 'discarded',
1022
+ authorityExpiresAt
1023
+ );
1024
+ return preparedInvocation;
1025
+ }
1026
+
1027
+ async invoke(
1028
+ invocation: EventActorPreparedInvocation<TEvent>,
1029
+ signal?: AbortSignal
1030
+ ): Promise<EventActorInvocationResult<TResult>> {
1031
+ const trustedInvocation = snapshotPreparedInvocation(invocation);
1032
+ const authorityExpiresAt =
1033
+ this.#validatePreparedInvocation(trustedInvocation);
1034
+ const preparationDigest = trustedInvocation.preparationDigest;
1035
+ if (this.#getPreparationPhase(preparationDigest) != null) {
1036
+ throw new Error('Event actor prepared invocation was already consumed');
1037
+ }
1038
+ this.#preparationPhases.set(preparationDigest, { status: 'invoking' });
1039
+ const settlementInvocation = snapshotInvocationReference(trustedInvocation);
1040
+ let terminal: EventActorTerminalResult<TResult>;
1041
+ try {
1042
+ terminal = await this.#invokeWithConfig(
1043
+ snapshotInvocation(trustedInvocation),
1044
+ signal,
1045
+ AsyncLocalStorageProviderSingleton.getRunnableConfig()
1046
+ );
1047
+ } catch (error) {
1048
+ if (isGraphInterrupt(error) || isParentCommand(error)) {
1049
+ this.#setTerminalPreparationPhase(
1050
+ preparationDigest,
1051
+ 'retained',
1052
+ authorityExpiresAt
1053
+ );
1054
+ throw error;
1055
+ }
1056
+ this.#setTerminalPreparationPhase(
1057
+ preparationDigest,
1058
+ 'discardable',
1059
+ authorityExpiresAt
1060
+ );
1061
+ await this.discard(
1062
+ trustedInvocation,
1063
+ isAborted(signal) ? 'cancelled' : 'failed'
1064
+ );
1065
+ throw error;
1066
+ }
1067
+ this.#setTerminalPreparationPhase(
1068
+ preparationDigest,
1069
+ 'retained',
1070
+ authorityExpiresAt
1071
+ );
1072
+ let status: unknown;
1073
+ try {
1074
+ status = terminal.status;
1075
+ } catch (error) {
1076
+ return createIndeterminateResult(settlementInvocation, error);
1077
+ }
1078
+ if (status === 'applied') {
1079
+ const snapshot = snapshotAppliedTerminal<TResult>(
1080
+ settlementInvocation,
1081
+ terminal as Extract<
1082
+ EventActorTerminalResult<TResult>,
1083
+ { status: 'applied' }
1084
+ >
1085
+ );
1086
+ return snapshot.status === 'snapshot_ready'
1087
+ ? this.#issueSettlement(snapshot)
1088
+ : snapshot;
1089
+ }
1090
+ if (status !== 'completed_no_action') {
1091
+ return createIndeterminateResult(
1092
+ settlementInvocation,
1093
+ new Error('Event actor invocation returned an invalid status')
1094
+ );
1095
+ }
1096
+ let completed: Extract<
1097
+ EventActorTerminalResult<TResult>,
1098
+ { status: 'completed_no_action' }
1099
+ >;
1100
+ try {
1101
+ completed = Object.freeze({
1102
+ status: 'completed_no_action' as const,
1103
+ ...(terminal.result === undefined
1104
+ ? {}
1105
+ : { result: snapshotEvent(terminal.result) }),
1106
+ });
1107
+ } catch (error) {
1108
+ this.#setTerminalPreparationPhase(
1109
+ preparationDigest,
1110
+ 'discardable',
1111
+ authorityExpiresAt
1112
+ );
1113
+ await this.discard(trustedInvocation, 'completed_no_action');
1114
+ throw error;
1115
+ }
1116
+ this.#setTerminalPreparationPhase(
1117
+ preparationDigest,
1118
+ 'discardable',
1119
+ authorityExpiresAt
1120
+ );
1121
+ await this.discard(trustedInvocation, 'completed_no_action');
1122
+ return completed;
1123
+ }
1124
+
1125
+ #issueSettlement(
1126
+ snapshot: EventActorAppliedSnapshot<TResult>
1127
+ ): EventActorAppliedResult<TResult> {
1128
+ const settlement = Object.freeze({
1129
+ status: 'applied' as const,
1130
+ result: snapshot.result,
1131
+ checkpoint: Object.freeze(snapshotCheckpointFork(snapshot.checkpoint)),
1132
+ invocation: freezeInvocationReference(snapshot.invocation),
1133
+ });
1134
+ this.#issuedSettlements.add(settlement);
1135
+ return settlement;
1136
+ }
1137
+
1138
+ async #invokeWithConfig(
1139
+ invocation: EventActorInvocation<TEvent>,
1140
+ signal: AbortSignal | undefined,
1141
+ ambientConfig: RunnableConfig | undefined
1142
+ ): Promise<EventActorTerminalResult<TResult>> {
1143
+ resolveExecutionDepth(invocation.depth, ambientConfig);
1144
+ validateInvocation(
1145
+ {
1146
+ actorThreadId: invocation.actorThreadId,
1147
+ invocationId: invocation.invocationId,
1148
+ depth: invocation.depth,
1149
+ event: invocation.event,
1150
+ },
1151
+ invocation,
1152
+ invocation.continuation,
1153
+ invocation.fork.checkpointNs,
1154
+ this.#maxDepth
1155
+ );
1156
+ const controller = new AbortController();
1157
+ const abort = (): void => controller.abort(signal?.reason);
1158
+ if (isAborted(signal)) {
1159
+ abort();
1160
+ } else {
1161
+ signal?.addEventListener('abort', abort, { once: true });
1162
+ }
1163
+ const config = createRunnableConfig(
1164
+ invocation,
1165
+ controller.signal,
1166
+ ambientConfig
1167
+ );
1168
+ try {
1169
+ if (controller.signal.aborted) {
1170
+ throw asError(controller.signal.reason ?? 'Event actor cancelled');
1171
+ }
1172
+ return await AsyncLocalStorageProviderSingleton.runWithConfig(
1173
+ config,
1174
+ () =>
1175
+ this.#adapter.invoke(invocation, {
1176
+ signal: controller.signal,
1177
+ config,
1178
+ })
1179
+ );
1180
+ } finally {
1181
+ signal?.removeEventListener('abort', abort);
1182
+ }
1183
+ }
1184
+
1185
+ async commit(
1186
+ settlement: EventActorAppliedResult<TResult>
1187
+ ): Promise<EventActorSettlementResult<TResult>> {
1188
+ if (!this.#issuedSettlements.has(settlement)) {
1189
+ throw new Error('Event actor settlement was not issued by this executor');
1190
+ }
1191
+ const trustedInvocation = snapshotInvocationReference(
1192
+ settlement.invocation
1193
+ );
1194
+ validateInvocationReference(trustedInvocation, this.#maxDepth);
1195
+ const trustedCheckpoint = snapshotCheckpointFork(settlement.checkpoint);
1196
+ validateTerminalCheckpoint(trustedInvocation, trustedCheckpoint);
1197
+ this.#issuedSettlements.delete(settlement);
1198
+ try {
1199
+ const committed = await this.#adapter.commit({
1200
+ invocation: snapshotInvocationReference(trustedInvocation),
1201
+ expectedHead: snapshotHead(trustedInvocation.base),
1202
+ checkpoint: { ...trustedCheckpoint },
1203
+ result: settlement.result,
1204
+ retention: {
1205
+ committedCheckpoints: 2,
1206
+ dormantCheckpointTtlMs: this.#dormantCheckpointTtlMs,
1207
+ },
1208
+ });
1209
+ const status: unknown = committed.status;
1210
+ if (status === 'committed') {
1211
+ const committedHead = snapshotHead(
1212
+ (committed as { status: 'committed'; head: EventActorHead }).head
1213
+ );
1214
+ validateCommittedHead(
1215
+ trustedInvocation,
1216
+ trustedCheckpoint,
1217
+ committedHead
1218
+ );
1219
+ return { status: 'committed', head: committedHead };
1220
+ }
1221
+ if (status !== 'stale') {
1222
+ throw new Error('Event actor commit returned an invalid status');
1223
+ }
1224
+ const staleHead = committed.head;
1225
+ if (staleHead != null) {
1226
+ const committedHead = snapshotHead(staleHead);
1227
+ validateHead(committedHead, trustedInvocation.actorThreadId);
1228
+ if (committedHead.generation <= trustedInvocation.base.generation) {
1229
+ throw new Error(
1230
+ 'Stale event actor head did not advance past its base'
1231
+ );
1232
+ }
1233
+ if (
1234
+ trustedInvocation.base.checkpoint != null &&
1235
+ committedHead.checkpoint?.threadId !==
1236
+ trustedInvocation.base.checkpoint.threadId
1237
+ ) {
1238
+ throw new Error(
1239
+ 'Stale event actor head changed its checkpoint thread'
1240
+ );
1241
+ }
1242
+ if (
1243
+ checkpointIdsMatch(
1244
+ committedHead.checkpoint,
1245
+ trustedInvocation.base.checkpoint
1246
+ ) ||
1247
+ checkpointIdsMatch(
1248
+ committedHead.checkpoint,
1249
+ trustedInvocation.fork
1250
+ ) ||
1251
+ checkpointsMatch(committedHead.checkpoint, trustedCheckpoint)
1252
+ ) {
1253
+ throw new Error(
1254
+ 'Stale event actor head does not identify a competing checkpoint'
1255
+ );
1256
+ }
1257
+ return { status: 'stale', head: committedHead };
1258
+ }
1259
+ return { status: 'stale' };
1260
+ } catch (error) {
1261
+ return createSettlementIndeterminateResult(settlement, error);
1262
+ }
1263
+ }
1264
+
1265
+ async discard(
1266
+ invocation: EventActorPreparedInvocation<TEvent>,
1267
+ reason: EventActorDiscardReason
1268
+ ): Promise<void> {
1269
+ const discardReason: unknown = reason;
1270
+ if (
1271
+ discardReason !== 'cancelled' &&
1272
+ discardReason !== 'completed_no_action' &&
1273
+ discardReason !== 'failed'
1274
+ ) {
1275
+ throw new Error('Event actor discard reason is invalid');
1276
+ }
1277
+ const trustedInvocation = snapshotPreparedInvocation(invocation);
1278
+ const expiresAt = this.#validatePreparedInvocation(trustedInvocation, true);
1279
+ const preparationDigest = trustedInvocation.preparationDigest;
1280
+ const previousPhase = this.#getPreparationPhase(preparationDigest);
1281
+ if (expiresAt <= Date.now() && previousPhase == null) {
1282
+ throw new Error('Event actor prepared invocation binding has expired');
1283
+ }
1284
+ if (previousPhase != null && previousPhase.status !== 'discardable') {
1285
+ throw new Error(
1286
+ 'Event actor prepared invocation is no longer discardable'
1287
+ );
1288
+ }
1289
+ this.#preparationPhases.set(preparationDigest, { status: 'discarding' });
1290
+ try {
1291
+ await this.#discardInvocationReference(trustedInvocation, reason);
1292
+ this.#setTerminalPreparationPhase(
1293
+ preparationDigest,
1294
+ 'discarded',
1295
+ expiresAt
1296
+ );
1297
+ } catch (error) {
1298
+ if (previousPhase == null) {
1299
+ this.#preparationPhases.delete(preparationDigest);
1300
+ } else {
1301
+ this.#preparationPhases.set(preparationDigest, previousPhase);
1302
+ if ('expiresAt' in previousPhase) {
1303
+ this.#nextPhaseExpiry = Math.min(
1304
+ this.#nextPhaseExpiry,
1305
+ previousPhase.expiresAt
1306
+ );
1307
+ }
1308
+ }
1309
+ throw error;
1310
+ }
1311
+ }
1312
+
1313
+ #discardInvocationReference(
1314
+ invocation: EventActorInvocationReference,
1315
+ reason: EventActorDiscardReason
1316
+ ): Promise<void> {
1317
+ const trustedInvocation = snapshotInvocationReference(invocation);
1318
+ validateInvocationReference(trustedInvocation, this.#maxDepth);
1319
+ return this.#adapter.discard({
1320
+ invocation: trustedInvocation,
1321
+ reason,
1322
+ });
1323
+ }
1324
+
1325
+ #validatePrepareRequest(request: EventActorPrepareRequest<TEvent>): void {
1326
+ requireNonEmpty(request.actorThreadId, 'actorThreadId');
1327
+ requireNonEmpty(request.invocationId, 'invocationId');
1328
+ if (
1329
+ !Number.isSafeInteger(request.depth) ||
1330
+ request.depth < 1 ||
1331
+ request.depth > this.#maxDepth
1332
+ ) {
1333
+ throw new Error(
1334
+ `Event actor depth ${request.depth} exceeds maximum ${this.#maxDepth}`
1335
+ );
1336
+ }
1337
+ }
1338
+
1339
+ async execute(
1340
+ request: EventActorExecutionRequest<TEvent>
1341
+ ): Promise<EventActorExecutionResult<TResult>> {
1342
+ const trustedRequest: EventActorExecutionRequest<TEvent> = {
1343
+ actorThreadId: request.actorThreadId,
1344
+ invocationId: request.invocationId,
1345
+ event: snapshotEvent(request.event),
1346
+ ...(request.depth == null ? {} : { depth: request.depth }),
1347
+ ...(request.signal == null ? {} : { signal: request.signal }),
1348
+ };
1349
+ const ambientConfig = snapshotAmbientConfig(
1350
+ AsyncLocalStorageProviderSingleton.getRunnableConfig()
1351
+ );
1352
+ const depth = resolveExecutionDepth(trustedRequest.depth, ambientConfig);
1353
+ const prepareRequest: EventActorPrepareRequest<TEvent> = {
1354
+ actorThreadId: trustedRequest.actorThreadId,
1355
+ invocationId: trustedRequest.invocationId,
1356
+ depth,
1357
+ event: trustedRequest.event,
1358
+ };
1359
+ let preparation;
1360
+ try {
1361
+ preparation = await this.prepare(prepareRequest, trustedRequest.signal);
1362
+ } catch (error) {
1363
+ if (error instanceof EventActorPreparationCancelledError) {
1364
+ return { status: 'cancelled', continuation: error.continuation };
1365
+ }
1366
+ throw error;
1367
+ }
1368
+ if (
1369
+ preparation.status === 'checkpoint_unavailable' &&
1370
+ isAborted(trustedRequest.signal)
1371
+ ) {
1372
+ return { status: 'cancelled', continuation: 'cold' };
1373
+ }
1374
+ let invocation;
1375
+ try {
1376
+ invocation =
1377
+ preparation.status === 'ready'
1378
+ ? preparation.invocation
1379
+ : await this.coldContinue(preparation, trustedRequest.signal);
1380
+ } catch (error) {
1381
+ if (error instanceof EventActorPreparationCancelledError) {
1382
+ return { status: 'cancelled', continuation: 'cold' };
1383
+ }
1384
+ throw error;
1385
+ }
1386
+ const continuation = preparation.status === 'ready' ? 'warm' : 'cold';
1387
+ const invocationReference = snapshotInvocationReference(invocation);
1388
+ const invocationForAdapter = snapshotInvocation(invocation);
1389
+ if (isAborted(trustedRequest.signal)) {
1390
+ await this.#discardInvocationReference(invocationReference, 'cancelled');
1391
+ return { status: 'cancelled', continuation };
1392
+ }
1393
+ let terminal;
1394
+ try {
1395
+ terminal = await this.#invokeWithConfig(
1396
+ invocationForAdapter,
1397
+ trustedRequest.signal,
1398
+ ambientConfig
1399
+ );
1400
+ } catch (error) {
1401
+ if (isGraphInterrupt(error) || isParentCommand(error)) {
1402
+ throw error;
1403
+ }
1404
+ const reason = isAborted(trustedRequest.signal) ? 'cancelled' : 'failed';
1405
+ await this.#discardInvocationReference(invocationReference, reason);
1406
+ if (reason === 'cancelled') {
1407
+ return { status: 'cancelled', continuation };
1408
+ }
1409
+ return { status: 'failed', error: asError(error), continuation };
1410
+ }
1411
+ let terminalStatus: unknown;
1412
+ try {
1413
+ terminalStatus = terminal.status;
1414
+ } catch (error) {
1415
+ return {
1416
+ ...createIndeterminateResult(invocationReference, error),
1417
+ continuation,
1418
+ };
1419
+ }
1420
+ if (terminalStatus === 'completed_no_action') {
1421
+ let result: TResult | undefined;
1422
+ try {
1423
+ result =
1424
+ terminal.result === undefined
1425
+ ? undefined
1426
+ : snapshotEvent(terminal.result);
1427
+ } catch (error) {
1428
+ await this.#discardInvocationReference(
1429
+ invocationReference,
1430
+ 'completed_no_action'
1431
+ );
1432
+ return { status: 'failed', error: asError(error), continuation };
1433
+ }
1434
+ await this.#discardInvocationReference(
1435
+ invocationReference,
1436
+ 'completed_no_action'
1437
+ );
1438
+ return {
1439
+ status: 'completed_no_action',
1440
+ ...(result === undefined ? {} : { result }),
1441
+ continuation,
1442
+ };
1443
+ }
1444
+ if (terminalStatus !== 'applied') {
1445
+ return {
1446
+ ...createIndeterminateResult(
1447
+ invocationReference,
1448
+ new Error('Event actor invocation returned an invalid status')
1449
+ ),
1450
+ continuation,
1451
+ };
1452
+ }
1453
+ const appliedSnapshot = snapshotAppliedTerminal<TResult>(
1454
+ invocationReference,
1455
+ terminal as Extract<
1456
+ EventActorTerminalResult<TResult>,
1457
+ { status: 'applied' }
1458
+ >
1459
+ );
1460
+ if (appliedSnapshot.status === 'commit_indeterminate') {
1461
+ return { ...appliedSnapshot, continuation };
1462
+ }
1463
+ const trustedTerminal = this.#issueSettlement(appliedSnapshot);
1464
+ const committed = await this.commit(trustedTerminal);
1465
+ if (committed.status === 'commit_indeterminate') {
1466
+ return {
1467
+ ...committed,
1468
+ continuation,
1469
+ };
1470
+ }
1471
+ if (committed.status === 'stale') {
1472
+ return {
1473
+ status: 'commit_conflict',
1474
+ result: trustedTerminal.result,
1475
+ checkpoint: { ...trustedTerminal.checkpoint },
1476
+ ...(committed.head == null
1477
+ ? {}
1478
+ : { head: snapshotHead(committed.head) }),
1479
+ continuation,
1480
+ };
1481
+ }
1482
+ return {
1483
+ status: 'applied',
1484
+ result: trustedTerminal.result,
1485
+ head: committed.head,
1486
+ continuation,
1487
+ };
1488
+ }
1489
+ }
1490
+
1491
+ export function createEventActorExecutor<
1492
+ TEvent extends EventActorEvent,
1493
+ TResult extends EventActorEvent,
1494
+ >(
1495
+ adapter: EventActorHostAdapter<TEvent, TResult>,
1496
+ options: EventActorExecutorOptions = {}
1497
+ ): EventActorExecutor<TEvent, TResult> {
1498
+ return new EventActorExecutor(adapter, options);
1499
+ }