@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.
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
@@ -28,6 +28,7 @@ const pendingBridgeRequests = new Map<
28
28
  context: QuickJSAsyncContext;
29
29
  deferred: QuickJSDeferredPromise;
30
30
  invocationId: string;
31
+ resetDateNow?: QuickJSHandle;
31
32
  }
32
33
  >();
33
34
  let activeInvocationId: string | undefined;
@@ -35,6 +36,20 @@ let bridgeRequestCounter = 0;
35
36
  let embeddedQuickJsWasmModulePromise: Promise<WebAssembly.Module> | undefined;
36
37
 
37
38
  parentPort.on('message', (message: MainToWorkerMessage) => {
39
+ if (message.type === 'bridge-drain') {
40
+ if (message.invocationId !== activeInvocationId) {
41
+ throw new CodeModeProtocolError(
42
+ `Bridge drain invocationId mismatch: expected ${activeInvocationId ?? 'no active invocation'}, received ${message.invocationId}.`,
43
+ );
44
+ }
45
+ parentPort?.postMessage({
46
+ type: 'bridge-drained',
47
+ invocationId: message.invocationId,
48
+ drainId: message.drainId,
49
+ });
50
+ return;
51
+ }
52
+
38
53
  if (message.type === 'bridge-response') {
39
54
  const pending = pendingBridgeRequests.get(message.requestId);
40
55
  if (!pending) {
@@ -59,7 +74,12 @@ parentPort.on('message', (message: MainToWorkerMessage) => {
59
74
  }
60
75
 
61
76
  pendingBridgeRequests.delete(message.requestId);
62
- resolveBridgeResponse(pending.context, pending.deferred, message);
77
+ resolveBridgeResponse(
78
+ pending.context,
79
+ pending.deferred,
80
+ message,
81
+ pending.resetDateNow,
82
+ );
63
83
  return;
64
84
  }
65
85
 
@@ -111,6 +131,8 @@ async function execute(message: WorkerRunMessage): Promise<string> {
111
131
  const deadline = Date.now() + message.options.timeoutMs;
112
132
  let interruptChecks = 0;
113
133
  let bridgeFunctions: { invokeTool: QuickJSHandle } | undefined;
134
+ let determinismHandle: QuickJSHandle | undefined;
135
+ let resetDateNowHandle: QuickJSHandle | undefined;
114
136
 
115
137
  runtime.setMemoryLimit(message.options.memoryLimitBytes);
116
138
  runtime.setMaxStackSize(message.options.maxStackSizeBytes);
@@ -121,7 +143,12 @@ async function execute(message: WorkerRunMessage): Promise<string> {
121
143
 
122
144
  try {
123
145
  installConsole(context, message.options.maxConsoleOutputBytes);
124
- bridgeFunctions = createBridgeFunctions(context, message);
146
+ bridgeFunctions = createBridgeFunctions(
147
+ context,
148
+ message,
149
+ () => resetDateNowHandle,
150
+ );
151
+ determinismHandle = jsToHandle(context, message.determinism);
125
152
  const setupSource = buildGuestRuntimeSetupSource();
126
153
  const setupEvalResult = await context.evalCodeAsync(
127
154
  setupSource,
@@ -139,6 +166,7 @@ async function execute(message: WorkerRunMessage): Promise<string> {
139
166
  setupEvalResult.value,
140
167
  context.undefined,
141
168
  bridgeFunctions.invokeTool,
169
+ determinismHandle,
142
170
  );
143
171
  if (setupCallResult.error) {
144
172
  const error = context.dump(setupCallResult.error);
@@ -148,6 +176,10 @@ async function execute(message: WorkerRunMessage): Promise<string> {
148
176
  throw toError(error);
149
177
  }
150
178
  if (setupCallResult.value.alive) {
179
+ resetDateNowHandle = context.getProp(
180
+ setupCallResult.value,
181
+ 'resetDateNow',
182
+ );
151
183
  setupCallResult.value.dispose();
152
184
  }
153
185
  } finally {
@@ -210,6 +242,12 @@ async function execute(message: WorkerRunMessage): Promise<string> {
210
242
  if (bridgeFunctions?.invokeTool.alive) {
211
243
  bridgeFunctions.invokeTool.dispose();
212
244
  }
245
+ if (determinismHandle?.alive) {
246
+ determinismHandle.dispose();
247
+ }
248
+ if (resetDateNowHandle?.alive) {
249
+ resetDateNowHandle.dispose();
250
+ }
213
251
  context.dispose();
214
252
  }
215
253
  }
@@ -339,16 +377,19 @@ function dumpConsoleArg(
339
377
  function createBridgeFunctions(
340
378
  context: QuickJSAsyncContext,
341
379
  message: WorkerRunMessage,
380
+ getResetDateNow: () => QuickJSHandle | undefined,
342
381
  ): { invokeTool: QuickJSHandle } {
343
382
  const invokeTool = context.newFunction(
344
383
  '__codeModeInvokeTool',
345
384
  (toolNameHandle: QuickJSHandle, inputJsonHandle: QuickJSHandle) => {
346
385
  const toolName = context.getString(toolNameHandle);
347
386
  const inputJson = context.getString(inputJsonHandle);
348
- return requestHost(context, message.invocationId, {
349
- toolName,
350
- inputJson,
351
- });
387
+ return requestHost(
388
+ context,
389
+ message.invocationId,
390
+ { toolName, inputJson },
391
+ getResetDateNow(),
392
+ );
352
393
  },
353
394
  );
354
395
 
@@ -359,6 +400,7 @@ function requestHost(
359
400
  context: QuickJSAsyncContext,
360
401
  invocationId: string,
361
402
  payload: Record<string, unknown>,
403
+ resetDateNow: QuickJSHandle | undefined,
362
404
  ): QuickJSHandle {
363
405
  const requestId = `${invocationId}:bridge-${++bridgeRequestCounter}`;
364
406
  const deferred = context.newPromise();
@@ -366,6 +408,7 @@ function requestHost(
366
408
  context,
367
409
  deferred,
368
410
  invocationId,
411
+ ...(resetDateNow !== undefined ? { resetDateNow } : {}),
369
412
  });
370
413
  deferred.settled.then(() => {
371
414
  context.runtime.executePendingJobs();
@@ -384,7 +427,9 @@ function resolveBridgeResponse(
384
427
  context: QuickJSAsyncContext,
385
428
  deferred: QuickJSDeferredPromise,
386
429
  message: WorkerBridgeResponse,
430
+ resetDateNow?: QuickJSHandle,
387
431
  ): void {
432
+ resetGuestDateNow(context, resetDateNow, message.dateNowMs);
388
433
  if (message.success) {
389
434
  const value = context.newString(message.valueJson ?? '');
390
435
  deferred.resolve(value);
@@ -396,6 +441,32 @@ function resolveBridgeResponse(
396
441
  error.dispose();
397
442
  }
398
443
 
444
+ function resetGuestDateNow(
445
+ context: QuickJSAsyncContext,
446
+ resetDateNow: QuickJSHandle | undefined,
447
+ dateNowMs: number,
448
+ ): void {
449
+ if (resetDateNow === undefined) {
450
+ return;
451
+ }
452
+ const value = context.newNumber(dateNowMs);
453
+ try {
454
+ const result = context.callFunction(resetDateNow, context.undefined, value);
455
+ if (result.error) {
456
+ const error = context.dump(result.error);
457
+ if (result.error.alive) {
458
+ result.error.dispose();
459
+ }
460
+ throw toError(error);
461
+ }
462
+ if (result.value.alive) {
463
+ result.value.dispose();
464
+ }
465
+ } finally {
466
+ value.dispose();
467
+ }
468
+ }
469
+
399
470
  function drainPendingJobs(context: QuickJSAsyncContext): void {
400
471
  while (context.runtime.hasPendingJob()) {
401
472
  const pending = context.runtime.executePendingJobs();
@@ -495,3 +566,42 @@ function createBridgeErrorHandle(
495
566
  }
496
567
  return handle;
497
568
  }
569
+
570
+ function jsToHandle(
571
+ context: QuickJSAsyncContext,
572
+ value: unknown,
573
+ ): QuickJSHandle {
574
+ if (value === null || value === undefined) {
575
+ return context.undefined;
576
+ }
577
+ if (typeof value === 'string') {
578
+ return context.newString(value);
579
+ }
580
+ if (typeof value === 'number') {
581
+ return context.newNumber(value);
582
+ }
583
+ if (typeof value === 'boolean') {
584
+ return value ? context.true : context.false;
585
+ }
586
+ if (Array.isArray(value)) {
587
+ const array = context.newArray();
588
+ value.forEach((item, index) => {
589
+ const itemHandle = jsToHandle(context, item);
590
+ context.setProp(array, index, itemHandle);
591
+ itemHandle.dispose();
592
+ });
593
+ return array;
594
+ }
595
+ if (typeof value === 'object') {
596
+ const object = context.newObject();
597
+ for (const [key, item] of Object.entries(
598
+ value as Record<string, unknown>,
599
+ )) {
600
+ const itemHandle = jsToHandle(context, item);
601
+ context.setProp(object, key, itemHandle);
602
+ itemHandle.dispose();
603
+ }
604
+ return object;
605
+ }
606
+ return context.undefined;
607
+ }
@@ -1,28 +1,57 @@
1
1
  import { asSchema } from 'ai';
2
- import { CodeModeToolError } from './errors.js';
3
- import type { CodeModeToolExecutionOptions, CodeModeToolSet } from './types.js';
2
+ import { CODE_MODE_TOOL_APPROVAL_KIND } from './approval.js';
3
+ import {
4
+ CodeModeProtocolError,
5
+ CodeModeToolApprovalDeniedError,
6
+ CodeModeToolApprovalRequiredError,
7
+ CodeModeToolError,
8
+ } from './errors.js';
9
+ import { isCodeModeHostInterruptSignal } from './host-interrupt.js';
10
+ import type {
11
+ CodeModeInterruptExecutionContext,
12
+ CodeModeInterruptPayload,
13
+ CodeModeOptions,
14
+ CodeModeToolExecutionOptions,
15
+ CodeModeToolSet,
16
+ } from './types.js';
4
17
  import {
5
18
  assertJsonSerializable,
6
19
  toJsonPayload,
7
20
  } from './utils/serialization.js';
8
21
 
22
+ export type HostToolInvocationResult =
23
+ | { type: 'success'; valueJson: string }
24
+ | {
25
+ type: 'interrupted';
26
+ toolName: string;
27
+ input: unknown;
28
+ toolCallId: string;
29
+ payload: CodeModeInterruptPayload;
30
+ };
31
+
9
32
  export async function invokeHostTool({
10
33
  toolName,
11
34
  inputJson,
12
35
  tools,
13
36
  baseExecutionOptions,
37
+ codeModeOptions,
14
38
  maxToolInputBytes,
15
39
  maxToolOutputBytes,
16
40
  toolCallId,
41
+ codeModeInterrupt,
42
+ skipApproval = false,
17
43
  }: {
18
44
  toolName: string;
19
45
  inputJson: string;
20
46
  tools: CodeModeToolSet;
21
47
  baseExecutionOptions: CodeModeToolExecutionOptions;
48
+ codeModeOptions: CodeModeOptions;
22
49
  maxToolInputBytes: number;
23
50
  maxToolOutputBytes: number;
24
51
  toolCallId: string;
25
- }): Promise<string> {
52
+ codeModeInterrupt?: CodeModeInterruptExecutionContext;
53
+ skipApproval?: boolean;
54
+ }): Promise<HostToolInvocationResult> {
26
55
  throwIfAborted(baseExecutionOptions.abortSignal);
27
56
 
28
57
  const hostTool = tools[toolName];
@@ -55,28 +84,100 @@ export async function invokeHostTool({
55
84
  const executionOptions: CodeModeToolExecutionOptions = {
56
85
  ...baseExecutionOptions,
57
86
  toolCallId,
87
+ ...(codeModeInterrupt !== undefined ? { codeModeInterrupt } : {}),
58
88
  };
59
89
 
60
- if (
61
- await raceAgainstAbort(
90
+ const needsApproval =
91
+ !skipApproval &&
92
+ (await raceAgainstAbort(
62
93
  requiresApproval(hostTool, validation.value, executionOptions),
63
94
  executionOptions.abortSignal,
64
- )
65
- ) {
66
- throw new CodeModeToolError(
67
- `Tool "${toolName}" requires approval, which code mode does not support yet.`,
68
- { toolName, input: validation.value, toolCallId },
95
+ ));
96
+
97
+ if (needsApproval) {
98
+ if (codeModeOptions.approval?.mode === 'interrupt') {
99
+ return {
100
+ type: 'interrupted',
101
+ toolName,
102
+ input: validation.value,
103
+ toolCallId,
104
+ payload: { kind: CODE_MODE_TOOL_APPROVAL_KIND },
105
+ };
106
+ }
107
+
108
+ const approval = await raceAgainstAbort(
109
+ Promise.resolve(
110
+ codeModeOptions.approval?.onApprovalRequired?.({
111
+ toolName,
112
+ input: validation.value,
113
+ toolCallId,
114
+ }),
115
+ ),
116
+ baseExecutionOptions.abortSignal,
69
117
  );
118
+ if (approval === undefined) {
119
+ throw new CodeModeToolApprovalRequiredError(
120
+ toolName,
121
+ validation.value,
122
+ toolCallId,
123
+ );
124
+ }
125
+ const approved =
126
+ typeof approval === 'string'
127
+ ? approval === 'approved'
128
+ : approval?.approved;
129
+ const reason = typeof approval === 'string' ? undefined : approval?.reason;
130
+ if (typeof approved !== 'boolean') {
131
+ throw new CodeModeProtocolError(
132
+ `Tool "${toolName}" approval callback returned a malformed approval decision.`,
133
+ { toolName, toolCallId },
134
+ );
135
+ }
136
+ if (reason !== undefined && typeof reason !== 'string') {
137
+ throw new CodeModeProtocolError(
138
+ `Tool "${toolName}" approval callback returned a malformed approval reason.`,
139
+ { toolName, toolCallId },
140
+ );
141
+ }
142
+ if (!approved) {
143
+ throw new CodeModeToolApprovalDeniedError(
144
+ toolName,
145
+ validation.value,
146
+ toolCallId,
147
+ reason,
148
+ );
149
+ }
70
150
  }
71
151
 
72
- const output = await raceAgainstAbort(
73
- executeHostTool(hostTool.execute.bind(hostTool), {
74
- input: validation.value,
75
- options: executionOptions,
76
- }),
77
- executionOptions.abortSignal,
78
- );
79
- return toJsonPayload(output, maxToolOutputBytes, `Tool "${toolName}" output`);
152
+ let output: unknown;
153
+ try {
154
+ output = await raceAgainstAbort(
155
+ executeHostTool(hostTool.execute.bind(hostTool), {
156
+ input: validation.value,
157
+ options: executionOptions,
158
+ }),
159
+ executionOptions.abortSignal,
160
+ );
161
+ } catch (error) {
162
+ if (isCodeModeHostInterruptSignal(error)) {
163
+ return {
164
+ type: 'interrupted',
165
+ toolName,
166
+ input: validation.value,
167
+ toolCallId,
168
+ payload: error.payload,
169
+ };
170
+ }
171
+ throw error;
172
+ }
173
+ return {
174
+ type: 'success',
175
+ valueJson: toJsonPayload(
176
+ output,
177
+ maxToolOutputBytes,
178
+ `Tool "${toolName}" output`,
179
+ ),
180
+ };
80
181
  }
81
182
 
82
183
  async function requiresApproval(
package/src/types.ts CHANGED
@@ -14,6 +14,7 @@ export interface CodeModeToolExecutionOptions {
14
14
  abortSignal?: AbortSignal;
15
15
  experimental_context?: unknown;
16
16
  context?: unknown;
17
+ codeModeInterrupt?: CodeModeInterruptExecutionContext;
17
18
  }
18
19
 
19
20
  /**
@@ -36,6 +37,124 @@ export interface CodeModeToolInput {
36
37
  js: string;
37
38
  }
38
39
 
40
+ export type ApprovalDecision =
41
+ | 'approved'
42
+ | 'denied'
43
+ | { approved: boolean; reason?: string };
44
+
45
+ export interface CodeModeApprovalRequest {
46
+ toolName: string;
47
+ input: unknown;
48
+ toolCallId: string;
49
+ }
50
+
51
+ export interface CodeModeApprovalResponse {
52
+ approvalId: string;
53
+ approved: boolean;
54
+ reason?: string;
55
+ }
56
+
57
+ export interface CodeModeApprovalResolution {
58
+ approved: boolean;
59
+ reason?: string;
60
+ }
61
+
62
+ export interface CodeModeInterruptPayload {
63
+ kind: string;
64
+ [key: string]: unknown;
65
+ }
66
+
67
+ export interface CodeModeInterruptResolution<TResolution = unknown> {
68
+ interruptId: string;
69
+ resolution: TResolution;
70
+ }
71
+
72
+ export interface CodeModeInterruptExecutionContext<
73
+ TPayload extends CodeModeInterruptPayload = CodeModeInterruptPayload,
74
+ TResolution = unknown,
75
+ > {
76
+ interruptId: string;
77
+ payload: TPayload;
78
+ resolution: TResolution;
79
+ }
80
+
81
+ export type CodeModeContinuationLedgerEntry =
82
+ | {
83
+ kind: 'tool';
84
+ name: string;
85
+ inputJson: string;
86
+ toolCallId: string;
87
+ status: 'fulfilled';
88
+ dateNowMs: number;
89
+ valueJson: string;
90
+ }
91
+ | {
92
+ kind: 'tool';
93
+ name: string;
94
+ inputJson: string;
95
+ toolCallId: string;
96
+ status: 'rejected';
97
+ dateNowMs: number;
98
+ error: SerializableError;
99
+ }
100
+ | {
101
+ kind: 'tool';
102
+ name: string;
103
+ inputJson: string;
104
+ toolCallId: string;
105
+ interruptId: string;
106
+ interruptPayload: CodeModeInterruptPayload;
107
+ status: 'interrupted';
108
+ };
109
+
110
+ export interface CodeModeDeterminismState {
111
+ dateNowMs: number;
112
+ randomSeed: string;
113
+ }
114
+
115
+ export interface CodeModeContinuationAuth {
116
+ alg: 'HMAC-SHA256';
117
+ nonce: string;
118
+ issuedAtMs: number;
119
+ expiresAtMs: number;
120
+ signature: string;
121
+ }
122
+
123
+ export interface CodeModeContinuation {
124
+ version: 1;
125
+ js: string;
126
+ outerToolCallId: string;
127
+ determinism: CodeModeDeterminismState;
128
+ ledger: CodeModeContinuationLedgerEntry[];
129
+ auth: CodeModeContinuationAuth;
130
+ }
131
+
132
+ export type UnsignedCodeModeContinuation = Omit<CodeModeContinuation, 'auth'>;
133
+
134
+ export interface CodeModeInterrupt<
135
+ TPayload extends CodeModeInterruptPayload = CodeModeInterruptPayload,
136
+ > {
137
+ type: 'code-mode-interrupt';
138
+ interruptId: string;
139
+ toolName: string;
140
+ toolCallId: string;
141
+ outerToolCallId: string;
142
+ input: unknown;
143
+ payload: TPayload;
144
+ continuation: CodeModeContinuation;
145
+ }
146
+
147
+ export type CodeModeUnwrappedResult =
148
+ | { status: 'completed'; output: unknown }
149
+ | { status: 'interrupted'; interrupt: CodeModeInterrupt };
150
+
151
+ export interface CodeModeApprovalInterruptPayload extends CodeModeInterruptPayload {
152
+ kind: 'ai-sdk-code-mode/tool-approval';
153
+ }
154
+
155
+ export type CodeModeApprovalInterrupt =
156
+ CodeModeInterrupt<CodeModeApprovalInterruptPayload>;
157
+
39
158
  /**
40
159
  * Execution limits applied to each sandbox invocation.
41
160
  */
@@ -67,6 +186,24 @@ export interface CodeModeExecutionPolicy {
67
186
  */
68
187
  export interface CodeModeOptions {
69
188
  executionPolicy?: CodeModeExecutionPolicy;
189
+ continuationSecurity?: CodeModeContinuationSecurityOptions;
190
+ approval?: {
191
+ /**
192
+ * @defaultValue `'callback'`
193
+ */
194
+ mode?: 'callback' | 'interrupt';
195
+ onApprovalRequired?: (
196
+ request: CodeModeApprovalRequest,
197
+ ) => Promise<ApprovalDecision> | ApprovalDecision;
198
+ };
199
+ }
200
+
201
+ export interface CodeModeContinuationSecurityOptions {
202
+ signingKey?: string | Uint8Array;
203
+ /**
204
+ * @defaultValue `60 * 60 * 1000`
205
+ */
206
+ maxAgeMs?: number;
70
207
  }
71
208
 
72
209
  /**
@@ -77,6 +214,8 @@ export interface RunCodeModeInput {
77
214
  tools: CodeModeToolSet;
78
215
  toolExecutionOptions?: Partial<CodeModeToolExecutionOptions>;
79
216
  options?: CodeModeOptions;
217
+ continuation?: CodeModeContinuation;
218
+ interruptResolution?: CodeModeInterruptResolution;
80
219
  }
81
220
 
82
221
  /**