@tanstack/ai-code-mode 0.3.10 → 0.4.0
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 +12 -8
- package/dist/esm/create-code-mode-tool.d.ts +1 -0
- package/dist/esm/create-code-mode-tool.js +71 -27
- package/dist/esm/create-code-mode-tool.js.map +1 -1
- package/dist/esm/strip-typescript.js +2 -1
- package/dist/esm/strip-typescript.js.map +1 -1
- package/dist/esm/types.d.ts +9 -8
- package/package.json +4 -4
- package/skills/ai-code-mode/SKILL.md +53 -38
- package/src/create-code-mode-tool.ts +123 -41
- package/src/types.ts +9 -8
package/README.md
CHANGED
|
@@ -21,6 +21,9 @@ pnpm add @tanstack/ai-isolate-node
|
|
|
21
21
|
# QuickJS WASM (browser-compatible, no native deps)
|
|
22
22
|
pnpm add @tanstack/ai-isolate-quickjs
|
|
23
23
|
|
|
24
|
+
# Bun servers (native QuickJS via bun:ffi)
|
|
25
|
+
bun add @tanstack/ai-isolate-quickjs-bun
|
|
26
|
+
|
|
24
27
|
# Cloudflare Workers (edge execution)
|
|
25
28
|
pnpm add @tanstack/ai-isolate-cloudflare
|
|
26
29
|
```
|
|
@@ -82,11 +85,11 @@ Creates both the `execute_typescript` tool and its matching system prompt. This
|
|
|
82
85
|
|
|
83
86
|
**Config:**
|
|
84
87
|
|
|
85
|
-
- `driver` — An `IsolateDriver` (Node, QuickJS, or Cloudflare)
|
|
88
|
+
- `driver` — An `IsolateDriver` (Node, QuickJS, QuickJS Bun, or Cloudflare)
|
|
86
89
|
- `tools` — Array of `ServerTool` or `ToolDefinition` instances. Exposed as `external_*` functions in the sandbox
|
|
87
90
|
- `timeout` — Execution timeout in ms (default: 30000)
|
|
88
|
-
- `memoryLimit` — Memory limit in MB (default: 128, supported by Node and QuickJS drivers)
|
|
89
|
-
- `
|
|
91
|
+
- `memoryLimit` — Memory limit in MB (default: 128, supported by the Node, QuickJS, and QuickJS Bun drivers)
|
|
92
|
+
- `getSnippetBindings` — Optional async function returning dynamic bindings
|
|
90
93
|
|
|
91
94
|
### `createCodeModeTool(config)` / `createCodeModeSystemPrompt(config)`
|
|
92
95
|
|
|
@@ -102,11 +105,12 @@ These utilities are used internally and exported for custom pipelines:
|
|
|
102
105
|
|
|
103
106
|
## Driver Selection Guide
|
|
104
107
|
|
|
105
|
-
| Driver
|
|
106
|
-
|
|
|
107
|
-
| `@tanstack/ai-isolate-node`
|
|
108
|
-
| `@tanstack/ai-isolate-quickjs`
|
|
109
|
-
| `@tanstack/ai-isolate-
|
|
108
|
+
| Driver | Best For | Native Deps | Browser | Memory Limit |
|
|
109
|
+
| ---------------------------------- | -------------------------------------------- | ------------------- | ------- | ------------ |
|
|
110
|
+
| `@tanstack/ai-isolate-node` | Server-side Node.js apps | Yes (`isolated-vm`) | No | Yes |
|
|
111
|
+
| `@tanstack/ai-isolate-quickjs` | Browser, edge, or no-native-dep environments | No (WASM) | Yes | Yes |
|
|
112
|
+
| `@tanstack/ai-isolate-quickjs-bun` | Bun servers (native QuickJS via `bun:ffi`) | No | No | Yes |
|
|
113
|
+
| `@tanstack/ai-isolate-cloudflare` | Cloudflare Workers deployments | No | N/A | N/A |
|
|
110
114
|
|
|
111
115
|
## Custom Events
|
|
112
116
|
|
|
@@ -18,6 +18,7 @@ declare const executeTypescriptOutputSchema: z.ZodObject<{
|
|
|
18
18
|
message: z.ZodString;
|
|
19
19
|
name: z.ZodOptional<z.ZodString>;
|
|
20
20
|
line: z.ZodOptional<z.ZodNumber>;
|
|
21
|
+
stack: z.ZodOptional<z.ZodString>;
|
|
21
22
|
}, z.core.$strip>>;
|
|
22
23
|
}, z.core.$strip>;
|
|
23
24
|
export type ExecuteTypescriptInput = z.infer<typeof executeTypescriptInputSchema>;
|
|
@@ -18,7 +18,8 @@ var executeTypescriptOutputSchema = z.object({
|
|
|
18
18
|
error: z.object({
|
|
19
19
|
message: z.string(),
|
|
20
20
|
name: z.string().optional(),
|
|
21
|
-
line: z.number().optional()
|
|
21
|
+
line: z.number().optional(),
|
|
22
|
+
stack: z.string().optional()
|
|
22
23
|
}).optional().describe("Error details if execution failed")
|
|
23
24
|
});
|
|
24
25
|
/**
|
|
@@ -46,7 +47,7 @@ var executeTypescriptOutputSchema = z.object({
|
|
|
46
47
|
* ```
|
|
47
48
|
*/
|
|
48
49
|
function createCodeModeTool(config) {
|
|
49
|
-
const { driver, tools, timeout = 3e4, memoryLimit = 128,
|
|
50
|
+
const { driver, tools, timeout = 3e4, memoryLimit = 128, getSnippetBindings, onSecretParameter, transpile = stripTypeScript } = config;
|
|
50
51
|
if (tools.length === 0) throw new Error("At least one tool must be provided to createCodeModeTool");
|
|
51
52
|
const staticBindings = toolsToBindings(tools, "external_");
|
|
52
53
|
const secretDedupCache = /* @__PURE__ */ new Set();
|
|
@@ -61,14 +62,39 @@ function createCodeModeTool(config) {
|
|
|
61
62
|
outputSchema: executeTypescriptOutputSchema
|
|
62
63
|
}).server(async (input, toolContext) => {
|
|
63
64
|
const { typescriptCode } = input;
|
|
65
|
+
const startedAt = Date.now();
|
|
64
66
|
const emitCustomEvent = toolContext?.emitCustomEvent || (() => {});
|
|
65
|
-
|
|
67
|
+
const finish = (result, phase) => {
|
|
68
|
+
const durationMs = Date.now() - startedAt;
|
|
69
|
+
const payload = {
|
|
70
|
+
timestamp: Date.now(),
|
|
71
|
+
durationMs,
|
|
72
|
+
phase,
|
|
73
|
+
success: result.success,
|
|
74
|
+
logCount: result.logs?.length ?? 0,
|
|
75
|
+
error: result.error ? {
|
|
76
|
+
name: result.error.name,
|
|
77
|
+
message: result.error.message,
|
|
78
|
+
...result.error.stack !== void 0 && { stack: result.error.stack },
|
|
79
|
+
...result.error.line !== void 0 && { line: result.error.line }
|
|
80
|
+
} : void 0
|
|
81
|
+
};
|
|
82
|
+
emitCustomEvent("code_mode:execution_finished", payload);
|
|
83
|
+
if (!result.success) console.error("[code-mode] execute_typescript failed", payload);
|
|
84
|
+
else if (typeof process !== "undefined" && process.env?.CODE_MODE_DEBUG === "1") console.info("[code-mode] execute_typescript ok", {
|
|
85
|
+
durationMs,
|
|
86
|
+
phase,
|
|
87
|
+
logCount: payload.logCount
|
|
88
|
+
});
|
|
89
|
+
return result;
|
|
90
|
+
};
|
|
91
|
+
if (!typescriptCode || typeof typescriptCode !== "string") return finish({
|
|
66
92
|
success: false,
|
|
67
93
|
error: {
|
|
68
94
|
message: "typescriptCode must be a non-empty string",
|
|
69
95
|
name: "ValidationError"
|
|
70
96
|
}
|
|
71
|
-
};
|
|
97
|
+
}, "validate-input");
|
|
72
98
|
let isolateContext = null;
|
|
73
99
|
emitCustomEvent("code_mode:execution_started", {
|
|
74
100
|
timestamp: Date.now(),
|
|
@@ -79,29 +105,42 @@ function createCodeModeTool(config) {
|
|
|
79
105
|
try {
|
|
80
106
|
strippedCode = await transpile(typescriptCode);
|
|
81
107
|
} catch (error) {
|
|
82
|
-
return {
|
|
108
|
+
return finish({
|
|
83
109
|
success: false,
|
|
84
110
|
error: {
|
|
85
111
|
message: error instanceof Error ? error.message : String(error),
|
|
86
|
-
name: "TypeScriptError"
|
|
112
|
+
name: "TypeScriptError",
|
|
113
|
+
...error instanceof Error && error.stack !== void 0 && { stack: error.stack }
|
|
87
114
|
}
|
|
88
|
-
};
|
|
115
|
+
}, "transpile");
|
|
89
116
|
}
|
|
90
|
-
const
|
|
91
|
-
const
|
|
92
|
-
if (
|
|
117
|
+
const snippetBindings = getSnippetBindings ? await getSnippetBindings() : {};
|
|
118
|
+
const snippetBindingValues = Object.values(snippetBindings);
|
|
119
|
+
if (snippetBindingValues.length > 0) warnIfBindingsExposeSecrets(snippetBindingValues, {
|
|
93
120
|
handler: onSecretParameter,
|
|
94
121
|
dedupCache: secretDedupCache
|
|
95
122
|
});
|
|
96
|
-
const
|
|
123
|
+
const allBindings = {
|
|
97
124
|
...staticBindings,
|
|
98
|
-
...
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
125
|
+
...snippetBindings
|
|
126
|
+
};
|
|
127
|
+
const eventAwareBindings = createEventAwareBindings(allBindings, emitCustomEvent);
|
|
128
|
+
try {
|
|
129
|
+
isolateContext = await driver.createContext({
|
|
130
|
+
bindings: eventAwareBindings,
|
|
131
|
+
timeout,
|
|
132
|
+
memoryLimit
|
|
133
|
+
});
|
|
134
|
+
} catch (error) {
|
|
135
|
+
return finish({
|
|
136
|
+
success: false,
|
|
137
|
+
error: {
|
|
138
|
+
message: error instanceof Error ? error.message : String(error),
|
|
139
|
+
name: error instanceof Error ? error.name : "CreateContextError",
|
|
140
|
+
...error instanceof Error && error.stack !== void 0 && { stack: error.stack }
|
|
141
|
+
}
|
|
142
|
+
}, "create-context");
|
|
143
|
+
}
|
|
105
144
|
const executionResult = await isolateContext.execute(strippedCode);
|
|
106
145
|
if (executionResult.logs && executionResult.logs.length > 0) for (const log of executionResult.logs) {
|
|
107
146
|
let level = "log";
|
|
@@ -122,27 +161,32 @@ function createCodeModeTool(config) {
|
|
|
122
161
|
timestamp: Date.now()
|
|
123
162
|
});
|
|
124
163
|
}
|
|
125
|
-
if (executionResult.success) return {
|
|
164
|
+
if (executionResult.success) return finish({
|
|
126
165
|
success: true,
|
|
127
166
|
result: executionResult.value,
|
|
128
167
|
logs: executionResult.logs
|
|
129
|
-
};
|
|
130
|
-
|
|
168
|
+
}, "execute");
|
|
169
|
+
return finish({
|
|
131
170
|
success: false,
|
|
132
171
|
error: executionResult.error ? {
|
|
133
172
|
message: executionResult.error.message,
|
|
134
|
-
name: executionResult.error.name
|
|
135
|
-
|
|
173
|
+
name: executionResult.error.name,
|
|
174
|
+
...executionResult.error.stack !== void 0 && { stack: executionResult.error.stack }
|
|
175
|
+
} : {
|
|
176
|
+
message: "Unknown execution error",
|
|
177
|
+
name: "UnknownError"
|
|
178
|
+
},
|
|
136
179
|
logs: executionResult.logs
|
|
137
|
-
};
|
|
180
|
+
}, "execute");
|
|
138
181
|
} catch (error) {
|
|
139
|
-
return {
|
|
182
|
+
return finish({
|
|
140
183
|
success: false,
|
|
141
184
|
error: {
|
|
142
185
|
message: error instanceof Error ? error.message : String(error),
|
|
143
|
-
name: error instanceof Error ? error.name : "Error"
|
|
186
|
+
name: error instanceof Error ? error.name : "Error",
|
|
187
|
+
...error instanceof Error && error.stack !== void 0 && { stack: error.stack }
|
|
144
188
|
}
|
|
145
|
-
};
|
|
189
|
+
}, "unhandled");
|
|
146
190
|
} finally {
|
|
147
191
|
if (isolateContext) await isolateContext.dispose();
|
|
148
192
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-code-mode-tool.js","names":[],"sources":["../../src/create-code-mode-tool.ts"],"sourcesContent":["import { z } from 'zod'\nimport { toolDefinition } from '@tanstack/ai'\nimport {\n createEventAwareBindings,\n toolsToBindings,\n} from './bindings/tool-to-binding'\nimport { stripTypeScript } from './strip-typescript'\nimport { warnIfBindingsExposeSecrets } from './validate-bindings'\nimport type { ServerTool, ToolExecutionContext } from '@tanstack/ai'\nimport type {\n CodeModeTool,\n CodeModeToolConfig,\n CodeModeToolResult,\n IsolateContext,\n} from './types'\n\n/**\n * Schema for the execute_typescript tool input\n */\nconst executeTypescriptInputSchema = z.object({\n typescriptCode: z\n .string()\n .describe(\n 'TypeScript code to execute in the sandbox. ' +\n 'Use external_* functions to call available APIs. ' +\n 'Return a value to pass results back.',\n ),\n})\n\n/**\n * Schema for the execute_typescript tool output\n */\nconst executeTypescriptOutputSchema = z.object({\n success: z.boolean().describe('Whether execution completed without errors'),\n result: z\n .unknown()\n .optional()\n .describe('Return value from the executed code'),\n logs: z\n .array(z.string())\n .optional()\n .describe('Console output captured during execution'),\n error: z\n .object({\n message: z.string(),\n name: z.string().optional(),\n line: z.number().optional(),\n })\n .optional()\n .describe('Error details if execution failed'),\n})\n\nexport type ExecuteTypescriptInput = z.infer<\n typeof executeTypescriptInputSchema\n>\nexport type ExecuteTypescriptOutput = z.infer<\n typeof executeTypescriptOutputSchema\n>\n\n/**\n * Create an execute_typescript tool that can be used alongside other agent tools.\n *\n * This tool allows an LLM to execute TypeScript code in a secure sandbox.\n * Tools passed in the config become `external_*` functions available inside the sandbox.\n *\n * @example\n * ```typescript\n * import { createCodeMode } from '@tanstack/ai-code-mode'\n * import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node'\n *\n * const { tool, systemPrompt } = createCodeMode({\n * driver: createNodeIsolateDriver(),\n * tools: [weatherTool, dbTool], // Become external_fetchWeather, external_dbQuery\n * timeout: 30000,\n * })\n *\n * chat({\n * systemPrompts: [myPrompt, systemPrompt],\n * tools: [tool, searchTool, emailTool],\n * messages,\n * })\n * ```\n */\nexport function createCodeModeTool(\n config: CodeModeToolConfig,\n): ServerTool<\n typeof executeTypescriptInputSchema,\n typeof executeTypescriptOutputSchema,\n 'execute_typescript'\n> {\n const {\n driver,\n tools,\n timeout = 30000,\n memoryLimit = 128,\n getSkillBindings,\n onSecretParameter,\n transpile = stripTypeScript,\n } = config\n\n // Validate tools\n if (tools.length === 0) {\n throw new Error('At least one tool must be provided to createCodeModeTool')\n }\n\n // Transform tools to bindings with external_ prefix (static bindings)\n const staticBindings = toolsToBindings(tools, 'external_')\n\n // Shared across static + dynamic (skill) binding scans so a given\n // (toolName, paramPath) pair surfaces at most once per code-mode instance.\n const secretDedupCache = new Set<string>()\n\n warnIfBindingsExposeSecrets(Object.values(staticBindings), {\n handler: onSecretParameter,\n dedupCache: secretDedupCache,\n })\n\n // Create the tool definition\n const definition = toolDefinition({\n name: 'execute_typescript' as const,\n description: buildToolDescription(tools),\n inputSchema: executeTypescriptInputSchema,\n outputSchema: executeTypescriptOutputSchema,\n })\n\n // Return server tool with execute function that accepts context\n return definition.server(\n async (\n input,\n toolContext?: ToolExecutionContext,\n ): Promise<CodeModeToolResult> => {\n const { typescriptCode } = input\n\n // Get emitCustomEvent from context or use no-op\n const emitCustomEvent = toolContext?.emitCustomEvent || (() => {})\n\n if (!typescriptCode || typeof typescriptCode !== 'string') {\n return {\n success: false,\n error: {\n message: 'typescriptCode must be a non-empty string',\n name: 'ValidationError',\n },\n }\n }\n\n // Create a fresh sandbox context for this execution\n let isolateContext: IsolateContext | null = null\n\n // Emit execution started event immediately\n emitCustomEvent('code_mode:execution_started', {\n timestamp: Date.now(),\n codeLength: typescriptCode.length,\n })\n\n try {\n // Step 1: Strip TypeScript (also serves as syntax validation via the\n // transpiler — sucrase by default, or a user-supplied `transpile`)\n let strippedCode: string\n try {\n strippedCode = await transpile(typescriptCode)\n } catch (error) {\n // Type/syntax error from the transpiler\n return {\n success: false,\n error: {\n message: error instanceof Error ? error.message : String(error),\n name: 'TypeScriptError',\n },\n }\n }\n\n // Step 2: Get dynamic skill bindings if available\n const skillBindings = getSkillBindings ? await getSkillBindings() : {}\n\n // Scan dynamic bindings too — their schemas are equally in-scope for\n // the same exfiltration threat. Dedup cache prevents repeat warnings\n // when the same binding reappears across executions.\n const skillBindingValues = Object.values(skillBindings)\n if (skillBindingValues.length > 0) {\n warnIfBindingsExposeSecrets(skillBindingValues, {\n handler: onSecretParameter,\n dedupCache: secretDedupCache,\n })\n }\n\n // Step 3: Merge static and dynamic bindings, then wrap with event awareness\n const allBindings = { ...staticBindings, ...skillBindings }\n const eventAwareBindings = createEventAwareBindings(\n allBindings,\n emitCustomEvent,\n )\n\n // Step 4: Create sandbox context with event-aware bindings\n isolateContext = await driver.createContext({\n bindings: eventAwareBindings,\n timeout,\n memoryLimit,\n })\n\n // Step 5: Execute the code in the sandbox\n const executionResult = await isolateContext.execute(strippedCode)\n\n // Emit console logs as custom events\n if (executionResult.logs && executionResult.logs.length > 0) {\n for (const log of executionResult.logs) {\n // Parse log level from prefix (added by sandbox console implementation)\n let level: 'log' | 'warn' | 'error' | 'info' = 'log'\n let message = log\n\n if (log.startsWith('ERROR: ')) {\n level = 'error'\n message = log.slice(7)\n } else if (log.startsWith('WARN: ')) {\n level = 'warn'\n message = log.slice(6)\n } else if (log.startsWith('INFO: ')) {\n level = 'info'\n message = log.slice(6)\n }\n\n emitCustomEvent('code_mode:console', {\n level,\n message,\n timestamp: Date.now(),\n })\n }\n }\n\n if (executionResult.success) {\n return {\n success: true,\n result: executionResult.value,\n logs: executionResult.logs,\n }\n } else {\n return {\n success: false,\n error: executionResult.error\n ? {\n message: executionResult.error.message,\n name: executionResult.error.name,\n }\n : { message: 'Unknown execution error' },\n logs: executionResult.logs,\n }\n }\n } catch (error) {\n return {\n success: false,\n error: {\n message: error instanceof Error ? error.message : String(error),\n name: error instanceof Error ? error.name : 'Error',\n },\n }\n } finally {\n // Always clean up the sandbox context\n if (isolateContext) {\n await isolateContext.dispose()\n }\n }\n },\n )\n}\n\n/**\n * Build the tool description including available external functions\n */\nfunction buildToolDescription(tools: Array<CodeModeTool>): string {\n const eager = tools.filter((t) => !t.lazy)\n const hasLazy = tools.some((t) => t.lazy)\n const externalFunctions = eager.map((t) => `external_${t.name}`).join(', ')\n\n const discoverable = hasLazy\n ? ` Additional functions can be discovered via the discover_tools tool.`\n : ''\n\n return (\n `Execute TypeScript code in a secure sandbox environment. ` +\n `The code can use these external API functions: ${externalFunctions}.${discoverable} ` +\n `All external_* calls are async and must be awaited. ` +\n `Return a value to pass results back. Use console.log() for debugging.`\n )\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"create-code-mode-tool.js","names":[],"sources":["../../src/create-code-mode-tool.ts"],"sourcesContent":["import { z } from 'zod'\nimport { toolDefinition } from '@tanstack/ai'\nimport {\n createEventAwareBindings,\n toolsToBindings,\n} from './bindings/tool-to-binding'\nimport { stripTypeScript } from './strip-typescript'\nimport { warnIfBindingsExposeSecrets } from './validate-bindings'\nimport type { ServerTool, ToolExecutionContext } from '@tanstack/ai'\nimport type {\n CodeModeTool,\n CodeModeToolConfig,\n CodeModeToolResult,\n IsolateContext,\n} from './types'\n\n/**\n * Schema for the execute_typescript tool input\n */\nconst executeTypescriptInputSchema = z.object({\n typescriptCode: z\n .string()\n .describe(\n 'TypeScript code to execute in the sandbox. ' +\n 'Use external_* functions to call available APIs. ' +\n 'Return a value to pass results back.',\n ),\n})\n\n/**\n * Schema for the execute_typescript tool output\n */\nconst executeTypescriptOutputSchema = z.object({\n success: z.boolean().describe('Whether execution completed without errors'),\n result: z\n .unknown()\n .optional()\n .describe('Return value from the executed code'),\n logs: z\n .array(z.string())\n .optional()\n .describe('Console output captured during execution'),\n error: z\n .object({\n message: z.string(),\n name: z.string().optional(),\n line: z.number().optional(),\n stack: z.string().optional(),\n })\n .optional()\n .describe('Error details if execution failed'),\n})\n\nexport type ExecuteTypescriptInput = z.infer<\n typeof executeTypescriptInputSchema\n>\nexport type ExecuteTypescriptOutput = z.infer<\n typeof executeTypescriptOutputSchema\n>\n\n/**\n * Create an execute_typescript tool that can be used alongside other agent tools.\n *\n * This tool allows an LLM to execute TypeScript code in a secure sandbox.\n * Tools passed in the config become `external_*` functions available inside the sandbox.\n *\n * @example\n * ```typescript\n * import { createCodeMode } from '@tanstack/ai-code-mode'\n * import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node'\n *\n * const { tool, systemPrompt } = createCodeMode({\n * driver: createNodeIsolateDriver(),\n * tools: [weatherTool, dbTool], // Become external_fetchWeather, external_dbQuery\n * timeout: 30000,\n * })\n *\n * chat({\n * systemPrompts: [myPrompt, systemPrompt],\n * tools: [tool, searchTool, emailTool],\n * messages,\n * })\n * ```\n */\nexport function createCodeModeTool(\n config: CodeModeToolConfig,\n): ServerTool<\n typeof executeTypescriptInputSchema,\n typeof executeTypescriptOutputSchema,\n 'execute_typescript'\n> {\n const {\n driver,\n tools,\n timeout = 30000,\n memoryLimit = 128,\n getSnippetBindings,\n onSecretParameter,\n transpile = stripTypeScript,\n } = config\n\n // Validate tools\n if (tools.length === 0) {\n throw new Error('At least one tool must be provided to createCodeModeTool')\n }\n\n // Transform tools to bindings with external_ prefix (static bindings)\n const staticBindings = toolsToBindings(tools, 'external_')\n\n // Shared across static + dynamic (snippet) binding scans so a given\n // (toolName, paramPath) pair surfaces at most once per code-mode instance.\n const secretDedupCache = new Set<string>()\n\n warnIfBindingsExposeSecrets(Object.values(staticBindings), {\n handler: onSecretParameter,\n dedupCache: secretDedupCache,\n })\n\n // Create the tool definition\n const definition = toolDefinition({\n name: 'execute_typescript' as const,\n description: buildToolDescription(tools),\n inputSchema: executeTypescriptInputSchema,\n outputSchema: executeTypescriptOutputSchema,\n })\n\n // Return server tool with execute function that accepts context\n return definition.server(\n async (\n input,\n toolContext?: ToolExecutionContext,\n ): Promise<CodeModeToolResult> => {\n const { typescriptCode } = input\n const startedAt = Date.now()\n\n // Get emitCustomEvent from context or use no-op\n const emitCustomEvent = toolContext?.emitCustomEvent || (() => {})\n\n const finish = (\n result: CodeModeToolResult,\n phase: string,\n ): CodeModeToolResult => {\n const durationMs = Date.now() - startedAt\n const payload = {\n timestamp: Date.now(),\n durationMs,\n phase,\n success: result.success,\n logCount: result.logs?.length ?? 0,\n error: result.error\n ? {\n name: result.error.name,\n message: result.error.message,\n ...(result.error.stack !== undefined && {\n stack: result.error.stack,\n }),\n ...(result.error.line !== undefined && {\n line: result.error.line,\n }),\n }\n : undefined,\n }\n emitCustomEvent('code_mode:execution_finished', payload)\n if (!result.success) {\n console.error('[code-mode] execute_typescript failed', payload)\n } else if (\n typeof process !== 'undefined' &&\n process.env?.CODE_MODE_DEBUG === '1'\n ) {\n console.info('[code-mode] execute_typescript ok', {\n durationMs,\n phase,\n logCount: payload.logCount,\n })\n }\n return result\n }\n\n if (!typescriptCode || typeof typescriptCode !== 'string') {\n return finish(\n {\n success: false,\n error: {\n message: 'typescriptCode must be a non-empty string',\n name: 'ValidationError',\n },\n },\n 'validate-input',\n )\n }\n\n // Create a fresh sandbox context for this execution\n let isolateContext: IsolateContext | null = null\n\n // Emit execution started event immediately\n emitCustomEvent('code_mode:execution_started', {\n timestamp: Date.now(),\n codeLength: typescriptCode.length,\n })\n\n try {\n // Step 1: Strip TypeScript (also serves as syntax validation via the\n // transpiler — sucrase by default, or a user-supplied `transpile`)\n let strippedCode: string\n try {\n strippedCode = await transpile(typescriptCode)\n } catch (error) {\n // Type/syntax error from the transpiler\n return finish(\n {\n success: false,\n error: {\n message: error instanceof Error ? error.message : String(error),\n name: 'TypeScriptError',\n ...(error instanceof Error &&\n error.stack !== undefined && { stack: error.stack }),\n },\n },\n 'transpile',\n )\n }\n\n // Step 2: Get dynamic snippet bindings if available\n const snippetBindings = getSnippetBindings\n ? await getSnippetBindings()\n : {}\n\n // Scan dynamic bindings too — their schemas are equally in-scope for\n // the same exfiltration threat. Dedup cache prevents repeat warnings\n // when the same binding reappears across executions.\n const snippetBindingValues = Object.values(snippetBindings)\n if (snippetBindingValues.length > 0) {\n warnIfBindingsExposeSecrets(snippetBindingValues, {\n handler: onSecretParameter,\n dedupCache: secretDedupCache,\n })\n }\n\n // Step 3: Merge static and dynamic bindings, then wrap with event awareness\n const allBindings = { ...staticBindings, ...snippetBindings }\n const eventAwareBindings = createEventAwareBindings(\n allBindings,\n emitCustomEvent,\n )\n\n // Step 4: Create sandbox context with event-aware bindings\n try {\n isolateContext = await driver.createContext({\n bindings: eventAwareBindings,\n timeout,\n memoryLimit,\n })\n } catch (error) {\n return finish(\n {\n success: false,\n error: {\n message: error instanceof Error ? error.message : String(error),\n name:\n error instanceof Error ? error.name : 'CreateContextError',\n ...(error instanceof Error &&\n error.stack !== undefined && { stack: error.stack }),\n },\n },\n 'create-context',\n )\n }\n\n // Step 5: Execute the code in the sandbox\n const executionResult = await isolateContext.execute(strippedCode)\n\n // Emit console logs as custom events\n if (executionResult.logs && executionResult.logs.length > 0) {\n for (const log of executionResult.logs) {\n // Parse log level from prefix (added by sandbox console implementation)\n let level: 'log' | 'warn' | 'error' | 'info' = 'log'\n let message = log\n\n if (log.startsWith('ERROR: ')) {\n level = 'error'\n message = log.slice(7)\n } else if (log.startsWith('WARN: ')) {\n level = 'warn'\n message = log.slice(6)\n } else if (log.startsWith('INFO: ')) {\n level = 'info'\n message = log.slice(6)\n }\n\n emitCustomEvent('code_mode:console', {\n level,\n message,\n timestamp: Date.now(),\n })\n }\n }\n\n if (executionResult.success) {\n return finish(\n {\n success: true,\n result: executionResult.value,\n logs: executionResult.logs,\n },\n 'execute',\n )\n }\n\n return finish(\n {\n success: false,\n error: executionResult.error\n ? {\n message: executionResult.error.message,\n name: executionResult.error.name,\n ...(executionResult.error.stack !== undefined && {\n stack: executionResult.error.stack,\n }),\n }\n : { message: 'Unknown execution error', name: 'UnknownError' },\n logs: executionResult.logs,\n },\n 'execute',\n )\n } catch (error) {\n return finish(\n {\n success: false,\n error: {\n message: error instanceof Error ? error.message : String(error),\n name: error instanceof Error ? error.name : 'Error',\n ...(error instanceof Error &&\n error.stack !== undefined && { stack: error.stack }),\n },\n },\n 'unhandled',\n )\n } finally {\n // Always clean up the sandbox context\n if (isolateContext) {\n await isolateContext.dispose()\n }\n }\n },\n )\n}\n\n/**\n * Build the tool description including available external functions\n */\nfunction buildToolDescription(tools: Array<CodeModeTool>): string {\n const eager = tools.filter((t) => !t.lazy)\n const hasLazy = tools.some((t) => t.lazy)\n const externalFunctions = eager.map((t) => `external_${t.name}`).join(', ')\n\n const discoverable = hasLazy\n ? ` Additional functions can be discovered via the discover_tools tool.`\n : ''\n\n return (\n `Execute TypeScript code in a secure sandbox environment. ` +\n `The code can use these external API functions: ${externalFunctions}.${discoverable} ` +\n `All external_* calls are async and must be awaited. ` +\n `Return a value to pass results back. Use console.log() for debugging.`\n )\n}\n"],"mappings":"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"}
|
|
@@ -46,7 +46,8 @@ var WRAPPER_END = "___TANSTACK_WRAPPER_END___";
|
|
|
46
46
|
* @throws Error if sucrase fails (e.g., syntax error) or wrapper extraction fails
|
|
47
47
|
*/
|
|
48
48
|
async function stripTypeScript(code) {
|
|
49
|
-
const
|
|
49
|
+
const wrappedCode = `async function ${WRAPPER_START}() {\n${code}\n}; ${WRAPPER_END}`;
|
|
50
|
+
const transformed = transform(wrappedCode, {
|
|
50
51
|
transforms: ["typescript"],
|
|
51
52
|
disableESTransforms: true
|
|
52
53
|
}).code;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"strip-typescript.js","names":[],"sources":["../../src/strip-typescript.ts"],"sourcesContent":["import { transform } from 'sucrase'\n\n// Unique markers for wrapping/unwrapping code\nconst WRAPPER_START = '___TANSTACK_WRAPPER_START___'\nconst WRAPPER_END = '___TANSTACK_WRAPPER_END___'\n\n/**\n * Strip TypeScript syntax from code, converting it to plain JavaScript.\n *\n * This is a safety net to ensure that even if an LLM generates TypeScript\n * code with type annotations, it will be converted to valid JavaScript\n * before being sent to the sandbox for execution.\n *\n * Uses sucrase's pure-JavaScript `transform`, which strips the TypeScript\n * syntax that LLM-generated snippets use in practice:\n * - Type annotations (: string, : number, etc.)\n * - Generic types (Array<T>, Record<K, V>, etc.)\n * - Interface and type declarations\n * - Type assertions\n * - Enums (converted to JavaScript objects)\n *\n * Unlike esbuild, sucrase has no native binary and pulls in no Node-only\n * built-ins on its `transform` path, so this module is safe to bundle for\n * browsers and edge runtimes (Cloudflare Workers/Pages etc.).\n *\n * Limitations vs esbuild: sucrase is a type-stripper, not a down-leveler.\n * `disableESTransforms` leaves modern ECMAScript syntax untouched (the sandbox\n * engines are modern), and sucrase does NOT compile a few exotic constructs:\n * - TypeScript value `namespace`/`module` blocks are DROPPED (not emitted as an\n * IIFE), so referencing the namespace at runtime throws `ReferenceError`.\n * - Decorators and the `accessor` keyword pass through un-lowered, so the\n * sandbox sees invalid syntax.\n * - Post-ES2022 syntax (`using` declarations, RegExp `/v`·`/d` flags) is passed\n * through; it runs on modern V8/Node sandboxes but may fail on older engines\n * (e.g. QuickJS).\n * If you need any of these, supply a heavier (Node-only) transpiler via the\n * `transpile` option on `createCodeModeTool`.\n *\n * The code is wrapped in an async function before transformation to allow\n * top-level `return` and `await` statements, then unwrapped after.\n *\n * Note on errors: sucrase reports syntax errors with a position relative to the\n * *wrapped* code (offset by the one-line wrapper prefix), so any line numbers\n * surfaced downstream (e.g. `CodeModeToolResult.error.line`) are approximate.\n *\n * @param code - TypeScript or JavaScript code\n * @returns Plain JavaScript code with all type syntax removed\n * @throws Error if sucrase fails (e.g., syntax error) or wrapper extraction fails\n */\n// sucrase's transform is synchronous, but we keep the published Promise-returning\n// signature so existing `await stripTypeScript(...)` callers (and a custom async\n// `transpile` hook) stay source-compatible across this swap.\n// eslint-disable-next-line @typescript-eslint/require-await\nexport async function stripTypeScript(code: string): Promise<string> {\n // Wrap the code in an async function to allow top-level return/await.\n // This is necessary because top-level `return` is invalid outside a function.\n const wrappedCode = `async function ${WRAPPER_START}() {\\n${code}\\n}; ${WRAPPER_END}`\n\n const result = transform(wrappedCode, {\n // Only strip/lower TypeScript-specific syntax...\n transforms: ['typescript'],\n // ...and leave modern ECMAScript syntax untouched for the sandbox engines.\n disableESTransforms: true,\n })\n\n // Extract the code from inside the wrapper function\n const transformed = result.code\n\n // Find the function declaration start\n const functionStart = transformed.indexOf(`async function ${WRAPPER_START}()`)\n if (functionStart === -1) {\n throw new Error(\n '[stripTypeScript] Could not find wrapper function start in transformed output',\n )\n }\n\n // Find the opening brace of the function\n const openBrace = transformed.indexOf('{', functionStart)\n if (openBrace === -1) {\n throw new Error(\n '[stripTypeScript] Could not find opening brace in transformed output',\n )\n }\n\n // Find the end marker (regardless of formatting)\n const endMarkerIndex = transformed.indexOf(WRAPPER_END)\n if (endMarkerIndex === -1) {\n throw new Error(\n '[stripTypeScript] Could not find end marker in transformed output',\n )\n }\n\n // Find the closing brace of the function (last } before the end marker)\n // We need to find the } that matches the function opening\n const codeBeforeEndMarker = transformed.substring(\n openBrace + 1,\n endMarkerIndex,\n )\n\n // Find the last } before the end marker, accounting for the semicolon\n // The code will be: ...function body...}; WRAPPER_END or ...};\\nWRAPPER_END\n const closingBraceIndex = codeBeforeEndMarker.lastIndexOf('}')\n\n if (closingBraceIndex === -1) {\n throw new Error(\n '[stripTypeScript] Could not find closing brace in transformed output',\n )\n }\n\n // Extract the function body (between { and })\n const functionBody = codeBeforeEndMarker\n .substring(0, closingBraceIndex)\n .trim()\n\n return functionBody\n}\n"],"mappings":";;AAGA,IAAM,gBAAgB;AACtB,IAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDpB,eAAsB,gBAAgB,MAA+B;
|
|
1
|
+
{"version":3,"file":"strip-typescript.js","names":[],"sources":["../../src/strip-typescript.ts"],"sourcesContent":["import { transform } from 'sucrase'\n\n// Unique markers for wrapping/unwrapping code\nconst WRAPPER_START = '___TANSTACK_WRAPPER_START___'\nconst WRAPPER_END = '___TANSTACK_WRAPPER_END___'\n\n/**\n * Strip TypeScript syntax from code, converting it to plain JavaScript.\n *\n * This is a safety net to ensure that even if an LLM generates TypeScript\n * code with type annotations, it will be converted to valid JavaScript\n * before being sent to the sandbox for execution.\n *\n * Uses sucrase's pure-JavaScript `transform`, which strips the TypeScript\n * syntax that LLM-generated snippets use in practice:\n * - Type annotations (: string, : number, etc.)\n * - Generic types (Array<T>, Record<K, V>, etc.)\n * - Interface and type declarations\n * - Type assertions\n * - Enums (converted to JavaScript objects)\n *\n * Unlike esbuild, sucrase has no native binary and pulls in no Node-only\n * built-ins on its `transform` path, so this module is safe to bundle for\n * browsers and edge runtimes (Cloudflare Workers/Pages etc.).\n *\n * Limitations vs esbuild: sucrase is a type-stripper, not a down-leveler.\n * `disableESTransforms` leaves modern ECMAScript syntax untouched (the sandbox\n * engines are modern), and sucrase does NOT compile a few exotic constructs:\n * - TypeScript value `namespace`/`module` blocks are DROPPED (not emitted as an\n * IIFE), so referencing the namespace at runtime throws `ReferenceError`.\n * - Decorators and the `accessor` keyword pass through un-lowered, so the\n * sandbox sees invalid syntax.\n * - Post-ES2022 syntax (`using` declarations, RegExp `/v`·`/d` flags) is passed\n * through; it runs on modern V8/Node sandboxes but may fail on older engines\n * (e.g. QuickJS).\n * If you need any of these, supply a heavier (Node-only) transpiler via the\n * `transpile` option on `createCodeModeTool`.\n *\n * The code is wrapped in an async function before transformation to allow\n * top-level `return` and `await` statements, then unwrapped after.\n *\n * Note on errors: sucrase reports syntax errors with a position relative to the\n * *wrapped* code (offset by the one-line wrapper prefix), so any line numbers\n * surfaced downstream (e.g. `CodeModeToolResult.error.line`) are approximate.\n *\n * @param code - TypeScript or JavaScript code\n * @returns Plain JavaScript code with all type syntax removed\n * @throws Error if sucrase fails (e.g., syntax error) or wrapper extraction fails\n */\n// sucrase's transform is synchronous, but we keep the published Promise-returning\n// signature so existing `await stripTypeScript(...)` callers (and a custom async\n// `transpile` hook) stay source-compatible across this swap.\n// eslint-disable-next-line @typescript-eslint/require-await\nexport async function stripTypeScript(code: string): Promise<string> {\n // Wrap the code in an async function to allow top-level return/await.\n // This is necessary because top-level `return` is invalid outside a function.\n const wrappedCode = `async function ${WRAPPER_START}() {\\n${code}\\n}; ${WRAPPER_END}`\n\n const result = transform(wrappedCode, {\n // Only strip/lower TypeScript-specific syntax...\n transforms: ['typescript'],\n // ...and leave modern ECMAScript syntax untouched for the sandbox engines.\n disableESTransforms: true,\n })\n\n // Extract the code from inside the wrapper function\n const transformed = result.code\n\n // Find the function declaration start\n const functionStart = transformed.indexOf(`async function ${WRAPPER_START}()`)\n if (functionStart === -1) {\n throw new Error(\n '[stripTypeScript] Could not find wrapper function start in transformed output',\n )\n }\n\n // Find the opening brace of the function\n const openBrace = transformed.indexOf('{', functionStart)\n if (openBrace === -1) {\n throw new Error(\n '[stripTypeScript] Could not find opening brace in transformed output',\n )\n }\n\n // Find the end marker (regardless of formatting)\n const endMarkerIndex = transformed.indexOf(WRAPPER_END)\n if (endMarkerIndex === -1) {\n throw new Error(\n '[stripTypeScript] Could not find end marker in transformed output',\n )\n }\n\n // Find the closing brace of the function (last } before the end marker)\n // We need to find the } that matches the function opening\n const codeBeforeEndMarker = transformed.substring(\n openBrace + 1,\n endMarkerIndex,\n )\n\n // Find the last } before the end marker, accounting for the semicolon\n // The code will be: ...function body...}; WRAPPER_END or ...};\\nWRAPPER_END\n const closingBraceIndex = codeBeforeEndMarker.lastIndexOf('}')\n\n if (closingBraceIndex === -1) {\n throw new Error(\n '[stripTypeScript] Could not find closing brace in transformed output',\n )\n }\n\n // Extract the function body (between { and })\n const functionBody = codeBeforeEndMarker\n .substring(0, closingBraceIndex)\n .trim()\n\n return functionBody\n}\n"],"mappings":";;AAGA,IAAM,gBAAgB;AACtB,IAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAiDpB,eAAsB,gBAAgB,MAA+B;CAGnE,MAAM,cAAc,kBAAkB,cAAc,QAAQ,KAAK,OAAO;CAUxE,MAAM,cARS,UAAU,aAAa;EAEpC,YAAY,CAAC,YAAY;EAEzB,qBAAqB;CACvB,CAGoB,CAAA,CAAO;CAG3B,MAAM,gBAAgB,YAAY,QAAQ,kBAAkB,cAAc,GAAG;CAC7E,IAAI,kBAAkB,IACpB,MAAM,IAAI,MACR,+EACF;CAIF,MAAM,YAAY,YAAY,QAAQ,KAAK,aAAa;CACxD,IAAI,cAAc,IAChB,MAAM,IAAI,MACR,sEACF;CAIF,MAAM,iBAAiB,YAAY,QAAQ,WAAW;CACtD,IAAI,mBAAmB,IACrB,MAAM,IAAI,MACR,mEACF;CAKF,MAAM,sBAAsB,YAAY,UACtC,YAAY,GACZ,cACF;CAIA,MAAM,oBAAoB,oBAAoB,YAAY,GAAG;CAE7D,IAAI,sBAAsB,IACxB,MAAM,IAAI,MACR,sEACF;CAQF,OAJqB,oBAClB,UAAU,GAAG,iBAAiB,CAAC,CAC/B,KAEI;AACT"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -139,20 +139,20 @@ export interface CodeModeToolConfig {
|
|
|
139
139
|
memoryLimit?: number;
|
|
140
140
|
/**
|
|
141
141
|
* Optional function to get additional bindings dynamically.
|
|
142
|
-
* Called at execution time (each execute_typescript call) to get current
|
|
142
|
+
* Called at execution time (each execute_typescript call) to get current snippet bindings.
|
|
143
143
|
* These are merged with the static external_* bindings.
|
|
144
144
|
*
|
|
145
|
-
* @returns Record of
|
|
145
|
+
* @returns Record of snippet bindings with snippet_ prefix
|
|
146
146
|
*
|
|
147
147
|
* @example
|
|
148
148
|
* ```typescript
|
|
149
|
-
*
|
|
150
|
-
* const
|
|
151
|
-
* return
|
|
149
|
+
* getSnippetBindings: async () => {
|
|
150
|
+
* const snippets = await storage.loadAll()
|
|
151
|
+
* return snippetsToBindings(snippets, 'snippet_')
|
|
152
152
|
* }
|
|
153
153
|
* ```
|
|
154
154
|
*/
|
|
155
|
-
|
|
155
|
+
getSnippetBindings?: () => Promise<Record<string, ToolBinding>>;
|
|
156
156
|
/**
|
|
157
157
|
* How to surface tool parameters whose names look like secrets.
|
|
158
158
|
* Defaults to `'warn'` (logs via `console.warn`).
|
|
@@ -186,8 +186,8 @@ export interface CodeModeToolConfig {
|
|
|
186
186
|
* and `await` in its input (the default wraps the code in an async function
|
|
187
187
|
* internally to allow this).
|
|
188
188
|
*
|
|
189
|
-
* NOTE: This only affects `createCodeModeTool`. The
|
|
190
|
-
* (`
|
|
189
|
+
* NOTE: This only affects `createCodeModeTool`. The snippet helpers
|
|
190
|
+
* (`snippetsToTools`, `codeModeWithSnippets` in `@tanstack/ai-code-mode-snippets`)
|
|
191
191
|
* call the exported `stripTypeScript` directly, so they ignore this hook — but
|
|
192
192
|
* they still get the edge-safe sucrase default, so #487 is fixed for them too;
|
|
193
193
|
* they just can't be pointed at a different transpiler.
|
|
@@ -233,6 +233,7 @@ export interface CodeModeToolResult {
|
|
|
233
233
|
message: string;
|
|
234
234
|
name?: string | undefined;
|
|
235
235
|
line?: number | undefined;
|
|
236
|
+
stack?: string | undefined;
|
|
236
237
|
} | undefined;
|
|
237
238
|
}
|
|
238
239
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-code-mode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Secure TypeScript Code Mode for TanStack AI agents to execute sandboxed tool orchestration programs.",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,12 +54,12 @@
|
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"zod": "^3.0.0 || ^4.0.0",
|
|
57
|
-
"@tanstack/ai": "0.
|
|
57
|
+
"@tanstack/ai": "^0.45.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
|
-
"@vitest/coverage-v8": "4.
|
|
60
|
+
"@vitest/coverage-v8": "4.1.10",
|
|
61
61
|
"zod": "^4.2.0",
|
|
62
|
-
"@tanstack/ai": "0.
|
|
62
|
+
"@tanstack/ai": "0.45.0"
|
|
63
63
|
},
|
|
64
64
|
"scripts": {
|
|
65
65
|
"build": "vite build",
|
|
@@ -3,9 +3,10 @@ name: ai-code-mode
|
|
|
3
3
|
description: >
|
|
4
4
|
LLM-generated TypeScript execution in sandboxed environments:
|
|
5
5
|
createCodeModeTool() with isolate drivers (createNodeIsolateDriver,
|
|
6
|
-
createQuickJSIsolateDriver,
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
createQuickJSIsolateDriver, createQuickJSBunIsolateDriver,
|
|
7
|
+
createCloudflareIsolateDriver),
|
|
8
|
+
codeModeWithSnippets() for persistent snippet libraries, trust strategies,
|
|
9
|
+
snippet storage (FileSystem, LocalStorage, InMemory, Mongo), client-side
|
|
9
10
|
execution progress via code_mode:* custom events in useChat.
|
|
10
11
|
type: core
|
|
11
12
|
library: tanstack-ai
|
|
@@ -13,7 +14,7 @@ library_version: '0.3.8'
|
|
|
13
14
|
sources:
|
|
14
15
|
- 'TanStack/ai:docs/code-mode/code-mode.md'
|
|
15
16
|
- 'TanStack/ai:docs/code-mode/code-mode-isolates.md'
|
|
16
|
-
- 'TanStack/ai:docs/code-mode/code-mode-with-
|
|
17
|
+
- 'TanStack/ai:docs/code-mode/code-mode-with-snippets.md'
|
|
17
18
|
- 'TanStack/ai:docs/code-mode/client-integration.md'
|
|
18
19
|
- 'TanStack/ai:docs/code-mode/lazy-tools.md'
|
|
19
20
|
---
|
|
@@ -90,7 +91,7 @@ const stream = chat({
|
|
|
90
91
|
|
|
91
92
|
### 1. Choosing an Isolate Driver
|
|
92
93
|
|
|
93
|
-
|
|
94
|
+
Four drivers implement the `IsolateDriver` interface. All are interchangeable.
|
|
94
95
|
|
|
95
96
|
**Node.js** (`createNodeIsolateDriver`) -- Full V8 with JIT. Fastest option. Requires `isolated-vm` native C++ addon.
|
|
96
97
|
|
|
@@ -116,6 +117,18 @@ const driver = createQuickJSIsolateDriver({
|
|
|
116
117
|
})
|
|
117
118
|
```
|
|
118
119
|
|
|
120
|
+
**QuickJS Bun** (`createQuickJSBunIsolateDriver`) -- Native QuickJS on the Bun runtime via `bun:ffi`. Requires Bun >= 1.3.14 (throws a descriptive error on Node.js). No native deps or build step. Each context gets a dedicated QuickJS runtime with its own memory limit, stack size, and interrupt-based timeout. Recommended QuickJS option on Bun, where the WASM driver's asyncify bridge is unreliable for async host tool calls.
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
import { createQuickJSBunIsolateDriver } from '@tanstack/ai-isolate-quickjs-bun'
|
|
124
|
+
|
|
125
|
+
const driver = createQuickJSBunIsolateDriver({
|
|
126
|
+
memoryLimit: 128, // MB, default 128
|
|
127
|
+
timeout: 30_000, // ms, default 30000
|
|
128
|
+
maxStackSize: 524288, // bytes, default 512 KiB
|
|
129
|
+
})
|
|
130
|
+
```
|
|
131
|
+
|
|
119
132
|
**Cloudflare** (`createCloudflareIsolateDriver`) -- Edge execution via a deployed Cloudflare Worker. Requires a `workerUrl` pointing to your deployed worker. Network latency on each tool call.
|
|
120
133
|
|
|
121
134
|
```typescript
|
|
@@ -129,29 +142,30 @@ const driver = createCloudflareIsolateDriver({
|
|
|
129
142
|
})
|
|
130
143
|
```
|
|
131
144
|
|
|
132
|
-
| Driver
|
|
133
|
-
|
|
|
134
|
-
| Node
|
|
135
|
-
| QuickJS
|
|
136
|
-
|
|
|
145
|
+
| Driver | Best for | Native deps | Browser support | Performance |
|
|
146
|
+
| ----------- | --------------------------- | --------------- | --------------- | --------------------- |
|
|
147
|
+
| Node | Server-side Node.js | Yes (C++ addon) | No | Fast (V8 JIT) |
|
|
148
|
+
| QuickJS | Browsers, edge, portability | None (WASM) | Yes | Slower (interpreted) |
|
|
149
|
+
| QuickJS Bun | Bun servers | None | No | Fast (native QuickJS) |
|
|
150
|
+
| Cloudflare | Edge deployments | None | N/A | Fast (V8 on edge) |
|
|
137
151
|
|
|
138
|
-
### 2. Adding Persistent
|
|
152
|
+
### 2. Adding Persistent Snippets with codeModeWithSnippets()
|
|
139
153
|
|
|
140
|
-
|
|
154
|
+
Snippets let the LLM save reusable code snippets. On future requests, relevant snippets are loaded and exposed as callable tools.
|
|
141
155
|
|
|
142
156
|
```typescript
|
|
143
157
|
import { chat, maxIterations } from '@tanstack/ai'
|
|
144
158
|
import { createNodeIsolateDriver } from '@tanstack/ai-isolate-node'
|
|
145
|
-
import {
|
|
146
|
-
import {
|
|
159
|
+
import { codeModeWithSnippets } from '@tanstack/ai-code-mode-snippets'
|
|
160
|
+
import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage'
|
|
147
161
|
import {
|
|
148
162
|
createDefaultTrustStrategy,
|
|
149
163
|
createAlwaysTrustedStrategy,
|
|
150
164
|
createCustomTrustStrategy,
|
|
151
|
-
} from '@tanstack/ai-code-mode-
|
|
165
|
+
} from '@tanstack/ai-code-mode-snippets'
|
|
152
166
|
import { openaiText } from '@tanstack/ai-openai'
|
|
153
167
|
|
|
154
|
-
// Trust strategies control how
|
|
168
|
+
// Trust strategies control how snippets earn trust through executions
|
|
155
169
|
// Default: untrusted -> provisional (10+ runs, >=90%) -> trusted (100+ runs, >=95%)
|
|
156
170
|
// Relaxed: untrusted -> provisional (3+ runs, >=80%) -> trusted (10+ runs, >=90%)
|
|
157
171
|
// Always trusted: immediately trusted (dev/testing)
|
|
@@ -159,26 +173,26 @@ import { openaiText } from '@tanstack/ai-openai'
|
|
|
159
173
|
const trustStrategy = createDefaultTrustStrategy()
|
|
160
174
|
|
|
161
175
|
// Storage options: file system (production) or memory (testing)
|
|
162
|
-
const storage =
|
|
163
|
-
directory: './.
|
|
176
|
+
const storage = createFileSnippetStorage({
|
|
177
|
+
directory: './.snippets',
|
|
164
178
|
trustStrategy,
|
|
165
179
|
})
|
|
166
180
|
|
|
167
181
|
const driver = createNodeIsolateDriver()
|
|
168
182
|
|
|
169
|
-
// High-level API: automatic LLM-based
|
|
170
|
-
const { toolsRegistry, systemPrompt,
|
|
171
|
-
await
|
|
183
|
+
// High-level API: automatic LLM-based snippet selection
|
|
184
|
+
const { toolsRegistry, systemPrompt, selectedSnippets } =
|
|
185
|
+
await codeModeWithSnippets({
|
|
172
186
|
config: {
|
|
173
187
|
driver,
|
|
174
188
|
tools: [myTool1, myTool2],
|
|
175
189
|
timeout: 60_000,
|
|
176
190
|
memoryLimit: 128,
|
|
177
191
|
},
|
|
178
|
-
adapter: openaiText('gpt-4o-mini'), // cheap model for
|
|
179
|
-
|
|
192
|
+
adapter: openaiText('gpt-4o-mini'), // cheap model for snippet selection
|
|
193
|
+
snippets: {
|
|
180
194
|
storage,
|
|
181
|
-
|
|
195
|
+
maxSnippetsInContext: 5,
|
|
182
196
|
},
|
|
183
197
|
messages,
|
|
184
198
|
})
|
|
@@ -192,7 +206,7 @@ const stream = chat({
|
|
|
192
206
|
})
|
|
193
207
|
```
|
|
194
208
|
|
|
195
|
-
The registry includes: `execute_typescript`, `
|
|
209
|
+
The registry includes: `execute_typescript`, `search_snippets`, `get_snippet`, `register_snippet`, and one tool per selected snippet.
|
|
196
210
|
|
|
197
211
|
Custom trust strategy example:
|
|
198
212
|
|
|
@@ -207,13 +221,13 @@ const strategy = createCustomTrustStrategy({
|
|
|
207
221
|
Storage implementations:
|
|
208
222
|
|
|
209
223
|
```typescript
|
|
210
|
-
// File storage (production) -- persists
|
|
211
|
-
import {
|
|
212
|
-
const fileStorage =
|
|
224
|
+
// File storage (production) -- persists snippets as files on disk
|
|
225
|
+
import { createFileSnippetStorage } from '@tanstack/ai-code-mode-snippets/storage'
|
|
226
|
+
const fileStorage = createFileSnippetStorage({ directory: './.snippets' })
|
|
213
227
|
|
|
214
228
|
// Memory storage (testing) -- in-memory, lost on restart
|
|
215
|
-
import {
|
|
216
|
-
const memStorage =
|
|
229
|
+
import { createMemorySnippetStorage } from '@tanstack/ai-code-mode-snippets/storage'
|
|
230
|
+
const memStorage = createMemorySnippetStorage()
|
|
217
231
|
```
|
|
218
232
|
|
|
219
233
|
### 3. Client-Side Execution Progress Display
|
|
@@ -320,14 +334,14 @@ The `onCustomEvent` callback signature is identical across all framework integra
|
|
|
320
334
|
(eventType: string, data: unknown, context: { toolCallId?: string }) => void
|
|
321
335
|
```
|
|
322
336
|
|
|
323
|
-
|
|
337
|
+
Snippet-specific events (when using `codeModeWithSnippets`):
|
|
324
338
|
|
|
325
|
-
| Event
|
|
326
|
-
|
|
|
327
|
-
| `code_mode:
|
|
328
|
-
| `code_mode:
|
|
329
|
-
| `code_mode:
|
|
330
|
-
| `
|
|
339
|
+
| Event | When | Key fields |
|
|
340
|
+
| -------------------------- | -------------------- | ------------------------------- |
|
|
341
|
+
| `code_mode:snippet_call` | Snippet tool invoked | `snippet`, `input`, `timestamp` |
|
|
342
|
+
| `code_mode:snippet_result` | Snippet completed | `snippet`, `result`, `duration` |
|
|
343
|
+
| `code_mode:snippet_error` | Snippet failed | `snippet`, `error`, `duration` |
|
|
344
|
+
| `snippet:registered` | New snippet saved | `id`, `name`, `description` |
|
|
331
345
|
|
|
332
346
|
### 4. Lazy Tools
|
|
333
347
|
|
|
@@ -495,10 +509,11 @@ Source: ai-isolate-node source (probeIsolatedVm implementation)
|
|
|
495
509
|
|
|
496
510
|
### MEDIUM: Expecting identical behavior across isolate drivers
|
|
497
511
|
|
|
498
|
-
The
|
|
512
|
+
The four drivers have different capabilities. Same code may work in Node but fail elsewhere.
|
|
499
513
|
|
|
500
514
|
- **Node**: Full V8 support, JIT compilation, configurable memory limit
|
|
501
515
|
- **QuickJS**: Interpreted, limited stdlib (no File I/O), configurable stack size, asyncified execution (serialized through global queue)
|
|
516
|
+
- **QuickJS Bun**: Bun runtime only (throws on Node.js), native QuickJS via `bun:ffi`, dedicated runtime per context with per-context memory/stack limits and normalized `MemoryLimitError`/`StackOverflowError`/`TimeoutError`
|
|
502
517
|
- **Cloudflare**: Network latency per tool call round-trip, `maxToolRounds` limit (default 10), requires deployed worker with `UNSAFE_EVAL` or `eval` unsafe binding
|
|
503
518
|
|
|
504
519
|
Test generated code against your target driver. If you need portability, target QuickJS's subset.
|
|
@@ -45,6 +45,7 @@ const executeTypescriptOutputSchema = z.object({
|
|
|
45
45
|
message: z.string(),
|
|
46
46
|
name: z.string().optional(),
|
|
47
47
|
line: z.number().optional(),
|
|
48
|
+
stack: z.string().optional(),
|
|
48
49
|
})
|
|
49
50
|
.optional()
|
|
50
51
|
.describe('Error details if execution failed'),
|
|
@@ -93,7 +94,7 @@ export function createCodeModeTool(
|
|
|
93
94
|
tools,
|
|
94
95
|
timeout = 30000,
|
|
95
96
|
memoryLimit = 128,
|
|
96
|
-
|
|
97
|
+
getSnippetBindings,
|
|
97
98
|
onSecretParameter,
|
|
98
99
|
transpile = stripTypeScript,
|
|
99
100
|
} = config
|
|
@@ -106,7 +107,7 @@ export function createCodeModeTool(
|
|
|
106
107
|
// Transform tools to bindings with external_ prefix (static bindings)
|
|
107
108
|
const staticBindings = toolsToBindings(tools, 'external_')
|
|
108
109
|
|
|
109
|
-
// Shared across static + dynamic (
|
|
110
|
+
// Shared across static + dynamic (snippet) binding scans so a given
|
|
110
111
|
// (toolName, paramPath) pair surfaces at most once per code-mode instance.
|
|
111
112
|
const secretDedupCache = new Set<string>()
|
|
112
113
|
|
|
@@ -130,18 +131,62 @@ export function createCodeModeTool(
|
|
|
130
131
|
toolContext?: ToolExecutionContext,
|
|
131
132
|
): Promise<CodeModeToolResult> => {
|
|
132
133
|
const { typescriptCode } = input
|
|
134
|
+
const startedAt = Date.now()
|
|
133
135
|
|
|
134
136
|
// Get emitCustomEvent from context or use no-op
|
|
135
137
|
const emitCustomEvent = toolContext?.emitCustomEvent || (() => {})
|
|
136
138
|
|
|
139
|
+
const finish = (
|
|
140
|
+
result: CodeModeToolResult,
|
|
141
|
+
phase: string,
|
|
142
|
+
): CodeModeToolResult => {
|
|
143
|
+
const durationMs = Date.now() - startedAt
|
|
144
|
+
const payload = {
|
|
145
|
+
timestamp: Date.now(),
|
|
146
|
+
durationMs,
|
|
147
|
+
phase,
|
|
148
|
+
success: result.success,
|
|
149
|
+
logCount: result.logs?.length ?? 0,
|
|
150
|
+
error: result.error
|
|
151
|
+
? {
|
|
152
|
+
name: result.error.name,
|
|
153
|
+
message: result.error.message,
|
|
154
|
+
...(result.error.stack !== undefined && {
|
|
155
|
+
stack: result.error.stack,
|
|
156
|
+
}),
|
|
157
|
+
...(result.error.line !== undefined && {
|
|
158
|
+
line: result.error.line,
|
|
159
|
+
}),
|
|
160
|
+
}
|
|
161
|
+
: undefined,
|
|
162
|
+
}
|
|
163
|
+
emitCustomEvent('code_mode:execution_finished', payload)
|
|
164
|
+
if (!result.success) {
|
|
165
|
+
console.error('[code-mode] execute_typescript failed', payload)
|
|
166
|
+
} else if (
|
|
167
|
+
typeof process !== 'undefined' &&
|
|
168
|
+
process.env?.CODE_MODE_DEBUG === '1'
|
|
169
|
+
) {
|
|
170
|
+
console.info('[code-mode] execute_typescript ok', {
|
|
171
|
+
durationMs,
|
|
172
|
+
phase,
|
|
173
|
+
logCount: payload.logCount,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
|
|
137
179
|
if (!typescriptCode || typeof typescriptCode !== 'string') {
|
|
138
|
-
return
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
180
|
+
return finish(
|
|
181
|
+
{
|
|
182
|
+
success: false,
|
|
183
|
+
error: {
|
|
184
|
+
message: 'typescriptCode must be a non-empty string',
|
|
185
|
+
name: 'ValidationError',
|
|
186
|
+
},
|
|
143
187
|
},
|
|
144
|
-
|
|
188
|
+
'validate-input',
|
|
189
|
+
)
|
|
145
190
|
}
|
|
146
191
|
|
|
147
192
|
// Create a fresh sandbox context for this execution
|
|
@@ -161,42 +206,65 @@ export function createCodeModeTool(
|
|
|
161
206
|
strippedCode = await transpile(typescriptCode)
|
|
162
207
|
} catch (error) {
|
|
163
208
|
// Type/syntax error from the transpiler
|
|
164
|
-
return
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
209
|
+
return finish(
|
|
210
|
+
{
|
|
211
|
+
success: false,
|
|
212
|
+
error: {
|
|
213
|
+
message: error instanceof Error ? error.message : String(error),
|
|
214
|
+
name: 'TypeScriptError',
|
|
215
|
+
...(error instanceof Error &&
|
|
216
|
+
error.stack !== undefined && { stack: error.stack }),
|
|
217
|
+
},
|
|
169
218
|
},
|
|
170
|
-
|
|
219
|
+
'transpile',
|
|
220
|
+
)
|
|
171
221
|
}
|
|
172
222
|
|
|
173
|
-
// Step 2: Get dynamic
|
|
174
|
-
const
|
|
223
|
+
// Step 2: Get dynamic snippet bindings if available
|
|
224
|
+
const snippetBindings = getSnippetBindings
|
|
225
|
+
? await getSnippetBindings()
|
|
226
|
+
: {}
|
|
175
227
|
|
|
176
228
|
// Scan dynamic bindings too — their schemas are equally in-scope for
|
|
177
229
|
// the same exfiltration threat. Dedup cache prevents repeat warnings
|
|
178
230
|
// when the same binding reappears across executions.
|
|
179
|
-
const
|
|
180
|
-
if (
|
|
181
|
-
warnIfBindingsExposeSecrets(
|
|
231
|
+
const snippetBindingValues = Object.values(snippetBindings)
|
|
232
|
+
if (snippetBindingValues.length > 0) {
|
|
233
|
+
warnIfBindingsExposeSecrets(snippetBindingValues, {
|
|
182
234
|
handler: onSecretParameter,
|
|
183
235
|
dedupCache: secretDedupCache,
|
|
184
236
|
})
|
|
185
237
|
}
|
|
186
238
|
|
|
187
239
|
// Step 3: Merge static and dynamic bindings, then wrap with event awareness
|
|
188
|
-
const allBindings = { ...staticBindings, ...
|
|
240
|
+
const allBindings = { ...staticBindings, ...snippetBindings }
|
|
189
241
|
const eventAwareBindings = createEventAwareBindings(
|
|
190
242
|
allBindings,
|
|
191
243
|
emitCustomEvent,
|
|
192
244
|
)
|
|
193
245
|
|
|
194
246
|
// Step 4: Create sandbox context with event-aware bindings
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
247
|
+
try {
|
|
248
|
+
isolateContext = await driver.createContext({
|
|
249
|
+
bindings: eventAwareBindings,
|
|
250
|
+
timeout,
|
|
251
|
+
memoryLimit,
|
|
252
|
+
})
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return finish(
|
|
255
|
+
{
|
|
256
|
+
success: false,
|
|
257
|
+
error: {
|
|
258
|
+
message: error instanceof Error ? error.message : String(error),
|
|
259
|
+
name:
|
|
260
|
+
error instanceof Error ? error.name : 'CreateContextError',
|
|
261
|
+
...(error instanceof Error &&
|
|
262
|
+
error.stack !== undefined && { stack: error.stack }),
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
'create-context',
|
|
266
|
+
)
|
|
267
|
+
}
|
|
200
268
|
|
|
201
269
|
// Step 5: Execute the code in the sandbox
|
|
202
270
|
const executionResult = await isolateContext.execute(strippedCode)
|
|
@@ -228,31 +296,45 @@ export function createCodeModeTool(
|
|
|
228
296
|
}
|
|
229
297
|
|
|
230
298
|
if (executionResult.success) {
|
|
231
|
-
return
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
299
|
+
return finish(
|
|
300
|
+
{
|
|
301
|
+
success: true,
|
|
302
|
+
result: executionResult.value,
|
|
303
|
+
logs: executionResult.logs,
|
|
304
|
+
},
|
|
305
|
+
'execute',
|
|
306
|
+
)
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return finish(
|
|
310
|
+
{
|
|
238
311
|
success: false,
|
|
239
312
|
error: executionResult.error
|
|
240
313
|
? {
|
|
241
314
|
message: executionResult.error.message,
|
|
242
315
|
name: executionResult.error.name,
|
|
316
|
+
...(executionResult.error.stack !== undefined && {
|
|
317
|
+
stack: executionResult.error.stack,
|
|
318
|
+
}),
|
|
243
319
|
}
|
|
244
|
-
: { message: 'Unknown execution error' },
|
|
320
|
+
: { message: 'Unknown execution error', name: 'UnknownError' },
|
|
245
321
|
logs: executionResult.logs,
|
|
246
|
-
}
|
|
247
|
-
|
|
322
|
+
},
|
|
323
|
+
'execute',
|
|
324
|
+
)
|
|
248
325
|
} catch (error) {
|
|
249
|
-
return
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
326
|
+
return finish(
|
|
327
|
+
{
|
|
328
|
+
success: false,
|
|
329
|
+
error: {
|
|
330
|
+
message: error instanceof Error ? error.message : String(error),
|
|
331
|
+
name: error instanceof Error ? error.name : 'Error',
|
|
332
|
+
...(error instanceof Error &&
|
|
333
|
+
error.stack !== undefined && { stack: error.stack }),
|
|
334
|
+
},
|
|
254
335
|
},
|
|
255
|
-
|
|
336
|
+
'unhandled',
|
|
337
|
+
)
|
|
256
338
|
} finally {
|
|
257
339
|
// Always clean up the sandbox context
|
|
258
340
|
if (isolateContext) {
|
package/src/types.ts
CHANGED
|
@@ -183,20 +183,20 @@ export interface CodeModeToolConfig {
|
|
|
183
183
|
|
|
184
184
|
/**
|
|
185
185
|
* Optional function to get additional bindings dynamically.
|
|
186
|
-
* Called at execution time (each execute_typescript call) to get current
|
|
186
|
+
* Called at execution time (each execute_typescript call) to get current snippet bindings.
|
|
187
187
|
* These are merged with the static external_* bindings.
|
|
188
188
|
*
|
|
189
|
-
* @returns Record of
|
|
189
|
+
* @returns Record of snippet bindings with snippet_ prefix
|
|
190
190
|
*
|
|
191
191
|
* @example
|
|
192
192
|
* ```typescript
|
|
193
|
-
*
|
|
194
|
-
* const
|
|
195
|
-
* return
|
|
193
|
+
* getSnippetBindings: async () => {
|
|
194
|
+
* const snippets = await storage.loadAll()
|
|
195
|
+
* return snippetsToBindings(snippets, 'snippet_')
|
|
196
196
|
* }
|
|
197
197
|
* ```
|
|
198
198
|
*/
|
|
199
|
-
|
|
199
|
+
getSnippetBindings?: () => Promise<Record<string, ToolBinding>>
|
|
200
200
|
|
|
201
201
|
/**
|
|
202
202
|
* How to surface tool parameters whose names look like secrets.
|
|
@@ -233,8 +233,8 @@ export interface CodeModeToolConfig {
|
|
|
233
233
|
* and `await` in its input (the default wraps the code in an async function
|
|
234
234
|
* internally to allow this).
|
|
235
235
|
*
|
|
236
|
-
* NOTE: This only affects `createCodeModeTool`. The
|
|
237
|
-
* (`
|
|
236
|
+
* NOTE: This only affects `createCodeModeTool`. The snippet helpers
|
|
237
|
+
* (`snippetsToTools`, `codeModeWithSnippets` in `@tanstack/ai-code-mode-snippets`)
|
|
238
238
|
* call the exported `stripTypeScript` directly, so they ignore this hook — but
|
|
239
239
|
* they still get the edge-safe sucrase default, so #487 is fixed for them too;
|
|
240
240
|
* they just can't be pointed at a different transpiler.
|
|
@@ -285,6 +285,7 @@ export interface CodeModeToolResult {
|
|
|
285
285
|
message: string
|
|
286
286
|
name?: string | undefined
|
|
287
287
|
line?: number | undefined
|
|
288
|
+
stack?: string | undefined
|
|
288
289
|
}
|
|
289
290
|
| undefined
|
|
290
291
|
}
|