@ai-sdk/code-mode 1.0.5 → 1.0.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.
Files changed (40) hide show
  1. package/README.md +8 -5
  2. package/dist/approval-continuation.js +84 -0
  3. package/dist/approval-continuation.js.map +1 -0
  4. package/dist/approval.js +32 -0
  5. package/dist/approval.js.map +1 -0
  6. package/dist/continuation-capability.js +125 -0
  7. package/dist/continuation-capability.js.map +1 -0
  8. package/dist/direct-tool-call.js +5 -0
  9. package/dist/direct-tool-call.js.map +1 -0
  10. package/dist/errors.js +49 -0
  11. package/dist/errors.js.map +1 -1
  12. package/dist/host-interrupt.js +28 -0
  13. package/dist/host-interrupt.js.map +1 -0
  14. package/dist/index.d.ts +152 -1
  15. package/dist/index.js +31 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/interrupt-continuation.js +115 -0
  18. package/dist/interrupt-continuation.js.map +1 -0
  19. package/dist/runtime/guest-sources.js +91 -1
  20. package/dist/runtime/guest-sources.js.map +1 -1
  21. package/dist/runtime/manager.js +388 -8
  22. package/dist/runtime/manager.js.map +1 -1
  23. package/dist/runtime/worker-source.js +1 -1
  24. package/dist/tool-invocation.js +93 -16
  25. package/dist/tool-invocation.js.map +1 -1
  26. package/package.json +3 -3
  27. package/src/approval-continuation.ts +115 -0
  28. package/src/approval.ts +60 -0
  29. package/src/continuation-capability.ts +201 -0
  30. package/src/direct-tool-call.ts +6 -0
  31. package/src/errors.ts +65 -0
  32. package/src/host-interrupt.ts +49 -0
  33. package/src/index.ts +28 -0
  34. package/src/interrupt-continuation.ts +173 -0
  35. package/src/runtime/guest-sources.ts +92 -1
  36. package/src/runtime/manager.ts +473 -8
  37. package/src/runtime/protocol.ts +24 -2
  38. package/src/runtime/worker.ts +116 -6
  39. package/src/tool-invocation.ts +119 -18
  40. package/src/types.ts +139 -0
@@ -1,6 +1,17 @@
1
1
  import { AsyncResource } from 'node:async_hooks';
2
2
  import { Buffer } from 'node:buffer';
3
+ import { randomBytes } from 'node:crypto';
3
4
  import { Worker } from 'node:worker_threads';
5
+ import {
6
+ isCodeModeApprovalInterruptPayload,
7
+ normalizeApprovalResolution,
8
+ } from '../approval.js';
9
+ import {
10
+ resolveCodeModeContinuationSecurity,
11
+ signCodeModeContinuation,
12
+ verifyCodeModeContinuation,
13
+ type ResolvedCodeModeContinuationSecurity,
14
+ } from '../continuation-capability.js';
4
15
  import {
5
16
  CodeModeAbortedError,
6
17
  CodeModeBridgeLimitError,
@@ -8,6 +19,7 @@ import {
8
19
  CodeModeDetachedBridgeRequestError,
9
20
  CodeModeProtocolError,
10
21
  CodeModeTimeoutError,
22
+ CodeModeToolApprovalDeniedError,
11
23
  deserializeError,
12
24
  serializeBridgeErrorForGuest,
13
25
  } from '../errors.js';
@@ -15,6 +27,9 @@ import { invokeHostTool } from '../tool-invocation.js';
15
27
  import { normalizeOptions } from '../utils/options.js';
16
28
  import { assertSourceSize, transformSource } from '../utils/source-cache.js';
17
29
  import type {
30
+ CodeModeContinuationLedgerEntry,
31
+ CodeModeInterrupt,
32
+ CodeModeInterruptExecutionContext,
18
33
  CodeModeToolExecutionOptions,
19
34
  NormalizedCodeModeOptions,
20
35
  RunCodeModeInput,
@@ -22,7 +37,6 @@ import type {
22
37
  import { getMaxWorkers } from './max-workers.js';
23
38
  import type {
24
39
  MainToWorkerMessage,
25
- WorkerBridgeResponse,
26
40
  WorkerReadyMessage,
27
41
  WorkerResultMessage,
28
42
  WorkerToolRequest,
@@ -48,6 +62,9 @@ export async function runManagedCodeMode(
48
62
  input: RunCodeModeInput,
49
63
  ): Promise<unknown> {
50
64
  const normalizedOptions = normalizeOptions(input.options);
65
+ const continuationSecurity = resolveCodeModeContinuationSecurity(
66
+ input.options?.continuationSecurity,
67
+ );
51
68
  const maxWorkers = getMaxWorkers({
52
69
  memoryLimitBytes: normalizedOptions.memoryLimitBytes,
53
70
  activeWorkers: activeInvocations,
@@ -63,11 +80,19 @@ export async function runManagedCodeMode(
63
80
  }
64
81
 
65
82
  assertSourceSize(input.js, normalizedOptions.maxSourceBytes);
83
+ const js = transformSource(input.js);
84
+ assertContinuationInput({
85
+ js,
86
+ continuation: input.continuation,
87
+ interruptResolution: input.interruptResolution,
88
+ continuationSecurity,
89
+ });
66
90
  const run = startWorkerRun({
67
91
  ...input,
68
- js: transformSource(input.js),
92
+ js,
69
93
  normalizedOptions,
70
94
  maxWorkers,
95
+ continuationSecurity,
71
96
  });
72
97
  return await run.result;
73
98
  } finally {
@@ -79,11 +104,16 @@ function startWorkerRun({
79
104
  js,
80
105
  tools,
81
106
  toolExecutionOptions,
107
+ options,
108
+ continuation,
109
+ interruptResolution,
82
110
  normalizedOptions,
83
111
  maxWorkers,
112
+ continuationSecurity,
84
113
  }: RunCodeModeInput & {
85
114
  normalizedOptions: NormalizedCodeModeOptions;
86
115
  maxWorkers: number;
116
+ continuationSecurity: ResolvedCodeModeContinuationSecurity;
87
117
  }): ManagedWorkerRun {
88
118
  const invocationId = `code-mode-${++invocationCounter}`;
89
119
  const pooledWorker = acquireWorker(maxWorkers);
@@ -99,12 +129,16 @@ function startWorkerRun({
99
129
 
100
130
  const outerAbortSignal = toolExecutionOptions?.abortSignal;
101
131
  const invocationAbortController = new AbortController();
132
+ let nestedToolCounter = getMaxNestedToolCounter(continuation?.ledger ?? []);
102
133
  const forwardedContext =
103
134
  toolExecutionOptions?.context ?? toolExecutionOptions?.experimental_context;
104
135
  const forwardedExperimentalContext =
105
136
  toolExecutionOptions?.experimental_context ?? toolExecutionOptions?.context;
106
137
  const baseExecutionOptions: CodeModeToolExecutionOptions = {
107
- toolCallId: toolExecutionOptions?.toolCallId ?? invocationId,
138
+ toolCallId:
139
+ toolExecutionOptions?.toolCallId ??
140
+ continuation?.outerToolCallId ??
141
+ invocationId,
108
142
  messages: toolExecutionOptions?.messages ?? [],
109
143
  abortSignal: invocationAbortController.signal,
110
144
  ...(forwardedExperimentalContext !== undefined
@@ -119,7 +153,13 @@ function startWorkerRun({
119
153
  let workerCleanedUp = false;
120
154
  let totalBridgeRequests = 0;
121
155
  let inFlightBridgeRequests = 0;
156
+ let interruptEntryIndex: number | undefined;
157
+ let interruptDrainCounter = 0;
158
+ let pendingInterruptDrainId: string | undefined;
159
+ let interruptResolutionConsumed = interruptResolution === undefined;
122
160
  const seenWorkerRequestIds = new Set<string>();
161
+ const bridgeLedger = cloneLedger(continuation?.ledger ?? []);
162
+ const determinism = continuation?.determinism ?? createDeterminismState();
123
163
 
124
164
  let resolveResult!: (value: unknown) => void;
125
165
  let rejectResult!: (reason?: unknown) => void;
@@ -219,6 +259,30 @@ function startWorkerRun({
219
259
  return;
220
260
  }
221
261
 
262
+ if (message.type === 'bridge-drained') {
263
+ if (
264
+ pendingInterruptDrainId === undefined ||
265
+ message.drainId !== pendingInterruptDrainId
266
+ ) {
267
+ failTerminal(
268
+ new CodeModeProtocolError(
269
+ `Worker sent unexpected bridge drain acknowledgement ${message.drainId}.`,
270
+ {
271
+ invocationId,
272
+ expectedDrainId: pendingInterruptDrainId,
273
+ receivedDrainId: message.drainId,
274
+ },
275
+ ),
276
+ );
277
+ return;
278
+ }
279
+ pendingInterruptDrainId = undefined;
280
+ if (inFlightBridgeRequests === 0) {
281
+ settleInterrupt();
282
+ }
283
+ return;
284
+ }
285
+
222
286
  const bridgeIndex = markWorkerRequest(message);
223
287
  if (bridgeIndex !== undefined) {
224
288
  void handleToolRequest(message, bridgeIndex);
@@ -251,6 +315,7 @@ function startWorkerRun({
251
315
  type: 'run',
252
316
  invocationId,
253
317
  js,
318
+ determinism,
254
319
  options: {
255
320
  timeoutMs: normalizedOptions.timeoutMs,
256
321
  memoryLimitBytes: normalizedOptions.memoryLimitBytes,
@@ -321,37 +386,286 @@ function startWorkerRun({
321
386
  message: WorkerToolRequest,
322
387
  bridgeIndex: number,
323
388
  ): Promise<void> {
389
+ const replayEntry = bridgeLedger[bridgeIndex - 1];
324
390
  try {
325
- const valueJson = await invokeHostTool({
391
+ if (replayEntry !== undefined) {
392
+ assertReplayEntryMatches(replayEntry, message, bridgeIndex);
393
+
394
+ if (replayEntry.status === 'fulfilled') {
395
+ postBridgeResponse({
396
+ type: 'bridge-response',
397
+ invocationId,
398
+ requestId: message.requestId,
399
+ success: true,
400
+ dateNowMs: replayEntry.dateNowMs,
401
+ valueJson: replayEntry.valueJson,
402
+ });
403
+ return;
404
+ }
405
+
406
+ if (replayEntry.status === 'rejected') {
407
+ postBridgeResponse({
408
+ type: 'bridge-response',
409
+ invocationId,
410
+ requestId: message.requestId,
411
+ success: false,
412
+ dateNowMs: replayEntry.dateNowMs,
413
+ error: replayEntry.error,
414
+ });
415
+ return;
416
+ }
417
+
418
+ if (interruptResolution?.interruptId !== replayEntry.interruptId) {
419
+ requestInterrupt(bridgeIndex - 1);
420
+ return;
421
+ }
422
+
423
+ interruptResolutionConsumed = true;
424
+ const isApproval = isCodeModeApprovalInterruptPayload(
425
+ replayEntry.interruptPayload,
426
+ );
427
+ if (isApproval) {
428
+ const decision = normalizeApprovalResolution(
429
+ interruptResolution.resolution,
430
+ );
431
+ if (!decision.approved) {
432
+ const error = new CodeModeToolApprovalDeniedError(
433
+ replayEntry.name,
434
+ fromJsonPayload(message.inputJson),
435
+ replayEntry.toolCallId,
436
+ decision.reason,
437
+ );
438
+ const dateNowMs = Date.now();
439
+ const guestError = serializeBridgeErrorForGuest(error, 'tool');
440
+ bridgeLedger[bridgeIndex - 1] = {
441
+ kind: 'tool',
442
+ name: message.toolName,
443
+ inputJson: message.inputJson,
444
+ toolCallId: replayEntry.toolCallId,
445
+ status: 'rejected',
446
+ dateNowMs,
447
+ error: guestError,
448
+ };
449
+ postBridgeResponse({
450
+ type: 'bridge-response',
451
+ invocationId,
452
+ requestId: message.requestId,
453
+ success: false,
454
+ dateNowMs,
455
+ error: guestError,
456
+ });
457
+ return;
458
+ }
459
+ }
460
+
461
+ const codeModeInterrupt: CodeModeInterruptExecutionContext | undefined =
462
+ isApproval
463
+ ? undefined
464
+ : {
465
+ interruptId: replayEntry.interruptId,
466
+ payload: replayEntry.interruptPayload,
467
+ resolution: interruptResolution.resolution,
468
+ };
469
+ const outcome = await invokeHostTool({
470
+ toolName: message.toolName,
471
+ inputJson: message.inputJson,
472
+ tools,
473
+ baseExecutionOptions,
474
+ codeModeOptions: options ?? {},
475
+ maxToolInputBytes: normalizedOptions.maxToolInputBytes,
476
+ maxToolOutputBytes: normalizedOptions.maxToolOutputBytes,
477
+ toolCallId: replayEntry.toolCallId,
478
+ ...(codeModeInterrupt !== undefined ? { codeModeInterrupt } : {}),
479
+ skipApproval: true,
480
+ });
481
+ if (outcome.type === 'interrupted') {
482
+ bridgeLedger[bridgeIndex - 1] = {
483
+ kind: 'tool',
484
+ name: message.toolName,
485
+ inputJson: message.inputJson,
486
+ toolCallId: replayEntry.toolCallId,
487
+ interruptId: `${replayEntry.toolCallId}:interrupt`,
488
+ interruptPayload: outcome.payload,
489
+ status: 'interrupted',
490
+ };
491
+ requestInterrupt(bridgeIndex - 1);
492
+ return;
493
+ }
494
+
495
+ const dateNowMs = Date.now();
496
+ bridgeLedger[bridgeIndex - 1] = {
497
+ kind: 'tool',
498
+ name: message.toolName,
499
+ inputJson: message.inputJson,
500
+ toolCallId: replayEntry.toolCallId,
501
+ status: 'fulfilled',
502
+ dateNowMs,
503
+ valueJson: outcome.valueJson,
504
+ };
505
+ postBridgeResponse({
506
+ type: 'bridge-response',
507
+ invocationId,
508
+ requestId: message.requestId,
509
+ success: true,
510
+ dateNowMs,
511
+ valueJson: outcome.valueJson,
512
+ });
513
+ return;
514
+ }
515
+
516
+ const toolCallId = `${baseExecutionOptions.toolCallId}:tool-${++nestedToolCounter}`;
517
+ const outcome = await invokeHostTool({
326
518
  toolName: message.toolName,
327
519
  inputJson: message.inputJson,
328
520
  tools,
329
521
  baseExecutionOptions,
522
+ codeModeOptions: options ?? {},
330
523
  maxToolInputBytes: normalizedOptions.maxToolInputBytes,
331
524
  maxToolOutputBytes: normalizedOptions.maxToolOutputBytes,
332
- toolCallId: `${baseExecutionOptions.toolCallId}:tool-${bridgeIndex}`,
525
+ toolCallId,
333
526
  });
527
+ if (outcome.type === 'interrupted') {
528
+ bridgeLedger[bridgeIndex - 1] = {
529
+ kind: 'tool',
530
+ name: message.toolName,
531
+ inputJson: message.inputJson,
532
+ toolCallId,
533
+ interruptId: `${toolCallId}:interrupt`,
534
+ interruptPayload: outcome.payload,
535
+ status: 'interrupted',
536
+ };
537
+ requestInterrupt(bridgeIndex - 1);
538
+ return;
539
+ }
540
+
541
+ const dateNowMs = Date.now();
542
+ bridgeLedger[bridgeIndex - 1] = {
543
+ kind: 'tool',
544
+ name: message.toolName,
545
+ inputJson: message.inputJson,
546
+ toolCallId,
547
+ status: 'fulfilled',
548
+ dateNowMs,
549
+ valueJson: outcome.valueJson,
550
+ };
334
551
  postBridgeResponse({
335
552
  type: 'bridge-response',
336
553
  invocationId,
337
554
  requestId: message.requestId,
338
555
  success: true,
339
- valueJson,
556
+ dateNowMs,
557
+ valueJson: outcome.valueJson,
340
558
  });
341
559
  } catch (error) {
560
+ if (error instanceof CodeModeProtocolError) {
561
+ failTerminal(error);
562
+ return;
563
+ }
564
+ const dateNowMs = Date.now();
565
+ const guestError = serializeBridgeErrorForGuest(error, 'tool');
566
+ const toolCallId =
567
+ replayEntry?.toolCallId ??
568
+ `${baseExecutionOptions.toolCallId}:tool-${nestedToolCounter}`;
569
+ if (
570
+ replayEntry === undefined ||
571
+ (replayEntry.status === 'interrupted' &&
572
+ interruptResolution?.interruptId === replayEntry.interruptId)
573
+ ) {
574
+ bridgeLedger[bridgeIndex - 1] = {
575
+ kind: 'tool',
576
+ name: message.toolName,
577
+ inputJson: message.inputJson,
578
+ toolCallId,
579
+ status: 'rejected',
580
+ dateNowMs,
581
+ error: guestError,
582
+ };
583
+ }
342
584
  postBridgeResponse({
343
585
  type: 'bridge-response',
344
586
  invocationId,
345
587
  requestId: message.requestId,
346
588
  success: false,
347
- error: serializeBridgeErrorForGuest(error, 'tool'),
589
+ dateNowMs,
590
+ error: guestError,
348
591
  });
349
592
  } finally {
350
593
  inFlightBridgeRequests--;
594
+ settleInterruptIfReady();
595
+ }
596
+ }
597
+
598
+ function requestInterrupt(entryIndex: number): void {
599
+ interruptEntryIndex ??= entryIndex;
600
+ }
601
+
602
+ function settleInterruptIfReady(): void {
603
+ if (
604
+ interruptEntryIndex === undefined ||
605
+ terminalReached ||
606
+ inFlightBridgeRequests > 0 ||
607
+ pendingInterruptDrainId !== undefined ||
608
+ !interruptResolutionConsumed
609
+ ) {
610
+ return;
611
+ }
612
+ pendingInterruptDrainId = `${invocationId}:drain-${++interruptDrainCounter}`;
613
+ postBridgeResponse({
614
+ type: 'bridge-drain',
615
+ invocationId,
616
+ drainId: pendingInterruptDrainId,
617
+ });
618
+ }
619
+
620
+ function settleInterrupt(): void {
621
+ if (
622
+ interruptEntryIndex === undefined ||
623
+ terminalReached ||
624
+ inFlightBridgeRequests > 0 ||
625
+ !interruptResolutionConsumed
626
+ ) {
627
+ return;
628
+ }
629
+
630
+ const entry = bridgeLedger[interruptEntryIndex];
631
+ if (entry === undefined || entry.status !== 'interrupted') {
632
+ failTerminal(
633
+ new CodeModeProtocolError(
634
+ 'Code mode interruption references a missing or resolved ledger entry.',
635
+ { invocationId, interruptEntryIndex },
636
+ ),
637
+ );
638
+ return;
351
639
  }
640
+
641
+ const continuationState = signCodeModeContinuation(
642
+ {
643
+ version: 1,
644
+ js,
645
+ outerToolCallId: baseExecutionOptions.toolCallId,
646
+ determinism: { ...determinism },
647
+ ledger: cloneLedger(bridgeLedger),
648
+ },
649
+ continuationSecurity,
650
+ );
651
+ const interrupt: CodeModeInterrupt = {
652
+ type: 'code-mode-interrupt',
653
+ interruptId: entry.interruptId,
654
+ toolName: entry.name,
655
+ toolCallId: entry.toolCallId,
656
+ outerToolCallId: continuationState.outerToolCallId,
657
+ input: fromJsonPayload(entry.inputJson),
658
+ payload: structuredClone(entry.interruptPayload),
659
+ continuation: continuationState,
660
+ };
661
+
662
+ terminalReached = true;
663
+ abortInvocation(interrupt);
664
+ cleanupWorker(false);
665
+ settleCaller(() => resolveResult(interrupt));
352
666
  }
353
667
 
354
- function postBridgeResponse(message: WorkerBridgeResponse): void {
668
+ function postBridgeResponse(message: MainToWorkerMessage): void {
355
669
  if (terminalReached) {
356
670
  return;
357
671
  }
@@ -367,6 +681,10 @@ function startWorkerRun({
367
681
  if (terminalReached) {
368
682
  return;
369
683
  }
684
+ if (interruptEntryIndex !== undefined) {
685
+ settleInterruptIfReady();
686
+ return;
687
+ }
370
688
  if (resultMessage === undefined) {
371
689
  failTerminal(
372
690
  new CodeModeProtocolError(
@@ -377,6 +695,31 @@ function startWorkerRun({
377
695
  return;
378
696
  }
379
697
 
698
+ if (!interruptResolutionConsumed) {
699
+ failTerminal(
700
+ new CodeModeProtocolError(
701
+ 'Code mode continuation completed without consuming its interrupt resolution.',
702
+ { interruptId: interruptResolution?.interruptId },
703
+ ),
704
+ );
705
+ return;
706
+ }
707
+ if (
708
+ continuation !== undefined &&
709
+ totalBridgeRequests < bridgeLedger.length
710
+ ) {
711
+ failTerminal(
712
+ new CodeModeProtocolError(
713
+ 'Code mode continuation returned before replaying the full bridge ledger.',
714
+ {
715
+ replayedBridgeRequests: totalBridgeRequests,
716
+ ledgerEntries: bridgeLedger.length,
717
+ },
718
+ ),
719
+ );
720
+ return;
721
+ }
722
+
380
723
  terminalReached = true;
381
724
  const finalResultMessage = resultMessage;
382
725
  if (inFlightBridgeRequests > 0) {
@@ -403,6 +746,128 @@ function startWorkerRun({
403
746
  }
404
747
  }
405
748
 
749
+ function assertContinuationInput({
750
+ js,
751
+ continuation,
752
+ interruptResolution,
753
+ continuationSecurity,
754
+ }: {
755
+ js: string;
756
+ continuation: RunCodeModeInput['continuation'];
757
+ interruptResolution: RunCodeModeInput['interruptResolution'];
758
+ continuationSecurity: ResolvedCodeModeContinuationSecurity;
759
+ }): void {
760
+ if (continuation === undefined) {
761
+ if (interruptResolution !== undefined) {
762
+ throw new CodeModeProtocolError(
763
+ 'A code-mode interrupt resolution was provided without continuation state.',
764
+ );
765
+ }
766
+ return;
767
+ }
768
+
769
+ verifyCodeModeContinuation(continuation, continuationSecurity);
770
+ if (continuation.version !== 1) {
771
+ throw new CodeModeProtocolError(
772
+ 'Unsupported code-mode continuation version.',
773
+ );
774
+ }
775
+ if (continuation.js !== js) {
776
+ throw new CodeModeProtocolError(
777
+ 'Code mode continuation source does not match the resumed source.',
778
+ );
779
+ }
780
+ if (
781
+ !Number.isInteger(continuation.determinism.dateNowMs) ||
782
+ !/^[0-9a-f]{32}$/i.test(continuation.determinism.randomSeed)
783
+ ) {
784
+ throw new CodeModeProtocolError(
785
+ 'Code mode continuation determinism state is malformed.',
786
+ );
787
+ }
788
+ for (const entry of continuation.ledger) {
789
+ if (
790
+ entry.kind !== 'tool' ||
791
+ typeof entry.name !== 'string' ||
792
+ typeof entry.inputJson !== 'string' ||
793
+ typeof entry.toolCallId !== 'string'
794
+ ) {
795
+ throw new CodeModeProtocolError(
796
+ 'Code mode continuation ledger is malformed.',
797
+ );
798
+ }
799
+ }
800
+ if (interruptResolution !== undefined) {
801
+ const matches = continuation.ledger.filter(
802
+ entry =>
803
+ entry.status === 'interrupted' &&
804
+ entry.interruptId === interruptResolution.interruptId,
805
+ );
806
+ if (matches.length !== 1) {
807
+ throw new CodeModeProtocolError(
808
+ 'Interrupt resolution does not match exactly one pending continuation ledger entry.',
809
+ {
810
+ interruptId: interruptResolution.interruptId,
811
+ matches: matches.length,
812
+ },
813
+ );
814
+ }
815
+ }
816
+ }
817
+
818
+ function assertReplayEntryMatches(
819
+ entry: CodeModeContinuationLedgerEntry,
820
+ message: WorkerToolRequest,
821
+ bridgeIndex: number,
822
+ ): void {
823
+ if (
824
+ entry.name !== message.toolName ||
825
+ entry.inputJson !== message.inputJson
826
+ ) {
827
+ throw new CodeModeProtocolError(
828
+ 'Code mode continuation replay diverged from its bridge ledger.',
829
+ {
830
+ bridgeIndex,
831
+ expectedToolName: entry.name,
832
+ receivedToolName: message.toolName,
833
+ },
834
+ );
835
+ }
836
+ }
837
+
838
+ function createDeterminismState(): {
839
+ dateNowMs: number;
840
+ randomSeed: string;
841
+ } {
842
+ return {
843
+ dateNowMs: Date.now(),
844
+ randomSeed: randomBytes(16).toString('hex'),
845
+ };
846
+ }
847
+
848
+ function cloneLedger(
849
+ ledger: CodeModeContinuationLedgerEntry[],
850
+ ): CodeModeContinuationLedgerEntry[] {
851
+ return structuredClone(ledger);
852
+ }
853
+
854
+ function getMaxNestedToolCounter(
855
+ ledger: CodeModeContinuationLedgerEntry[],
856
+ ): number {
857
+ let max = 0;
858
+ for (const entry of ledger) {
859
+ const match = /:tool-(\d+)$/u.exec(entry.toolCallId);
860
+ if (match !== null) {
861
+ max = Math.max(max, Number(match[1]));
862
+ }
863
+ }
864
+ return max;
865
+ }
866
+
867
+ function fromJsonPayload(valueJson: string): unknown {
868
+ return valueJson === '' ? undefined : JSON.parse(valueJson);
869
+ }
870
+
406
871
  function acquireWorker(maxPoolSize: number): PooledWorker {
407
872
  let pooledWorker = idleWorkers.pop();
408
873
  while (pooledWorker?.destroyed) {
@@ -1,9 +1,14 @@
1
- import type { NormalizedCodeModeOptions, SerializableError } from '../types.js';
1
+ import type {
2
+ CodeModeDeterminismState,
3
+ NormalizedCodeModeOptions,
4
+ SerializableError,
5
+ } from '../types.js';
2
6
 
3
7
  export interface WorkerRunMessage {
4
8
  type: 'run';
5
9
  invocationId: string;
6
10
  js: string;
11
+ determinism: CodeModeDeterminismState;
7
12
  options: Pick<
8
13
  NormalizedCodeModeOptions,
9
14
  | 'timeoutMs'
@@ -27,10 +32,23 @@ export interface WorkerBridgeResponse {
27
32
  invocationId: string;
28
33
  requestId: string;
29
34
  success: boolean;
35
+ dateNowMs: number;
30
36
  valueJson?: string;
31
37
  error?: SerializableError;
32
38
  }
33
39
 
40
+ export interface WorkerBridgeDrainRequest {
41
+ type: 'bridge-drain';
42
+ invocationId: string;
43
+ drainId: string;
44
+ }
45
+
46
+ export interface WorkerBridgeDrainedMessage {
47
+ type: 'bridge-drained';
48
+ invocationId: string;
49
+ drainId: string;
50
+ }
51
+
34
52
  export interface WorkerResultMessage {
35
53
  type: 'result';
36
54
  invocationId: string;
@@ -46,7 +64,11 @@ export interface WorkerReadyMessage {
46
64
 
47
65
  export type WorkerToMainMessage =
48
66
  | WorkerToolRequest
67
+ | WorkerBridgeDrainedMessage
49
68
  | WorkerResultMessage
50
69
  | WorkerReadyMessage;
51
70
 
52
- export type MainToWorkerMessage = WorkerRunMessage | WorkerBridgeResponse;
71
+ export type MainToWorkerMessage =
72
+ | WorkerRunMessage
73
+ | WorkerBridgeResponse
74
+ | WorkerBridgeDrainRequest;