@ai-sdk/code-mode 1.0.6 → 1.0.8

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,5 +1,12 @@
1
1
  import { asSchema } from "ai";
2
- import { CodeModeToolError } from "./errors.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";
3
10
  import {
4
11
  assertJsonSerializable,
5
12
  toJsonPayload
@@ -9,9 +16,12 @@ async function invokeHostTool({
9
16
  inputJson,
10
17
  tools,
11
18
  baseExecutionOptions,
19
+ codeModeOptions,
12
20
  maxToolInputBytes,
13
21
  maxToolOutputBytes,
14
- toolCallId
22
+ toolCallId,
23
+ codeModeInterrupt,
24
+ skipApproval = false
15
25
  }) {
16
26
  throwIfAborted(baseExecutionOptions.abortSignal);
17
27
  const hostTool = tools[toolName];
@@ -40,25 +50,92 @@ async function invokeHostTool({
40
50
  }
41
51
  const executionOptions = {
42
52
  ...baseExecutionOptions,
43
- toolCallId
53
+ toolCallId,
54
+ ...codeModeInterrupt !== void 0 ? { codeModeInterrupt } : {}
44
55
  };
45
- if (await raceAgainstAbort(
56
+ const needsApproval = !skipApproval && await raceAgainstAbort(
46
57
  requiresApproval(hostTool, validation.value, executionOptions),
47
58
  executionOptions.abortSignal
48
- )) {
49
- throw new CodeModeToolError(
50
- `Tool "${toolName}" requires approval, which code mode does not support yet.`,
51
- { toolName, input: validation.value, toolCallId }
59
+ );
60
+ if (needsApproval) {
61
+ if (codeModeOptions.approval?.mode === "interrupt") {
62
+ return {
63
+ type: "interrupted",
64
+ toolName,
65
+ input: validation.value,
66
+ toolCallId,
67
+ payload: { kind: CODE_MODE_TOOL_APPROVAL_KIND }
68
+ };
69
+ }
70
+ const approval = await raceAgainstAbort(
71
+ Promise.resolve(
72
+ codeModeOptions.approval?.onApprovalRequired?.({
73
+ toolName,
74
+ input: validation.value,
75
+ toolCallId
76
+ })
77
+ ),
78
+ baseExecutionOptions.abortSignal
52
79
  );
80
+ if (approval === void 0) {
81
+ throw new CodeModeToolApprovalRequiredError(
82
+ toolName,
83
+ validation.value,
84
+ toolCallId
85
+ );
86
+ }
87
+ const approved = typeof approval === "string" ? approval === "approved" : approval?.approved;
88
+ const reason = typeof approval === "string" ? void 0 : approval?.reason;
89
+ if (typeof approved !== "boolean") {
90
+ throw new CodeModeProtocolError(
91
+ `Tool "${toolName}" approval callback returned a malformed approval decision.`,
92
+ { toolName, toolCallId }
93
+ );
94
+ }
95
+ if (reason !== void 0 && typeof reason !== "string") {
96
+ throw new CodeModeProtocolError(
97
+ `Tool "${toolName}" approval callback returned a malformed approval reason.`,
98
+ { toolName, toolCallId }
99
+ );
100
+ }
101
+ if (!approved) {
102
+ throw new CodeModeToolApprovalDeniedError(
103
+ toolName,
104
+ validation.value,
105
+ toolCallId,
106
+ reason
107
+ );
108
+ }
53
109
  }
54
- const output = await raceAgainstAbort(
55
- executeHostTool(hostTool.execute.bind(hostTool), {
56
- input: validation.value,
57
- options: executionOptions
58
- }),
59
- executionOptions.abortSignal
60
- );
61
- return toJsonPayload(output, maxToolOutputBytes, `Tool "${toolName}" output`);
110
+ let output;
111
+ try {
112
+ output = await raceAgainstAbort(
113
+ executeHostTool(hostTool.execute.bind(hostTool), {
114
+ input: validation.value,
115
+ options: executionOptions
116
+ }),
117
+ executionOptions.abortSignal
118
+ );
119
+ } catch (error) {
120
+ if (isCodeModeHostInterruptSignal(error)) {
121
+ return {
122
+ type: "interrupted",
123
+ toolName,
124
+ input: validation.value,
125
+ toolCallId,
126
+ payload: error.payload
127
+ };
128
+ }
129
+ throw error;
130
+ }
131
+ return {
132
+ type: "success",
133
+ valueJson: toJsonPayload(
134
+ output,
135
+ maxToolOutputBytes,
136
+ `Tool "${toolName}" output`
137
+ )
138
+ };
62
139
  }
63
140
  async function requiresApproval(hostTool, input, options) {
64
141
  if (hostTool.needsApproval == null) {
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/tool-invocation.ts"],"sourcesContent":["import { asSchema } from 'ai';\nimport { CodeModeToolError } from './errors.js';\nimport type { CodeModeToolExecutionOptions, CodeModeToolSet } from './types.js';\nimport {\n assertJsonSerializable,\n toJsonPayload,\n} from './utils/serialization.js';\n\nexport async function invokeHostTool({\n toolName,\n inputJson,\n tools,\n baseExecutionOptions,\n maxToolInputBytes,\n maxToolOutputBytes,\n toolCallId,\n}: {\n toolName: string;\n inputJson: string;\n tools: CodeModeToolSet;\n baseExecutionOptions: CodeModeToolExecutionOptions;\n maxToolInputBytes: number;\n maxToolOutputBytes: number;\n toolCallId: string;\n}): Promise<string> {\n throwIfAborted(baseExecutionOptions.abortSignal);\n\n const hostTool = tools[toolName];\n if (!hostTool) {\n throw new CodeModeToolError(`Unknown tool: ${toolName}`, {\n toolName,\n availableTools: Object.keys(tools),\n });\n }\n if (hostTool.execute == null) {\n throw new CodeModeToolError(`Tool \"${toolName}\" does not have execute().`, {\n toolName,\n });\n }\n\n const input = inputJson === '' ? undefined : JSON.parse(inputJson);\n assertJsonSerializable(input, maxToolInputBytes, `Tool \"${toolName}\" input`);\n\n const validation = await raceAgainstAbort(\n validateToolInput(hostTool.inputSchema, input),\n baseExecutionOptions.abortSignal,\n );\n if (!validation.success) {\n throw new CodeModeToolError(\n `Invalid input for tool \"${toolName}\": ${validation.error.message}`,\n { toolName, input, cause: validation.error.message },\n );\n }\n\n const executionOptions: CodeModeToolExecutionOptions = {\n ...baseExecutionOptions,\n toolCallId,\n };\n\n if (\n await raceAgainstAbort(\n requiresApproval(hostTool, validation.value, executionOptions),\n executionOptions.abortSignal,\n )\n ) {\n throw new CodeModeToolError(\n `Tool \"${toolName}\" requires approval, which code mode does not support yet.`,\n { toolName, input: validation.value, toolCallId },\n );\n }\n\n const output = await raceAgainstAbort(\n executeHostTool(hostTool.execute.bind(hostTool), {\n input: validation.value,\n options: executionOptions,\n }),\n executionOptions.abortSignal,\n );\n return toJsonPayload(output, maxToolOutputBytes, `Tool \"${toolName}\" output`);\n}\n\nasync function requiresApproval(\n hostTool: CodeModeToolSet[string],\n input: unknown,\n options: CodeModeToolExecutionOptions,\n): Promise<boolean> {\n if (hostTool.needsApproval == null) {\n return false;\n }\n if (typeof hostTool.needsApproval === 'boolean') {\n return hostTool.needsApproval;\n }\n return await hostTool.needsApproval(input as never, options as never);\n}\n\nasync function validateToolInput(\n schema: CodeModeToolSet[string]['inputSchema'],\n input: unknown,\n): Promise<\n { success: true; value: unknown } | { success: false; error: Error }\n> {\n const normalizedSchema = asSchema(schema);\n if (normalizedSchema.validate === undefined) {\n return { success: true, value: input };\n }\n return await normalizedSchema.validate(input);\n}\n\nasync function executeHostTool(\n execute: NonNullable<CodeModeToolSet[string]['execute']>,\n {\n input,\n options,\n }: {\n input: unknown;\n options: CodeModeToolExecutionOptions;\n },\n): Promise<unknown> {\n const output = execute(input as never, options as never);\n if (isAsyncIterable(output)) {\n let finalOutput: unknown;\n for await (const part of output) {\n finalOutput = part;\n }\n return finalOutput;\n }\n return await output;\n}\n\nfunction isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n Symbol.asyncIterator in value &&\n typeof (value as { [Symbol.asyncIterator]?: unknown })[\n Symbol.asyncIterator\n ] === 'function'\n );\n}\n\nasync function raceAgainstAbort<T>(\n operation: Promise<T>,\n abortSignal: AbortSignal | undefined,\n): Promise<T> {\n if (abortSignal === undefined) {\n return await operation;\n }\n throwIfAborted(abortSignal);\n\n let rejectOnAbort!: (reason?: unknown) => void;\n const aborted = new Promise<never>((_resolve, reject) => {\n rejectOnAbort = reject;\n });\n const onAbort = () => {\n rejectOnAbort(abortReason(abortSignal));\n };\n\n abortSignal.addEventListener('abort', onAbort, { once: true });\n if (abortSignal.aborted) {\n onAbort();\n }\n\n try {\n return await Promise.race([operation, aborted]);\n } finally {\n abortSignal.removeEventListener('abort', onAbort);\n }\n}\n\nfunction throwIfAborted(abortSignal: AbortSignal | undefined): void {\n if (abortSignal?.aborted) {\n throw abortReason(abortSignal);\n }\n}\n\nfunction abortReason(abortSignal: AbortSignal): unknown {\n return (\n abortSignal.reason ??\n new DOMException('The operation was aborted.', 'AbortError')\n );\n}\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../src/tool-invocation.ts"],"sourcesContent":["import { asSchema } from 'ai';\nimport { CODE_MODE_TOOL_APPROVAL_KIND } from './approval.js';\nimport {\n CodeModeProtocolError,\n CodeModeToolApprovalDeniedError,\n CodeModeToolApprovalRequiredError,\n CodeModeToolError,\n} from './errors.js';\nimport { isCodeModeHostInterruptSignal } from './host-interrupt.js';\nimport type {\n CodeModeInterruptExecutionContext,\n CodeModeInterruptPayload,\n CodeModeOptions,\n CodeModeToolExecutionOptions,\n CodeModeToolSet,\n} from './types.js';\nimport {\n assertJsonSerializable,\n toJsonPayload,\n} from './utils/serialization.js';\n\nexport type HostToolInvocationResult =\n | { type: 'success'; valueJson: string }\n | {\n type: 'interrupted';\n toolName: string;\n input: unknown;\n toolCallId: string;\n payload: CodeModeInterruptPayload;\n };\n\nexport async function invokeHostTool({\n toolName,\n inputJson,\n tools,\n baseExecutionOptions,\n codeModeOptions,\n maxToolInputBytes,\n maxToolOutputBytes,\n toolCallId,\n codeModeInterrupt,\n skipApproval = false,\n}: {\n toolName: string;\n inputJson: string;\n tools: CodeModeToolSet;\n baseExecutionOptions: CodeModeToolExecutionOptions;\n codeModeOptions: CodeModeOptions;\n maxToolInputBytes: number;\n maxToolOutputBytes: number;\n toolCallId: string;\n codeModeInterrupt?: CodeModeInterruptExecutionContext;\n skipApproval?: boolean;\n}): Promise<HostToolInvocationResult> {\n throwIfAborted(baseExecutionOptions.abortSignal);\n\n const hostTool = tools[toolName];\n if (!hostTool) {\n throw new CodeModeToolError(`Unknown tool: ${toolName}`, {\n toolName,\n availableTools: Object.keys(tools),\n });\n }\n if (hostTool.execute == null) {\n throw new CodeModeToolError(`Tool \"${toolName}\" does not have execute().`, {\n toolName,\n });\n }\n\n const input = inputJson === '' ? undefined : JSON.parse(inputJson);\n assertJsonSerializable(input, maxToolInputBytes, `Tool \"${toolName}\" input`);\n\n const validation = await raceAgainstAbort(\n validateToolInput(hostTool.inputSchema, input),\n baseExecutionOptions.abortSignal,\n );\n if (!validation.success) {\n throw new CodeModeToolError(\n `Invalid input for tool \"${toolName}\": ${validation.error.message}`,\n { toolName, input, cause: validation.error.message },\n );\n }\n\n const executionOptions: CodeModeToolExecutionOptions = {\n ...baseExecutionOptions,\n toolCallId,\n ...(codeModeInterrupt !== undefined ? { codeModeInterrupt } : {}),\n };\n\n const needsApproval =\n !skipApproval &&\n (await raceAgainstAbort(\n requiresApproval(hostTool, validation.value, executionOptions),\n executionOptions.abortSignal,\n ));\n\n if (needsApproval) {\n if (codeModeOptions.approval?.mode === 'interrupt') {\n return {\n type: 'interrupted',\n toolName,\n input: validation.value,\n toolCallId,\n payload: { kind: CODE_MODE_TOOL_APPROVAL_KIND },\n };\n }\n\n const approval = await raceAgainstAbort(\n Promise.resolve(\n codeModeOptions.approval?.onApprovalRequired?.({\n toolName,\n input: validation.value,\n toolCallId,\n }),\n ),\n baseExecutionOptions.abortSignal,\n );\n if (approval === undefined) {\n throw new CodeModeToolApprovalRequiredError(\n toolName,\n validation.value,\n toolCallId,\n );\n }\n const approved =\n typeof approval === 'string'\n ? approval === 'approved'\n : approval?.approved;\n const reason = typeof approval === 'string' ? undefined : approval?.reason;\n if (typeof approved !== 'boolean') {\n throw new CodeModeProtocolError(\n `Tool \"${toolName}\" approval callback returned a malformed approval decision.`,\n { toolName, toolCallId },\n );\n }\n if (reason !== undefined && typeof reason !== 'string') {\n throw new CodeModeProtocolError(\n `Tool \"${toolName}\" approval callback returned a malformed approval reason.`,\n { toolName, toolCallId },\n );\n }\n if (!approved) {\n throw new CodeModeToolApprovalDeniedError(\n toolName,\n validation.value,\n toolCallId,\n reason,\n );\n }\n }\n\n let output: unknown;\n try {\n output = await raceAgainstAbort(\n executeHostTool(hostTool.execute.bind(hostTool), {\n input: validation.value,\n options: executionOptions,\n }),\n executionOptions.abortSignal,\n );\n } catch (error) {\n if (isCodeModeHostInterruptSignal(error)) {\n return {\n type: 'interrupted',\n toolName,\n input: validation.value,\n toolCallId,\n payload: error.payload,\n };\n }\n throw error;\n }\n return {\n type: 'success',\n valueJson: toJsonPayload(\n output,\n maxToolOutputBytes,\n `Tool \"${toolName}\" output`,\n ),\n };\n}\n\nasync function requiresApproval(\n hostTool: CodeModeToolSet[string],\n input: unknown,\n options: CodeModeToolExecutionOptions,\n): Promise<boolean> {\n if (hostTool.needsApproval == null) {\n return false;\n }\n if (typeof hostTool.needsApproval === 'boolean') {\n return hostTool.needsApproval;\n }\n return await hostTool.needsApproval(input as never, options as never);\n}\n\nasync function validateToolInput(\n schema: CodeModeToolSet[string]['inputSchema'],\n input: unknown,\n): Promise<\n { success: true; value: unknown } | { success: false; error: Error }\n> {\n const normalizedSchema = asSchema(schema);\n if (normalizedSchema.validate === undefined) {\n return { success: true, value: input };\n }\n return await normalizedSchema.validate(input);\n}\n\nasync function executeHostTool(\n execute: NonNullable<CodeModeToolSet[string]['execute']>,\n {\n input,\n options,\n }: {\n input: unknown;\n options: CodeModeToolExecutionOptions;\n },\n): Promise<unknown> {\n const output = execute(input as never, options as never);\n if (isAsyncIterable(output)) {\n let finalOutput: unknown;\n for await (const part of output) {\n finalOutput = part;\n }\n return finalOutput;\n }\n return await output;\n}\n\nfunction isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {\n return (\n typeof value === 'object' &&\n value !== null &&\n Symbol.asyncIterator in value &&\n typeof (value as { [Symbol.asyncIterator]?: unknown })[\n Symbol.asyncIterator\n ] === 'function'\n );\n}\n\nasync function raceAgainstAbort<T>(\n operation: Promise<T>,\n abortSignal: AbortSignal | undefined,\n): Promise<T> {\n if (abortSignal === undefined) {\n return await operation;\n }\n throwIfAborted(abortSignal);\n\n let rejectOnAbort!: (reason?: unknown) => void;\n const aborted = new Promise<never>((_resolve, reject) => {\n rejectOnAbort = reject;\n });\n const onAbort = () => {\n rejectOnAbort(abortReason(abortSignal));\n };\n\n abortSignal.addEventListener('abort', onAbort, { once: true });\n if (abortSignal.aborted) {\n onAbort();\n }\n\n try {\n return await Promise.race([operation, aborted]);\n } finally {\n abortSignal.removeEventListener('abort', onAbort);\n }\n}\n\nfunction throwIfAborted(abortSignal: AbortSignal | undefined): void {\n if (abortSignal?.aborted) {\n throw abortReason(abortSignal);\n }\n}\n\nfunction abortReason(abortSignal: AbortSignal): unknown {\n return (\n abortSignal.reason ??\n new DOMException('The operation was aborted.', 'AbortError')\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/code-mode",
3
- "version": "1.0.6",
3
+ "version": "1.0.8",
4
4
  "description": "QuickJS-backed code mode tool for AI SDK",
5
5
  "keywords": [
6
6
  "ai",
@@ -37,7 +37,7 @@
37
37
  "README.md"
38
38
  ],
39
39
  "peerDependencies": {
40
- "ai": "7.0.49"
40
+ "ai": "7.0.51"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "22.19.19",
@@ -47,7 +47,7 @@
47
47
  "typescript": "5.8.3",
48
48
  "zod": "3.25.76",
49
49
  "@vercel/ai-tsconfig": "0.0.0",
50
- "ai": "7.0.49"
50
+ "ai": "7.0.51"
51
51
  },
52
52
  "engines": {
53
53
  "node": ">=22"
@@ -0,0 +1,115 @@
1
+ import type { ModelMessage } from 'ai';
2
+ import {
3
+ assertCodeModeApprovalResponse,
4
+ CODE_MODE_TOOL_APPROVAL_KIND,
5
+ } from './approval.js';
6
+ import { CodeModeProtocolError } from './errors.js';
7
+ import {
8
+ continueCodeModeInterrupt,
9
+ isCodeModeInterrupt,
10
+ } from './interrupt-continuation.js';
11
+ import type {
12
+ CodeModeApprovalInterrupt,
13
+ CodeModeApprovalResponse,
14
+ CodeModeContinuationSecurityOptions,
15
+ CodeModeOptions,
16
+ CodeModeToolExecutionOptions,
17
+ CodeModeToolSet,
18
+ } from './types.js';
19
+
20
+ export function isCodeModeApprovalInterrupt(
21
+ value: unknown,
22
+ continuationSecurity: CodeModeContinuationSecurityOptions = {},
23
+ ): value is CodeModeApprovalInterrupt {
24
+ return (
25
+ isCodeModeInterrupt(value, continuationSecurity) &&
26
+ value.payload.kind === CODE_MODE_TOOL_APPROVAL_KIND
27
+ );
28
+ }
29
+
30
+ export async function continueCodeModeApproval({
31
+ interrupt,
32
+ approvalResponse,
33
+ tools,
34
+ options = {},
35
+ toolExecutionOptions,
36
+ }: {
37
+ interrupt: CodeModeApprovalInterrupt;
38
+ approvalResponse: CodeModeApprovalResponse;
39
+ tools: CodeModeToolSet;
40
+ options?: CodeModeOptions;
41
+ toolExecutionOptions?: Partial<CodeModeToolExecutionOptions>;
42
+ }): Promise<unknown> {
43
+ assertCodeModeApprovalResponse(approvalResponse);
44
+ if (approvalResponse.approvalId !== interrupt.interruptId) {
45
+ throw new CodeModeProtocolError(
46
+ `Approval response ${approvalResponse.approvalId} does not match pending code-mode approval ${interrupt.interruptId}.`,
47
+ );
48
+ }
49
+
50
+ return await continueCodeModeInterrupt({
51
+ interrupt,
52
+ resolution: {
53
+ approved: approvalResponse.approved,
54
+ ...(approvalResponse.reason !== undefined
55
+ ? { reason: approvalResponse.reason }
56
+ : {}),
57
+ },
58
+ tools,
59
+ options: {
60
+ ...options,
61
+ approval: { ...options.approval, mode: 'interrupt' },
62
+ },
63
+ ...(toolExecutionOptions !== undefined ? { toolExecutionOptions } : {}),
64
+ });
65
+ }
66
+
67
+ export function toCodeModeApprovalMessages(
68
+ interrupt: CodeModeApprovalInterrupt,
69
+ ): ModelMessage[] {
70
+ return [
71
+ {
72
+ role: 'assistant',
73
+ content: [
74
+ {
75
+ type: 'tool-call',
76
+ toolCallId: interrupt.toolCallId,
77
+ toolName: interrupt.toolName,
78
+ input: interrupt.input,
79
+ },
80
+ {
81
+ type: 'tool-approval-request',
82
+ approvalId: interrupt.interruptId,
83
+ toolCallId: interrupt.toolCallId,
84
+ },
85
+ ],
86
+ },
87
+ ];
88
+ }
89
+
90
+ export function getCodeModeApprovalResponse(
91
+ messages: ModelMessage[],
92
+ interrupt: CodeModeApprovalInterrupt,
93
+ ): CodeModeApprovalResponse | undefined {
94
+ for (let index = messages.length - 1; index >= 0; index--) {
95
+ const message = messages[index];
96
+ if (message?.role !== 'tool') {
97
+ continue;
98
+ }
99
+ for (const part of message.content) {
100
+ if (
101
+ part.type === 'tool-approval-response' &&
102
+ part.approvalId === interrupt.interruptId &&
103
+ typeof part.approved === 'boolean' &&
104
+ (part.reason === undefined || typeof part.reason === 'string')
105
+ ) {
106
+ return {
107
+ approvalId: part.approvalId,
108
+ approved: part.approved,
109
+ ...(part.reason !== undefined ? { reason: part.reason } : {}),
110
+ };
111
+ }
112
+ }
113
+ }
114
+ return undefined;
115
+ }
@@ -0,0 +1,60 @@
1
+ import { CodeModeProtocolError } from './errors.js';
2
+ import type {
3
+ CodeModeApprovalInterruptPayload,
4
+ CodeModeApprovalResolution,
5
+ CodeModeApprovalResponse,
6
+ CodeModeInterruptPayload,
7
+ } from './types.js';
8
+
9
+ export const CODE_MODE_TOOL_APPROVAL_KIND =
10
+ 'ai-sdk-code-mode/tool-approval' as const;
11
+
12
+ export function isCodeModeApprovalInterruptPayload(
13
+ payload: CodeModeInterruptPayload,
14
+ ): payload is CodeModeApprovalInterruptPayload {
15
+ return payload.kind === CODE_MODE_TOOL_APPROVAL_KIND;
16
+ }
17
+
18
+ export function assertCodeModeApprovalResponse(
19
+ value: unknown,
20
+ ): asserts value is CodeModeApprovalResponse {
21
+ if (
22
+ typeof value !== 'object' ||
23
+ value === null ||
24
+ Array.isArray(value) ||
25
+ typeof (value as { approvalId?: unknown }).approvalId !== 'string' ||
26
+ typeof (value as { approved?: unknown }).approved !== 'boolean' ||
27
+ ('reason' in value &&
28
+ (value as { reason?: unknown }).reason !== undefined &&
29
+ typeof (value as { reason?: unknown }).reason !== 'string')
30
+ ) {
31
+ throw new CodeModeProtocolError(
32
+ 'Code mode approval response is malformed.',
33
+ );
34
+ }
35
+ }
36
+
37
+ export function normalizeApprovalResolution(
38
+ resolution: unknown,
39
+ ): CodeModeApprovalResolution {
40
+ if (
41
+ typeof resolution !== 'object' ||
42
+ resolution === null ||
43
+ Array.isArray(resolution) ||
44
+ typeof (resolution as { approved?: unknown }).approved !== 'boolean' ||
45
+ ('reason' in resolution &&
46
+ (resolution as { reason?: unknown }).reason !== undefined &&
47
+ typeof (resolution as { reason?: unknown }).reason !== 'string')
48
+ ) {
49
+ throw new CodeModeProtocolError(
50
+ 'Code mode approval resolution must be a boolean approval decision.',
51
+ { resolution },
52
+ );
53
+ }
54
+
55
+ const { approved, reason } = resolution as CodeModeApprovalResolution;
56
+ return {
57
+ approved,
58
+ ...(reason !== undefined ? { reason } : {}),
59
+ };
60
+ }
@@ -0,0 +1,201 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { CodeModeProtocolError } from './errors.js';
3
+ import type {
4
+ CodeModeContinuation,
5
+ CodeModeContinuationAuth,
6
+ CodeModeContinuationSecurityOptions,
7
+ UnsignedCodeModeContinuation,
8
+ } from './types.js';
9
+
10
+ const SIGNATURE_ALGORITHM = 'HMAC-SHA256';
11
+ const DEFAULT_MAX_AGE_MS = 60 * 60 * 1000;
12
+
13
+ let defaultSigningKey: Uint8Array = randomBytes(32);
14
+ let defaultMaxAgeMs = DEFAULT_MAX_AGE_MS;
15
+
16
+ export interface ResolvedCodeModeContinuationSecurity {
17
+ signingKey: Buffer;
18
+ maxAgeMs: number;
19
+ }
20
+
21
+ export function setCodeModeContinuationSigningKey(
22
+ key?: string | Uint8Array,
23
+ options: { maxAgeMs?: number } = {},
24
+ ): void {
25
+ const resolved = resolveCodeModeContinuationSecurity({
26
+ signingKey: key ?? randomBytes(32),
27
+ maxAgeMs: options.maxAgeMs ?? DEFAULT_MAX_AGE_MS,
28
+ });
29
+ defaultSigningKey = resolved.signingKey;
30
+ defaultMaxAgeMs = resolved.maxAgeMs;
31
+ }
32
+
33
+ export function resolveCodeModeContinuationSecurity(
34
+ options: CodeModeContinuationSecurityOptions = {},
35
+ ): ResolvedCodeModeContinuationSecurity {
36
+ const signingKey =
37
+ options.signingKey === undefined
38
+ ? Buffer.from(defaultSigningKey)
39
+ : typeof options.signingKey === 'string'
40
+ ? Buffer.from(options.signingKey)
41
+ : Buffer.from(options.signingKey);
42
+ if (signingKey.byteLength === 0) {
43
+ throw new TypeError('Continuation signing key must not be empty.');
44
+ }
45
+
46
+ const maxAgeMs = options.maxAgeMs ?? defaultMaxAgeMs;
47
+ if (
48
+ !Number.isInteger(maxAgeMs) ||
49
+ !Number.isFinite(maxAgeMs) ||
50
+ maxAgeMs <= 0
51
+ ) {
52
+ throw new TypeError('Continuation maxAgeMs must be a positive integer.');
53
+ }
54
+
55
+ return { signingKey, maxAgeMs };
56
+ }
57
+
58
+ export function signCodeModeContinuation(
59
+ continuation: UnsignedCodeModeContinuation,
60
+ security = resolveCodeModeContinuationSecurity(),
61
+ ): CodeModeContinuation {
62
+ const issuedAtMs = Date.now();
63
+ const auth: Omit<CodeModeContinuationAuth, 'signature'> = {
64
+ alg: SIGNATURE_ALGORITHM,
65
+ nonce: randomBytes(16).toString('hex'),
66
+ issuedAtMs,
67
+ expiresAtMs: issuedAtMs + security.maxAgeMs,
68
+ };
69
+ return {
70
+ ...structuredClone(continuation),
71
+ auth: {
72
+ ...auth,
73
+ signature: signContinuationPayload(
74
+ { ...continuation, auth },
75
+ security.signingKey,
76
+ ),
77
+ },
78
+ };
79
+ }
80
+
81
+ export function verifyCodeModeContinuation(
82
+ continuation: CodeModeContinuation,
83
+ security: CodeModeContinuationSecurityOptions = {},
84
+ ): void {
85
+ assertAuthShape(continuation.auth);
86
+ const now = Date.now();
87
+ if (continuation.auth.expiresAtMs < now) {
88
+ throw new CodeModeProtocolError('Code mode continuation has expired.', {
89
+ expiresAtMs: continuation.auth.expiresAtMs,
90
+ now,
91
+ });
92
+ }
93
+ if (continuation.auth.issuedAtMs > now + 60_000) {
94
+ throw new CodeModeProtocolError(
95
+ 'Code mode continuation was issued in the future.',
96
+ { issuedAtMs: continuation.auth.issuedAtMs, now },
97
+ );
98
+ }
99
+
100
+ const { signingKey } = resolveCodeModeContinuationSecurity(security);
101
+ const expected = signContinuationPayload(
102
+ stripSignature(continuation),
103
+ signingKey,
104
+ );
105
+ if (!constantTimeEqual(continuation.auth.signature, expected)) {
106
+ throw new CodeModeProtocolError(
107
+ 'Code mode continuation signature is invalid.',
108
+ );
109
+ }
110
+ }
111
+
112
+ export function hasValidCodeModeContinuationCapability(
113
+ value: unknown,
114
+ security: CodeModeContinuationSecurityOptions = {},
115
+ ): value is CodeModeContinuation {
116
+ try {
117
+ verifyCodeModeContinuation(value as CodeModeContinuation, security);
118
+ return true;
119
+ } catch {
120
+ return false;
121
+ }
122
+ }
123
+
124
+ function signContinuationPayload(
125
+ continuation: UnsignedCodeModeContinuation & {
126
+ auth: Omit<CodeModeContinuationAuth, 'signature'>;
127
+ },
128
+ signingKey: Uint8Array,
129
+ ): string {
130
+ return createHmac('sha256', signingKey)
131
+ .update(canonicalJson(continuation))
132
+ .digest('base64url');
133
+ }
134
+
135
+ function stripSignature(
136
+ continuation: CodeModeContinuation,
137
+ ): UnsignedCodeModeContinuation & {
138
+ auth: Omit<CodeModeContinuationAuth, 'signature'>;
139
+ } {
140
+ const { auth, ...rest } = continuation;
141
+ const { signature: _signature, ...unsignedAuth } = auth;
142
+ return { ...rest, auth: unsignedAuth };
143
+ }
144
+
145
+ function assertAuthShape(
146
+ auth: unknown,
147
+ ): asserts auth is CodeModeContinuationAuth {
148
+ if (
149
+ typeof auth !== 'object' ||
150
+ auth === null ||
151
+ Array.isArray(auth) ||
152
+ (auth as { alg?: unknown }).alg !== SIGNATURE_ALGORITHM ||
153
+ typeof (auth as { nonce?: unknown }).nonce !== 'string' ||
154
+ !/^[0-9a-f]{32}$/i.test((auth as { nonce: string }).nonce) ||
155
+ typeof (auth as { issuedAtMs?: unknown }).issuedAtMs !== 'number' ||
156
+ !Number.isInteger((auth as { issuedAtMs: number }).issuedAtMs) ||
157
+ typeof (auth as { expiresAtMs?: unknown }).expiresAtMs !== 'number' ||
158
+ !Number.isInteger((auth as { expiresAtMs: number }).expiresAtMs) ||
159
+ (auth as { expiresAtMs: number }).expiresAtMs <=
160
+ (auth as { issuedAtMs: number }).issuedAtMs ||
161
+ typeof (auth as { signature?: unknown }).signature !== 'string' ||
162
+ (auth as { signature: string }).signature.length === 0
163
+ ) {
164
+ throw new CodeModeProtocolError(
165
+ 'Code mode continuation is missing valid signed auth metadata.',
166
+ );
167
+ }
168
+ }
169
+
170
+ function constantTimeEqual(left: string, right: string): boolean {
171
+ const leftBytes = Buffer.from(left);
172
+ const rightBytes = Buffer.from(right);
173
+ return (
174
+ leftBytes.byteLength === rightBytes.byteLength &&
175
+ timingSafeEqual(leftBytes, rightBytes)
176
+ );
177
+ }
178
+
179
+ function canonicalJson(value: unknown): string {
180
+ if (value === null) {
181
+ return 'null';
182
+ }
183
+ if (typeof value === 'string') {
184
+ return JSON.stringify(value);
185
+ }
186
+ if (typeof value === 'number' || typeof value === 'boolean') {
187
+ return JSON.stringify(value);
188
+ }
189
+ if (Array.isArray(value)) {
190
+ return `[${value.map(canonicalJson).join(',')}]`;
191
+ }
192
+ if (typeof value === 'object') {
193
+ const entries = Object.entries(value as Record<string, unknown>)
194
+ .filter(([, item]) => item !== undefined)
195
+ .sort(([left], [right]) => left.localeCompare(right));
196
+ return `{${entries
197
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
198
+ .join(',')}}`;
199
+ }
200
+ throw new TypeError('Continuation contains a non-JSON-serializable value.');
201
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Allows the model to call a tool directly.
3
+ *
4
+ * Use a string instead of a Symbol to allow for serializability.
5
+ */
6
+ export const DIRECT_TOOL_CALL = 'AI_SDK_DIRECT_TOOL_CALL';
package/src/errors.ts CHANGED
@@ -112,6 +112,37 @@ export class CodeModeToolError extends CodeModeError {
112
112
  }
113
113
  }
114
114
 
115
+ export class CodeModeToolApprovalRequiredError extends CodeModeToolError {
116
+ constructor(toolName: string, input: unknown, toolCallId: string) {
117
+ super(`Tool "${toolName}" requires approval before execution.`, {
118
+ toolName,
119
+ input,
120
+ toolCallId,
121
+ });
122
+ this.code = 'CODE_MODE_TOOL_APPROVAL_REQUIRED';
123
+ }
124
+ }
125
+
126
+ export class CodeModeToolApprovalDeniedError extends CodeModeToolError {
127
+ constructor(
128
+ toolName: string,
129
+ input: unknown,
130
+ toolCallId: string,
131
+ reason?: string,
132
+ ) {
133
+ super(
134
+ `Tool "${toolName}" approval was denied${reason ? `: ${reason}` : '.'}`,
135
+ {
136
+ toolName,
137
+ input,
138
+ toolCallId,
139
+ ...(reason !== undefined ? { reason } : {}),
140
+ },
141
+ );
142
+ this.code = 'CODE_MODE_TOOL_APPROVAL_DENIED';
143
+ }
144
+ }
145
+
115
146
  /**
116
147
  * Converts an unknown thrown value into a worker-safe serializable shape.
117
148
  *
@@ -250,6 +281,40 @@ export function deserializeError(error: SerializableError): Error {
250
281
  return result;
251
282
  }
252
283
 
284
+ if (error.code === 'CODE_MODE_TOOL_APPROVAL_REQUIRED') {
285
+ const details = error.details as
286
+ | { toolName?: string; input?: unknown; toolCallId?: string }
287
+ | undefined;
288
+ const result = new CodeModeToolApprovalRequiredError(
289
+ details?.toolName ?? 'unknown',
290
+ details?.input,
291
+ details?.toolCallId ?? 'unknown',
292
+ );
293
+ result.message = error.message;
294
+ restoreStack(result, error);
295
+ return result;
296
+ }
297
+
298
+ if (error.code === 'CODE_MODE_TOOL_APPROVAL_DENIED') {
299
+ const details = error.details as
300
+ | {
301
+ toolName?: string;
302
+ input?: unknown;
303
+ toolCallId?: string;
304
+ reason?: string;
305
+ }
306
+ | undefined;
307
+ const result = new CodeModeToolApprovalDeniedError(
308
+ details?.toolName ?? 'unknown',
309
+ details?.input,
310
+ details?.toolCallId ?? 'unknown',
311
+ details?.reason,
312
+ );
313
+ result.message = error.message;
314
+ restoreStack(result, error);
315
+ return result;
316
+ }
317
+
253
318
  const result = new CodeModeError(
254
319
  error.message,
255
320
  error.code ?? 'CODE_MODE_ERROR',