@ai-sdk/code-mode 1.0.6 → 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.
- package/README.md +8 -5
- package/dist/approval-continuation.js +84 -0
- package/dist/approval-continuation.js.map +1 -0
- package/dist/approval.js +32 -0
- package/dist/approval.js.map +1 -0
- package/dist/continuation-capability.js +125 -0
- package/dist/continuation-capability.js.map +1 -0
- package/dist/direct-tool-call.js +5 -0
- package/dist/direct-tool-call.js.map +1 -0
- package/dist/errors.js +49 -0
- package/dist/errors.js.map +1 -1
- package/dist/host-interrupt.js +28 -0
- package/dist/host-interrupt.js.map +1 -0
- package/dist/index.d.ts +152 -1
- package/dist/index.js +31 -1
- package/dist/index.js.map +1 -1
- package/dist/interrupt-continuation.js +115 -0
- package/dist/interrupt-continuation.js.map +1 -0
- package/dist/runtime/guest-sources.js +91 -1
- package/dist/runtime/guest-sources.js.map +1 -1
- package/dist/runtime/manager.js +388 -8
- package/dist/runtime/manager.js.map +1 -1
- package/dist/runtime/worker-source.js +1 -1
- package/dist/tool-invocation.js +93 -16
- package/dist/tool-invocation.js.map +1 -1
- package/package.json +3 -3
- package/src/approval-continuation.ts +115 -0
- package/src/approval.ts +60 -0
- package/src/continuation-capability.ts +201 -0
- package/src/direct-tool-call.ts +6 -0
- package/src/errors.ts +65 -0
- package/src/host-interrupt.ts +49 -0
- package/src/index.ts +28 -0
- package/src/interrupt-continuation.ts +173 -0
- package/src/runtime/guest-sources.ts +92 -1
- package/src/runtime/manager.ts +473 -8
- package/src/runtime/protocol.ts +24 -2
- package/src/runtime/worker.ts +116 -6
- package/src/tool-invocation.ts +119 -18
- package/src/types.ts +139 -0
package/src/runtime/manager.ts
CHANGED
|
@@ -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
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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) {
|
package/src/runtime/protocol.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import type {
|
|
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 =
|
|
71
|
+
export type MainToWorkerMessage =
|
|
72
|
+
| WorkerRunMessage
|
|
73
|
+
| WorkerBridgeResponse
|
|
74
|
+
| WorkerBridgeDrainRequest;
|