From de6d4cc8f3ee8252458d3de0b3637ae732e2b0f1 Mon Sep 17 00:00:00 2001 From: Kenta Iwasaki Date: Fri, 12 Jun 2026 01:56:37 +0800 Subject: [PATCH 1/8] feat(ai-isolate): add native QuickJS Code Mode isolate driver for Bun Adds @tanstack/ai-isolate-quickjs-bun, a Code Mode IsolateDriver that runs QuickJS natively on the Bun runtime via bun:ffi (through quickjs-bun) instead of WebAssembly. It is a drop-in replacement for @tanstack/ai-isolate-quickjs on Bun servers. - Per-context native QuickJS runtime with its own memory/stack/timeout limits; contexts execute independently (the WASM driver serializes all executions through one asyncified module). - Same JSON tool-call protocol, console prefixes, and normalized MemoryLimit/StackOverflowError/DisposedError contract as the other drivers, plus a normalized TimeoutError for deadline expiry. - maxToolCalls (default 1000) and conosle log caps to bound stack and memory growth from untrusted snadbox code. - Requires Bun >= 1.3.14; throws an error on Node.js. Unit tests mirror the WASM/Node suites and run under `bun test`; the Node-side rejection test runs in normal CI. Docs, the ai-code-mode README + skill, and the code-mode example have been updated. --- .changeset/quickjs-bun-isolate-driver.md | 29 + docs/code-mode/code-mode-isolates.md | 107 ++- docs/code-mode/code-mode.md | 10 +- docs/comparison/vercel-ai-sdk.md | 7 +- docs/config.json | 6 +- examples/ts-code-mode-web/package.json | 1 + .../src/components/ToolSidebar.tsx | 8 +- .../src/lib/create-isolate-driver.ts | 8 +- examples/ts-code-mode-web/vite.config.ts | 3 +- knip.json | 3 + packages/ai-code-mode/README.md | 18 +- .../ai-code-mode/skills/ai-code-mode/SKILL.md | 31 +- packages/ai-isolate-quickjs-bun/README.md | 90 +++ .../benchmarks/compare-with-wasm.ts | 327 ++++++++ packages/ai-isolate-quickjs-bun/package.json | 64 ++ .../src/error-normalizer.ts | 102 +++ packages/ai-isolate-quickjs-bun/src/index.ts | 13 + .../src/isolate-context.ts | 642 +++++++++++++++ .../src/isolate-driver.ts | 183 +++++ .../tests/escape-attempts.test.ts | 145 ++++ .../tests/isolate-driver.test.ts | 739 ++++++++++++++++++ packages/ai-isolate-quickjs-bun/tsconfig.json | 11 + .../ai-isolate-quickjs-bun/vite.config.ts | 36 + pnpm-lock.yaml | 42 + 24 files changed, 2572 insertions(+), 53 deletions(-) create mode 100644 .changeset/quickjs-bun-isolate-driver.md create mode 100644 packages/ai-isolate-quickjs-bun/README.md create mode 100644 packages/ai-isolate-quickjs-bun/benchmarks/compare-with-wasm.ts create mode 100644 packages/ai-isolate-quickjs-bun/package.json create mode 100644 packages/ai-isolate-quickjs-bun/src/error-normalizer.ts create mode 100644 packages/ai-isolate-quickjs-bun/src/index.ts create mode 100644 packages/ai-isolate-quickjs-bun/src/isolate-context.ts create mode 100644 packages/ai-isolate-quickjs-bun/src/isolate-driver.ts create mode 100644 packages/ai-isolate-quickjs-bun/tests/escape-attempts.test.ts create mode 100644 packages/ai-isolate-quickjs-bun/tests/isolate-driver.test.ts create mode 100644 packages/ai-isolate-quickjs-bun/tsconfig.json create mode 100644 packages/ai-isolate-quickjs-bun/vite.config.ts diff --git a/.changeset/quickjs-bun-isolate-driver.md b/.changeset/quickjs-bun-isolate-driver.md new file mode 100644 index 0000000000..1affd320d3 --- /dev/null +++ b/.changeset/quickjs-bun-isolate-driver.md @@ -0,0 +1,29 @@ +--- +'@tanstack/ai-isolate-quickjs-bun': minor +'@tanstack/ai-code-mode': patch +--- + +Add `@tanstack/ai-isolate-quickjs-bun`, a Code Mode isolate driver that runs QuickJS natively on the Bun runtime through `bun:ffi` (via [`quickjs-bun`](https://github.com/superpowerdotcom/quickjs-bun)). + +It implements the same `IsolateDriver` contract as the existing drivers and is a drop-in replacement for `@tanstack/ai-isolate-quickjs` on Bun servers: + +```typescript +import { createQuickJSBunIsolateDriver } from '@tanstack/ai-isolate-quickjs-bun' +import { createCodeModeTool } from '@tanstack/ai-code-mode' + +const executeTypescript = createCodeModeTool({ + driver: createQuickJSBunIsolateDriver(), + tools: [myTool], +}) +``` + +Compared to the WASM driver: + +- Native QuickJS through `bun:ffi` — no WebAssembly or asyncify overhead, and no native build step (the QuickJS sources are compiled once per process by Bun's embedded TinyCC). +- Each context gets a dedicated QuickJS runtime with its own memory and stack limits, so executions on different contexts are not serialized through a shared VM. +- Same normalized `MemoryLimitError` / `StackOverflowError` / `DisposedError` contract, console capture prefixes, and JSON tool-call protocol as the other drivers, plus a normalized `TimeoutError` for deadline expiry (the WASM driver surfaces timeouts as `InternalError: interrupted`). +- A configurable `maxToolCalls` limit (default 1000) bounds output and memory growth from untrusted sandbox code. + +The driver requires Bun `>= 1.3.14` and throws an error when used on Node.js. + +The `@tanstack/ai-code-mode` README and bundled skill are updated to document the new driver. diff --git a/docs/code-mode/code-mode-isolates.md b/docs/code-mode/code-mode-isolates.md index c7dcaec4e9..3142e7c909 100644 --- a/docs/code-mode/code-mode-isolates.md +++ b/docs/code-mode/code-mode-isolates.md @@ -2,31 +2,35 @@ title: Code Mode Isolate Drivers id: code-mode-isolates order: 4 -description: "Compare Code Mode sandbox drivers — Node isolated-vm, QuickJS WASM, and Cloudflare Workers — and choose the right runtime for your deployment." +description: "Compare Code Mode sandbox drivers — Node isolated-vm, QuickJS WASM, QuickJS Bun (bun:ffi), and Cloudflare Workers — and choose the right runtime for your deployment." keywords: - tanstack ai - code mode - isolate driver - isolated-vm - quickjs + - quickjs-bun + - bun + - bun:ffi - cloudflare workers - sandbox - secure execution ---- Isolate drivers provide the secure sandbox runtimes that [Code Mode](./code-mode.md) uses to execute generated TypeScript. All drivers implement the same `IsolateDriver` interface, so you can swap them without changing any other code. ## Choosing a Driver -| | Node (`isolated-vm`) | QuickJS (WASM) | Cloudflare Workers | -|---|---|---|---| -| **Best for** | Server-side Node.js apps | Browsers, edge, portability | Edge deployments on Cloudflare | -| **Performance** | Fast (V8 JIT) | Slower (interpreted) | Fast (V8 on Cloudflare edge) | -| **Native deps** | Yes (C++ addon) | None | None | -| **Browser support** | No | Yes | N/A | -| **Memory limit** | Configurable | Configurable | N/A | -| **Stack size limit** | N/A | Configurable | N/A | -| **Setup** | `pnpm add` | `pnpm add` | Deploy a Worker first | + +| | Node (`isolated-vm`) | QuickJS (WASM) | QuickJS Bun (`bun:ffi`) | Cloudflare Workers | +| -------------------- | ------------------------ | --------------------------- | ------------------------ | ------------------------------ | +| **Best for** | Server-side Node.js apps | Browsers, edge, portability | Bun servers | Edge deployments on Cloudflare | +| **Performance** | Fast (V8 JIT) | Slower (interpreted) | Fast (native QuickJS) | Fast (V8 on Cloudflare edge) | +| **Native deps** | Yes (C++ addon) | None | None (TinyCC on the fly) | None | +| **Browser support** | No | Yes | No (Bun only) | N/A | +| **Memory limit** | Configurable | Configurable | Configurable | N/A | +| **Stack size limit** | N/A | Configurable | Configurable | N/A | +| **Setup** | `pnpm add` | `pnpm add` | `bun add` | Deploy a Worker first | + --- @@ -55,10 +59,12 @@ const driver = createNodeIsolateDriver({ ### Options -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `memoryLimit` | `number` | `128` | Maximum heap size for the V8 isolate, in megabytes. Execution is terminated if this limit is exceeded. | -| `timeout` | `number` | `30000` | Maximum wall-clock time per execution, in milliseconds. | + +| Option | Type | Default | Description | +| ------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------ | +| `memoryLimit` | `number` | `128` | Maximum heap size for the V8 isolate, in megabytes. Execution is terminated if this limit is exceeded. | +| `timeout` | `number` | `30000` | Maximum wall-clock time per execution, in milliseconds. | + ### How it works @@ -90,17 +96,61 @@ const driver = createQuickJSIsolateDriver({ ### Options -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `memoryLimit` | `number` | `128` | Maximum heap memory for the QuickJS VM, in megabytes. | -| `timeout` | `number` | `30000` | Maximum wall-clock time per execution, in milliseconds. | + +| Option | Type | Default | Description | +| -------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `memoryLimit` | `number` | `128` | Maximum heap memory for the QuickJS VM, in megabytes. | +| `timeout` | `number` | `30000` | Maximum wall-clock time per execution, in milliseconds. | | `maxStackSize` | `number` | `524288` | Maximum call stack size in bytes (default: 512 KiB). Increase for deeply recursive code; decrease to catch runaway recursion sooner. | + ### How it works QuickJS WASM uses an asyncified execution model — the WASM module can pause while awaiting host async functions (your tools). Executions are serialized through a global queue to prevent concurrent WASM calls, which the asyncify model does not support. Fatal errors (memory exhaustion, stack overflow) are detected, the VM is disposed, and a structured error is returned. Console output is captured and returned with the result. -> **Performance note:** QuickJS interprets JavaScript rather than JIT-compiling it, so compute-heavy scripts run slower than with the Node driver. For typical LLM-generated scripts that are mostly waiting on `external_*` tool calls, this difference is not significant. +> **Performance note:** QuickJS interprets JavaScript rather than JIT-compiling it, so compute-heavy scripts run slower than with the Node driver. For typical LLM-generated scripts that are mostly waiting on `external_`* tool calls, this difference is not significant. + +--- + +## QuickJS Bun Driver (`@tanstack/ai-isolate-quickjs-bun`) + +Runs [QuickJS](https://bellard.org/quickjs/) natively on the [Bun](https://bun.sh/) runtime through `bun:ffi`, via the `[quickjs-bun](https://github.com/superpowerdotcom/quickjs-bun)` package. There are no native dependencies and no build step — the vendored QuickJS C sources are compiled on the fly with Bun's embedded TinyCC, once per process. This makes it the fastest sandboxed option for Code Mode on Bun. + +### Installation + +```bash +bun add @tanstack/ai-isolate-quickjs-bun +``` + +Requires Bun 1.3.14 or later. On Windows, provide a prebuilt QuickJS dynamic library via the `QUICKJS_BUN_NATIVE_LIBRARY` environment variable. + +### Usage + +```typescript +import { createQuickJSBunIsolateDriver } from '@tanstack/ai-isolate-quickjs-bun' + +const driver = createQuickJSBunIsolateDriver({ + memoryLimit: 128, // MB + timeout: 30_000, // ms + maxStackSize: 524288, // bytes (512 KiB) +}) +``` + +### Options + + +| Option | Type | Default | Description | +| -------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `memoryLimit` | `number` | `128` | Maximum heap memory for the QuickJS runtime, in megabytes. | +| `timeout` | `number` | `30000` | Maximum wall-clock time per execution, in milliseconds. | +| `maxStackSize` | `number` | `524288` | Maximum call stack size in bytes (default: 512 KiB). Increase for deeply recursive code; decrease to catch runaway recursion sooner. | + + +### How it works + +Each context gets a dedicated native QuickJS runtime with its own memory limit, stack size, and interrupt-based timeout, so contexts execute independently — unlike the WASM driver, which serializes all executions through one shared asyncified WASM module. Fatal errors (memory exhaustion, stack overflow) are detected, the VM is disposed, and a structured error is returned; create a fresh context afterwards. Console output is captured and returned with the result. + +> **Bun only:** This driver requires Bun 1.3.14 or later and throws a descriptive error when creating a context on Node.js — use the Node or QuickJS WASM driver there. On Bun, prefer this driver over the WASM one: it runs QuickJS natively, and quickjs-emscripten's asyncify bridge is unreliable for async host tool calls under Bun. --- @@ -129,12 +179,14 @@ const driver = createCloudflareIsolateDriver({ ### Options -| Option | Type | Default | Description | -|--------|------|---------|-------------| -| `workerUrl` | `string` | — | **Required.** Full URL of the deployed Cloudflare Worker. | -| `authorization` | `string` | — | Optional value sent as the `Authorization` header on every request. Use this to prevent unauthorized access to your Worker. | -| `timeout` | `number` | `30000` | Maximum wall-clock time for the entire execution (including all tool round-trips), in milliseconds. | -| `maxToolRounds` | `number` | `10` | Maximum number of tool-call/result cycles per execution. Prevents infinite loops when generated code calls tools in a loop. | + +| Option | Type | Default | Description | +| --------------- | -------- | ------- | --------------------------------------------------------------------------------------------------------------------------- | +| `workerUrl` | `string` | — | **Required.** Full URL of the deployed Cloudflare Worker. | +| `authorization` | `string` | — | Optional value sent as the `Authorization` header on every request. Use this to prevent unauthorized access to your Worker. | +| `timeout` | `number` | `30000` | Maximum wall-clock time for the entire execution (including all tool round-trips), in milliseconds. | +| `maxToolRounds` | `number` | `10` | Maximum number of tool-call/result cycles per execution. Prevents infinite loops when generated code calls tools in a loop. | + ### Deploying the Worker @@ -184,7 +236,7 @@ Each round-trip adds network latency, so the `maxToolRounds` limit both prevents ## The `IsolateDriver` Interface -All three drivers satisfy this interface, exported from `@tanstack/ai-code-mode`: +All four drivers satisfy this interface, exported from `@tanstack/ai-code-mode`: ```typescript import type { ToolBinding, NormalizedError } from "@tanstack/ai-code-mode"; @@ -219,3 +271,4 @@ You can implement this interface to build a custom driver — for example, a Doc - [Code Mode](./code-mode) — Core setup, API reference, and getting started guide - [Showing Code Mode in the UI](./client-integration) — Display execution progress in your React app - [Code Mode with Skills](./code-mode-with-skills) — Add persistent, reusable skill libraries + diff --git a/docs/code-mode/code-mode.md b/docs/code-mode/code-mode.md index 04f4ff5f7f..82a2593071 100644 --- a/docs/code-mode/code-mode.md +++ b/docs/code-mode/code-mode.md @@ -35,7 +35,7 @@ Tools you pass to Code Mode are converted to typed function stubs that appear in ### Secure sandboxing -Generated code runs in an isolated environment (V8 isolate, QuickJS WASM, or Cloudflare Worker) with no access to the host file system, network, or process. The sandbox has configurable timeouts and memory limits. +Generated code runs in an isolated environment (V8 isolate, QuickJS WASM, native QuickJS on Bun, or Cloudflare Worker) with no access to the host file system, network, or process. The sandbox has configurable timeouts and memory limits. ## Getting Started @@ -54,6 +54,9 @@ pnpm add @tanstack/ai-isolate-node # QuickJS WASM — no native deps, works in browsers and edge runtimes pnpm add @tanstack/ai-isolate-quickjs +# QuickJS Bun — native QuickJS via bun:ffi, fastest option on Bun +bun add @tanstack/ai-isolate-quickjs-bun + # Cloudflare Workers — run on the edge pnpm add @tanstack/ai-isolate-cloudflare ``` @@ -210,6 +213,7 @@ interface IsolateDriver { |---------|-----------------|-------------| | `@tanstack/ai-isolate-node` | `createNodeIsolateDriver()` | Node.js | | `@tanstack/ai-isolate-quickjs` | `createQuickJSIsolateDriver()` | Node.js, browser, edge | +| `@tanstack/ai-isolate-quickjs-bun` | `createQuickJSBunIsolateDriver()` | Bun | | `@tanstack/ai-isolate-cloudflare` | `createCloudflareIsolateDriver()` | Cloudflare Workers | For full configuration options for each driver, see [Isolate Drivers](./code-mode-isolates.md). @@ -226,7 +230,7 @@ These utilities are used internally and are exported for custom pipelines: For a full comparison of drivers with all configuration options, see [Isolate Drivers](./code-mode-isolates.md). -In brief: use the **Node driver** for server-side Node.js (fastest, V8 JIT), **QuickJS** for browsers or portable edge deployments (no native deps), and the **Cloudflare driver** when you deploy to Cloudflare Workers. +In brief: use the **Node driver** for server-side Node.js (fastest, V8 JIT), **QuickJS** for browsers or portable edge deployments (no native deps), **QuickJS Bun** for Bun servers (native QuickJS via `bun:ffi`), and the **Cloudflare driver** when you deploy to Cloudflare Workers. ## Custom Events @@ -292,4 +296,4 @@ pnpm eval -- --no-judge # skip Anthropic-based judging - [Showing Code Mode in the UI](./client-integration) — Display execution progress in your React app - [Code Mode with Skills](./code-mode-with-skills) — Add persistent, reusable skill libraries -- [Isolate Drivers](./code-mode-isolates) — Compare Node, QuickJS, and Cloudflare sandbox runtimes +- [Isolate Drivers](./code-mode-isolates) — Compare Node, QuickJS, QuickJS Bun, and Cloudflare sandbox runtimes diff --git a/docs/comparison/vercel-ai-sdk.md b/docs/comparison/vercel-ai-sdk.md index 8f32d0acd4..1aaa5ebee3 100644 --- a/docs/comparison/vercel-ai-sdk.md +++ b/docs/comparison/vercel-ai-sdk.md @@ -51,7 +51,7 @@ Versions referenced below: TanStack AI as of this writing; Vercel AI SDK `ai@6.x | Transcription | Stable API with word timestamps and diarization (OpenAI, Grok, ElevenLabs, fal.ai) | `transcribe()` (experimental) | | Audio / Music Generation | `generateAudio()` for music & sound effects (Gemini, ElevenLabs, fal.ai) | - | | Summarization | Dedicated `summarize()` with streaming and style options | - | -| Code Execution | Node.js, Cloudflare Workers, QuickJS sandboxes you run yourself | Provider-hosted code-execution tools (Anthropic, xAI, OpenAI) | +| Code Execution | Node.js, Cloudflare Workers, QuickJS (WASM + Bun-native) sandboxes you run yourself | Provider-hosted code-execution tools (Anthropic, xAI, OpenAI) | | Code Mode Skills | LLM-writable persistent skill library | - | | Coding Agent Sandboxes | First-party Grok Build, Claude Code, Codex, OpenCode harnesses **+ any ACP agent** via `acpCompatible`; runs on local-process, Docker, Daytona, Vercel, Sprites, or Cloudflare | `HarnessAgent` (experimental) — Claude Code, Codex, Pi, OpenCode, Deep Agents; centered on Vercel Sandbox | | Realtime Voice | OpenAI, Grok, and ElevenLabs with VAD modes and tool support | - | @@ -439,13 +439,14 @@ This isn't just philosophical - it means no accidental dependencies on platform- ### Code Execution Sandboxes -TanStack AI provides three isolate drivers for safe code execution in AI workflows: +TanStack AI provides four isolate drivers for safe code execution in AI workflows: - **`@tanstack/ai-isolate-node`** - Node.js sandbox via `isolated-vm` - **`@tanstack/ai-isolate-cloudflare`** - Cloudflare Workers sandbox - **`@tanstack/ai-isolate-quickjs`** - QuickJS lightweight sandbox +- **`@tanstack/ai-isolate-quickjs-bun`** - Native QuickJS sandbox for Bun via `bun:ffi` -All three implement the same `IsolateDriver` interface, so you can swap execution environments without changing application code. This powers TanStack AI's code mode - where the LLM writes and executes code as part of the agent loop. A companion `@tanstack/ai-code-mode-skills` package lets you give code mode a persistent, reusable library of runtime skills. Skills are LLM-writable: the model can save working TypeScript snippets, list and reuse them across sessions, with trust strategies controlling what gets promoted to a first-class tool. The closest AI SDK analogues - Anthropic's provider-hosted code execution and developer-uploaded skills, or pre-authored file skills loaded into a sandbox - are provider-specific and static; none give the model a persistent, provider-agnostic skill library it builds itself. +All four implement the same `IsolateDriver` interface, so you can swap execution environments without changing application code. This powers TanStack AI's code mode - where the LLM writes and executes code as part of the agent loop. A companion `@tanstack/ai-code-mode-skills` package lets you give code mode a persistent, reusable library of runtime skills. Skills are LLM-writable: the model can save working TypeScript snippets, list and reuse them across sessions, with trust strategies controlling what gets promoted to a first-class tool. The closest AI SDK analogues - Anthropic's provider-hosted code execution and developer-uploaded skills, or pre-authored file skills loaded into a sandbox - are provider-specific and static; none give the model a persistent, provider-agnostic skill library it builds itself. Vercel AI SDK does not provide built-in code execution sandboxes (though some providers expose their own server-side code execution as provider-executed tools). diff --git a/docs/config.json b/docs/config.json index 0e8982869f..f43db303dc 100644 --- a/docs/config.json +++ b/docs/config.json @@ -211,7 +211,8 @@ { "label": "Code Mode", "to": "code-mode/code-mode", - "addedAt": "2026-04-15" + "addedAt": "2026-04-15", + "updatedAt": "2026-06-11" }, { "label": "Showing Code Mode in the UI", @@ -227,7 +228,8 @@ { "label": "Code Mode Isolate Drivers", "to": "code-mode/code-mode-isolates", - "addedAt": "2026-04-15" + "addedAt": "2026-04-15", + "updatedAt": "2026-06-11" }, { "label": "Lazy Tools", diff --git a/examples/ts-code-mode-web/package.json b/examples/ts-code-mode-web/package.json index 0f74ed7b68..f2fd025cb0 100644 --- a/examples/ts-code-mode-web/package.json +++ b/examples/ts-code-mode-web/package.json @@ -21,6 +21,7 @@ "@tanstack/ai-isolate-cloudflare": "workspace:*", "@tanstack/ai-isolate-node": "workspace:*", "@tanstack/ai-isolate-quickjs": "workspace:*", + "@tanstack/ai-isolate-quickjs-bun": "workspace:*", "@tanstack/ai-ollama": "workspace:*", "@tanstack/ai-openai": "workspace:*", "@tanstack/ai-react": "workspace:*", diff --git a/examples/ts-code-mode-web/src/components/ToolSidebar.tsx b/examples/ts-code-mode-web/src/components/ToolSidebar.tsx index 30d9f3ebed..eb51575451 100644 --- a/examples/ts-code-mode-web/src/components/ToolSidebar.tsx +++ b/examples/ts-code-mode-web/src/components/ToolSidebar.tsx @@ -15,7 +15,7 @@ export interface LLMTool { } // Isolate VM options -export type IsolateVM = 'node' | 'quickjs' | 'cloudflare' +export type IsolateVM = 'node' | 'quickjs' | 'quickjs-bun' | 'cloudflare' export interface IsolateVMOption { id: IsolateVM @@ -103,6 +103,12 @@ export const DEFAULT_ISOLATE_VM_OPTIONS: Array = [ description: 'Lightweight JavaScript engine', available: true, }, + { + id: 'quickjs-bun', + name: 'QuickJS Bun', + description: 'Native QuickJS engine (requires running the server with Bun)', + available: true, + }, { id: 'cloudflare', name: 'Cloudflare Workers', diff --git a/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts b/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts index fa6b33ef5e..b3b4263bc4 100644 --- a/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts +++ b/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts @@ -1,6 +1,6 @@ import type { IsolateDriver } from '@tanstack/ai-code-mode' -export type IsolateVM = 'node' | 'quickjs' | 'cloudflare' +export type IsolateVM = 'node' | 'quickjs' | 'quickjs-bun' | 'cloudflare' const driverCache = new Map() @@ -19,6 +19,12 @@ export async function createIsolateDriver( driver = createQuickJSIsolateDriver() break } + case 'quickjs-bun': { + const { createQuickJSBunIsolateDriver } = + await import('@tanstack/ai-isolate-quickjs-bun') + driver = createQuickJSBunIsolateDriver() + break + } case 'cloudflare': { const { createCloudflareIsolateDriver } = await import('@tanstack/ai-isolate-cloudflare') diff --git a/examples/ts-code-mode-web/vite.config.ts b/examples/ts-code-mode-web/vite.config.ts index c60f52d0f9..8a84f68e98 100644 --- a/examples/ts-code-mode-web/vite.config.ts +++ b/examples/ts-code-mode-web/vite.config.ts @@ -28,6 +28,7 @@ const config = defineConfig({ '@jitl/quickjs-wasmfile-release-sync', '@jitl/quickjs-wasmfile-debug-asyncify', '@jitl/quickjs-wasmfile-debug-sync', + 'quickjs-bun', 'esbuild', // Google/Gemini related CJS packages 'google-auth-library', @@ -45,7 +46,7 @@ const config = defineConfig({ ], }, optimizeDeps: { - exclude: ['isolated-vm', 'quickjs-emscripten'], + exclude: ['isolated-vm', 'quickjs-emscripten', 'quickjs-bun'], }, }) diff --git a/knip.json b/knip.json index b977ce0342..ab81d908ba 100644 --- a/knip.json +++ b/knip.json @@ -40,6 +40,9 @@ "packages/ai-client": { "ignoreDependencies": ["@standard-schema/spec"] }, + "packages/ai-isolate-quickjs-bun": { + "entry": ["src/index.ts", "benchmarks/*.ts"] + }, "packages/ai-sandbox": { "ignoreDependencies": ["@ngrok/ngrok"] }, diff --git a/packages/ai-code-mode/README.md b/packages/ai-code-mode/README.md index cc4a95b3cb..8b85dd3dff 100644 --- a/packages/ai-code-mode/README.md +++ b/packages/ai-code-mode/README.md @@ -21,6 +21,9 @@ pnpm add @tanstack/ai-isolate-node # QuickJS WASM (browser-compatible, no native deps) pnpm add @tanstack/ai-isolate-quickjs +# Bun servers (native QuickJS via bun:ffi) +bun add @tanstack/ai-isolate-quickjs-bun + # Cloudflare Workers (edge execution) pnpm add @tanstack/ai-isolate-cloudflare ``` @@ -82,10 +85,10 @@ Creates both the `execute_typescript` tool and its matching system prompt. This **Config:** -- `driver` — An `IsolateDriver` (Node, QuickJS, or Cloudflare) +- `driver` — An `IsolateDriver` (Node, QuickJS, QuickJS Bun, or Cloudflare) - `tools` — Array of `ServerTool` or `ToolDefinition` instances. Exposed as `external_*` functions in the sandbox - `timeout` — Execution timeout in ms (default: 30000) -- `memoryLimit` — Memory limit in MB (default: 128, supported by Node and QuickJS drivers) +- `memoryLimit` — Memory limit in MB (default: 128, supported by the Node, QuickJS, and QuickJS Bun drivers) - `getSkillBindings` — Optional async function returning dynamic bindings ### `createCodeModeTool(config)` / `createCodeModeSystemPrompt(config)` @@ -102,11 +105,12 @@ These utilities are used internally and exported for custom pipelines: ## Driver Selection Guide -| Driver | Best For | Native Deps | Browser | Memory Limit | -| --------------------------------- | -------------------------------------------- | ------------------- | ------- | ------------ | -| `@tanstack/ai-isolate-node` | Server-side Node.js apps | Yes (`isolated-vm`) | No | Yes | -| `@tanstack/ai-isolate-quickjs` | Browser, edge, or no-native-dep environments | No (WASM) | Yes | Yes | -| `@tanstack/ai-isolate-cloudflare` | Cloudflare Workers deployments | No | N/A | N/A | +| Driver | Best For | Native Deps | Browser | Memory Limit | +| ---------------------------------- | -------------------------------------------- | ------------------- | ------- | ------------ | +| `@tanstack/ai-isolate-node` | Server-side Node.js apps | Yes (`isolated-vm`) | No | Yes | +| `@tanstack/ai-isolate-quickjs` | Browser, edge, or no-native-dep environments | No (WASM) | Yes | Yes | +| `@tanstack/ai-isolate-quickjs-bun` | Bun servers (native QuickJS via `bun:ffi`) | No | No | Yes | +| `@tanstack/ai-isolate-cloudflare` | Cloudflare Workers deployments | No | N/A | N/A | ## Custom Events diff --git a/packages/ai-code-mode/skills/ai-code-mode/SKILL.md b/packages/ai-code-mode/skills/ai-code-mode/SKILL.md index 09e1a1945d..9b0d7ca7c2 100644 --- a/packages/ai-code-mode/skills/ai-code-mode/SKILL.md +++ b/packages/ai-code-mode/skills/ai-code-mode/SKILL.md @@ -3,7 +3,8 @@ name: ai-code-mode description: > LLM-generated TypeScript execution in sandboxed environments: createCodeModeTool() with isolate drivers (createNodeIsolateDriver, - createQuickJSIsolateDriver, createCloudflareIsolateDriver), + createQuickJSIsolateDriver, createQuickJSBunIsolateDriver, + createCloudflareIsolateDriver), codeModeWithSkills() for persistent skill libraries, trust strategies, skill storage (FileSystem, LocalStorage, InMemory, Mongo), client-side execution progress via code_mode:* custom events in useChat. @@ -90,7 +91,7 @@ const stream = chat({ ### 1. Choosing an Isolate Driver -Three drivers implement the `IsolateDriver` interface. All are interchangeable. +Four drivers implement the `IsolateDriver` interface. All are interchangeable. **Node.js** (`createNodeIsolateDriver`) -- Full V8 with JIT. Fastest option. Requires `isolated-vm` native C++ addon. @@ -116,6 +117,18 @@ const driver = createQuickJSIsolateDriver({ }) ``` +**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. + +```typescript +import { createQuickJSBunIsolateDriver } from '@tanstack/ai-isolate-quickjs-bun' + +const driver = createQuickJSBunIsolateDriver({ + memoryLimit: 128, // MB, default 128 + timeout: 30_000, // ms, default 30000 + maxStackSize: 524288, // bytes, default 512 KiB +}) +``` + **Cloudflare** (`createCloudflareIsolateDriver`) -- Edge execution via a deployed Cloudflare Worker. Requires a `workerUrl` pointing to your deployed worker. Network latency on each tool call. ```typescript @@ -129,11 +142,12 @@ const driver = createCloudflareIsolateDriver({ }) ``` -| Driver | Best for | Native deps | Browser support | Performance | -| ---------- | --------------------------- | --------------- | --------------- | -------------------- | -| Node | Server-side Node.js | Yes (C++ addon) | No | Fast (V8 JIT) | -| QuickJS | Browsers, edge, portability | None (WASM) | Yes | Slower (interpreted) | -| Cloudflare | Edge deployments | None | N/A | Fast (V8 on edge) | +| Driver | Best for | Native deps | Browser support | Performance | +| ----------- | --------------------------- | --------------- | --------------- | --------------------- | +| Node | Server-side Node.js | Yes (C++ addon) | No | Fast (V8 JIT) | +| QuickJS | Browsers, edge, portability | None (WASM) | Yes | Slower (interpreted) | +| QuickJS Bun | Bun servers | None | No | Fast (native QuickJS) | +| Cloudflare | Edge deployments | None | N/A | Fast (V8 on edge) | ### 2. Adding Persistent Skills with codeModeWithSkills() @@ -495,10 +509,11 @@ Source: ai-isolate-node source (probeIsolatedVm implementation) ### MEDIUM: Expecting identical behavior across isolate drivers -The three drivers have different capabilities. Same code may work in Node but fail elsewhere. +The four drivers have different capabilities. Same code may work in Node but fail elsewhere. - **Node**: Full V8 support, JIT compilation, configurable memory limit - **QuickJS**: Interpreted, limited stdlib (no File I/O), configurable stack size, asyncified execution (serialized through global queue) +- **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` - **Cloudflare**: Network latency per tool call round-trip, `maxToolRounds` limit (default 10), requires deployed worker with `UNSAFE_EVAL` or `eval` unsafe binding Test generated code against your target driver. If you need portability, target QuickJS's subset. diff --git a/packages/ai-isolate-quickjs-bun/README.md b/packages/ai-isolate-quickjs-bun/README.md new file mode 100644 index 0000000000..3eb177f4db --- /dev/null +++ b/packages/ai-isolate-quickjs-bun/README.md @@ -0,0 +1,90 @@ +# @tanstack/ai-isolate-quickjs-bun + +Native QuickJS driver for TanStack AI Code Mode on the [Bun](https://bun.sh) runtime. Runs the same QuickJS engine as `@tanstack/ai-isolate-quickjs`, but natively through [`bun:ffi`](https://bun.sh/docs/api/ffi) instead of WebAssembly — substantially faster context creation and execution, with the same sandboxing guarantees. + +## Requirements + +- Bun `>= 1.3.14` — the driver throws a descriptive error when used on Node.js (use `@tanstack/ai-isolate-node` or `@tanstack/ai-isolate-quickjs` there). +- macOS or Linux on `x86_64`/`aarch64` work out of the box ([`quickjs-bun`](https://github.com/superpowerdotcom/quickjs-bun) compiles the vendored QuickJS sources on the fly with Bun's embedded TinyCC — no build tools needed). On Windows, point the `QUICKJS_BUN_NATIVE_LIBRARY` environment variable at a prebuilt QuickJS dynamic library. + +## Installation + +```bash +bun add @tanstack/ai-isolate-quickjs-bun +``` + +## Usage + +```typescript +import { createQuickJSBunIsolateDriver } from '@tanstack/ai-isolate-quickjs-bun' +import { createCodeModeTool } from '@tanstack/ai-code-mode' + +const driver = createQuickJSBunIsolateDriver({ + timeout: 30000, // execution timeout in ms (default: 30000) + memoryLimit: 128, // memory limit in MB (default: 128) + maxStackSize: 512 * 1024, // max stack size in bytes (default: 512 KiB) + maxToolCalls: 1000, // max host tool calls per execution (default: 1000) +}) + +const executeTypescript = createCodeModeTool({ + driver, + tools: [myTool], +}) +``` + +## Config Options + +- `timeout` — Default execution timeout in milliseconds (default: 30000) +- `memoryLimit` — Default QuickJS runtime memory limit in MB (default: 128) +- `maxStackSize` — Default QuickJS runtime max stack size in bytes (default: 524288) +- `maxToolCalls` — Maximum host tool calls per execution (default: 1000). Bounds output and memory growth from untrusted sandbox code; exceeding it throws a catchable error inside the sandbox. + +Console output is captured and returned to the model; it is bounded to 10,000 entries / ~1 MB per execution, after which a `[log output truncated]` marker is appended and further output dropped. + +## Tradeoffs vs QuickJS WASM Driver + +| | QuickJS Bun (`bun:ffi`) | QuickJS (WASM) | +| ------------ | ------------------------------ | -------------------------- | +| Runtime | Bun only | Node, browser, edge | +| Native deps | None (TinyCC compiles QuickJS) | None | +| Performance | Fast (native QuickJS) | Slower (WASM + asyncify) | +| Memory limit | Per-context runtime | Configurable | +| Concurrency | Independent contexts | Serialized (one WASM VM) | +| Best for | Bun servers | Browser, edge, portability | + +Each context gets a dedicated native QuickJS runtime, so executions on different contexts run independently — the WASM driver has to serialize all executions through one asyncified WASM module. + +## How It Works + +Uses [QuickJS](https://bellard.org/quickjs/) bound natively through [`quickjs-bun`](https://github.com/superpowerdotcom/quickjs-bun) (`bun:ffi`). The QuickJS library is compiled once per process (~100ms); each execution then creates a fresh QuickJS runtime + context (~1-2ms) with its own memory and stack limits, and tools injected as global async functions that bridge back to the host. Async tool calls resolve through QuickJS promises driven by the host event loop. + +## Runtime Limits and Errors + +- Every context enforces its own memory and stack limits via QuickJS `JS_SetMemoryLimit` / `JS_SetMaxStackSize`; timeouts use `JS_SetInterruptHandler` and also bound async tool waits. +- Exceeding limits produces normalized errors such as `MemoryLimitError`, `StackOverflowError`, or `TimeoutError`. +- Fatal limit conditions dispose the underlying VM; create a fresh context before running more code after disposal. + +## Benchmarks + +`benchmarks/compare-with-wasm.ts` compares this driver against `@tanstack/ai-isolate-quickjs` (WASM) through the public `IsolateDriver` interface: + +```bash +bun benchmarks/compare-with-wasm.ts +``` + +Representative numbers (Apple M-series, darwin/arm64; Bun 1.3.14 for this driver, Node 22 for the WASM driver): + +| Scenario (fresh context per run) | QuickJS Bun | QuickJS WASM (Node) | +| -------------------------------- | ----------: | ------------------: | +| Cold start (first context + run) | ~150 ms | ~24 ms | +| `return 1 + 1` | 0.63 ms | 13.5 ms (median) | +| 3 sequential tool calls | 0.77 ms | see note ¹ | +| 8 sequential tool calls | 0.85 ms | see note ¹ | +| compute (recursive `fib(20)`) | 4.5 ms | see note ¹ | +| `return 1 + 1` (reused context) | 0.04 ms | — | + +¹ In our benchmark runs, the WASM driver's asyncified host tool calls repeatedly crashed the shared WASM module (`memory access out of bounds`) and hung subsequent executions, on both Node 22 and Bun 1.3.14 — e.g. deterministically after running executions with 1, 2, 3, then 4 sequential awaited tool calls in one process. Sync-only workloads were unaffected. + +## License + +MIT diff --git a/packages/ai-isolate-quickjs-bun/benchmarks/compare-with-wasm.ts b/packages/ai-isolate-quickjs-bun/benchmarks/compare-with-wasm.ts new file mode 100644 index 0000000000..002982bc80 --- /dev/null +++ b/packages/ai-isolate-quickjs-bun/benchmarks/compare-with-wasm.ts @@ -0,0 +1,327 @@ +/** + * Benchmark: quickjs-bun (native, bun:ffi) vs quickjs-emscripten (WASM). + * + * Both drivers are exercised through the public IsolateDriver interface so + * the numbers include each driver's marshalling/bridging overhead. + * + * Run under Bun (benchmarks both drivers): + * + * bun benchmarks/compare-with-wasm.ts + * + * Run under Node (benchmarks the WASM driver only) + * + * pnpm exec tsx benchmarks/compare-with-wasm.ts + */ +import * as os from 'node:os' +import process from 'node:process' +import { createQuickJSIsolateDriver } from '@tanstack/ai-isolate-quickjs' +import { createQuickJSBunIsolateDriver } from '../src/index' +import type { IsolateDriver, ToolBinding } from '@tanstack/ai-code-mode' + +const FRESH_CONTEXT_ITERATIONS = 30 +const WARM_CONTEXT_ITERATIONS = 100 +const WARMUP_ITERATIONS = 3 + +interface Stats { + mean: number + p50: number + p95: number +} + +interface ScenarioResult { + name: string + stats: Stats + perSecond: number +} + +/** Compute mean / p50 / p95 latency (ms) over a set of timing samples. */ +function summarize(samplesMs: Array): Stats { + const sorted = [...samplesMs].sort((a, b) => a - b) + const at = (q: number) => + sorted[Math.min(sorted.length - 1, Math.floor(q * sorted.length))] ?? 0 + const mean = sorted.reduce((sum, v) => sum + v, 0) / sorted.length + return { mean, p50: at(0.5), p95: at(0.95) } +} + +/** A trivial host tool binding that echoes its input — used to measure host tool-call overhead. */ +function echoBinding(): ToolBinding { + return { + name: 'echo', + description: 'echo tool', + inputSchema: { type: 'object', properties: {} }, + execute: (args: unknown) => Promise.resolve(args), + } +} + +/** + * Create a context, execute `code` once with the given bindings, and dispose + * the context. Throws if the execution fails, so callers can surface it. + */ +async function runOnce( + driver: IsolateDriver, + code: string, + bindings: Record = {}, +): Promise { + const context = await driver.createContext({ bindings, timeout: 30000 }) + try { + const result = await context.execute(code) + if (!result.success) { + throw new Error( + `benchmark execution failed: ${result.error?.name}: ${result.error?.message}`, + ) + } + } finally { + await context.dispose() + } +} + +/** + * quickjs-emscripten's asyncify bridge is known to break under Bun (host + * tool calls crash with "Out of bounds memory access" and never settle), so + * every scenario is raced against a guard. A scenario that hangs poisons + * the WASM driver's global execution queue, so the remaining scenarios are + * skipped once that happens. + */ +async function withGuard( + fn: () => Promise, + guardMs: number, +): Promise<{ ok: true; value: T } | { ok: false; reason: string }> { + let timer: ReturnType | undefined + const guard = new Promise<{ ok: false; reason: string }>((resolve) => { + timer = setTimeout( + () => resolve({ ok: false, reason: `hung (> ${guardMs}ms)` }), + guardMs, + ) + }) + try { + return await Promise.race([ + fn().then((value) => ({ ok: true as const, value })), + guard, + ]) + } catch (error) { + return { + ok: false, + reason: error instanceof Error ? error.message : String(error), + } + } finally { + clearTimeout(timer) + } +} + +/** + * Time `fn` over `iterations` runs (after a fixed warmup) and return the + * latency stats plus throughput (runs per second). + */ +async function measure( + iterations: number, + fn: () => Promise, +): Promise<{ stats: Stats; perSecond: number }> { + for (let i = 0; i < WARMUP_ITERATIONS; i++) await fn() + const samples: Array = [] + const startedAt = performance.now() + for (let i = 0; i < iterations; i++) { + const start = performance.now() + await fn() + samples.push(performance.now() - start) + } + const totalSeconds = (performance.now() - startedAt) / 1000 + return { stats: summarize(samples), perSecond: iterations / totalSeconds } +} + +const SCENARIOS: Array<{ + name: string + code: string + bindings?: Record +}> = [ + { + name: 'trivial (`return 1 + 1`)', + code: 'return 1 + 1', + }, + { + // The WASM driver can complete at most 3 sequential asyncified host + // calls per execution (see the 8-call scenario below), so this is the + // largest like-for-like tool-call comparison. + name: '3 sequential tool calls', + code: ` + let out = [] + for (let i = 0; i < 3; i++) { + out.push(await echo({ i })) + } + return out.length + `, + bindings: { echo: echoBinding() }, + }, + { + // quickjs-emscripten's asyncify bridge crashes ("memory access out of + // bounds") and hangs at >= 4 sequential awaited host calls in one + // execution — on both Node and Bun. Kept to document the limit; the + // native driver has no such cap. + name: '8 sequential tool calls', + code: ` + let out = [] + for (let i = 0; i < 8; i++) { + out.push(await echo({ i })) + } + return out.length + `, + bindings: { echo: echoBinding() }, + }, + { + name: 'compute (fib(20), recursive)', + code: ` + function fib(n) { return n < 2 ? n : fib(n - 1) + fib(n - 2) } + return fib(20) + `, + }, + { + name: 'json (build + roundtrip 5k rows)', + code: ` + const rows = [] + for (let i = 0; i < 5000; i++) { + rows.push({ id: i, name: 'row-' + i, score: i * 1.5 }) + } + return JSON.parse(JSON.stringify(rows)).length + `, + }, +] + +/** + * Run every scenario against one driver and print a Markdown results table: + * cold start, each scenario with a fresh context per run, then a warm-context + * variant. Once a scenario hangs, the driver's queue is poisoned and the rest + * are reported as skipped. + */ +async function benchmarkDriver( + label: string, + driver: IsolateDriver, +): Promise { + console.log(`\n## ${label}`) + + // Cold start: first context creation pays one-time engine initialization + // (TinyCC compile for quickjs-bun, WASM instantiation for emscripten). + const coldStart = performance.now() + const cold = await withGuard(() => runOnce(driver, 'return 1'), 30000) + if (!cold.ok) { + console.log(`\ncold start FAILED: ${cold.reason} — skipping driver\n`) + return + } + const coldMs = performance.now() - coldStart + console.log( + `\ncold start (first context + execute): ${coldMs.toFixed(1)}ms\n`, + ) + + const results: Array = [] + let poisoned = false + + for (const scenario of SCENARIOS) { + const name = `${scenario.name} — fresh context per run` + if (poisoned) { + results.push({ name, failed: 'skipped (driver hung earlier)' }) + continue + } + const fresh = await withGuard( + () => + measure(FRESH_CONTEXT_ITERATIONS, () => + runOnce(driver, scenario.code, scenario.bindings), + ), + 120000, + ) + if (fresh.ok) { + results.push({ + name, + stats: fresh.value.stats, + perSecond: fresh.value.perSecond, + }) + } else { + results.push({ name, failed: fresh.reason }) + if (fresh.reason.startsWith('hung')) poisoned = true + } + } + + // Warm-context variant for the trivial case isolates per-execute overhead + // from context creation cost. + if (!poisoned) { + const warm = await withGuard(async () => { + const context = await driver.createContext({ + bindings: {}, + timeout: 30000, + }) + try { + return await measure(WARM_CONTEXT_ITERATIONS, async () => { + const result = await context.execute('return 1 + 1') + if (!result.success) throw new Error('warm execution failed') + }) + } finally { + await context.dispose() + } + }, 120000) + const name = 'trivial (`return 1 + 1`) — reused context' + if (warm.ok) { + results.push({ + name, + stats: warm.value.stats, + perSecond: warm.value.perSecond, + }) + } else { + results.push({ name, failed: warm.reason }) + } + } + + console.log('| Scenario | mean | p50 | p95 | ops/s |') + console.log('| --- | ---: | ---: | ---: | ---: |') + for (const result of results) { + if ('failed' in result) { + console.log(`| ${result.name} | — | — | — | FAILED: ${result.failed} |`) + continue + } + console.log( + `| ${result.name} | ${result.stats.mean.toFixed(2)}ms | ${result.stats.p50.toFixed(2)}ms | ${result.stats.p95.toFixed(2)}ms | ${result.perSecond.toFixed(0)} |`, + ) + } +} + +/** + * Benchmark entry point: print the environment header, then benchmark the + * native bun:ffi driver (Bun only) followed by the WASM driver. Installs + * process-level handlers so a hung asyncify call can't tear the run down + * before the table prints. + */ +async function main(): Promise { + // A hung asyncify call surfaces as a late unhandled rejection / + // uncaught WASM RuntimeError; keep the benchmark alive so the table + // still prints. + process.on('unhandledRejection', (reason) => { + console.error( + `[unhandled rejection] ${reason instanceof Error ? reason.message : String(reason)}`, + ) + }) + process.on('uncaughtException', (error) => { + console.error(`[uncaught exception] ${error.message}`) + }) + + const isBun = typeof Bun !== 'undefined' + const runtime = isBun ? `Bun ${Bun.version}` : `Node ${process.version}` + console.log( + `isolate driver benchmark — ${runtime}, ${process.platform}/${process.arch}, ${os.cpus().length} CPUs`, + ) + console.log( + `iterations: fresh-context=${FRESH_CONTEXT_ITERATIONS}, warm-context=${WARM_CONTEXT_ITERATIONS}, warmup=${WARMUP_ITERATIONS}`, + ) + + if (isBun) { + await benchmarkDriver( + '@tanstack/ai-isolate-quickjs-bun (native QuickJS via bun:ffi)', + createQuickJSBunIsolateDriver(), + ) + } + + await benchmarkDriver( + '@tanstack/ai-isolate-quickjs (QuickJS WASM via quickjs-emscripten)', + createQuickJSIsolateDriver(), + ) +} + +await main() +// Force exit: a poisoned WASM execution queue can otherwise keep stuck +// handles alive after the benchmark completes. +process.exit(0) diff --git a/packages/ai-isolate-quickjs-bun/package.json b/packages/ai-isolate-quickjs-bun/package.json new file mode 100644 index 0000000000..9acb141edf --- /dev/null +++ b/packages/ai-isolate-quickjs-bun/package.json @@ -0,0 +1,64 @@ +{ + "name": "@tanstack/ai-isolate-quickjs-bun", + "version": "0.0.0", + "description": "Native QuickJS sandbox driver for TanStack AI Code Mode TypeScript execution on Bun via bun:ffi.", + "author": "", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/TanStack/ai.git", + "directory": "packages/ai-isolate-quickjs-bun" + }, + "type": "module", + "module": "./dist/esm/index.js", + "types": "./dist/esm/index.d.ts", + "exports": { + ".": { + "types": "./dist/esm/index.d.ts", + "import": "./dist/esm/index.js" + } + }, + "sideEffects": false, + "files": [ + "dist", + "src" + ], + "engines": { + "bun": ">=1.3.14" + }, + "scripts": { + "build": "vite build", + "clean": "premove ./build ./dist", + "lint:fix": "eslint ./src --fix", + "test:build": "publint --strict", + "test:bun": "bun test ./tests", + "test:eslint": "eslint ./src", + "test:lib": "vitest --passWithNoTests", + "test:lib:dev": "pnpm test:lib --watch", + "test:types": "tsc" + }, + "keywords": [ + "ai", + "ai-sdk", + "typescript", + "tanstack", + "code-mode", + "quickjs", + "bun", + "ffi", + "isolate", + "sandbox", + "code-execution" + ], + "dependencies": { + "quickjs-bun": "0.1.2" + }, + "peerDependencies": { + "@tanstack/ai-code-mode": "workspace:*" + }, + "devDependencies": { + "@tanstack/ai-isolate-quickjs": "workspace:*", + "@types/bun": "^1.3.14", + "@vitest/coverage-v8": "4.0.14" + } +} diff --git a/packages/ai-isolate-quickjs-bun/src/error-normalizer.ts b/packages/ai-isolate-quickjs-bun/src/error-normalizer.ts new file mode 100644 index 0000000000..3fafac9e5d --- /dev/null +++ b/packages/ai-isolate-quickjs-bun/src/error-normalizer.ts @@ -0,0 +1,102 @@ +import type { NormalizedError } from '@tanstack/ai-code-mode' + +const MEMORY_LIMIT_ERROR = 'MemoryLimitError' +const STACK_OVERFLOW_ERROR = 'StackOverflowError' +const TIMEOUT_ERROR = 'TimeoutError' + +/** + * Whether this normalized error indicates the QuickJS VM should not be reused + * (memory or stack limit exceeded). + */ +export function isFatalQuickJSLimitError(error: NormalizedError): boolean { + return ( + error.name === MEMORY_LIMIT_ERROR || error.name === STACK_OVERFLOW_ERROR + ) +} + +/** + * Normalized error for code that exhausted the QuickJS heap so thoroughly + * that QuickJS could not even allocate an Error object — it throws a bare + * `null` exception value in that situation. + */ +export function memoryLimitError(stack?: string): NormalizedError { + return { + name: MEMORY_LIMIT_ERROR, + message: 'Code execution exceeded memory limit', + ...(stack !== undefined && { stack }), + } +} + +/** + * Normalize various error types into a consistent format + */ +export function normalizeError(error: unknown): NormalizedError { + if (error instanceof Error) { + const msg = error.message + const lower = msg.toLowerCase() + + if ( + lower.includes('out of memory') || + lower.includes('memory alloc') || + (error.name === 'InternalError' && lower.includes('memory')) + ) { + return { + name: MEMORY_LIMIT_ERROR, + message: 'Code execution exceeded memory limit', + stack: error.stack, + } + } + + if (lower.includes('stack overflow')) { + return { + name: STACK_OVERFLOW_ERROR, + message: 'Code execution exceeded stack size limit', + stack: error.stack, + } + } + + // quickjs-bun reports deadline expiry as a TimeoutError ("QuickJS + // execution timed out"); a raw QuickJS interrupt surfaces as + // `InternalError: interrupted`. + if ( + error.name === TIMEOUT_ERROR || + (error.name === 'InternalError' && msg === 'interrupted') + ) { + return { + name: TIMEOUT_ERROR, + message: + error.name === TIMEOUT_ERROR ? msg : 'Code execution timed out', + stack: error.stack, + } + } + + return { + name: error.name, + message: error.message, + stack: error.stack, + } + } + + if (typeof error === 'string') { + return { + name: 'Error', + message: error, + } + } + + if (typeof error === 'object' && error !== null) { + const errObj = error as Record + return { + name: String(errObj.name || 'Error'), + message: String(errObj.message || 'Unknown error'), + ...(errObj['stack'] !== undefined && { + stack: String(errObj['stack']), + }), + } + } + + return { + name: 'UnknownError', + message: String(error), + } +} diff --git a/packages/ai-isolate-quickjs-bun/src/index.ts b/packages/ai-isolate-quickjs-bun/src/index.ts new file mode 100644 index 0000000000..70b05e2289 --- /dev/null +++ b/packages/ai-isolate-quickjs-bun/src/index.ts @@ -0,0 +1,13 @@ +export { + createQuickJSBunIsolateDriver, + type QuickJSBunIsolateDriverConfig, +} from './isolate-driver' + +// Re-export types from ai-code-mode for convenience +export type { + IsolateDriver, + IsolateConfig, + IsolateContext, + ExecutionResult, + NormalizedError, +} from '@tanstack/ai-code-mode' diff --git a/packages/ai-isolate-quickjs-bun/src/isolate-context.ts b/packages/ai-isolate-quickjs-bun/src/isolate-context.ts new file mode 100644 index 0000000000..2a3d86368e --- /dev/null +++ b/packages/ai-isolate-quickjs-bun/src/isolate-context.ts @@ -0,0 +1,642 @@ +import { wrapCode } from '@tanstack/ai-code-mode' +import { + isFatalQuickJSLimitError, + memoryLimitError, + normalizeError, +} from './error-normalizer' +import type { + ExecutionResult, + IsolateContext, + NormalizedError, + ToolBinding, +} from '@tanstack/ai-code-mode' +import type * as QuickJSBun from 'quickjs-bun' +import type { Deferred, JSContext, JSRuntime, JSValue } from 'quickjs-bun' + +/** + * The `quickjs-bun` module namespace. The module is imported dynamically by + * the driver (it only loads under the Bun runtime), so the classes and enums + * it exports are threaded through here instead of being imported statically. + */ +export type QuickJSBunModule = typeof QuickJSBun + +/** + * An in-flight host tool call. The sandbox holds a QuickJS promise that is + * resolved from the host once the binding's `execute` settles; `settled` + * lets the execution loop wait for host work without polling. + */ +interface HostTask { + deferred: Deferred + settle: () => void + settled: Promise +} + +/** + * Result envelope passed across the sandbox boundary as a JSON string. + */ +interface ToolResultEnvelope { + success: boolean + value?: unknown + error?: string +} + +/** + * Caps on captured console output. Logs are held on the host and flow back + * to the model, so an unbounded `while (true) console.log(...)` loop in + * untrusted code could grow host memory without ever tripping the sandbox + * heap limit (the sandbox only holds one string at a time). Once either cap + * is reached a single truncation marker is appended and further output is + * dropped for the rest of the execution. + */ +const MAX_LOG_ENTRIES = 10_000 +const MAX_LOG_BYTES = 1_000_000 + +/** Default ceiling on host tool-call invocations per execution. */ +export const DEFAULT_MAX_TOOL_CALLS = 1000 + +/** Placeholder used when a console argument cannot be coerced to a string. */ +const UNPRINTABLE_LOG_VALUE = '[unprintable]' + +/** + * Rebuild a throwable carrying a normalized error's name/message/stack so it + * round-trips through `normalizeError` (which preserves the name, keeping + * fatal-limit classification intact). + */ +function normalizedErrorToThrowable(error: NormalizedError): Error { + const throwable = new Error(error.message) + throwable.name = error.name + if (error.stack !== undefined) throwable.stack = error.stack + return throwable +} + +/** + * Generic wrapper factory evaluated once per context. Tool functions are + * created by calling it with the host implementation and installed on the + * global object with `setGlobal`, so binding names are never interpolated + * into evaluated source code. + */ +const TOOL_WRAPPER_FACTORY = `(function (impl) { + return async function (input) { + const resultJson = await impl(JSON.stringify(input ?? {})); + const result = JSON.parse(resultJson); + if (!result.success) { + throw new Error(result.error); + } + return result.value; + }; +})` + +/** + * IsolateContext implementation backed by a dedicated native QuickJS + * runtime + context pair (via `quickjs-bun`). + */ +export class QuickJSBunIsolateContext implements IsolateContext { + private readonly quickjs: QuickJSBunModule + private readonly runtime: JSRuntime + private readonly vm: JSContext + private readonly timeout: number + private readonly maxToolCalls: number + private readonly logs: Array = [] + private logBytes = 0 + private logTruncated = false + private readonly tasks = new Set() + private toolCallsUsed = 0 + private disposed = false + /** + * A VM-level failure raised while settling a host tool call (e.g. the + * sandbox heap was exhausted when allocating the result string). It is + * surfaced through the execution loop rather than left to escape the + * floating settle callback as an unhandled rejection. + */ + private hostSettleError: NormalizedError | undefined + /** + * Serializes executions on this context. A context only ever runs one + * program at a time; QuickJS contexts are single-threaded and the + * execution loop drives the runtime's job queue. + */ + private execQueue: Promise = Promise.resolve() + + /** + * Wrap a live `quickjs-bun` runtime + context pair, then install the captured + * `console` and the host tool bindings on the sandbox global before the first + * `execute`. Takes ownership of `runtime`/`vm`; `dispose()` frees them. + */ + constructor(options: { + quickjs: QuickJSBunModule + runtime: JSRuntime + vm: JSContext + timeout: number + maxToolCalls: number + bindings: Record + }) { + this.quickjs = options.quickjs + this.runtime = options.runtime + this.vm = options.vm + this.timeout = options.timeout + this.maxToolCalls = options.maxToolCalls + this.installConsole() + this.installBindings(options.bindings) + } + + /** + * Run `code` to completion inside the sandbox and return its result. + * Executions are serialized through a per-context queue, so a second + * `execute` (or a concurrent `dispose`) waits for the in-flight run rather + * than interleaving. Console output produced during the run is captured and + * returned in `logs`. Never throws: tool errors, timeouts, and limit + * violations come back as a failed `ExecutionResult` with a normalized error. + */ + async execute(code: string): Promise> { + if (this.disposed) { + return this.disposedResult() + } + + // Serialize through the per-context queue so a second execute (or a + // concurrent dispose) never interleaves with an in-flight run. + let release!: () => void + const myTurn = new Promise((resolve) => { + release = resolve + }) + const waitForPrev = this.execQueue + this.execQueue = myTurn + + await waitForPrev + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- dispose() may run while awaiting the queue + if (this.disposed) { + release() + return this.disposedResult() + } + + this.logs.length = 0 + this.logBytes = 0 + this.logTruncated = false + this.toolCallsUsed = 0 + this.hostSettleError = undefined + + try { + const value = await this.runToCompletion(wrapCode(code)) + return { + success: true, + value: value as T, + logs: [...this.logs], + } + } catch (error) { + return this.fail(error) + } finally { + // Abandon host tool calls that are still in flight (e.g. after a + // timeout) so a late completion cannot touch the VM. + this.abortTasks() + release() + } + } + + /** + * Release the underlying QuickJS runtime and abandon any in-flight host tool + * calls. Waits for a running execution to finish first (the execution loop + * touches native handles throughout). Idempotent. + */ + async dispose(): Promise { + if (this.disposed) return + + // Wait for any in-flight execution to finish before freeing the + // runtime; the execution loop touches native handles throughout. + await this.execQueue + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- a fatal limit error may dispose the VM while awaiting the queue + if (this.disposed) return + this.disposed = true + this.abortTasks() + this.runtime.dispose() + } + + /** + * Evaluate the wrapped program and drive the QuickJS job queue (and any + * in-flight host tool calls) until the program's promise settles or the + * deadline passes. Returns the parsed result value. + */ + private async runToCompletion(wrappedCode: string): Promise { + const { QuickJSPromiseState, JSException } = this.quickjs + const deadline = performance.now() + this.timeout + + // The synchronous portion of the program is bounded by the QuickJS + // interrupt handler; async continuations are bounded by the deadline + // checks in the loop below. + const resultHandle = this.vm.evalCode(wrappedCode, { + filename: '', + timeoutMs: this.timeout, + }) + + let settledHandle: JSValue + try { + if (resultHandle.promiseState === QuickJSPromiseState.NOT_PROMISE) { + // wrapCode always produces an async IIFE, but stay defensive. + settledHandle = resultHandle.dup() + } else { + for (;;) { + const state = resultHandle.promiseState + if (state === QuickJSPromiseState.FULFILLED) { + settledHandle = resultHandle.promiseResult() + break + } + if (state === QuickJSPromiseState.REJECTED) { + throw new JSException(resultHandle.promiseResult()) + } + + // A host tool call failed to settle the sandbox promise because + // the VM itself errored (e.g. OOM allocating the result string). + // Surface it here so fail() can classify it and release the VM if + // it was fatal, rather than letting it escape as an unhandled + // rejection from the floating settle callback. + if (this.hostSettleError !== undefined) { + throw normalizedErrorToThrowable(this.hostSettleError) + } + + const remainingMs = deadline - performance.now() + if (remainingMs <= 0) { + throw this.timeoutError() + } + + // Run one queued microtask (promise reaction) if there is one... + if (this.runtime.executePendingJob(Math.max(1, remainingMs))) { + continue + } + + // ...otherwise the program is waiting on host work. A settle + // failure raised while draining the last task is caught by the + // hostSettleError check at the top of the next iteration before we + // ever reach this branch. + if (this.tasks.size === 0) { + throw new Error( + 'Code execution is pending on a promise that no host work will ever resolve', + ) + } + await this.waitForAnyTask(remainingMs) + } + } + } finally { + resultHandle.dispose() + } + + try { + const dumped = this.vm.dump(settledHandle) + + // The wrapper returns JSON.stringify(userResult) — parse it back. + // A bare string that isn't valid JSON is returned as-is, and code + // that returned nothing yields undefined. + if (typeof dumped === 'string') { + try { + return JSON.parse(dumped) + } catch { + return dumped + } + } + return dumped + } finally { + settledHandle.dispose() + } + } + + /** Wait until any in-flight host tool call settles, or `timeoutMs` passes. */ + private async waitForAnyTask(timeoutMs: number): Promise { + const settled = Array.from(this.tasks, (task) => task.settled) + let timer: ReturnType | undefined + try { + await Promise.race([ + Promise.race(settled), + new Promise((resolve) => { + // Resolve (not reject): the execution loop re-checks the deadline. + timer = setTimeout(resolve, Math.max(1, timeoutMs)) + }), + ]) + } finally { + clearTimeout(timer) + } + } + + /** + * Install a `console` object on the sandbox global whose `log`/`error`/ + * `warn`/`info` methods funnel into the host log buffer. Non-`log` levels are + * prefixed (e.g. `ERROR: …`) to mirror the other drivers' capture format. + */ + private installConsole(): void { + const { vm } = this + const methods: Array<[name: string, prefix: string]> = [ + ['log', ''], + ['error', 'ERROR'], + ['warn', 'WARN'], + ['info', 'INFO'], + ] + + const consoleObj = vm.newObject() + try { + for (const [method, prefix] of methods) { + const fn = vm.newFunction((...args) => { + const parts = args.map((arg) => this.stringifyConsoleArg(arg)) + const msg = prefix ? `${prefix}: ${parts.join(' ')}` : parts.join(' ') + this.pushLog(msg) + }) + try { + consoleObj.setProp(method, fn) + } finally { + fn.dispose() + } + } + vm.setGlobal('console', consoleObj) + } finally { + consoleObj.dispose() + } + } + + /** + * Coerce a console argument to a string. quickjs-bun's `JSValue.toString()` + * asserts the value is already a string and throws for numbers, objects, + * booleans, etc.; `coerceToString()` performs a real `ToString` (matching + * the WASM driver's `getString`). A coercion that throws (e.g. a symbol or + * a `toString` that throws) degrades to a placeholder rather than aborting + * the whole execution. + */ + private stringifyConsoleArg(arg: JSValue): string { + const { JSException } = this.quickjs + try { + const coerced = arg.coerceToString() + try { + return coerced.toString() + } finally { + coerced.dispose() + } + } catch (error) { + if (error instanceof JSException) error.dispose() + return UNPRINTABLE_LOG_VALUE + } + } + + /** Append a captured log line, enforcing the entry-count and byte caps. */ + private pushLog(msg: string): void { + if (this.logTruncated) return + if ( + this.logs.length >= MAX_LOG_ENTRIES || + this.logBytes + msg.length > MAX_LOG_BYTES + ) { + this.logTruncated = true + this.logs.push('[log output truncated]') + return + } + this.logs.push(msg) + this.logBytes += msg.length + } + + /** + * Install each host tool `binding` as an async function on the sandbox + * global. Wrappers are produced by evaluating `TOOL_WRAPPER_FACTORY` once and + * calling it per binding, so binding names are never interpolated into + * evaluated source. No-op when there are no bindings. + */ + private installBindings(bindings: Record): void { + const { vm } = this + const entries = Object.entries(bindings) + if (entries.length === 0) return + + const factory = vm.evalCode(TOOL_WRAPPER_FACTORY, { + filename: '', + }) + try { + for (const [name, binding] of entries) { + const impl = vm.newFunction((argsHandle) => + this.runBinding(binding, argsHandle), + ) + try { + const wrapped = vm.callFunction(factory, vm.undefined, impl) + try { + vm.setGlobal(name, wrapped) + } finally { + wrapped.dispose() + } + } finally { + impl.dispose() + } + } + } finally { + factory.dispose() + } + } + + /** + * Host side of a tool call. Invoked synchronously from inside the VM; + * returns a QuickJS promise immediately and settles it from the host + * once `binding.execute` finishes. Never rejects the promise for tool + * errors — failures travel in the JSON envelope so the sandbox wrapper + * can rethrow them as regular errors. + */ + private runBinding( + binding: ToolBinding, + argsHandle: JSValue | undefined, + ): JSValue { + const { vm } = this + + // Bound the number of host tool calls per execution. Untrusted sandbox + // code can otherwise fan out (e.g. `Promise.all` over a huge array) into + // unbounded concurrent host work — the deadline only bounds wall-clock, + // not the burst. Throwing here surfaces as a catchable error at the call + // site inside the sandbox. + if (this.toolCallsUsed >= this.maxToolCalls) { + throw new Error( + `Exceeded the maximum of ${this.maxToolCalls} tool calls per execution`, + ) + } + this.toolCallsUsed++ + + const deferred = vm.newPromise() + const promise = deferred.promise.dup() + + let settle: () => void = () => undefined + const settledPromise = new Promise((resolve) => { + settle = resolve + }) + const task: HostTask = { deferred, settle, settled: settledPromise } + this.tasks.add(task) + + const settleWith = (envelope: ToolResultEnvelope): void => { + // The task may have been abandoned by a timeout or dispose — never + // touch the VM in that case. + if (this.disposed || !this.tasks.has(task)) return + this.tasks.delete(task) + try { + let json: string + try { + json = JSON.stringify(envelope) + } catch (error) { + json = JSON.stringify({ + success: false, + error: `Tool result is not JSON-serializable: ${ + error instanceof Error ? error.message : String(error) + }`, + }) + } + const handle = vm.newString(json) + try { + deferred.resolve(handle) + } finally { + handle.dispose() + } + } catch (error) { + // Allocating the result string or resolving the promise can fail if + // the sandbox heap is exhausted. Record it so the execution loop can + // classify it (and release the VM if fatal) instead of letting it + // escape the floating async callback as an unhandled rejection. + this.hostSettleError ??= this.toNormalizedError(error) + } finally { + deferred.dispose() + task.settle() + } + } + + // The wrapper always passes a JSON string, but degrade gracefully if + // the handle dumps to something else. + let argsJson = '{}' + try { + const dumped = vm.dump(argsHandle ?? vm.undefined) + if (typeof dumped === 'string') { + argsJson = dumped + } + } catch (error) { + // Fall through with empty args (JSON.parse below cannot fail on '{}'). + // dump() rethrows VM failures as a JSException whose owned value must + // be released. + if (error instanceof this.quickjs.JSException) error.dispose() + } + + void (async () => { + let envelope: ToolResultEnvelope + try { + const args: unknown = JSON.parse(argsJson) + const value = await binding.execute(args) + envelope = { success: true, value } + } catch (error) { + envelope = { + success: false, + error: error instanceof Error ? error.message : String(error), + } + } + settleWith(envelope) + })() + + return promise + } + + /** Abandon all in-flight host tool calls and release their VM handles. */ + private abortTasks(): void { + for (const task of this.tasks) { + task.deferred.dispose() + task.settle() + } + this.tasks.clear() + } + + /** + * Build a failed `ExecutionResult` from a thrown value, attaching the + * captured logs. If the error is a fatal QuickJS limit (memory/stack), the + * runtime is released first since it may be left unusable. + */ + private fail(error: unknown): ExecutionResult { + const normalized = this.toNormalizedError(error) + if (isFatalQuickJSLimitError(normalized)) { + this.releaseVmAfterFatalLimit() + } + return { + success: false, + error: normalized, + logs: [...this.logs], + } + } + + /** + * Convert any thrown value into a `NormalizedError`, unwrapping a quickjs-bun + * `JSException`: a bare `null` exception value (heap too exhausted to + * allocate an Error) becomes a `MemoryLimitError`, and a thrown plain + * object/array has its `message`/`name` recovered so the model still gets + * useful feedback — parity with the WASM driver. Always disposes the owned + * exception value. + */ + private toNormalizedError(error: unknown): NormalizedError { + const { JSException } = this.quickjs + if (error instanceof JSException) { + try { + const value = error.value + // QuickJS throws a bare `null` exception value when the heap is too + // exhausted to allocate an Error object. (A literal `throw null` in + // sandbox code is indistinguishable and is treated the same way.) + if (value.type === 'null') { + return memoryLimitError(error.stack) + } + // A thrown plain object/array surfaces through quickjs-bun's + // JSException as the generic "QuickJS object was thrown"; recover its + // `message` (and `name`) so the model still gets useful feedback for + // self-correction — parity with the WASM driver. + if (value.type === 'object' || value.type === 'array') { + const message = this.readValueProp(value, 'message') + if (message !== undefined) { + return { + name: this.readValueProp(value, 'name') ?? error.name, + message, + ...(error.stack !== undefined && { stack: error.stack }), + } + } + } + return normalizeError(error) + } finally { + error.dispose() + } + } + return normalizeError(error) + } + + /** + * Read a string property from a thrown QuickJS value, tolerating throwing + * getters (the value is attacker-controlled). Returns undefined on any + * failure and releases the owned exception in that case. + */ + private readValueProp(value: JSValue, name: string): string | undefined { + try { + return value.errorProperty(name) + } catch (error) { + if (error instanceof this.quickjs.JSException) error.dispose() + return undefined + } + } + + /** + * After a memory/stack limit error the QuickJS runtime may be left in an + * unusable state — release it eagerly so the host process reclaims the + * native memory. Matches the QuickJS WASM driver's behavior. + */ + private releaseVmAfterFatalLimit(): void { + if (this.disposed) return + this.disposed = true + this.abortTasks() + try { + this.runtime.dispose() + } catch { + // ignore if the runtime is already torn down + } + } + + /** Construct the `TimeoutError` thrown when an execution exceeds its deadline. */ + private timeoutError(): Error { + const error = new Error(`Code execution timed out after ${this.timeout}ms`) + error.name = 'TimeoutError' + return error + } + + /** The failed `ExecutionResult` returned once the context has been disposed. */ + private disposedResult(): ExecutionResult { + return { + success: false, + error: { + name: 'DisposedError', + message: 'Context has been disposed', + }, + logs: [], + } + } +} diff --git a/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts b/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts new file mode 100644 index 0000000000..75e5a66264 --- /dev/null +++ b/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts @@ -0,0 +1,183 @@ +import { + DEFAULT_MAX_TOOL_CALLS, + QuickJSBunIsolateContext, +} from './isolate-context' +import type { + IsolateConfig, + IsolateContext, + IsolateDriver, +} from '@tanstack/ai-code-mode' +import type { QuickJS } from 'quickjs-bun' +import type { QuickJSBunModule } from './isolate-context' + +/** Default execution timeout in ms (matches the other isolate drivers). */ +const DEFAULT_TIMEOUT_MS = 30000 + +/** Default memory limit in MB (matches the other isolate drivers). */ +const DEFAULT_MEMORY_LIMIT_MB = 128 + +/** Default max stack size in bytes (matches the QuickJS WASM driver). */ +const DEFAULT_MAX_STACK_SIZE_BYTES = 512 * 1024 + +/** + * quickjs-bun's exports map only declares a `bun` condition, so build- and + * test-time resolvers running on Node.js cannot resolve it. The non-literal + * specifier keeps the import out of Vite's static analysis; it only ever + * executes under the Bun runtime. + */ +const QUICKJS_BUN_SPECIFIER = 'quickjs-bun' + +/** + * Dynamically import the `quickjs-bun` module namespace. Kept as a function + * (not a static import) because the package only resolves under the Bun + * runtime; see `QUICKJS_BUN_SPECIFIER` for why the specifier is non-literal and + * hidden from Vite's static analysis. + */ +function importQuickJSBun(): Promise { + return import(/* @vite-ignore */ QUICKJS_BUN_SPECIFIER) +} + +let libraryPromise: Promise | undefined + +/** + * Load the QuickJS library once per process, memoized in `libraryPromise`. + * `quickjs-bun` compiles the vendored QuickJS C sources with Bun's embedded + * TinyCC on first use (~100ms), after which creating a runtime + context costs + * ~1-2ms. A failed load (e.g. a missing prebuilt library path on Windows) is + * not cached, so a corrected environment can retry. + */ +async function loadQuickJSLibrary(): Promise { + libraryPromise ??= importQuickJSBun().then((mod) => new mod.QuickJS()) + try { + return await libraryPromise + } catch (error) { + // Don't cache failures (e.g. a missing prebuilt library path on + // Windows) so a corrected environment can retry. + libraryPromise = undefined + throw error + } +} + +/** + * Configuration for the QuickJS Bun isolate driver + */ +export interface QuickJSBunIsolateDriverConfig { + /** + * Default execution timeout in ms (default: 30000) + */ + timeout?: number + + /** + * Default memory limit in MB (default: 128). + * Applied via QuickJS `JS_SetMemoryLimit` on the per-context runtime. + */ + memoryLimit?: number + + /** + * Default max stack size in bytes (default: 512 KiB). + * Applied via QuickJS `JS_SetMaxStackSize` on the per-context runtime. + */ + maxStackSize?: number + + /** + * Maximum number of host tool calls a single execution may make (default: + * 1000). Bounds output and memory growth from untrusted sandbox code (e.g. a + * `Promise.all` over a huge array); exceeding it throws a catchable error + * inside the sandbox. The execution timeout still bounds wall-clock time. + */ + maxToolCalls?: number +} + +/** + * Create a QuickJS isolate driver for the Bun runtime + * + * This driver runs QuickJS natively through `bun:ffi` (via `quickjs-bun`) + * instead of WebAssembly. Each context gets its own QuickJS runtime with + * dedicated memory and stack limits, so sandboxes are fully isolated from + * each other and from the host. It requires Bun >= 1.3.14 — on Node.js use + * `@tanstack/ai-isolate-node` or `@tanstack/ai-isolate-quickjs` instead. + * + * Tools are injected as async functions that bridge back to the host. + * + * @example + * ```typescript + * import { createQuickJSBunIsolateDriver } from '@tanstack/ai-isolate-quickjs-bun' + * + * const driver = createQuickJSBunIsolateDriver({ + * timeout: 30000, + * }) + * + * const context = await driver.createContext({ + * bindings: { + * readFile: { + * name: 'readFile', + * description: 'Read a file', + * inputSchema: { type: 'object', properties: { path: { type: 'string' } } }, + * execute: async ({ path }) => fs.readFile(path, 'utf-8'), + * }, + * }, + * }) + * + * const result = await context.execute(` + * const content = await readFile({ path: './data.json' }) + * return JSON.parse(content) + * `) + * ``` + */ +export function createQuickJSBunIsolateDriver( + config: QuickJSBunIsolateDriverConfig = {}, +): IsolateDriver { + const defaultTimeout = config.timeout ?? DEFAULT_TIMEOUT_MS + const defaultMemoryLimit = config.memoryLimit ?? DEFAULT_MEMORY_LIMIT_MB + const defaultMaxStackSize = + config.maxStackSize ?? DEFAULT_MAX_STACK_SIZE_BYTES + const maxToolCalls = config.maxToolCalls ?? DEFAULT_MAX_TOOL_CALLS + + return { + /** + * Create a fresh isolate context backed by its own QuickJS runtime + + * context. Each context gets a dedicated runtime, so its memory/stack + * limits and job queue are independent of every other context. Throws on + * Node.js — this driver requires the Bun runtime. + */ + async createContext(isolateConfig: IsolateConfig): Promise { + if (typeof Bun === 'undefined') { + throw new Error( + '@tanstack/ai-isolate-quickjs-bun requires the Bun runtime (https://bun.sh). ' + + 'On Node.js, use @tanstack/ai-isolate-node or @tanstack/ai-isolate-quickjs instead.', + ) + } + + const timeout = Math.max(1, isolateConfig.timeout ?? defaultTimeout) + const memoryLimitMb = isolateConfig.memoryLimit ?? defaultMemoryLimit + const maxStackSizeBytes = defaultMaxStackSize + + const quickjs = await importQuickJSBun() + const library = await loadQuickJSLibrary() + + // A dedicated runtime per context gives every sandbox its own heap + // and stack limits, mirroring `setMemoryLimit`/`setMaxStackSize` in + // the QuickJS WASM driver. + const runtime = new quickjs.JSRuntime({ + library, + memoryBytes: memoryLimitMb * 1024 * 1024, + stackBytes: maxStackSizeBytes, + }) + + try { + const vm = runtime.createContext({ timeoutMs: timeout }) + return new QuickJSBunIsolateContext({ + quickjs, + runtime, + vm, + timeout, + maxToolCalls, + bindings: isolateConfig.bindings, + }) + } catch (error) { + runtime.dispose() + throw error + } + }, + } +} diff --git a/packages/ai-isolate-quickjs-bun/tests/escape-attempts.test.ts b/packages/ai-isolate-quickjs-bun/tests/escape-attempts.test.ts new file mode 100644 index 0000000000..1b2b03343a --- /dev/null +++ b/packages/ai-isolate-quickjs-bun/tests/escape-attempts.test.ts @@ -0,0 +1,145 @@ +import { describe, expect, it } from 'vitest' +import { createQuickJSBunIsolateDriver } from '../src/isolate-driver' + +async function runInIsolate( + code: string, + opts?: { timeout?: number }, +): Promise<{ + success: boolean + value: unknown + error?: { name: string; message: string } +}> { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ + bindings: {}, + ...(opts?.timeout !== undefined && { timeout: opts.timeout }), + }) + try { + const res = await context.execute(code) + return { + success: res.success, + value: res.value, + ...(res.error !== undefined && { error: res.error }), + } + } finally { + await context.dispose() + } +} + +// quickjs-bun is built on bun:ffi, so this suite only runs under Bun. +// Run locally with: bun test ./tests +describe.skipIf(typeof Bun === 'undefined')( + 'QuickJS Bun isolate — sandbox escape attempts', + () => { + it('does not expose `process`', async () => { + const res = await runInIsolate('return typeof process') + expect(res.success).toBe(true) + expect(res.value).toBe('undefined') + }) + + it('does not expose `require`', async () => { + const res = await runInIsolate('return typeof require') + expect(res.success).toBe(true) + expect(res.value).toBe('undefined') + }) + + it('does not expose `fetch`', async () => { + const res = await runInIsolate('return typeof fetch') + expect(res.success).toBe(true) + expect(res.value).toBe('undefined') + }) + + it('does not expose `Bun`', async () => { + const res = await runInIsolate('return typeof Bun') + expect(res.success).toBe(true) + expect(res.value).toBe('undefined') + }) + + it('does not expose timers or the host event loop', async () => { + const res = await runInIsolate( + 'return [typeof setTimeout, typeof setInterval, typeof queueMicrotask].join(",")', + ) + expect(res.success).toBe(true) + expect(res.value).toBe('undefined,undefined,undefined') + }) + + it('does not include the QuickJS std and os modules', async () => { + const res = await runInIsolate(` + try { + const std = await import('std') + return 'loaded' + } catch (e) { + return 'blocked' + } + `) + expect(res.success).toBe(true) + expect(res.value).toBe('blocked') + }) + + it('does not leak Object.prototype pollution to the host', async () => { + await runInIsolate(` + Object.prototype.__qjsLeak = 'leaked' + return 1 + `) + expect( + (Object.prototype as { __qjsLeak?: unknown }).__qjsLeak, + ).toBeUndefined() + }) + + it('does not leak Object.prototype pollution between separate contexts', async () => { + const driver = createQuickJSBunIsolateDriver() + const ctxA = await driver.createContext({ bindings: {} }) + const ctxB = await driver.createContext({ bindings: {} }) + try { + await ctxA.execute(`Object.prototype.__qjsCtxProbe = 'a'; return 1;`) + const res = await ctxB.execute(`return ({}).__qjsCtxProbe`) + expect(res.success).toBe(true) + expect(res.value).toBeUndefined() + } finally { + await ctxA.dispose() + await ctxB.dispose() + } + }) + + it('terminates a synchronous CPU-spin loop via timeout (does not hang)', async () => { + const start = Date.now() + const res = await runInIsolate('while (true) {}', { timeout: 200 }) + const elapsed = Date.now() - start + expect(res.success).toBe(false) + expect(elapsed).toBeLessThan(5000) + }) + + it('rejects Function-constructor escape attempts', async () => { + const res = await runInIsolate(` + try { + const C = (function(){}).constructor + return typeof C('return process')() + } catch (e) { + return 'blocked: ' + e.message + } + `) + expect(res.success).toBe(true) + // Either undefined (Function runs in isolate where process doesn't exist) + // or a blocked message. The sandbox must not return a real process object. + expect( + res.value === 'undefined' || + (typeof res.value === 'string' && res.value.startsWith('blocked:')), + ).toBe(true) + }) + + it('treats global mutations within a context as scoped to that context only', async () => { + const driver = createQuickJSBunIsolateDriver() + const ctxA = await driver.createContext({ bindings: {} }) + const ctxB = await driver.createContext({ bindings: {} }) + try { + await ctxA.execute(`globalThis.__ctxMarker = 'A'; return 1;`) + const res = await ctxB.execute(`return typeof globalThis.__ctxMarker`) + expect(res.success).toBe(true) + expect(res.value).toBe('undefined') + } finally { + await ctxA.dispose() + await ctxB.dispose() + } + }) + }, +) diff --git a/packages/ai-isolate-quickjs-bun/tests/isolate-driver.test.ts b/packages/ai-isolate-quickjs-bun/tests/isolate-driver.test.ts new file mode 100644 index 0000000000..20988bf294 --- /dev/null +++ b/packages/ai-isolate-quickjs-bun/tests/isolate-driver.test.ts @@ -0,0 +1,739 @@ +import { describe, expect, it } from 'vitest' +import { createQuickJSBunIsolateDriver } from '../src/isolate-driver' +import type { ToolBinding } from '@tanstack/ai-code-mode' + +function makeBinding( + name: string, + execute: (args: unknown) => Promise, +): ToolBinding { + return { + name, + description: `${name} tool`, + inputSchema: { type: 'object', properties: {} }, + execute, + } +} + +// The driver only runs under Bun (quickjs-bun is built on bun:ffi). Under +// Node the suite is skipped, mirroring how ai-isolate-node skips when the +// isolated-vm addon is unavailable. Run locally with: bun test ./tests +describe.skipIf(typeof Bun === 'undefined')( + 'createQuickJSBunIsolateDriver', + () => { + describe('createContext', () => { + it('returns a context with execute and dispose', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + expect(context).toBeDefined() + expect(typeof context.execute).toBe('function') + expect(typeof context.dispose).toBe('function') + + const result = await context.execute('return 42') + expect(result.success).toBe(true) + expect(result.value).toBe(42) + await context.dispose() + }) + }) + + describe('execute - basic execution', () => { + it('evaluates arithmetic and returns value', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute('return 3 + 4') + + expect(result.success).toBe(true) + expect(result.value).toBe(7) + await context.dispose() + }) + + it('evaluates string operations', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute('return "hello" + " " + "world"') + + expect(result.success).toBe(true) + expect(result.value).toBe('hello world') + await context.dispose() + }) + + it('evaluates async code and returns value', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute(` + const x = await Promise.resolve(10); + return x + 2; + `) + + expect(result.success).toBe(true) + expect(result.value).toBe(12) + await context.dispose() + }) + + it('returns object and array values', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute('return { a: 1, b: [2, 3] }') + + expect(result.success).toBe(true) + expect(result.value).toEqual({ a: 1, b: [2, 3] }) + await context.dispose() + }) + + it('returns undefined when code returns nothing', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute('const x = 1;') + + expect(result.success).toBe(true) + expect(result.value).toBeUndefined() + await context.dispose() + }) + }) + + describe('execute - tool bindings', () => { + it('injects tool and executes tool call', async () => { + const add = makeBinding('add', async (args: unknown) => { + const { a, b } = args as { a: number; b: number } + return a + b + }) + + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ + bindings: { add }, + }) + + const result = await context.execute(` + const sum = await add({ a: 2, b: 3 }); + return sum; + `) + + expect(result.success).toBe(true) + expect(result.value).toBe(5) + await context.dispose() + }) + + it('supports multiple tools in one execution', async () => { + const getA = makeBinding('getA', async () => 'A') + const getB = makeBinding('getB', async () => 'B') + + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ + bindings: { getA, getB }, + }) + + const result = await context.execute(` + return (await getA({})) + (await getB({})); + `) + + expect(result.success).toBe(true) + expect(result.value).toBe('AB') + await context.dispose() + }) + + it('supports concurrent tool calls via Promise.all', async () => { + const double = makeBinding('double', async (args: unknown) => { + const { n } = args as { n: number } + await new Promise((resolve) => setTimeout(resolve, 10)) + return n * 2 + }) + + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ + bindings: { double }, + }) + + const result = await context.execute(` + const [a, b, c] = await Promise.all([ + double({ n: 1 }), + double({ n: 2 }), + double({ n: 3 }), + ]); + return a + b + c; + `) + + expect(result.success).toBe(true) + expect(result.value).toBe(12) + await context.dispose() + }) + + it('passes empty input when a tool is called without arguments', async () => { + let received: unknown = 'unset' + const probe = makeBinding('probe', async (args: unknown) => { + received = args + return 'ok' + }) + + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ + bindings: { probe }, + }) + + const result = await context.execute('return await probe()') + + expect(result.success).toBe(true) + expect(result.value).toBe('ok') + expect(received).toEqual({}) + await context.dispose() + }) + + it('returns undefined for tools that resolve with undefined', async () => { + const noop = makeBinding('noop', async () => undefined) + + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ + bindings: { noop }, + }) + + const result = await context.execute('return typeof (await noop({}))') + + expect(result.success).toBe(true) + expect(result.value).toBe('undefined') + await context.dispose() + }) + + it('surfaces tool execution errors', async () => { + const failTool = makeBinding('failTool', async () => { + throw new Error('Tool failed') + }) + + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ + bindings: { failTool }, + }) + + const result = await context.execute('return await failTool({})') + + expect(result.success).toBe(false) + expect(result.error?.message).toContain('Tool failed') + await context.dispose() + }) + + it('lets sandbox code catch tool errors', async () => { + const failTool = makeBinding('failTool', async () => { + throw new Error('Tool failed') + }) + + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ + bindings: { failTool }, + }) + + const result = await context.execute(` + try { + await failTool({}); + return 'no error'; + } catch (e) { + return 'caught: ' + e.message; + } + `) + + expect(result.success).toBe(true) + expect(result.value).toBe('caught: Tool failed') + await context.dispose() + }) + }) + + describe('execute - timeout', () => { + it('returns timeout error when code runs too long', async () => { + const driver = createQuickJSBunIsolateDriver({ timeout: 50 }) + const context = await driver.createContext({ + bindings: {}, + timeout: 50, + }) + + // Busy loop that should trigger interrupt handler + const result = await context.execute(` + const start = Date.now(); + while (Date.now() - start < 500) { + // spin + } + return 1; + `) + + expect(result.success).toBe(false) + expect(result.error?.name).toBe('TimeoutError') + await context.dispose() + }) + + it('returns timeout error when a tool call outlives the deadline', async () => { + const slow = makeBinding('slow', async () => { + await new Promise((resolve) => setTimeout(resolve, 500)) + return 'too late' + }) + + const driver = createQuickJSBunIsolateDriver({ timeout: 50 }) + const context = await driver.createContext({ + bindings: { slow }, + timeout: 50, + }) + + const start = Date.now() + const result = await context.execute('return await slow({})') + const elapsed = Date.now() - start + + expect(result.success).toBe(false) + expect(result.error?.name).toBe('TimeoutError') + expect(elapsed).toBeLessThan(5000) + await context.dispose() + }) + + it('fails fast for promises no host work will resolve', async () => { + const driver = createQuickJSBunIsolateDriver({ timeout: 5000 }) + const context = await driver.createContext({ bindings: {} }) + + const start = Date.now() + const result = await context.execute( + 'return await new Promise(() => {})', + ) + const elapsed = Date.now() - start + + expect(result.success).toBe(false) + expect(result.error?.message).toContain('no host work') + // Should not wait for the full 5s timeout + expect(elapsed).toBeLessThan(1000) + await context.dispose() + }) + }) + + describe('execute - error handling', () => { + it('returns error for syntax errors', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute('syntax error!!!') + + expect(result.success).toBe(false) + expect(result.error?.message).toBeDefined() + await context.dispose() + }) + + it('returns error for runtime errors', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute('throw new Error("oops")') + + expect(result.success).toBe(false) + expect(result.error?.message).toContain('oops') + await context.dispose() + }) + + it('includes captured logs in failure results', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute(` + console.log("before failure"); + throw new Error("oops"); + `) + + expect(result.success).toBe(false) + expect(result.logs).toContain('before failure') + await context.dispose() + }) + }) + + describe('execute - console capture', () => { + it('captures console.log in logs', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute(` + console.log("hello"); + console.log("world"); + return 1; + `) + + expect(result.success).toBe(true) + expect(result.logs).toContain('hello') + expect(result.logs).toContain('world') + await context.dispose() + }) + + it('captures console.error with ERROR prefix', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute(` + console.error("fail"); + return 1; + `) + + expect(result.success).toBe(true) + expect(result.logs?.some((l) => l.includes('fail'))).toBe(true) + expect(result.logs).toContain('ERROR: fail') + await context.dispose() + }) + + it('captures console.warn and console.info with prefixes', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute(` + console.warn("careful"); + console.info("fyi"); + return 1; + `) + + expect(result.success).toBe(true) + expect(result.logs).toContain('WARN: careful') + expect(result.logs).toContain('INFO: fyi') + await context.dispose() + }) + }) + + describe('dispose', () => { + it('execute returns DisposedError after dispose', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + await context.dispose() + + const result = await context.execute('return 1') + + expect(result.success).toBe(false) + expect(result.error?.name).toBe('DisposedError') + expect(result.error?.message).toContain('disposed') + }) + + it('dispose is idempotent', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + await context.dispose() + await expect(context.dispose()).resolves.toBeUndefined() + }) + }) + + describe('memory isolation', () => { + it('contexts do not share state', async () => { + const driver = createQuickJSBunIsolateDriver() + const ctx1 = await driver.createContext({ bindings: {} }) + const ctx2 = await driver.createContext({ bindings: {} }) + + await ctx1.execute('globalThis.__secret = 100; return 1') + const result2 = await ctx2.execute('return typeof globalThis.__secret') + + expect(result2.success).toBe(true) + expect(result2.value).toBe('undefined') + + await ctx1.dispose() + await ctx2.dispose() + }) + }) + + describe('memoryLimit config', () => { + it('accepts memoryLimit via createContext and runs successfully', async () => { + const driver = createQuickJSBunIsolateDriver({ memoryLimit: 64 }) + const context = await driver.createContext({ + bindings: {}, + memoryLimit: 64, + }) + + const result = await context.execute('return 1 + 1') + + expect(result.success).toBe(true) + expect(result.value).toBe(2) + await context.dispose() + }) + + it('returns MemoryLimitError when allocation exceeds limit (does not crash)', async () => { + const driver = createQuickJSBunIsolateDriver({ memoryLimit: 1 }) + const context = await driver.createContext({ + bindings: {}, + memoryLimit: 1, + }) + + const result = await context.execute( + `return "x".repeat(8 * 1024 * 1024);`, + ) + + expect(result.success).toBe(false) + expect(result.error?.name).toBe('MemoryLimitError') + expect(result.error?.message).toContain('memory limit') + }) + + it('returns MemoryLimitError when the heap is fully exhausted', async () => { + const driver = createQuickJSBunIsolateDriver({ memoryLimit: 1 }) + const context = await driver.createContext({ + bindings: {}, + memoryLimit: 1, + }) + + const result = await context.execute(` + const items = []; + while (true) items.push(new Array(1000).fill('x').join('')); + `) + + expect(result.success).toBe(false) + expect(result.error?.name).toBe('MemoryLimitError') + expect(result.error?.message).toContain('memory limit') + }) + + it('dispose is safe after memory limit error', async () => { + const driver = createQuickJSBunIsolateDriver({ memoryLimit: 1 }) + const context = await driver.createContext({ + bindings: {}, + memoryLimit: 1, + }) + + await context.execute(`return "x".repeat(8 * 1024 * 1024);`) + + await expect(context.dispose()).resolves.toBeUndefined() + }) + }) + + describe('maxStackSize config', () => { + it('returns StackOverflowError for deep recursion when stack is small', async () => { + const driver = createQuickJSBunIsolateDriver({ + maxStackSize: 32 * 1024, + timeout: 30000, + }) + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute(` + function f(n) { + if (n <= 0) return 0; + return 1 + f(n - 1); + } + return f(200000); + `) + + expect(result.success).toBe(false) + expect(result.error?.name).toBe('StackOverflowError') + expect(result.error?.message).toContain('stack') + await context.dispose() + }) + }) + + describe('execute after fatal memory limit', () => { + it('returns DisposedError on subsequent execute after OOM', async () => { + const driver = createQuickJSBunIsolateDriver({ memoryLimit: 1 }) + const context = await driver.createContext({ + bindings: {}, + memoryLimit: 1, + }) + + const first = await context.execute( + `return "x".repeat(8 * 1024 * 1024);`, + ) + expect(first.success).toBe(false) + expect(first.error?.name).toBe('MemoryLimitError') + + const second = await context.execute('return 42') + expect(second.success).toBe(false) + expect(second.error?.name).toBe('DisposedError') + expect(second.error?.message).toContain('disposed') + }) + }) + + describe('execution serialization', () => { + it('serializes concurrent executes on the same context', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const [first, second] = await Promise.all([ + context.execute( + 'globalThis.__order = (globalThis.__order ?? "") + "a"; return globalThis.__order', + ), + context.execute( + 'globalThis.__order = (globalThis.__order ?? "") + "b"; return globalThis.__order', + ), + ]) + + expect(first.success).toBe(true) + expect(second.success).toBe(true) + expect(first.value).toBe('a') + expect(second.value).toBe('ab') + await context.dispose() + }) + }) + + describe('console capture - non-string arguments', () => { + it('coerces numbers, booleans, objects, and mixed args', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute(` + console.log(42); + console.log(true); + console.log({ a: 1 }); + console.log('count:', 3); + console.log(null, undefined); + return 'done'; + `) + + expect(result.success).toBe(true) + expect(result.value).toBe('done') + expect(result.logs).toContain('42') + expect(result.logs).toContain('true') + expect(result.logs).toContain('[object Object]') + expect(result.logs).toContain('count: 3') + expect(result.logs).toContain('null undefined') + await context.dispose() + }) + + it('does not let a throwing toString abort the execution', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute(` + const evil = { toString() { throw new Error('nope') } }; + console.log(evil); + return 'survived'; + `) + + expect(result.success).toBe(true) + expect(result.value).toBe('survived') + expect(result.logs).toContain('[unprintable]') + await context.dispose() + }) + }) + + describe('console capture - log buffer cap', () => { + it('truncates runaway log output instead of growing unbounded', async () => { + const driver = createQuickJSBunIsolateDriver({ timeout: 10000 }) + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute(` + for (let i = 0; i < 100000; i++) console.log('x'.repeat(1000)); + return 'done'; + `) + + expect(result.success).toBe(true) + expect(result.logs).toContain('[log output truncated]') + // Cap is 1,000,000 bytes; allow generous slack for the marker. + const totalBytes = (result.logs ?? []).reduce( + (sum, line) => sum + line.length, + 0, + ) + expect(totalBytes).toBeLessThan(1_100_000) + await context.dispose() + }) + }) + + describe('maxToolCalls config', () => { + it('throws inside the sandbox once the tool-call budget is exhausted', async () => { + const ping = makeBinding('ping', async () => 'pong') + const driver = createQuickJSBunIsolateDriver({ maxToolCalls: 3 }) + const context = await driver.createContext({ bindings: { ping } }) + + const result = await context.execute(` + let calls = 0; + try { + for (let i = 0; i < 100; i++) { await ping({}); calls++; } + } catch (e) { + return { calls, error: e.message }; + } + return { calls, error: null }; + `) + + expect(result.success).toBe(true) + const value = result.value as { calls: number; error: string | null } + expect(value.calls).toBe(3) + expect(value.error).toContain('maximum of 3 tool calls') + await context.dispose() + }) + }) + + describe('tool result exceeding the memory limit', () => { + it('fails with a normalized error and no unhandled rejection', async () => { + const onRejection = (reason: unknown) => { + throw reason instanceof Error ? reason : new Error(String(reason)) + } + process.on('unhandledRejection', onRejection) + try { + const huge = makeBinding('huge', async () => + 'y'.repeat(16 * 1024 * 1024), + ) + const driver = createQuickJSBunIsolateDriver({ memoryLimit: 2 }) + const context = await driver.createContext({ + bindings: { huge }, + memoryLimit: 2, + }) + + const result = await context.execute('return await huge({})') + + expect(result.success).toBe(false) + expect(result.error?.name).toBe('MemoryLimitError') + + // The VM was released as fatal; the next execute is DisposedError. + const next = await context.execute('return 1') + expect(next.error?.name).toBe('DisposedError') + + // Give any stray rejection a tick to surface before we detach. + await new Promise((resolve) => setTimeout(resolve, 50)) + } finally { + process.off('unhandledRejection', onRejection) + } + }) + }) + + describe('error normalization - thrown values', () => { + it('preserves the message of a thrown plain object', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute( + `throw { name: 'CustomError', message: 'custom obj' }`, + ) + + expect(result.success).toBe(false) + expect(result.error?.message).toBe('custom obj') + expect(result.error?.name).toBe('CustomError') + await context.dispose() + }) + + it('preserves a thrown string', async () => { + const driver = createQuickJSBunIsolateDriver() + const context = await driver.createContext({ bindings: {} }) + + const result = await context.execute(`throw 'bare string'`) + + expect(result.success).toBe(false) + expect(result.error?.message).toBe('bare string') + await context.dispose() + }) + }) + + describe('context reuse after non-fatal timeout', () => { + it('remains usable after a timed-out execution', async () => { + const driver = createQuickJSBunIsolateDriver({ timeout: 50 }) + const context = await driver.createContext({ + bindings: {}, + timeout: 50, + }) + + const timedOut = await context.execute('while (true) {}') + expect(timedOut.success).toBe(false) + expect(timedOut.error?.name).toBe('TimeoutError') + + const ok = await context.execute('return 7') + expect(ok.success).toBe(true) + expect(ok.value).toBe(7) + await context.dispose() + }) + }) + }, +) + +// This part of the contract is observable on Node.js (where bun:ffi is +// unavailable), so it runs in regular CI. +describe.skipIf(typeof Bun !== 'undefined')( + 'createQuickJSBunIsolateDriver on Node.js', + () => { + it('rejects createContext with a descriptive runtime error', async () => { + const driver = createQuickJSBunIsolateDriver() + + await expect(driver.createContext({ bindings: {} })).rejects.toThrow( + /requires the Bun runtime/, + ) + }) + }, +) diff --git a/packages/ai-isolate-quickjs-bun/tsconfig.json b/packages/ai-isolate-quickjs-bun/tsconfig.json new file mode 100644 index 0000000000..3948f968d7 --- /dev/null +++ b/packages/ai-isolate-quickjs-bun/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + // quickjs-bun ships TypeScript sources that use import attributes + // (`with { type: "file" }`), which require a newer module target. + "module": "ESNext", + "outDir": "dist" + }, + "include": ["src", "tests", "benchmarks"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ai-isolate-quickjs-bun/vite.config.ts b/packages/ai-isolate-quickjs-bun/vite.config.ts new file mode 100644 index 0000000000..77bcc2e60b --- /dev/null +++ b/packages/ai-isolate-quickjs-bun/vite.config.ts @@ -0,0 +1,36 @@ +import { defineConfig, mergeConfig } from 'vitest/config' +import { tanstackViteConfig } from '@tanstack/vite-config' +import packageJson from './package.json' + +const config = defineConfig({ + test: { + name: packageJson.name, + dir: './', + watch: false, + globals: true, + environment: 'node', + include: ['tests/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html', 'lcov'], + exclude: [ + 'node_modules/', + 'dist/', + 'tests/', + '**/*.test.ts', + '**/*.config.ts', + '**/types.ts', + ], + include: ['src/**/*.ts'], + }, + }, +}) + +export default mergeConfig( + config, + tanstackViteConfig({ + entry: ['./src/index.ts'], + srcDir: './src', + cjs: false, + }), +) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c2abbdf71c..44411677a8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -410,6 +410,9 @@ importers: '@tanstack/ai-isolate-quickjs': specifier: workspace:* version: link:../../packages/ai-isolate-quickjs + '@tanstack/ai-isolate-quickjs-bun': + specifier: workspace:* + version: link:../../packages/ai-isolate-quickjs-bun '@tanstack/ai-ollama': specifier: workspace:* version: link:../../packages/ai-ollama @@ -1842,6 +1845,25 @@ importers: specifier: 4.0.14 version: 4.0.14(vitest@4.1.4) + packages/ai-isolate-quickjs-bun: + dependencies: + '@tanstack/ai-code-mode': + specifier: workspace:* + version: link:../ai-code-mode + quickjs-bun: + specifier: 0.1.2 + version: 0.1.2 + devDependencies: + '@tanstack/ai-isolate-quickjs': + specifier: workspace:* + version: link:../ai-isolate-quickjs + '@types/bun': + specifier: ^1.3.14 + version: 1.3.14 + '@vitest/coverage-v8': + specifier: 4.0.14 + version: 4.0.14(vitest@4.1.4) + packages/ai-mcp: dependencies: '@modelcontextprotocol/sdk': @@ -8829,6 +8851,9 @@ packages: '@types/braces@3.0.5': resolution: {integrity: sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==} + '@types/bun@1.3.14': + resolution: {integrity: sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -9864,6 +9889,9 @@ packages: resolution: {integrity: sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==} engines: {node: '>=10.0.0'} + bun-types@1.3.14: + resolution: {integrity: sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ==} + bundle-require@5.1.0: resolution: {integrity: sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -13711,6 +13739,10 @@ packages: queue@6.0.2: resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + quickjs-bun@0.1.2: + resolution: {integrity: sha512-mmbG4UwB+FqckVGJXttx4vAg92X/mjcp5e42UZXWrIe80m48x6/Vs3SW3UFTHzRzGFHuzCPnChCKB+W7AK8OSA==} + engines: {bun: '>=1.3.14'} + quickjs-emscripten-core@0.31.0: resolution: {integrity: sha512-oQz8p0SiKDBc1TC7ZBK2fr0GoSHZKA0jZIeXxsnCyCs4y32FStzCW4d1h6E1sE0uHDMbGITbk2zhNaytaoJwXQ==} @@ -23099,6 +23131,10 @@ snapshots: '@types/braces@3.0.5': {} + '@types/bun@1.3.14': + dependencies: + bun-types: 1.3.14 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -24415,6 +24451,10 @@ snapshots: buildcheck@0.0.7: optional: true + bun-types@1.3.14: + dependencies: + '@types/node': 24.10.3 + bundle-require@5.1.0(esbuild@0.27.7): dependencies: esbuild: 0.27.7 @@ -29261,6 +29301,8 @@ snapshots: dependencies: inherits: 2.0.4 + quickjs-bun@0.1.2: {} + quickjs-emscripten-core@0.31.0: dependencies: '@jitl/quickjs-ffi-types': 0.31.0 From ec8c161ae8db7a068889818a0808d2c086937de5 Mon Sep 17 00:00:00 2001 From: Kenta Iwasaki <63115601+lithdew@users.noreply.github.com> Date: Fri, 12 Jun 2026 02:49:53 +0800 Subject: [PATCH 2/8] fix: docs/code-mode/code-mode-isolates.md Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> --- docs/code-mode/code-mode-isolates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/code-mode/code-mode-isolates.md b/docs/code-mode/code-mode-isolates.md index 3142e7c909..e37471741b 100644 --- a/docs/code-mode/code-mode-isolates.md +++ b/docs/code-mode/code-mode-isolates.md @@ -114,7 +114,7 @@ QuickJS WASM uses an asyncified execution model — the WASM module can pause wh ## QuickJS Bun Driver (`@tanstack/ai-isolate-quickjs-bun`) -Runs [QuickJS](https://bellard.org/quickjs/) natively on the [Bun](https://bun.sh/) runtime through `bun:ffi`, via the `[quickjs-bun](https://github.com/superpowerdotcom/quickjs-bun)` package. There are no native dependencies and no build step — the vendored QuickJS C sources are compiled on the fly with Bun's embedded TinyCC, once per process. This makes it the fastest sandboxed option for Code Mode on Bun. +Runs [QuickJS](https://bellard.org/quickjs/) natively on the [Bun](https://bun.sh/) runtime through `bun:ffi`, via the [`quickjs-bun`](https://github.com/superpowerdotcom/quickjs-bun) package. There are no native dependencies and no build step — the vendored QuickJS C sources are compiled on the fly with Bun's embedded TinyCC, once per process. This makes it the fastest sandboxed option for Code Mode on Bun. ### Installation From b4dd0491a047c7cb18ce07696126fb63417c4bb3 Mon Sep 17 00:00:00 2001 From: Jack Herrington Date: Sun, 2 Aug 2026 10:20:02 -0700 Subject: [PATCH 3/8] fix(ai-isolate-quickjs-bun): address review feedback Correctness: - Classify fatal OOM/stack-limit errors thrown as Error *objects* (not just bare-null) as fatal so an exhausted VM is released, not reused, by routing the recovered name/message back through normalizeError. - When vm.dump throws while materializing a tool call's args (sandbox heap exhausted), surface it via hostSettleError and abandon the call instead of running the host tool with empty {} args. - Drain the QuickJS job queue between runs so a prior aborted/timed-out execution's queued promise reactions can't bleed their output, tool-call budget, or wall-clock into the next execution (+ regression test). - Rename MAX_LOG_BYTES/logBytes -> MAX_LOG_CHARS/logChars to match the actual UTF-16 code-unit measure. Docs / metadata: - Close the unterminated YAML frontmatter in code-mode-isolates.md, fix the broken `external_*` inline code, and add the missing maxToolCalls option row. - Soften the isolate-driver "fully isolated from the host" wording: this is an in-process bun:ffi VM, not an OS/VM sandbox boundary. - Fill in package.json author/homepage/bugs/funding; pin quickjs-bun exactly and document the pre-1.0 supply-chain/compat caveat in the README + changeset. - Convert the package's stray eslint scripts to oxlint (repo convention; also brings its source under the CI lint gate and clears knip). - Document that the full behavioral suite is Bun-only (test:bun). Example: - Gate the "QuickJS Bun" sidebar option honestly: create-isolate-driver falls back to QuickJS (WASM) with a warning when not running under Bun, and the option description states the requirement. Co-Authored-By: Claude Opus 4.8 (1M context) --- .changeset/quickjs-bun-isolate-driver.md | 2 + docs/code-mode/code-mode-isolates.md | 6 +- .../src/components/ToolSidebar.tsx | 3 +- .../src/lib/create-isolate-driver.ts | 20 ++++- packages/ai-isolate-quickjs-bun/README.md | 12 +++ packages/ai-isolate-quickjs-bun/package.json | 14 +++- .../src/isolate-context.ts | 80 +++++++++++++++---- .../src/isolate-driver.ts | 14 +++- .../tests/isolate-driver.test.ts | 31 +++++++ 9 files changed, 155 insertions(+), 27 deletions(-) diff --git a/.changeset/quickjs-bun-isolate-driver.md b/.changeset/quickjs-bun-isolate-driver.md index 1affd320d3..70ec5cd05d 100644 --- a/.changeset/quickjs-bun-isolate-driver.md +++ b/.changeset/quickjs-bun-isolate-driver.md @@ -26,4 +26,6 @@ Compared to the WASM driver: The driver requires Bun `>= 1.3.14` and throws an error when used on Node.js. +It pins `quickjs-bun` to an exact version (`0.1.2`) rather than a range, because that package is still pre-1.0 and compiles QuickJS through TinyCC/`bun:ffi` — its API and platform support may shift between patch releases (see the package README for the compatibility note and the Windows `QUICKJS_BUN_NATIVE_LIBRARY` requirement). + The `@tanstack/ai-code-mode` README and bundled skill are updated to document the new driver. diff --git a/docs/code-mode/code-mode-isolates.md b/docs/code-mode/code-mode-isolates.md index e37471741b..10885c799d 100644 --- a/docs/code-mode/code-mode-isolates.md +++ b/docs/code-mode/code-mode-isolates.md @@ -15,6 +15,7 @@ keywords: - cloudflare workers - sandbox - secure execution +--- Isolate drivers provide the secure sandbox runtimes that [Code Mode](./code-mode.md) uses to execute generated TypeScript. All drivers implement the same `IsolateDriver` interface, so you can swap them without changing any other code. @@ -108,7 +109,7 @@ const driver = createQuickJSIsolateDriver({ QuickJS WASM uses an asyncified execution model — the WASM module can pause while awaiting host async functions (your tools). Executions are serialized through a global queue to prevent concurrent WASM calls, which the asyncify model does not support. Fatal errors (memory exhaustion, stack overflow) are detected, the VM is disposed, and a structured error is returned. Console output is captured and returned with the result. -> **Performance note:** QuickJS interprets JavaScript rather than JIT-compiling it, so compute-heavy scripts run slower than with the Node driver. For typical LLM-generated scripts that are mostly waiting on `external_`* tool calls, this difference is not significant. +> **Performance note:** QuickJS interprets JavaScript rather than JIT-compiling it, so compute-heavy scripts run slower than with the Node driver. For typical LLM-generated scripts that are mostly waiting on `external_*` tool calls, this difference is not significant. --- @@ -144,11 +145,12 @@ const driver = createQuickJSBunIsolateDriver({ | `memoryLimit` | `number` | `128` | Maximum heap memory for the QuickJS runtime, in megabytes. | | `timeout` | `number` | `30000` | Maximum wall-clock time per execution, in milliseconds. | | `maxStackSize` | `number` | `524288` | Maximum call stack size in bytes (default: 512 KiB). Increase for deeply recursive code; decrease to catch runaway recursion sooner. | +| `maxToolCalls` | `number` | `1000` | Maximum host tool calls per execution. Bounds output and memory growth from untrusted sandbox code that fans out (e.g. `Promise.all` over a huge array); exceeding it throws a catchable error inside the sandbox. | ### How it works -Each context gets a dedicated native QuickJS runtime with its own memory limit, stack size, and interrupt-based timeout, so contexts execute independently — unlike the WASM driver, which serializes all executions through one shared asyncified WASM module. Fatal errors (memory exhaustion, stack overflow) are detected, the VM is disposed, and a structured error is returned; create a fresh context afterwards. Console output is captured and returned with the result. +Each context gets a dedicated native QuickJS runtime with its own memory limit, stack size, and interrupt-based timeout, so contexts execute independently — unlike the WASM driver, which serializes all executions through one shared asyncified WASM module. Fatal errors (memory exhaustion, stack overflow) are detected, the VM is disposed, and a structured error is returned; create a fresh context afterwards. A per-execution `maxToolCalls` budget bounds host tool-call fan-out. Console output is captured and returned with the result. > **Bun only:** This driver requires Bun 1.3.14 or later and throws a descriptive error when creating a context on Node.js — use the Node or QuickJS WASM driver there. On Bun, prefer this driver over the WASM one: it runs QuickJS natively, and quickjs-emscripten's asyncify bridge is unreliable for async host tool calls under Bun. diff --git a/examples/ts-code-mode-web/src/components/ToolSidebar.tsx b/examples/ts-code-mode-web/src/components/ToolSidebar.tsx index eb51575451..2c30d56edb 100644 --- a/examples/ts-code-mode-web/src/components/ToolSidebar.tsx +++ b/examples/ts-code-mode-web/src/components/ToolSidebar.tsx @@ -106,7 +106,8 @@ export const DEFAULT_ISOLATE_VM_OPTIONS: Array = [ { id: 'quickjs-bun', name: 'QuickJS Bun', - description: 'Native QuickJS engine (requires running the server with Bun)', + description: + 'Native QuickJS via bun:ffi — requires running the server with Bun; falls back to QuickJS (WASM) on a Node server', available: true, }, { diff --git a/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts b/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts index b3b4263bc4..80af83bed5 100644 --- a/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts +++ b/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts @@ -20,9 +20,23 @@ export async function createIsolateDriver( break } case 'quickjs-bun': { - const { createQuickJSBunIsolateDriver } = - await import('@tanstack/ai-isolate-quickjs-bun') - driver = createQuickJSBunIsolateDriver() + // The native bun:ffi driver only loads under Bun. On a Node server (the + // default `pnpm dev` workflow) fall back to the WASM QuickJS driver with + // a warning instead of throwing an opaque createContext error, so the + // sidebar option degrades gracefully rather than breaking the request. + const isBun = typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined' + if (isBun) { + const { createQuickJSBunIsolateDriver } = + await import('@tanstack/ai-isolate-quickjs-bun') + driver = createQuickJSBunIsolateDriver() + } else { + console.warn( + '[createIsolateDriver] QuickJS Bun driver requires running the server under Bun; falling back to QuickJS (WASM).', + ) + const { createQuickJSIsolateDriver } = + await import('@tanstack/ai-isolate-quickjs') + driver = createQuickJSIsolateDriver() + } break } case 'cloudflare': { diff --git a/packages/ai-isolate-quickjs-bun/README.md b/packages/ai-isolate-quickjs-bun/README.md index 3eb177f4db..8096c481c2 100644 --- a/packages/ai-isolate-quickjs-bun/README.md +++ b/packages/ai-isolate-quickjs-bun/README.md @@ -7,6 +7,8 @@ Native QuickJS driver for TanStack AI Code Mode on the [Bun](https://bun.sh) run - Bun `>= 1.3.14` — the driver throws a descriptive error when used on Node.js (use `@tanstack/ai-isolate-node` or `@tanstack/ai-isolate-quickjs` there). - macOS or Linux on `x86_64`/`aarch64` work out of the box ([`quickjs-bun`](https://github.com/superpowerdotcom/quickjs-bun) compiles the vendored QuickJS sources on the fly with Bun's embedded TinyCC — no build tools needed). On Windows, point the `QUICKJS_BUN_NATIVE_LIBRARY` environment variable at a prebuilt QuickJS dynamic library. +> **Dependency note:** this driver is built against [`quickjs-bun`](https://github.com/superpowerdotcom/quickjs-bun) `0.1.2` and pins it exactly, because `quickjs-bun` is an early-stage package (pre-1.0) that compiles QuickJS through TinyCC/`bun:ffi` — its API and platform support may change between patch releases. The version is intentionally not widened to a `^`/`~` range until the upstream API stabilizes. Consumers inherit `quickjs-bun`'s supply-chain and platform-support surface (notably the Windows `QUICKJS_BUN_NATIVE_LIBRARY` requirement above). + ## Installation ```bash @@ -85,6 +87,16 @@ Representative numbers (Apple M-series, darwin/arm64; Bun 1.3.14 for this driver ¹ In our benchmark runs, the WASM driver's asyncified host tool calls repeatedly crashed the shared WASM module (`memory access out of bounds`) and hung subsequent executions, on both Node 22 and Bun 1.3.14 — e.g. deterministically after running executions with 1, 2, 3, then 4 sequential awaited tool calls in one process. Sync-only workloads were unaffected. +## Testing + +The full behavioral suite (`tests/*.test.ts` — escape attempts, timeouts, memory/stack limits, `maxToolCalls`, concurrency) exercises a live QuickJS runtime and therefore **only runs under Bun**: + +```bash +bun test ./tests # or: pnpm --filter @tanstack/ai-isolate-quickjs-bun test:bun +``` + +The tests are guarded with `describe.skipIf(typeof Bun === 'undefined')`, so the repo's default Node/Vitest gate (`test:lib`) runs only the "rejects `createContext` on Node.js" case and skips the rest rather than failing. Run `test:bun` locally (or in a Bun-provisioned CI job) to cover the full matrix. + ## License MIT diff --git a/packages/ai-isolate-quickjs-bun/package.json b/packages/ai-isolate-quickjs-bun/package.json index 9acb141edf..20e3733073 100644 --- a/packages/ai-isolate-quickjs-bun/package.json +++ b/packages/ai-isolate-quickjs-bun/package.json @@ -2,13 +2,21 @@ "name": "@tanstack/ai-isolate-quickjs-bun", "version": "0.0.0", "description": "Native QuickJS sandbox driver for TanStack AI Code Mode TypeScript execution on Bun via bun:ffi.", - "author": "", + "author": "Tanner Linsley", "license": "MIT", + "homepage": "https://tanstack.com/ai", "repository": { "type": "git", "url": "git+https://github.com/TanStack/ai.git", "directory": "packages/ai-isolate-quickjs-bun" }, + "bugs": { + "url": "https://github.com/TanStack/ai/issues" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, "type": "module", "module": "./dist/esm/index.js", "types": "./dist/esm/index.d.ts", @@ -29,10 +37,10 @@ "scripts": { "build": "vite build", "clean": "premove ./build ./dist", - "lint:fix": "eslint ./src --fix", + "lint:fix": "oxlint src --type-aware --fix", "test:build": "publint --strict", "test:bun": "bun test ./tests", - "test:eslint": "eslint ./src", + "test:oxlint": "oxlint src --type-aware", "test:lib": "vitest --passWithNoTests", "test:lib:dev": "pnpm test:lib --watch", "test:types": "tsc" diff --git a/packages/ai-isolate-quickjs-bun/src/isolate-context.ts b/packages/ai-isolate-quickjs-bun/src/isolate-context.ts index 2a3d86368e..18ff313a13 100644 --- a/packages/ai-isolate-quickjs-bun/src/isolate-context.ts +++ b/packages/ai-isolate-quickjs-bun/src/isolate-context.ts @@ -49,11 +49,21 @@ interface ToolResultEnvelope { * dropped for the rest of the execution. */ const MAX_LOG_ENTRIES = 10_000 -const MAX_LOG_BYTES = 1_000_000 +// Measured in UTF-16 code units (`String.length`), not bytes: a cheap, bounded +// proxy for output size. Worst-case multi-byte content can reach ~3-4x this in +// real bytes, which is still safely bounded. +const MAX_LOG_CHARS = 1_000_000 /** Default ceiling on host tool-call invocations per execution. */ export const DEFAULT_MAX_TOOL_CALLS = 1000 +/** + * Safety cap on how many stale promise reactions to flush before a run. A + * bounded loop guards against a pathological job that perpetually reschedules + * itself; a healthy queue drains in far fewer iterations. + */ +const MAX_STALE_JOB_DRAIN = 10_000 + /** Placeholder used when a console argument cannot be coerced to a string. */ const UNPRINTABLE_LOG_VALUE = '[unprintable]' @@ -97,7 +107,7 @@ export class QuickJSBunIsolateContext implements IsolateContext { private readonly timeout: number private readonly maxToolCalls: number private readonly logs: Array = [] - private logBytes = 0 + private logChars = 0 private logTruncated = false private readonly tasks = new Set() private toolCallsUsed = 0 @@ -168,8 +178,16 @@ export class QuickJSBunIsolateContext implements IsolateContext { return this.disposedResult() } + // A prior aborted or timed-out execution can leave promise reactions queued + // in the VM. Left in place, the loop in runToCompletion would run them + // first and attribute their console output, tool-call budget, and + // wall-clock to this run. Flush them now — and abandon any host tool calls + // a flushed reaction kicked off — before installing this run's state. + this.drainStaleJobs() + this.abortTasks() + this.logs.length = 0 - this.logBytes = 0 + this.logChars = 0 this.logTruncated = false this.toolCallsUsed = 0 this.hostSettleError = undefined @@ -376,14 +394,14 @@ export class QuickJSBunIsolateContext implements IsolateContext { if (this.logTruncated) return if ( this.logs.length >= MAX_LOG_ENTRIES || - this.logBytes + msg.length > MAX_LOG_BYTES + this.logChars + msg.length > MAX_LOG_CHARS ) { this.logTruncated = true this.logs.push('[log output truncated]') return } this.logs.push(msg) - this.logBytes += msg.length + this.logChars += msg.length } /** @@ -492,7 +510,9 @@ export class QuickJSBunIsolateContext implements IsolateContext { } // The wrapper always passes a JSON string, but degrade gracefully if - // the handle dumps to something else. + // the handle dumps to something else. A non-string dump is harmless — the + // tool just sees empty args — but a *thrown* dump is not: it means the + // sandbox heap was exhausted while materializing the args string. let argsJson = '{}' try { const dumped = vm.dump(argsHandle ?? vm.undefined) @@ -500,10 +520,17 @@ export class QuickJSBunIsolateContext implements IsolateContext { argsJson = dumped } } catch (error) { - // Fall through with empty args (JSON.parse below cannot fail on '{}'). - // dump() rethrows VM failures as a JSException whose owned value must - // be released. - if (error instanceof this.quickjs.JSException) error.dispose() + // Running the tool now would invoke a real host side effect (e.g. + // `readFile`/`deleteRecords`) with empty `{}` args instead of the + // caller's real inputs, and the underlying OOM would go unclassified. + // Record it for the execution loop to surface (and release the VM if + // fatal), and abandon this tool call instead of executing it. + // `toNormalizedError` disposes the owned JSException value. + this.hostSettleError ??= this.toNormalizedError(error) + this.tasks.delete(task) + deferred.dispose() + task.settle() + return promise } void (async () => { @@ -524,6 +551,23 @@ export class QuickJSBunIsolateContext implements IsolateContext { return promise } + /** + * Flush promise reactions left queued by a prior aborted/timed-out execution + * so they cannot bleed into the next run. Runs the queue to completion and + * discards the effects (the caller resets per-run state and re-aborts tasks + * immediately after). Bounded, and swallows a throwing reaction so a single + * bad job cannot abort the whole drain. + */ + private drainStaleJobs(): void { + for (let i = 0; i < MAX_STALE_JOB_DRAIN; i++) { + try { + if (!this.runtime.executePendingJob(1)) return + } catch { + // A stale reaction threw; discard it and keep draining the rest. + } + } + } + /** Abandon all in-flight host tool calls and release their VM handles. */ private abortTasks(): void { for (const task of this.tasks) { @@ -576,11 +620,17 @@ export class QuickJSBunIsolateContext implements IsolateContext { if (value.type === 'object' || value.type === 'array') { const message = this.readValueProp(value, 'message') if (message !== undefined) { - return { - name: this.readValueProp(value, 'name') ?? error.name, - message, - ...(error.stack !== undefined && { stack: error.stack }), - } + // Route the recovered name/message back through normalizeError so a + // fatal limit thrown as an Error *object* (with some heap headroom + // QuickJS throws `InternalError: out of memory`, and stack overflow + // surfaces as an `InternalError`/`RangeError` object rather than a + // bare `null`) is still classified as MemoryLimit/StackOverflow and + // releases the VM, instead of being returned verbatim and letting + // an exhausted VM be reused. + const recovered = new Error(message) + recovered.name = this.readValueProp(value, 'name') ?? error.name + if (error.stack !== undefined) recovered.stack = error.stack + return normalizeError(recovered) } } return normalizeError(error) diff --git a/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts b/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts index 75e5a66264..6772b6d4bf 100644 --- a/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts +++ b/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts @@ -92,9 +92,17 @@ export interface QuickJSBunIsolateDriverConfig { * Create a QuickJS isolate driver for the Bun runtime * * This driver runs QuickJS natively through `bun:ffi` (via `quickjs-bun`) - * instead of WebAssembly. Each context gets its own QuickJS runtime with - * dedicated memory and stack limits, so sandboxes are fully isolated from - * each other and from the host. It requires Bun >= 1.3.14 — on Node.js use + * instead of WebAssembly. Each context gets its own QuickJS runtime with a + * dedicated heap, stack limit, and interrupt-based timeout, so sandboxes are + * isolated from each other at the language level and each enforces its own + * resource limits. + * + * Note this is *not* an OS/VM sandbox: QuickJS is compiled by TinyCC and called + * through `bun:ffi`, so it executes in the host process's address space. The + * memory "limit" is QuickJS's internal accounting (`JS_SetMemoryLimit`), not a + * hardware/OS boundary. For a stronger boundary use `@tanstack/ai-isolate-node` + * (a separate V8 isolate) or the WASM driver (WebAssembly linear-memory + * sandbox). It requires Bun >= 1.3.14 — on Node.js use * `@tanstack/ai-isolate-node` or `@tanstack/ai-isolate-quickjs` instead. * * Tools are injected as async functions that bridge back to the host. diff --git a/packages/ai-isolate-quickjs-bun/tests/isolate-driver.test.ts b/packages/ai-isolate-quickjs-bun/tests/isolate-driver.test.ts index 20988bf294..ac6b6a639a 100644 --- a/packages/ai-isolate-quickjs-bun/tests/isolate-driver.test.ts +++ b/packages/ai-isolate-quickjs-bun/tests/isolate-driver.test.ts @@ -299,6 +299,37 @@ describe.skipIf(typeof Bun === 'undefined')( expect(elapsed).toBeLessThan(1000) await context.dispose() }) + + it('does not leak a timed-out run’s queued jobs into the next execution', async () => { + const driver = createQuickJSBunIsolateDriver({ timeout: 50 }) + const context = await driver.createContext({ + bindings: {}, + timeout: 50, + }) + + // Queue a microtask, then busy-loop past the deadline so the interrupt + // fires before the reaction can drain. Its console.log stays queued on + // the runtime's job queue. + const first = await context.execute(` + Promise.resolve().then(() => console.log('STALE_FROM_FIRST_RUN')); + const start = Date.now(); + while (Date.now() - start < 500) { /* spin */ } + return 1; + `) + expect(first.success).toBe(false) + expect(first.error?.name).toBe('TimeoutError') + + // Reusing the context must not run the previous run's stale reaction as + // part of — nor attribute its output to — this execution. + const second = await context.execute( + `console.log('SECOND_RUN'); return 2;`, + ) + expect(second.success).toBe(true) + expect(second.value).toBe(2) + expect(second.logs).toContain('SECOND_RUN') + expect(second.logs ?? []).not.toContain('STALE_FROM_FIRST_RUN') + await context.dispose() + }) }) describe('execute - error handling', () => { From 3e3184a27397269ec518d6bdcac354296885e2c4 Mon Sep 17 00:00:00 2001 From: Jack Herrington Date: Sun, 2 Aug 2026 14:03:57 -0700 Subject: [PATCH 4/8] ci(ai-isolate-quickjs-bun): run the Bun-gated test suite in CI The driver's substantive suite (escape attempts, timeouts, memory limits, maxToolCalls, concurrency) is gated behind `describe.skipIf(typeof Bun === 'undefined')` and only runs under Bun. The Node PR job exercised only the "rejects on Node.js" case, leaving that behavior unverified in CI. Add a separate, path-filtered `Bun Tests` workflow that installs Bun, builds the package's workspace dependencies via nx (`@tanstack/ai-code-mode` is a runtime peer dep), and runs `test:bun`. Path-filtered to the package so it stays off unrelated PRs. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/bun-test.yml | 54 ++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 .github/workflows/bun-test.yml diff --git a/.github/workflows/bun-test.yml b/.github/workflows/bun-test.yml new file mode 100644 index 0000000000..51d4fb4460 --- /dev/null +++ b/.github/workflows/bun-test.yml @@ -0,0 +1,54 @@ +name: Bun Tests + +# The `@tanstack/ai-isolate-quickjs-bun` driver runs QuickJS natively through +# `bun:ffi`, so its substantive test suite (escape attempts, timeouts, memory +# limits, maxToolCalls, concurrency) is gated with +# `describe.skipIf(typeof Bun === 'undefined')` and only runs under Bun. The +# standard PR job (`pr.yml`) runs Vitest on Node and therefore only exercises +# the "rejects createContext on Node.js" case. This workflow runs the Bun-gated +# suite so that behavior is actually covered in CI. +# +# It is path-filtered to the package (and this file) to keep it off unrelated +# PRs. + +on: + pull_request: + paths: + - 'packages/ai-isolate-quickjs-bun/**' + - 'packages/ai-code-mode/**' + - '.github/workflows/bun-test.yml' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.number || github.ref }} + cancel-in-progress: true + +env: + NX_CLOUD_ACCESS_TOKEN: ${{ secrets.NX_CLOUD_ACCESS_TOKEN }} + +permissions: + contents: read + +jobs: + test: + name: Test (Bun) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Setup Tools + uses: TanStack/config/.github/setup@190f659075ff0845850e330883eb26d7ffd0671f # main + - name: Setup Bun + uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0 + with: + # The driver requires Bun >= 1.3.14 (see the package `engines` field). + bun-version: latest + - name: Build package and its workspace dependencies + # `@tanstack/ai-code-mode` is a `workspace:*` peer dependency and is + # imported at runtime (`wrapCode`) by the driver, so it must be built to + # `dist/` before the Bun tests can import it. nx builds the dependency + # graph first via `^build`. + run: pnpm exec nx build @tanstack/ai-isolate-quickjs-bun + - name: Run Bun test suite + run: pnpm --filter @tanstack/ai-isolate-quickjs-bun run test:bun From 548c41832eacbe77d54548ef3e9087e7eb9071af Mon Sep 17 00:00:00 2001 From: Jack Herrington Date: Sun, 2 Aug 2026 14:33:32 -0700 Subject: [PATCH 5/8] fix(workspace): stop @types/bun from polluting other packages' global types Adding @types/bun as a devDependency of @tanstack/ai-isolate-quickjs-bun let pnpm hoist it into the shared virtual store (node_modules/.pnpm/node_modules), where it became resolvable workspace-wide. Unrelated deps in the nitro/SSR stack (srvx, crossws) carry phantom `import "bun"` type statements that previously no-op'd; once @types/bun was reachable they resolved to it and pulled bun-types into every dependent's `tsc` run. bun-types globally redeclares `fetch` with `init: RequestInit | BunFetchRequestInit`, dropping the DOM `preconnect` member, which broke `@tanstack/sandbox-web-example:test:types` (and any other app using `typeof fetch`). Exclude @types/bun from the shared hoist so only its direct dependent resolves it via its own node_modules symlink. quickjs-bun still type-checks, builds, and passes its Bun test suite; sandbox-web no longer pulls in bun-types. Co-Authored-By: Claude Opus 4.8 (1M context) --- pnpm-workspace.yaml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 0c87a0383d..66a5017691 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -56,6 +56,19 @@ packageExtensions: dependencies: typescript: 5.9.3 +# Keep @types/bun out of pnpm's shared virtual store (node_modules/.pnpm/node_modules). +# @types/bun (a devDep of @tanstack/ai-isolate-quickjs-bun) globally augments Node/DOM +# types — notably it redeclares `fetch` with `init: RequestInit | BunFetchRequestInit`, +# dropping the DOM `preconnect` member. When it's hoisted into the shared store, the +# phantom `import "bun"` type statements inside unrelated deps (srvx, crossws in the +# nitro/SSR stack) resolve to it, leaking that augmentation into example apps' `tsc` +# runs and breaking their `typeof fetch` usage. Excluding it from the shared store +# keeps it resolvable only by its direct dependent (which links it in its own +# node_modules), so the Bun types stay scoped to the package that needs them. +hoistPattern: + - '*' + - '!@types/bun' + patchedDependencies: '@changesets/assemble-release-plan@6.0.9': patches/@changesets__assemble-release-plan@6.0.9.patch From d6b9fbfc8276daebf7b2032d5b8a575f0a9eff2c Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:00:49 +1000 Subject: [PATCH 6/8] fix(ai-isolate-quickjs-bun): make Bun isolate work under Vite/Start demos Load quickjs-bun via Bun-aware path resolution and a host-native dynamic import so Vite's module runner no longer fails package exports or inlines bun:ffi sources. Add CODE_MODE_BUN/dev:bun for ts-code-mode-web (default VM quickjs-bun, Nitro bun preset, SSR-only bun resolve conditions), runtime sidebar warnings, and richer execute_typescript tracing (phase, stack, execution_finished events). --- examples/ts-code-mode-web/README.md | 27 +- examples/ts-code-mode-web/package.json | 4 + .../src/components/ExecutionResult.tsx | 58 +++- .../src/components/JavaScriptVM.tsx | 59 +++- .../src/components/ToolSidebar.tsx | 84 ++++- .../src/lib/create-isolate-driver.ts | 72 ++++- .../src/lib/reports/evaluate-watchers.ts | 2 +- .../src/lib/reports/refresh-component.ts | 2 +- .../ts-code-mode-web/src/routeTree.gen.ts | 21 ++ .../routes/_banking-demo/api.banking-demo.ts | 2 +- .../_database-demo/api.database-demo.ts | 2 +- .../_execute-prompt/api.execute-prompt.ts | 2 +- .../src/routes/_home/api.product-codemode.ts | 2 +- .../routes/_npm-github-chat/api.codemode.ts | 7 + .../_npm-github-chat/npm-github-chat.tsx | 24 +- .../src/routes/_reporting/api.report-event.ts | 2 +- .../src/routes/_reporting/api.reports.ts | 2 +- .../api.structured-output.ts | 2 +- .../src/routes/api.runtime.ts | 22 ++ examples/ts-code-mode-web/vite.config.ts | 110 ++++++- .../ai-code-mode/src/create-code-mode-tool.ts | 148 +++++++-- packages/ai-code-mode/src/types.ts | 1 + .../src/isolate-driver.ts | 84 ++++- pnpm-lock.yaml | 292 ++++++++++++++---- 24 files changed, 901 insertions(+), 130 deletions(-) create mode 100644 examples/ts-code-mode-web/src/routes/api.runtime.ts diff --git a/examples/ts-code-mode-web/README.md b/examples/ts-code-mode-web/README.md index 3873392a41..ff610bcd76 100644 --- a/examples/ts-code-mode-web/README.md +++ b/examples/ts-code-mode-web/README.md @@ -71,11 +71,36 @@ Even after successfully compiling from source, `isolated-vm@6.1.0` crashes the s ## Development ```bash -pnpm dev # starts the Vite dev server on port 3001 +# Node (default) — Node isolate / QuickJS WASM / Cloudflare drivers +pnpm dev # http://localhost:3001 + +# Bun — native QuickJS via bun:ffi (@tanstack/ai-isolate-quickjs-bun) +pnpm dev:bun +# equivalent: +# CODE_MODE_BUN=1 bun --bun vite dev --port 3001 ``` +### Bun mode (`CODE_MODE_BUN=1`) + +When set (or when the process is Bun), this example: + +1. Defaults the isolate VM to **`quickjs-bun`** (sidebar + server routes that call `createIsolateDriver()`) +2. Uses Nitro `preset: 'bun'` for the server build +3. Keeps client `resolve.conditions` **without** `bun` (required so TanStack Router hydrates — see router-core `isServer` dual package) + +Optional overrides: + +| Env | Effect | +|---|---| +| `CODE_MODE_BUN=1` | Bun defaults + Nitro bun preset (also set by `pnpm dev:bun`) | +| `CODE_MODE_DEFAULT_VM=node\|quickjs\|quickjs-bun\|cloudflare` | Force default isolate regardless of Bun | + ## Build ```bash pnpm build + +# Bun production build + run +pnpm build:bun +pnpm start:bun ``` diff --git a/examples/ts-code-mode-web/package.json b/examples/ts-code-mode-web/package.json index 7ee1d91fbd..e999848eec 100644 --- a/examples/ts-code-mode-web/package.json +++ b/examples/ts-code-mode-web/package.json @@ -4,7 +4,10 @@ "type": "module", "scripts": { "dev": "vite dev --port 3001", + "dev:bun": "CODE_MODE_BUN=1 bun --bun vite dev --port 3001", "build": "vite build", + "build:bun": "CODE_MODE_BUN=1 bun --bun vite build", + "start:bun": "bun run .output/server/index.mjs", "serve": "vite preview", "test": "exit 0", "test:types": "tsc --noEmit" @@ -40,6 +43,7 @@ "marked": "^15.0.6", "nitro": "3.0.260610-beta", "puppeteer": "^24.34.0", + "quickjs-bun": "0.1.2", "quickjs-emscripten": "^0.31.0", "quickjs-emscripten-core": "^0.31.0", "react": "^19.2.3", diff --git a/examples/ts-code-mode-web/src/components/ExecutionResult.tsx b/examples/ts-code-mode-web/src/components/ExecutionResult.tsx index 5fc85dfc29..f3f981ea1b 100644 --- a/examples/ts-code-mode-web/src/components/ExecutionResult.tsx +++ b/examples/ts-code-mode-web/src/components/ExecutionResult.tsx @@ -10,13 +10,17 @@ import { useState, useEffect, useRef } from 'react' interface ExecutionResultProps { result?: unknown error?: string - logs?: string[] + errorName?: string + errorStack?: string + logs?: Array status: 'running' | 'success' | 'error' } export default function ExecutionResult({ result, error, + errorName, + errorStack, logs, status, }: ExecutionResultProps) { @@ -25,20 +29,18 @@ export default function ExecutionResult({ const [isCollapsed, setIsCollapsed] = useState(false) const prevStatusRef = useRef(status) - // Auto-collapse when status changes from running to complete (with delay) + // Auto-collapse success only — keep errors expanded so the failure is visible useEffect(() => { let timeoutId: ReturnType | null = null if (!userControlled) { const wasRunning = prevStatusRef.current === 'running' - const isComplete = status === 'success' || status === 'error' - if (wasRunning && isComplete) { - // Delay auto-collapse by 3 seconds so user can see the result + if (wasRunning && status === 'success') { timeoutId = setTimeout(() => { setIsCollapsed(true) }, 3000) - } else if (status === 'running') { + } else if (status === 'running' || status === 'error') { setIsCollapsed(false) } } @@ -54,7 +56,12 @@ export default function ExecutionResult({ setIsCollapsed(!isCollapsed) } - const hasContent = (logs && logs.length > 0) || error || result !== undefined + const hasContent = + (logs && logs.length > 0) || + error || + errorName || + errorStack || + result !== undefined return (
{status === 'error' - ? 'Execution Failed' + ? errorName + ? `Execution Failed · ${errorName}` + : 'Execution Failed' : status === 'success' ? 'Execution Complete' : 'Executing...'} @@ -129,9 +138,36 @@ export default function ExecutionResult({
)} - {error && ( -
- Error: {error} + {(error || errorName) && ( +
+ {errorName && ( +
+ + Name + +
{errorName}
+
+ )} + {error && ( +
+ + Message + +
+ {error} +
+
+ )} + {errorStack && ( +
+ + Stack + +
+                    {errorStack}
+                  
+
+ )}
)} diff --git a/examples/ts-code-mode-web/src/components/JavaScriptVM.tsx b/examples/ts-code-mode-web/src/components/JavaScriptVM.tsx index ed0c042bae..0606cdc4fa 100644 --- a/examples/ts-code-mode-web/src/components/JavaScriptVM.tsx +++ b/examples/ts-code-mode-web/src/components/JavaScriptVM.tsx @@ -32,7 +32,17 @@ export default function JavaScriptVM({ const [isCollapsed, setIsCollapsed] = useState(false) const prevExecutingRef = useRef(isExecuting) - // Auto-collapse when execution completes (with delay) + const hasFailureEvent = events.some( + (e) => + e.eventType === 'code_mode:external_error' || + (e.eventType === 'code_mode:execution_finished' && + typeof e.data === 'object' && + e.data !== null && + (e.data as { success?: unknown }).success === false), + ) + + // Auto-collapse when execution completes successfully (with delay). + // Keep open on failures so the finish/error event stays visible. useEffect(() => { let timeoutId: ReturnType | null = null @@ -40,12 +50,11 @@ export default function JavaScriptVM({ const wasExecuting = prevExecutingRef.current const isComplete = !isExecuting - if (wasExecuting && isComplete && events.length > 0) { - // Delay auto-collapse by 3 seconds so user can see the events + if (wasExecuting && isComplete && events.length > 0 && !hasFailureEvent) { timeoutId = setTimeout(() => { setIsCollapsed(true) }, 3000) - } else if (isExecuting) { + } else if (isExecuting || hasFailureEvent) { setIsCollapsed(false) } } @@ -54,7 +63,7 @@ export default function JavaScriptVM({ return () => { if (timeoutId) clearTimeout(timeoutId) } - }, [isExecuting, userControlled, events.length]) + }, [isExecuting, userControlled, events.length, hasFailureEvent]) // Auto-scroll to bottom when new events arrive useEffect(() => { @@ -98,6 +107,17 @@ export default function JavaScriptVM({ if (eventType === 'code_mode:external_error') { return } + if (eventType === 'code_mode:execution_finished') { + const ok = + typeof data === 'object' && + data !== null && + (data as { success?: unknown }).success === true + return ok ? ( + + ) : ( + + ) + } return } @@ -185,6 +205,35 @@ export default function JavaScriptVM({ ) } + if (eventType === 'code_mode:execution_finished') { + const success = typedData.success === true + const durationMs = typedData.durationMs as number | undefined + const phase = typedData.phase as string | undefined + const err = typedData.error as + | { name?: string; message?: string } + | undefined + if (success) { + return ( + + Finished + {phase ? ` · ${phase}` : ''} + {durationMs !== undefined ? ` · ${durationMs}ms` : ''} + + ) + } + return ( + + Failed + {phase ? ` · phase=${phase}` : ''} + {durationMs !== undefined ? ` · ${durationMs}ms` : ''} + {err?.name ? ` · ${err.name}` : ''} + {err?.message ? ( + : {err.message} + ) : null} + + ) + } + // Default for unknown events return ( diff --git a/examples/ts-code-mode-web/src/components/ToolSidebar.tsx b/examples/ts-code-mode-web/src/components/ToolSidebar.tsx index 2c30d56edb..0ab0cff01b 100644 --- a/examples/ts-code-mode-web/src/components/ToolSidebar.tsx +++ b/examples/ts-code-mode-web/src/components/ToolSidebar.tsx @@ -1,5 +1,6 @@ -import { useState } from 'react' +import { useEffect, useState } from 'react' import { + AlertTriangle, ChevronDown, ChevronRight, Code2, @@ -407,12 +408,63 @@ interface IsolateVMSectionProps { defaultOpen?: boolean } +type ServerRuntimeInfo = { + isBun: boolean + runtime: 'bun' | 'node' +} + export function IsolateVMSection({ selectedVM, onVMChange, options = DEFAULT_ISOLATE_VM_OPTIONS, defaultOpen = true, }: IsolateVMSectionProps) { + const [serverRuntime, setServerRuntime] = useState< + ServerRuntimeInfo | null | 'error' + >(null) + + useEffect(() => { + let cancelled = false + fetch('/api/runtime') + .then(async (res) => { + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const data: unknown = await res.json() + if ( + typeof data !== 'object' || + data === null || + !('isBun' in data) || + typeof (data as { isBun: unknown }).isBun !== 'boolean' + ) { + throw new Error('Invalid runtime response') + } + const isBun = (data as { isBun: boolean }).isBun + if (!cancelled) { + setServerRuntime({ + isBun, + runtime: isBun ? 'bun' : 'node', + }) + } + }) + .catch(() => { + if (!cancelled) setServerRuntime('error') + }) + return () => { + cancelled = true + } + }, []) + + const showBunFallbackWarning = + selectedVM === 'quickjs-bun' && + serverRuntime !== null && + serverRuntime !== 'error' && + !serverRuntime.isBun + + const showBunActive = + selectedVM === 'quickjs-bun' && + serverRuntime !== null && + serverRuntime !== 'error' && + serverRuntime.isBun + return ( {options.find((o) => o.id === selectedVM)?.description}

+ {showBunFallbackWarning && ( +
+ +
+

+ Server is not Bun — using QuickJS (WASM) fallback +

+

+ The native bun:ffi driver + only loads when the app server process has a global{' '} + Bun. Right now the server + reports node, so selections + of QuickJS Bun silently fall back to the WASM driver (which is + less reliable for multi-tool sandbox runs). Start the example + under a real Bun server process to exercise the native driver. +

+
+
+ )} + {showBunActive && ( +

+ Server runtime is Bun — native QuickJS Bun driver will be used. +

+ )}
) } diff --git a/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts b/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts index 80af83bed5..edd4b12fc3 100644 --- a/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts +++ b/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts @@ -2,21 +2,78 @@ import type { IsolateDriver } from '@tanstack/ai-code-mode' export type IsolateVM = 'node' | 'quickjs' | 'quickjs-bun' | 'cloudflare' +function isIsolateVM(value: unknown): value is IsolateVM { + return ( + value === 'node' || + value === 'quickjs' || + value === 'quickjs-bun' || + value === 'cloudflare' + ) +} + +/** + * Default isolate for this process. + * + * - `CODE_MODE_DEFAULT_VM=node|quickjs|quickjs-bun|cloudflare` overrides everything + * - `CODE_MODE_BUN=1` (or a real Bun runtime) → `quickjs-bun` + * - otherwise → `node` + * + * Client UI defaults use `import.meta.env.VITE_CODE_MODE_BUN` (set from the + * same flag in vite.config). + */ +export function getDefaultIsolateVM(): IsolateVM { + const fromEnv = + typeof process !== 'undefined' + ? process.env.CODE_MODE_DEFAULT_VM + : undefined + if (isIsolateVM(fromEnv)) { + return fromEnv + } + + const bunMode = + (typeof process !== 'undefined' && process.env.CODE_MODE_BUN === '1') || + typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined' + + return bunMode ? 'quickjs-bun' : 'node' +} + +/** Client-safe default (Vite injects `VITE_CODE_MODE_BUN` from CODE_MODE_BUN). */ +export function getClientDefaultIsolateVM(): IsolateVM { + try { + if (isIsolateVM(import.meta.env.VITE_CODE_MODE_DEFAULT_VM)) { + return import.meta.env.VITE_CODE_MODE_DEFAULT_VM + } + if (import.meta.env.VITE_CODE_MODE_BUN === '1') { + return 'quickjs-bun' + } + } catch { + // import.meta.env may be unavailable outside Vite + } + return 'node' +} + const driverCache = new Map() export async function createIsolateDriver( - vm: IsolateVM = 'node', + vm: IsolateVM = getDefaultIsolateVM(), ): Promise { const cached = driverCache.get(vm) - if (cached) return cached + if (cached) { + console.info( + `[createIsolateDriver] reusing cached driver for vm=${vm}`, + ) + return cached + } let driver: IsolateDriver + let resolved: string switch (vm) { case 'quickjs': { const { createQuickJSIsolateDriver } = await import('@tanstack/ai-isolate-quickjs') driver = createQuickJSIsolateDriver() + resolved = 'quickjs-wasm' break } case 'quickjs-bun': { @@ -29,6 +86,7 @@ export async function createIsolateDriver( const { createQuickJSBunIsolateDriver } = await import('@tanstack/ai-isolate-quickjs-bun') driver = createQuickJSBunIsolateDriver() + resolved = 'quickjs-bun-native' } else { console.warn( '[createIsolateDriver] QuickJS Bun driver requires running the server under Bun; falling back to QuickJS (WASM).', @@ -36,6 +94,7 @@ export async function createIsolateDriver( const { createQuickJSIsolateDriver } = await import('@tanstack/ai-isolate-quickjs') driver = createQuickJSIsolateDriver() + resolved = 'quickjs-wasm-fallback (requested quickjs-bun, no global Bun)' } break } @@ -47,6 +106,7 @@ export async function createIsolateDriver( authorization: process.env.CLOUDFLARE_WORKER_AUTH, timeout: 60000, }) + resolved = 'cloudflare' break } case 'node': @@ -55,6 +115,7 @@ export async function createIsolateDriver( const { createNodeIsolateDriver } = await import('@tanstack/ai-isolate-node') driver = createNodeIsolateDriver() + resolved = 'node-isolated-vm' } catch (err) { console.warn( `[createIsolateDriver] Node isolate driver unavailable, falling back to QuickJS: ${err instanceof Error ? err.message : String(err)}`, @@ -62,11 +123,18 @@ export async function createIsolateDriver( const { createQuickJSIsolateDriver } = await import('@tanstack/ai-isolate-quickjs') driver = createQuickJSIsolateDriver() + resolved = 'quickjs-wasm-fallback (node addon unavailable)' } break } } + const runtime = + typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined' ? 'bun' : 'node' + console.info( + `[createIsolateDriver] vm=${vm} resolved=${resolved} serverRuntime=${runtime}`, + ) + driverCache.set(vm, driver) return driver } diff --git a/examples/ts-code-mode-web/src/lib/reports/evaluate-watchers.ts b/examples/ts-code-mode-web/src/lib/reports/evaluate-watchers.ts index 17e576061b..aab5273c34 100644 --- a/examples/ts-code-mode-web/src/lib/reports/evaluate-watchers.ts +++ b/examples/ts-code-mode-web/src/lib/reports/evaluate-watchers.ts @@ -46,7 +46,7 @@ export async function evaluateWatcher( collectedEffects: Array<{ type: string; params: Record }>, ): Promise { const { createIsolateDriver } = await import('@/lib/create-isolate-driver') - const driver = await createIsolateDriver('node') + const driver = await createIsolateDriver() // First, evaluate the condition const conditionContext = await driver.createContext({ diff --git a/examples/ts-code-mode-web/src/lib/reports/refresh-component.ts b/examples/ts-code-mode-web/src/lib/reports/refresh-component.ts index 05747f8392..c46237d7a7 100644 --- a/examples/ts-code-mode-web/src/lib/reports/refresh-component.ts +++ b/examples/ts-code-mode-web/src/lib/reports/refresh-component.ts @@ -70,7 +70,7 @@ export async function refreshComponent( // Dynamic import to avoid RSC module runner issues const { createIsolateDriver } = await import('@/lib/create-isolate-driver') - const driver = await createIsolateDriver('node') + const driver = await createIsolateDriver() // Capture the result via a binding since isolate doesn't return async values properly let capturedResult: unknown = undefined diff --git a/examples/ts-code-mode-web/src/routeTree.gen.ts b/examples/ts-code-mode-web/src/routeTree.gen.ts index ac579c29a8..c4227bc047 100644 --- a/examples/ts-code-mode-web/src/routeTree.gen.ts +++ b/examples/ts-code-mode-web/src/routeTree.gen.ts @@ -17,6 +17,7 @@ import { Route as ExecutePromptRouteRouteImport } from './routes/_execute-prompt import { Route as DatabaseDemoRouteRouteImport } from './routes/_database-demo/route' import { Route as BankingDemoRouteRouteImport } from './routes/_banking-demo/route' import { Route as HomeIndexRouteImport } from './routes/_home/index' +import { Route as ApiRuntimeRouteImport } from './routes/api.runtime' import { Route as StructuredOutputStructuredOutputRouteImport } from './routes/_structured-output/structured-output' import { Route as ReportingReportingAgentRouteImport } from './routes/_reporting/reporting-agent' import { Route as NpmGithubChatNpmGithubChatRouteImport } from './routes/_npm-github-chat/npm-github-chat' @@ -76,6 +77,11 @@ const HomeIndexRoute = HomeIndexRouteImport.update({ path: '/', getParentRoute: () => HomeRouteRoute, } as any) +const ApiRuntimeRoute = ApiRuntimeRouteImport.update({ + id: '/api/runtime', + path: '/api/runtime', + getParentRoute: () => rootRouteImport, +} as any) const StructuredOutputStructuredOutputRoute = StructuredOutputStructuredOutputRouteImport.update({ id: '/structured-output', @@ -223,6 +229,7 @@ export interface FileRoutesByFullPath { '/npm-github-chat': typeof NpmGithubChatNpmGithubChatRoute '/reporting-agent': typeof ReportingReportingAgentRoute '/structured-output': typeof StructuredOutputStructuredOutputRoute + '/api/runtime': typeof ApiRuntimeRoute '/api/banking-demo': typeof BankingDemoApiBankingDemoRoute '/api/banking-init': typeof BankingDemoApiBankingInitRoute '/api/database-demo': typeof DatabaseDemoApiDatabaseDemoRoute @@ -251,6 +258,7 @@ export interface FileRoutesByTo { '/npm-github-chat': typeof NpmGithubChatNpmGithubChatRoute '/reporting-agent': typeof ReportingReportingAgentRoute '/structured-output': typeof StructuredOutputStructuredOutputRoute + '/api/runtime': typeof ApiRuntimeRoute '/api/banking-demo': typeof BankingDemoApiBankingDemoRoute '/api/banking-init': typeof BankingDemoApiBankingInitRoute '/api/database-demo': typeof DatabaseDemoApiDatabaseDemoRoute @@ -286,6 +294,7 @@ export interface FileRoutesById { '/_npm-github-chat/npm-github-chat': typeof NpmGithubChatNpmGithubChatRoute '/_reporting/reporting-agent': typeof ReportingReportingAgentRoute '/_structured-output/structured-output': typeof StructuredOutputStructuredOutputRoute + '/api/runtime': typeof ApiRuntimeRoute '/_home/': typeof HomeIndexRoute '/_banking-demo/api/banking-demo': typeof BankingDemoApiBankingDemoRoute '/_banking-demo/api/banking-init': typeof BankingDemoApiBankingInitRoute @@ -317,6 +326,7 @@ export interface FileRouteTypes { | '/npm-github-chat' | '/reporting-agent' | '/structured-output' + | '/api/runtime' | '/api/banking-demo' | '/api/banking-init' | '/api/database-demo' @@ -345,6 +355,7 @@ export interface FileRouteTypes { | '/npm-github-chat' | '/reporting-agent' | '/structured-output' + | '/api/runtime' | '/api/banking-demo' | '/api/banking-init' | '/api/database-demo' @@ -379,6 +390,7 @@ export interface FileRouteTypes { | '/_npm-github-chat/npm-github-chat' | '/_reporting/reporting-agent' | '/_structured-output/structured-output' + | '/api/runtime' | '/_home/' | '/_banking-demo/api/banking-demo' | '/_banking-demo/api/banking-init' @@ -409,6 +421,7 @@ export interface RootRouteChildren { NpmGithubChatRouteRoute: typeof NpmGithubChatRouteRouteWithChildren ReportingRouteRoute: typeof ReportingRouteRouteWithChildren StructuredOutputRouteRoute: typeof StructuredOutputRouteRouteWithChildren + ApiRuntimeRoute: typeof ApiRuntimeRoute } declare module '@tanstack/react-router' { @@ -469,6 +482,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof HomeIndexRouteImport parentRoute: typeof HomeRouteRoute } + '/api/runtime': { + id: '/api/runtime' + path: '/api/runtime' + fullPath: '/api/runtime' + preLoaderRoute: typeof ApiRuntimeRouteImport + parentRoute: typeof rootRouteImport + } '/_structured-output/structured-output': { id: '/_structured-output/structured-output' path: '/structured-output' @@ -776,6 +796,7 @@ const rootRouteChildren: RootRouteChildren = { NpmGithubChatRouteRoute: NpmGithubChatRouteRouteWithChildren, ReportingRouteRoute: ReportingRouteRouteWithChildren, StructuredOutputRouteRoute: StructuredOutputRouteRouteWithChildren, + ApiRuntimeRoute: ApiRuntimeRoute, } export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) diff --git a/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts b/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts index 4fb9154f47..8a33568e1b 100644 --- a/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts +++ b/examples/ts-code-mode-web/src/routes/_banking-demo/api.banking-demo.ts @@ -186,7 +186,7 @@ let codeModeCache: { async function getCodeModeTools() { if (!codeModeCache) { const { createIsolateDriver } = await import('@/lib/create-isolate-driver') - const driver = await createIsolateDriver('node') + const driver = await createIsolateDriver() const { tool, systemPrompt } = createCodeMode({ driver, tools: allTools, diff --git a/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts b/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts index 16313747b1..8bfbea84ef 100644 --- a/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts +++ b/examples/ts-code-mode-web/src/routes/_database-demo/api.database-demo.ts @@ -66,7 +66,7 @@ let codeModeCache: { async function getCodeModeTools() { if (!codeModeCache) { const { createIsolateDriver } = await import('@/lib/create-isolate-driver') - const driver = await createIsolateDriver('node') + const driver = await createIsolateDriver() const { tool, systemPrompt } = createCodeMode({ driver, tools: databaseTools, diff --git a/examples/ts-code-mode-web/src/routes/_execute-prompt/api.execute-prompt.ts b/examples/ts-code-mode-web/src/routes/_execute-prompt/api.execute-prompt.ts index fb3306abdf..f5c83f3f42 100644 --- a/examples/ts-code-mode-web/src/routes/_execute-prompt/api.execute-prompt.ts +++ b/examples/ts-code-mode-web/src/routes/_execute-prompt/api.execute-prompt.ts @@ -28,7 +28,7 @@ let cachedDriver: IsolateDriver | null = null async function getDriver(): Promise { if (!cachedDriver) { const { createIsolateDriver } = await import('@/lib/create-isolate-driver') - cachedDriver = await createIsolateDriver('node') + cachedDriver = await createIsolateDriver() } return cachedDriver } diff --git a/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts b/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts index 1972c171a4..5bbe90ed13 100644 --- a/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts +++ b/examples/ts-code-mode-web/src/routes/_home/api.product-codemode.ts @@ -60,7 +60,7 @@ let codeModeCache: { async function getCodeModeTools() { if (!codeModeCache) { const { createIsolateDriver } = await import('@/lib/create-isolate-driver') - const driver = await createIsolateDriver('node') + const driver = await createIsolateDriver() const { tool, systemPrompt } = createCodeMode({ driver, tools: productTools, diff --git a/examples/ts-code-mode-web/src/routes/_npm-github-chat/api.codemode.ts b/examples/ts-code-mode-web/src/routes/_npm-github-chat/api.codemode.ts index a5054e29b3..a4b1e244fb 100644 --- a/examples/ts-code-mode-web/src/routes/_npm-github-chat/api.codemode.ts +++ b/examples/ts-code-mode-web/src/routes/_npm-github-chat/api.codemode.ts @@ -67,6 +67,13 @@ export const Route = createFileRoute('/_npm-github-chat/api/codemode')({ const provider: Provider = data?.provider || 'anthropic' const model: string | undefined = data?.model const vm: IsolateVM = data?.vm || 'node' + const serverRuntime = + typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined' + ? 'bun' + : 'node' + console.info( + `[api/codemode] request provider=${provider} model=${model ?? 'default'} vm=${vm} serverRuntime=${serverRuntime}`, + ) const adapter = getAdapter(provider, model) const baseChatStream = adapter.chatStream.bind(adapter) diff --git a/examples/ts-code-mode-web/src/routes/_npm-github-chat/npm-github-chat.tsx b/examples/ts-code-mode-web/src/routes/_npm-github-chat/npm-github-chat.tsx index 66e3f1355c..e9da7004b6 100644 --- a/examples/ts-code-mode-web/src/routes/_npm-github-chat/npm-github-chat.tsx +++ b/examples/ts-code-mode-web/src/routes/_npm-github-chat/npm-github-chat.tsx @@ -17,6 +17,7 @@ import { parsePartialJSON } from '@tanstack/ai' import { fetchServerSentEvents, useChat } from '@tanstack/ai-react' import type { UIMessage } from '@tanstack/ai-react' import type { VMEvent, IsolateVM } from '@/components' +import { getClientDefaultIsolateVM } from '@/lib/create-isolate-driver' import { CodeBlock, ExecutionResult, @@ -374,7 +375,7 @@ function Messages({ )} {/* Show JavaScript VM when input is complete (executing or has output) */} {isInputComplete && - (events.length > 0 || isExecuting) && ( + (events.length > 0 || isExecuting || hasError) && ( @@ -448,7 +462,11 @@ function CodeModePage() { const [selectedModel, setSelectedModel] = useState( MODEL_OPTIONS[0], ) - const [selectedVM, setSelectedVM] = useState('node') + const [selectedVM, setSelectedVM] = useState( + // Prefer Bun native isolate when started with `pnpm dev:bun` / + // CODE_MODE_BUN=1 (see vite.config + getClientDefaultIsolateVM). + getClientDefaultIsolateVM, + ) const [chatLayout, setChatLayout] = useState< 'tools-data' | 'full' | 'tools' | 'data' >('tools-data') diff --git a/examples/ts-code-mode-web/src/routes/_reporting/api.report-event.ts b/examples/ts-code-mode-web/src/routes/_reporting/api.report-event.ts index 8be1c40f9c..90bf68b2f5 100644 --- a/examples/ts-code-mode-web/src/routes/_reporting/api.report-event.ts +++ b/examples/ts-code-mode-web/src/routes/_reporting/api.report-event.ts @@ -69,7 +69,7 @@ export const Route = createFileRoute('/_reporting/api/report-event' as any)({ const { createIsolateDriver } = await import('@/lib/create-isolate-driver') - const driver = await createIsolateDriver('node') + const driver = await createIsolateDriver() const effects: UIEffect[] = [] const uiUpdates: UIUpdate[] = [] const calledBindings: string[] = [] diff --git a/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts b/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts index 2cf4d761c7..f05a76aa3e 100644 --- a/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts +++ b/examples/ts-code-mode-web/src/routes/_reporting/api.reports.ts @@ -36,7 +36,7 @@ let codeModeCache: { async function getCodeModeTools() { if (!codeModeCache) { const { createIsolateDriver } = await import('@/lib/create-isolate-driver') - const driver = await createIsolateDriver('node') + const driver = await createIsolateDriver() const { tool, systemPrompt } = createCodeMode({ driver, tools: allTools, diff --git a/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output.ts b/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output.ts index 98ad879bd3..a5d66afb90 100644 --- a/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output.ts +++ b/examples/ts-code-mode-web/src/routes/_structured-output/api.structured-output.ts @@ -62,7 +62,7 @@ let codeModeCache: { async function getCodeModeTools() { if (!codeModeCache) { const { createIsolateDriver } = await import('@/lib/create-isolate-driver') - const driver = await createIsolateDriver('node') + const driver = await createIsolateDriver() const { tool, systemPrompt } = createCodeMode({ driver, tools: cityTools, diff --git a/examples/ts-code-mode-web/src/routes/api.runtime.ts b/examples/ts-code-mode-web/src/routes/api.runtime.ts new file mode 100644 index 0000000000..85083cd675 --- /dev/null +++ b/examples/ts-code-mode-web/src/routes/api.runtime.ts @@ -0,0 +1,22 @@ +import { createFileRoute } from '@tanstack/react-router' + +/** + * Reports whether the *server* process is running under Bun. + * The browser cannot see this — `typeof Bun` is only meaningful in the + * process that creates isolate drivers — so the Isolate VM sidebar fetches + * this endpoint to warn when "QuickJS Bun" would fall back to WASM. + */ +export const Route = createFileRoute('/api/runtime')({ + server: { + handlers: { + GET: async () => { + const isBun = + typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined' + return Response.json({ + isBun, + runtime: isBun ? 'bun' : 'node', + }) + }, + }, + }, +}) diff --git a/examples/ts-code-mode-web/vite.config.ts b/examples/ts-code-mode-web/vite.config.ts index 22ed3f02d0..a9d83501b0 100644 --- a/examples/ts-code-mode-web/vite.config.ts +++ b/examples/ts-code-mode-web/vite.config.ts @@ -1,4 +1,7 @@ -import { defineConfig } from 'vite' +import { existsSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' +import { defineConfig, type Plugin } from 'vite' import { tanstackStart } from '@tanstack/react-start/plugin/vite' import { nitro } from 'nitro/vite' import viteReact from '@vitejs/plugin-react' @@ -28,11 +31,112 @@ const SERVER_ONLY_NATIVE = [ 'quickjs-bun', ] +/** + * quickjs-bun only publishes `exports["."].bun` (no import/default). Vite/Node + * package resolution always fails on that map — even with `conditions: ['bun']` + * under Nitro's module runner (which still hits resolvePackageEntry). Point at + * the package's `index.ts` on disk and mark it external so the Bun runtime + * loads it natively (bun:ffi + TypeScript). + */ +function resolveQuickjsBunIndex(): string { + const starts = [ + dirname(fileURLToPath(import.meta.url)), + process.cwd(), + ] + for (const start of starts) { + let dir = start + for (let i = 0; i < 14; i++) { + const candidates = [ + join(dir, 'node_modules', 'quickjs-bun', 'index.ts'), + join( + dir, + 'packages', + 'ai-isolate-quickjs-bun', + 'node_modules', + 'quickjs-bun', + 'index.ts', + ), + ] + for (const candidate of candidates) { + if (existsSync(candidate)) return candidate + } + const parent = dirname(dir) + if (parent === dir) break + dir = parent + } + } + throw new Error( + 'Could not locate quickjs-bun/index.ts. Run pnpm install from the monorepo root.', + ) +} + +const quickjsBunIndex = resolveQuickjsBunIndex() + +function quickjsBunResolvePlugin(): Plugin { + return { + name: 'resolve-quickjs-bun-entry', + enforce: 'pre', + resolveId(id) { + // Bare package or deep imports → absolute entry, always external so the + // Vite module runner never runInlinedModule()'s quickjs-bun (that path + // throws strict-mode SyntaxError on Bun-oriented source). + if ( + id === 'quickjs-bun' || + id.startsWith('quickjs-bun/') || + id === quickjsBunIndex || + id.endsWith('/quickjs-bun/index.ts') || + id.includes('/quickjs-bun/src/') + ) { + return { id: quickjsBunIndex, external: true } + } + return null + }, + } +} + +/** `CODE_MODE_BUN=1` or running the Vite CLI under Bun enables Bun isolate defaults. */ +const codeModeBun = + process.env.CODE_MODE_BUN === '1' || + typeof (process.versions as { bun?: string }).bun === 'string' + const config = defineConfig({ - resolve: { tsconfigPaths: true }, - plugins: [devtools(), nitro(), tailwindcss(), tanstackStart(), viteReact()], + define: { + // Client UI defaults (selected isolate VM) follow the same flag. + 'import.meta.env.VITE_CODE_MODE_BUN': JSON.stringify( + codeModeBun ? '1' : '', + ), + 'import.meta.env.VITE_CODE_MODE_DEFAULT_VM': JSON.stringify( + process.env.CODE_MODE_DEFAULT_VM ?? '', + ), + }, + resolve: { + tsconfigPaths: true, + // Client must prefer `browser` over `bun`. router-core's isServer maps + // `bun` → server build (isServer=true); if the browser gets that, hydrate + // crashes on router.state. quickjs-bun is handled by the plugin + ssr.conditions. + conditions: ['import', 'module', 'browser', 'default'], + alias: { + // Belt-and-suspenders for static analysis / tools that don't use resolveId + 'quickjs-bun': quickjsBunIndex, + }, + }, + plugins: [ + quickjsBunResolvePlugin(), + devtools(), + // Bun-optimized server only when in Bun mode — default Node Nitro for `pnpm dev`. + // https://bun.com/docs/guides/ecosystem/tanstack-start + nitro(codeModeBun ? { preset: 'bun' } : {}), + tailwindcss(), + tanstackStart(), + viteReact(), + ], ssr: { external: SERVER_ONLY_NATIVE, + resolve: { + // Always allow `bun` on the server so quickjs-bun resolves when the + // process is actually Bun (even without CODE_MODE_BUN). + conditions: ['bun', 'node', 'import', 'module', 'default'], + }, }, optimizeDeps: { exclude: ['isolated-vm', 'quickjs-emscripten', 'quickjs-bun'], diff --git a/packages/ai-code-mode/src/create-code-mode-tool.ts b/packages/ai-code-mode/src/create-code-mode-tool.ts index 981b43b88d..a4a113a32f 100644 --- a/packages/ai-code-mode/src/create-code-mode-tool.ts +++ b/packages/ai-code-mode/src/create-code-mode-tool.ts @@ -45,6 +45,7 @@ const executeTypescriptOutputSchema = z.object({ message: z.string(), name: z.string().optional(), line: z.number().optional(), + stack: z.string().optional(), }) .optional() .describe('Error details if execution failed'), @@ -130,18 +131,62 @@ export function createCodeModeTool( toolContext?: ToolExecutionContext, ): Promise => { const { typescriptCode } = input + const startedAt = Date.now() // Get emitCustomEvent from context or use no-op const emitCustomEvent = toolContext?.emitCustomEvent || (() => {}) + const finish = ( + result: CodeModeToolResult, + phase: string, + ): CodeModeToolResult => { + const durationMs = Date.now() - startedAt + const payload = { + timestamp: Date.now(), + durationMs, + phase, + success: result.success, + logCount: result.logs?.length ?? 0, + error: result.error + ? { + name: result.error.name, + message: result.error.message, + ...(result.error.stack !== undefined && { + stack: result.error.stack, + }), + ...(result.error.line !== undefined && { + line: result.error.line, + }), + } + : undefined, + } + emitCustomEvent('code_mode:execution_finished', payload) + if (!result.success) { + console.error('[code-mode] execute_typescript failed', payload) + } else if ( + typeof process !== 'undefined' && + process.env?.CODE_MODE_DEBUG === '1' + ) { + console.info('[code-mode] execute_typescript ok', { + durationMs, + phase, + logCount: payload.logCount, + }) + } + return result + } + if (!typescriptCode || typeof typescriptCode !== 'string') { - return { - success: false, - error: { - message: 'typescriptCode must be a non-empty string', - name: 'ValidationError', + return finish( + { + success: false, + error: { + message: 'typescriptCode must be a non-empty string', + name: 'ValidationError', + }, }, - } + 'validate-input', + ) } // Create a fresh sandbox context for this execution @@ -161,13 +206,19 @@ export function createCodeModeTool( strippedCode = await transpile(typescriptCode) } catch (error) { // Type/syntax error from the transpiler - return { - success: false, - error: { - message: error instanceof Error ? error.message : String(error), - name: 'TypeScriptError', + return finish( + { + success: false, + error: { + message: + error instanceof Error ? error.message : String(error), + name: 'TypeScriptError', + ...(error instanceof Error && + error.stack !== undefined && { stack: error.stack }), + }, }, - } + 'transpile', + ) } // Step 2: Get dynamic skill bindings if available @@ -192,11 +243,28 @@ export function createCodeModeTool( ) // Step 4: Create sandbox context with event-aware bindings - isolateContext = await driver.createContext({ - bindings: eventAwareBindings, - timeout, - memoryLimit, - }) + try { + isolateContext = await driver.createContext({ + bindings: eventAwareBindings, + timeout, + memoryLimit, + }) + } catch (error) { + return finish( + { + success: false, + error: { + message: + error instanceof Error ? error.message : String(error), + name: + error instanceof Error ? error.name : 'CreateContextError', + ...(error instanceof Error && + error.stack !== undefined && { stack: error.stack }), + }, + }, + 'create-context', + ) + } // Step 5: Execute the code in the sandbox const executionResult = await isolateContext.execute(strippedCode) @@ -228,31 +296,45 @@ export function createCodeModeTool( } if (executionResult.success) { - return { - success: true, - result: executionResult.value, - logs: executionResult.logs, - } - } else { - return { + return finish( + { + success: true, + result: executionResult.value, + logs: executionResult.logs, + }, + 'execute', + ) + } + + return finish( + { success: false, error: executionResult.error ? { message: executionResult.error.message, name: executionResult.error.name, + ...(executionResult.error.stack !== undefined && { + stack: executionResult.error.stack, + }), } - : { message: 'Unknown execution error' }, + : { message: 'Unknown execution error', name: 'UnknownError' }, logs: executionResult.logs, - } - } + }, + 'execute', + ) } catch (error) { - return { - success: false, - error: { - message: error instanceof Error ? error.message : String(error), - name: error instanceof Error ? error.name : 'Error', + return finish( + { + success: false, + error: { + message: error instanceof Error ? error.message : String(error), + name: error instanceof Error ? error.name : 'Error', + ...(error instanceof Error && + error.stack !== undefined && { stack: error.stack }), + }, }, - } + 'unhandled', + ) } finally { // Always clean up the sandbox context if (isolateContext) { diff --git a/packages/ai-code-mode/src/types.ts b/packages/ai-code-mode/src/types.ts index 85a45622c5..7dbbd02a38 100644 --- a/packages/ai-code-mode/src/types.ts +++ b/packages/ai-code-mode/src/types.ts @@ -285,6 +285,7 @@ export interface CodeModeToolResult { message: string name?: string | undefined line?: number | undefined + stack?: string | undefined } | undefined } diff --git a/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts b/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts index 6772b6d4bf..2f4e75037e 100644 --- a/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts +++ b/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts @@ -1,3 +1,6 @@ +import { existsSync } from 'node:fs' +import { dirname, join } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' import { DEFAULT_MAX_TOOL_CALLS, QuickJSBunIsolateContext, @@ -20,21 +23,84 @@ const DEFAULT_MEMORY_LIMIT_MB = 128 const DEFAULT_MAX_STACK_SIZE_BYTES = 512 * 1024 /** - * quickjs-bun's exports map only declares a `bun` condition, so build- and - * test-time resolvers running on Node.js cannot resolve it. The non-literal - * specifier keeps the import out of Vite's static analysis; it only ever - * executes under the Bun runtime. + * quickjs-bun's exports map only declares a `bun` condition (no import/default). + * Bun's resolver understands that; Vite/Node package resolution does not — + * even with `conditions: ['bun']` under Nitro's Vite module runner, which + * still goes through resolvePackageEntry and fails with: + * "No known conditions for \".\" specifier in \"quickjs-bun\" package" + * + * Prefer Bun.resolveSync, then fall back to locating `index.ts` on disk so + * tools that ignore the bun export condition can still load the entry. */ const QUICKJS_BUN_SPECIFIER = 'quickjs-bun' +type BunResolveSync = { + resolveSync?: (specifier: string, from: string) => string +} + +function moduleDir(): string { + // import.meta.dirname is Node 20.11+ / Bun; fall back for older runtimes. + if (typeof import.meta.dirname === 'string') return import.meta.dirname + return dirname(fileURLToPath(import.meta.url)) +} + +function findQuickjsBunEntryOnDisk(): string | undefined { + const starts = [moduleDir(), process.cwd()] + for (const start of starts) { + let dir = start + for (let i = 0; i < 14; i++) { + const candidate = join(dir, 'node_modules', 'quickjs-bun', 'index.ts') + if (existsSync(candidate)) return candidate + const parent = dirname(dir) + if (parent === dir) break + dir = parent + } + } + return undefined +} + +function resolveQuickjsBunEntry(): string { + const bun = (globalThis as { Bun?: BunResolveSync }).Bun + if (typeof bun?.resolveSync === 'function') { + try { + return bun.resolveSync(QUICKJS_BUN_SPECIFIER, moduleDir()) + } catch { + // Fall through — e.g. package not visible from this module path. + } + } + + const onDisk = findQuickjsBunEntryOnDisk() + if (onDisk !== undefined) return onDisk + + // Last resort: bare specifier (works only under a bun-aware resolver). + return QUICKJS_BUN_SPECIFIER +} + +/** + * Runtime-native dynamic import that Vite/Nitro cannot rewrite into the + * module runner. A plain `import()` in source is often transformed to + * `runInlinedModule`, which then parses quickjs-bun as ESM and dies on + * Bun-only syntax / strict-mode edge cases. Building the importer via + * `Function` keeps a true host `import()` (Bun loads the .ts entry + bun:ffi). + */ +const nativeImport = new Function( + 'specifier', + 'return import(specifier)', +) as (specifier: string) => Promise + /** - * Dynamically import the `quickjs-bun` module namespace. Kept as a function - * (not a static import) because the package only resolves under the Bun - * runtime; see `QUICKJS_BUN_SPECIFIER` for why the specifier is non-literal and - * hidden from Vite's static analysis. + * Dynamically import the `quickjs-bun` module namespace. Never a static import: + * the package only resolves under Bun (or via the absolute-path fallbacks + * above). Prefer an absolute file: URL so package.json `exports` (bun-only) + * is never consulted by Node/Vite resolvers. */ function importQuickJSBun(): Promise { - return import(/* @vite-ignore */ QUICKJS_BUN_SPECIFIER) + const entry = resolveQuickjsBunEntry() + const specifier = + entry === QUICKJS_BUN_SPECIFIER || entry.startsWith('file:') + ? entry + : pathToFileURL(entry).href + return nativeImport(specifier) } let libraryPromise: Promise | undefined diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9793f65301..f27934555c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -429,6 +429,79 @@ importers: specifier: ^7.3.3 version: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + examples/ts-code-mode-bun: + dependencies: + '@tailwindcss/vite': + specifier: ^4.1.18 + version: 4.1.18(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@tanstack/ai': + specifier: workspace:* + version: link:../../packages/ai + '@tanstack/ai-anthropic': + specifier: workspace:* + version: link:../../packages/ai-anthropic + '@tanstack/ai-client': + specifier: workspace:* + version: link:../../packages/ai-client + '@tanstack/ai-code-mode': + specifier: workspace:* + version: link:../../packages/ai-code-mode + '@tanstack/ai-isolate-quickjs-bun': + specifier: workspace:* + version: link:../../packages/ai-isolate-quickjs-bun + '@tanstack/ai-openai': + specifier: workspace:* + version: link:../../packages/ai-openai + '@tanstack/ai-react': + specifier: workspace:* + version: link:../../packages/ai-react + '@tanstack/react-router': + specifier: ^1.158.4 + version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@tanstack/react-start': + specifier: ^1.159.0 + version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@tanstack/router-plugin': + specifier: ^1.158.4 + version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + nitro: + specifier: 3.0.260610-beta + version: 3.0.260610-beta(@electric-sql/pglite@0.4.3)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(chokidar@5.0.0)(dotenv@17.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(giget@3.3.0)(jiti@2.7.0)(miniflare@4.20260617.1)(mysql2@3.15.3)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) + quickjs-bun: + specifier: 0.1.2 + version: 0.1.2 + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + tailwindcss: + specifier: ^4.1.18 + version: 4.1.18 + zod: + specifier: ^4.2.0 + version: 4.3.6 + devDependencies: + '@types/bun': + specifier: ^1.3.14 + version: 1.3.14 + '@types/react': + specifier: ^19.2.7 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.2.3 + version: 19.2.3(@types/react@19.2.7) + '@vitejs/plugin-react': + specifier: ^5.1.2 + version: 5.1.2(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: ^8.1.4 + version: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + examples/ts-code-mode-web: dependencies: '@jitl/quickjs-wasmfile-debug-asyncify': @@ -521,6 +594,9 @@ importers: puppeteer: specifier: ^24.34.0 version: 24.39.1(supports-color@7.2.0)(typescript@5.9.3) + quickjs-bun: + specifier: 0.1.2 + version: 0.1.2 quickjs-emscripten: specifier: ^0.31.0 version: 0.31.0 @@ -1269,13 +1345,13 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^3.3.1 - version: 3.3.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))) + version: 3.3.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))) '@sveltejs/kit': specifier: ^2.15.10 - version: 2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@tailwindcss/vite': specifier: ^4.1.18 version: 4.1.18(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) @@ -1448,10 +1524,10 @@ importers: devDependencies: '@analogjs/vite-plugin-angular': specifier: ^2.6.2 - version: 2.6.3(@angular/build@21.2.15(8fad83f643b8ab70eeb3183426533450))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 2.6.3(@angular/build@21.2.15(9b25d723ebc5560eb21fc2f98d972433))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@angular/build': specifier: ^21.2.0 - version: 21.2.15(8fad83f643b8ab70eeb3183426533450) + version: 21.2.15(9b25d723ebc5560eb21fc2f98d972433) '@angular/common': specifier: ^21.2.0 version: 21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) @@ -1484,7 +1560,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(@vitest/coverage-v8@4.0.14)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.19))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.19))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) zod: specifier: ^4.2.0 version: 4.3.6 @@ -2008,7 +2084,7 @@ importers: version: link:../ai-event-client ioredis: specifier: '>=5.0.0' - version: 5.9.2 + version: 5.9.2(supports-color@7.2.0) devDependencies: '@honcho-ai/sdk': specifier: ^2.1.1 @@ -2024,7 +2100,7 @@ importers: version: 4.0.14(supports-color@7.2.0)(vitest@4.1.10) ioredis-mock: specifier: ^8.9.0 - version: 8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.9.2))(ioredis@5.9.2) + version: 8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.9.2(supports-color@7.2.0)))(ioredis@5.9.2(supports-color@7.2.0)) redis: specifier: ^4.7.0 version: 4.7.1 @@ -2449,7 +2525,7 @@ importers: version: 2.5.7(svelte@5.45.10) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@tanstack/ai': specifier: workspace:* version: link:../ai @@ -16819,7 +16895,7 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - '@analogjs/vite-plugin-angular@2.6.3(@angular/build@21.2.15(8fad83f643b8ab70eeb3183426533450))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@analogjs/vite-plugin-angular@2.6.3(@angular/build@21.2.15(9b25d723ebc5560eb21fc2f98d972433))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: magic-string: 0.30.21 obug: 2.1.1 @@ -16828,8 +16904,8 @@ snapshots: ts-morph: 21.0.1 typescript: 5.9.3 optionalDependencies: - '@angular/build': 21.2.15(8fad83f643b8ab70eeb3183426533450) - vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + '@angular/build': 21.2.15(9b25d723ebc5560eb21fc2f98d972433) + vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -16909,7 +16985,7 @@ snapshots: - tsx - yaml - '@angular/build@21.2.15(8fad83f643b8ab70eeb3183426533450)': + '@angular/build@21.2.15(9b25d723ebc5560eb21fc2f98d972433)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.15(chokidar@5.0.0) @@ -16950,7 +17026,7 @@ snapshots: ng-packagr: 21.2.5(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.17)(typescript@5.9.3))(tailwindcss@4.1.18)(tslib@2.8.1)(typescript@5.9.3) postcss: 8.5.19 tailwindcss: 4.1.18 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(@vitest/coverage-v8@4.0.14)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.19))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.19))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -17830,9 +17906,9 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.28.5)': + '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.28.5(supports-color@7.2.0))': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.28.5(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 '@babel/plugin-syntax-flow@7.28.6(@babel/core@7.29.0)': @@ -17947,11 +18023,11 @@ snapshots: '@babel/core': 7.29.0 '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.28.5)': + '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.28.5(supports-color@7.2.0))': dependencies: - '@babel/core': 7.28.5 + '@babel/core': 7.28.5(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.29.7 - '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.28.5) + '@babel/plugin-syntax-flow': 7.28.6(@babel/core@7.28.5(supports-color@7.2.0)) '@babel/plugin-transform-flow-strip-types@7.27.1(@babel/core@7.29.0)': dependencies: @@ -18150,7 +18226,7 @@ snapshots: '@babel/core': 7.28.5(supports-color@7.2.0) '@babel/helper-plugin-utils': 7.27.1 '@babel/helper-validator-option': 7.27.1 - '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.5) + '@babel/plugin-transform-flow-strip-types': 7.27.1(@babel/core@7.28.5(supports-color@7.2.0)) '@babel/preset-typescript@7.28.5(@babel/core@7.28.5(supports-color@7.2.0))': dependencies: @@ -19899,7 +19975,7 @@ snapshots: '@mcp-ui/client@7.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@modelcontextprotocol/ext-apps': 1.7.4(@modelcontextprotocol/sdk@1.29.0(zod@4.2.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76) + '@modelcontextprotocol/ext-apps': 1.7.4(@modelcontextprotocol/sdk@1.29.0(zod@3.25.76))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76) '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -19956,9 +20032,9 @@ snapshots: - bufferutil - utf-8-validate - '@modelcontextprotocol/ext-apps@1.7.4(@modelcontextprotocol/sdk@1.29.0(zod@4.2.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)': + '@modelcontextprotocol/ext-apps@1.7.4(@modelcontextprotocol/sdk@1.29.0(zod@3.25.76))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)': dependencies: - '@modelcontextprotocol/sdk': 1.29.0(zod@4.2.1) + '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) '@standard-schema/spec': 1.1.0 zod: 3.25.76 optionalDependencies: @@ -22600,16 +22676,16 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))': + '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))': dependencies: - '@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) import-meta-resolve: 4.2.0 - '@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -22636,47 +22712,47 @@ snapshots: svelte2tsx: 0.7.45(@typescript/typescript6@6.0.2)(svelte@5.45.10) typescript: '@typescript/typescript6@6.0.2' - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte': 5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) debug: 4.4.3(supports-color@7.2.0) svelte: 5.45.10 - vite: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) debug: 4.4.3(supports-color@7.2.0) svelte: 5.45.10 - vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) debug: 4.4.3(supports-color@7.2.0) deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 svelte: 5.45.10 - vite: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - vitefu: 1.1.1(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vitefu: 1.1.1(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) debug: 4.4.3(supports-color@7.2.0) deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 svelte: 5.45.10 - vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - vitefu: 1.1.1(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + vite: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vitefu: 1.1.1(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - supports-color @@ -24313,9 +24389,9 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/ioredis-mock@8.2.7(ioredis@5.9.2)': + '@types/ioredis-mock@8.2.7(ioredis@5.9.2(supports-color@7.2.0))': dependencies: - ioredis: 5.9.2 + ioredis: 5.9.2(supports-color@7.2.0) '@types/istanbul-lib-coverage@2.0.6': {} @@ -24863,6 +24939,14 @@ snapshots: optionalDependencies: vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + '@vitest/mocker@4.1.10(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + '@vitest/pretty-format@4.0.14': dependencies: tinyrainbow: 3.1.0 @@ -27940,17 +28024,17 @@ snapshots: dependencies: loose-envify: 1.4.0 - ioredis-mock@8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.9.2))(ioredis@5.9.2): + ioredis-mock@8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.9.2(supports-color@7.2.0)))(ioredis@5.9.2(supports-color@7.2.0)): dependencies: '@ioredis/as-callback': 3.0.0 '@ioredis/commands': 1.5.0 - '@types/ioredis-mock': 8.2.7(ioredis@5.9.2) + '@types/ioredis-mock': 8.2.7(ioredis@5.9.2(supports-color@7.2.0)) fengari: 0.1.5 fengari-interop: 0.1.4(fengari@0.1.5) - ioredis: 5.9.2 + ioredis: 5.9.2(supports-color@7.2.0) semver: 7.8.4 - ioredis@5.9.2: + ioredis@5.9.2(supports-color@7.2.0): dependencies: '@ioredis/commands': 1.5.0 cluster-key-slot: 1.1.2 @@ -29693,6 +29777,60 @@ snapshots: - uploadthing - wrangler + nitro@3.0.260610-beta(@electric-sql/pglite@0.4.3)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(chokidar@5.0.0)(dotenv@17.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(giget@3.3.0)(jiti@2.7.0)(miniflare@4.20260617.1)(mysql2@3.15.3)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0): + dependencies: + consola: 3.4.2 + crossws: 0.4.6(srvx@0.11.17) + db0: 0.3.4(@electric-sql/pglite@0.4.3)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3) + env-runner: 0.1.14(miniflare@4.20260617.1)(wrangler@4.103.0) + h3: 2.0.1-rc.22(crossws@0.4.6(srvx@0.11.17)) + hookable: 6.1.1 + nf3: 0.3.17 + ocache: 0.1.5 + ofetch: 2.0.0-alpha.3 + ohash: 2.0.11 + rolldown: 1.1.5 + srvx: 0.11.17 + unenv: 2.0.0-rc.24 + unstorage: 2.0.0-alpha.7(aws4fetch@1.0.20)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.3)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3))(ofetch@2.0.0-alpha.3) + optionalDependencies: + dotenv: 17.4.2 + giget: 3.3.0 + jiti: 2.7.0 + rollup: 4.60.1 + vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + transitivePeerDependencies: + - '@azure/app-configuration' + - '@azure/cosmos' + - '@azure/data-tables' + - '@azure/identity' + - '@azure/keyvault-secrets' + - '@azure/storage-blob' + - '@capacitor/preferences' + - '@deno/kv' + - '@electric-sql/pglite' + - '@libsql/client' + - '@netlify/blobs' + - '@netlify/runtime' + - '@planetscale/database' + - '@upstash/redis' + - '@vercel/blob' + - '@vercel/functions' + - '@vercel/kv' + - aws4fetch + - better-sqlite3 + - chokidar + - drizzle-orm + - idb-keyval + - ioredis + - lru-cache + - miniflare + - mongodb + - mysql2 + - sqlite3 + - uploadthing + - wrangler + nitropack@2.13.1(@electric-sql/pglite@0.4.3)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.1.5): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 @@ -29727,7 +29865,7 @@ snapshots: h3: 1.15.5 hookable: 5.5.3 httpxy: 0.1.7 - ioredis: 5.9.2 + ioredis: 5.9.2(supports-color@7.2.0) jiti: 2.7.0 klona: 2.0.6 knitwork: 1.3.0 @@ -29829,7 +29967,7 @@ snapshots: h3: 1.15.5 hookable: 5.5.3 httpxy: 0.1.7 - ioredis: 5.9.2 + ioredis: 5.9.2(supports-color@7.2.0) jiti: 2.7.0 klona: 2.0.6 knitwork: 1.3.0 @@ -32326,8 +32464,8 @@ snapshots: tinyglobby@0.2.17: dependencies: - fdir: 6.5.0(picomatch@4.0.4) - picomatch: 4.0.4 + fdir: 6.5.0(picomatch@4.0.5) + picomatch: 4.0.5 tinypool@2.1.0: {} @@ -32789,7 +32927,7 @@ snapshots: optionalDependencies: aws4fetch: 1.0.20 db0: 0.3.4(@electric-sql/pglite@0.4.3)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3) - ioredis: 5.9.2 + ioredis: 5.9.2(supports-color@7.2.0) unstorage@1.17.4(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.4.3)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3))(ioredis@5.9.2): dependencies: @@ -32804,7 +32942,7 @@ snapshots: optionalDependencies: aws4fetch: 1.0.20 db0: 0.3.4(@electric-sql/pglite@0.4.3)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3) - ioredis: 5.9.2 + ioredis: 5.9.2(supports-color@7.2.0) unstorage@2.0.0-alpha.7(aws4fetch@1.0.20)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.3)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3))(ofetch@2.0.0-alpha.3): optionalDependencies: @@ -33177,6 +33315,24 @@ snapshots: tsx: 4.21.0 yaml: 2.9.0 + vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.19 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.10.3 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.7.0 + less: 4.6.6 + sass: 1.97.3 + terser: 5.44.1 + tsx: 4.21.0 + yaml: 2.9.0 + vitefu@1.1.1(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): optionalDependencies: vite: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) @@ -33277,6 +33433,36 @@ snapshots: - msw optional: true + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.19))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.0.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.0.0 + tinybench: 2.9.0 + tinyexec: 1.1.1 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 24.10.3 + happy-dom: 20.0.11 + jsdom: 27.3.0(postcss@8.5.19) + transitivePeerDependencies: + - msw + vlq@1.0.1: {} vscode-uri@3.1.0: {} From b47c62a0e40a3150b8795cd4e1c156b9489565ee Mon Sep 17 00:00:00 2001 From: Tom Beckenham <34339192+tombeckenham@users.noreply.github.com> Date: Fri, 7 Aug 2026 18:03:58 +1000 Subject: [PATCH 7/8] refactor(ts-code-mode-web): simplify Bun Vite config Drop the quickjs-bun path walker/plugin/alias. Keep the working pattern: browser-only client resolve conditions, bun on SSR only, externalize quickjs-bun + the isolate driver, and CODE_MODE_BUN/Nitro bun preset. --- examples/ts-code-mode-web/vite.config.ts | 119 +++------- pnpm-lock.yaml | 273 ++++------------------- 2 files changed, 72 insertions(+), 320 deletions(-) diff --git a/examples/ts-code-mode-web/vite.config.ts b/examples/ts-code-mode-web/vite.config.ts index a9d83501b0..9fa40c8e3b 100644 --- a/examples/ts-code-mode-web/vite.config.ts +++ b/examples/ts-code-mode-web/vite.config.ts @@ -1,7 +1,4 @@ -import { existsSync } from 'node:fs' -import { dirname, join } from 'node:path' -import { fileURLToPath } from 'node:url' -import { defineConfig, type Plugin } from 'vite' +import { defineConfig } from 'vite' import { tanstackStart } from '@tanstack/react-start/plugin/vite' import { nitro } from 'nitro/vite' import viteReact from '@vitejs/plugin-react' @@ -9,15 +6,9 @@ import tailwindcss from '@tailwindcss/vite' import { devtools } from '@tanstack/devtools-vite' // Native / wasm / binary server-only modules that can't be bundled by esbuild -// or rolldown (isolated-vm is a `.node` addon, the quickjs engines ship wasm, -// esbuild/puppeteer carry platform binaries). They stay external in every pass. -// nitro 3's server build (rolldown) externalizes node_modules but must *resolve* -// each external at build time; under pnpm these live under the isolate adapters' -// nested store dirs, so they're declared as direct dependencies of this example -// (see package.json) so the resolve succeeds. The pure-JS server deps that the -// old nitro-v2 externals list also named (google-auth-library, gaxios, jws, -// gcp-metadata, google-logging-utils, ws, node-fetch, openai) are left to be -// bundled normally — nitro 3 handles them without an explicit external entry. +// or rolldown. They stay external so the host runtime loads them. +// Under pnpm, declare them as direct deps of this example so nitro can resolve +// them (see package.json). const SERVER_ONLY_NATIVE = [ 'isolated-vm', 'esbuild', @@ -28,80 +19,30 @@ const SERVER_ONLY_NATIVE = [ '@jitl/quickjs-wasmfile-release-sync', '@jitl/quickjs-wasmfile-debug-asyncify', '@jitl/quickjs-wasmfile-debug-sync', + // Bun-native QuickJS (exports.bun only) — host Bun loads it, not Vite. 'quickjs-bun', + '@tanstack/ai-isolate-quickjs-bun', + 'bun:ffi', ] -/** - * quickjs-bun only publishes `exports["."].bun` (no import/default). Vite/Node - * package resolution always fails on that map — even with `conditions: ['bun']` - * under Nitro's module runner (which still hits resolvePackageEntry). Point at - * the package's `index.ts` on disk and mark it external so the Bun runtime - * loads it natively (bun:ffi + TypeScript). - */ -function resolveQuickjsBunIndex(): string { - const starts = [ - dirname(fileURLToPath(import.meta.url)), - process.cwd(), - ] - for (const start of starts) { - let dir = start - for (let i = 0; i < 14; i++) { - const candidates = [ - join(dir, 'node_modules', 'quickjs-bun', 'index.ts'), - join( - dir, - 'packages', - 'ai-isolate-quickjs-bun', - 'node_modules', - 'quickjs-bun', - 'index.ts', - ), - ] - for (const candidate of candidates) { - if (existsSync(candidate)) return candidate - } - const parent = dirname(dir) - if (parent === dir) break - dir = parent - } - } - throw new Error( - 'Could not locate quickjs-bun/index.ts. Run pnpm install from the monorepo root.', - ) -} - -const quickjsBunIndex = resolveQuickjsBunIndex() - -function quickjsBunResolvePlugin(): Plugin { - return { - name: 'resolve-quickjs-bun-entry', - enforce: 'pre', - resolveId(id) { - // Bare package or deep imports → absolute entry, always external so the - // Vite module runner never runInlinedModule()'s quickjs-bun (that path - // throws strict-mode SyntaxError on Bun-oriented source). - if ( - id === 'quickjs-bun' || - id.startsWith('quickjs-bun/') || - id === quickjsBunIndex || - id.endsWith('/quickjs-bun/index.ts') || - id.includes('/quickjs-bun/src/') - ) { - return { id: quickjsBunIndex, external: true } - } - return null - }, - } -} - /** `CODE_MODE_BUN=1` or running the Vite CLI under Bun enables Bun isolate defaults. */ const codeModeBun = process.env.CODE_MODE_BUN === '1' || typeof (process.versions as { bun?: string }).bun === 'string' -const config = defineConfig({ +/** + * Same lessons as the Bun Code Mode path: + * + * - Do **not** put `bun` in top-level `resolve.conditions`. router-core's + * `isServer` maps `bun` → server build; the browser would get isServer=true, + * createRouter never builds a store, hydrateStart crashes on `router.state`. + * - Put `bun` only on `ssr.resolve.conditions` so `quickjs-bun` resolves under Bun. + * - Externalize quickjs-bun + the isolate driver so Vite never inlines them. + * + * `pnpm dev:bun` → CODE_MODE_BUN=1 bun --bun vite dev + */ +export default defineConfig({ define: { - // Client UI defaults (selected isolate VM) follow the same flag. 'import.meta.env.VITE_CODE_MODE_BUN': JSON.stringify( codeModeBun ? '1' : '', ), @@ -111,19 +52,11 @@ const config = defineConfig({ }, resolve: { tsconfigPaths: true, - // Client must prefer `browser` over `bun`. router-core's isServer maps - // `bun` → server build (isServer=true); if the browser gets that, hydrate - // crashes on router.state. quickjs-bun is handled by the plugin + ssr.conditions. + // Client: browser/import only — never prefer `bun` here. conditions: ['import', 'module', 'browser', 'default'], - alias: { - // Belt-and-suspenders for static analysis / tools that don't use resolveId - 'quickjs-bun': quickjsBunIndex, - }, }, plugins: [ - quickjsBunResolvePlugin(), devtools(), - // Bun-optimized server only when in Bun mode — default Node Nitro for `pnpm dev`. // https://bun.com/docs/guides/ecosystem/tanstack-start nitro(codeModeBun ? { preset: 'bun' } : {}), tailwindcss(), @@ -133,14 +66,16 @@ const config = defineConfig({ ssr: { external: SERVER_ONLY_NATIVE, resolve: { - // Always allow `bun` on the server so quickjs-bun resolves when the - // process is actually Bun (even without CODE_MODE_BUN). + // Server under Bun: need `bun` for quickjs-bun's export map. conditions: ['bun', 'node', 'import', 'module', 'default'], }, }, optimizeDeps: { - exclude: ['isolated-vm', 'quickjs-emscripten', 'quickjs-bun'], + exclude: [ + 'isolated-vm', + 'quickjs-emscripten', + 'quickjs-bun', + '@tanstack/ai-isolate-quickjs-bun', + ], }, }) - -export default config diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f27934555c..f02aa77033 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -429,79 +429,6 @@ importers: specifier: ^7.3.3 version: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - examples/ts-code-mode-bun: - dependencies: - '@tailwindcss/vite': - specifier: ^4.1.18 - version: 4.1.18(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - '@tanstack/ai': - specifier: workspace:* - version: link:../../packages/ai - '@tanstack/ai-anthropic': - specifier: workspace:* - version: link:../../packages/ai-anthropic - '@tanstack/ai-client': - specifier: workspace:* - version: link:../../packages/ai-client - '@tanstack/ai-code-mode': - specifier: workspace:* - version: link:../../packages/ai-code-mode - '@tanstack/ai-isolate-quickjs-bun': - specifier: workspace:* - version: link:../../packages/ai-isolate-quickjs-bun - '@tanstack/ai-openai': - specifier: workspace:* - version: link:../../packages/ai-openai - '@tanstack/ai-react': - specifier: workspace:* - version: link:../../packages/ai-react - '@tanstack/react-router': - specifier: ^1.158.4 - version: 1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@tanstack/react-start': - specifier: ^1.159.0 - version: 1.159.5(crossws@0.4.6(srvx@0.11.17))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - '@tanstack/router-plugin': - specifier: ^1.158.4 - version: 1.159.5(@tanstack/react-router@1.159.5(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vite-plugin-solid@2.11.10(solid-js@1.9.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - nitro: - specifier: 3.0.260610-beta - version: 3.0.260610-beta(@electric-sql/pglite@0.4.3)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(chokidar@5.0.0)(dotenv@17.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(giget@3.3.0)(jiti@2.7.0)(miniflare@4.20260617.1)(mysql2@3.15.3)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0) - quickjs-bun: - specifier: 0.1.2 - version: 0.1.2 - react: - specifier: ^19.2.3 - version: 19.2.3 - react-dom: - specifier: ^19.2.3 - version: 19.2.3(react@19.2.3) - tailwindcss: - specifier: ^4.1.18 - version: 4.1.18 - zod: - specifier: ^4.2.0 - version: 4.3.6 - devDependencies: - '@types/bun': - specifier: ^1.3.14 - version: 1.3.14 - '@types/react': - specifier: ^19.2.7 - version: 19.2.7 - '@types/react-dom': - specifier: ^19.2.3 - version: 19.2.3(@types/react@19.2.7) - '@vitejs/plugin-react': - specifier: ^5.1.2 - version: 5.1.2(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - typescript: - specifier: 5.9.3 - version: 5.9.3 - vite: - specifier: ^8.1.4 - version: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - examples/ts-code-mode-web: dependencies: '@jitl/quickjs-wasmfile-debug-asyncify': @@ -1345,13 +1272,13 @@ importers: devDependencies: '@sveltejs/adapter-auto': specifier: ^3.3.1 - version: 3.3.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))) + version: 3.3.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))) '@sveltejs/kit': specifier: ^2.15.10 - version: 2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@tailwindcss/vite': specifier: ^4.1.18 version: 4.1.18(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) @@ -1524,10 +1451,10 @@ importers: devDependencies: '@analogjs/vite-plugin-angular': specifier: ^2.6.2 - version: 2.6.3(@angular/build@21.2.15(9b25d723ebc5560eb21fc2f98d972433))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 2.6.3(@angular/build@21.2.15(8fad83f643b8ab70eeb3183426533450))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@angular/build': specifier: ^21.2.0 - version: 21.2.15(9b25d723ebc5560eb21fc2f98d972433) + version: 21.2.15(8fad83f643b8ab70eeb3183426533450) '@angular/common': specifier: ^21.2.0 version: 21.2.17(@angular/core@21.2.17(@angular/compiler@21.2.17)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) @@ -1560,7 +1487,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.19))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(@vitest/coverage-v8@4.0.14)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.19))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) zod: specifier: ^4.2.0 version: 4.3.6 @@ -2084,7 +2011,7 @@ importers: version: link:../ai-event-client ioredis: specifier: '>=5.0.0' - version: 5.9.2(supports-color@7.2.0) + version: 5.9.2 devDependencies: '@honcho-ai/sdk': specifier: ^2.1.1 @@ -2100,7 +2027,7 @@ importers: version: 4.0.14(supports-color@7.2.0)(vitest@4.1.10) ioredis-mock: specifier: ^8.9.0 - version: 8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.9.2(supports-color@7.2.0)))(ioredis@5.9.2(supports-color@7.2.0)) + version: 8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.9.2))(ioredis@5.9.2) redis: specifier: ^4.7.0 version: 4.7.1 @@ -2525,7 +2452,7 @@ importers: version: 2.5.7(svelte@5.45.10) '@sveltejs/vite-plugin-svelte': specifier: ^5.1.1 - version: 5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + version: 5.1.1(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@tanstack/ai': specifier: workspace:* version: link:../ai @@ -16895,7 +16822,7 @@ snapshots: - '@emnapi/core' - '@emnapi/runtime' - '@analogjs/vite-plugin-angular@2.6.3(@angular/build@21.2.15(9b25d723ebc5560eb21fc2f98d972433))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@analogjs/vite-plugin-angular@2.6.3(@angular/build@21.2.15(8fad83f643b8ab70eeb3183426533450))(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: magic-string: 0.30.21 obug: 2.1.1 @@ -16904,8 +16831,8 @@ snapshots: ts-morph: 21.0.1 typescript: 5.9.3 optionalDependencies: - '@angular/build': 21.2.15(9b25d723ebc5560eb21fc2f98d972433) - vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + '@angular/build': 21.2.15(8fad83f643b8ab70eeb3183426533450) + vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -16985,7 +16912,7 @@ snapshots: - tsx - yaml - '@angular/build@21.2.15(9b25d723ebc5560eb21fc2f98d972433)': + '@angular/build@21.2.15(8fad83f643b8ab70eeb3183426533450)': dependencies: '@ampproject/remapping': 2.3.0 '@angular-devkit/architect': 0.2102.15(chokidar@5.0.0) @@ -17026,7 +16953,7 @@ snapshots: ng-packagr: 21.2.5(@angular/compiler-cli@21.2.17(@angular/compiler@21.2.17)(typescript@5.9.3))(tailwindcss@4.1.18)(tslib@2.8.1)(typescript@5.9.3) postcss: 8.5.19 tailwindcss: 4.1.18 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.19))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(@vitest/coverage-v8@4.0.14)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.19))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - '@emnapi/core' - '@emnapi/runtime' @@ -19975,7 +19902,7 @@ snapshots: '@mcp-ui/client@7.1.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@modelcontextprotocol/ext-apps': 1.7.4(@modelcontextprotocol/sdk@1.29.0(zod@3.25.76))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76) + '@modelcontextprotocol/ext-apps': 1.7.4(@modelcontextprotocol/sdk@1.29.0(zod@4.2.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76) '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) @@ -20032,9 +19959,9 @@ snapshots: - bufferutil - utf-8-validate - '@modelcontextprotocol/ext-apps@1.7.4(@modelcontextprotocol/sdk@1.29.0(zod@3.25.76))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)': + '@modelcontextprotocol/ext-apps@1.7.4(@modelcontextprotocol/sdk@1.29.0(zod@4.2.1))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(zod@3.25.76)': dependencies: - '@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.29.0(zod@4.2.1) '@standard-schema/spec': 1.1.0 zod: 3.25.76 optionalDependencies: @@ -22676,16 +22603,16 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))': + '@sveltejs/adapter-auto@3.3.1(@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))': dependencies: - '@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@sveltejs/kit': 2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) import-meta-resolve: 4.2.0 - '@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@sveltejs/kit@2.49.2(@opentelemetry/api@1.9.1)(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.8(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte': 5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -22712,47 +22639,47 @@ snapshots: svelte2tsx: 0.7.45(@typescript/typescript6@6.0.2)(svelte@5.45.10) typescript: '@typescript/typescript6@6.0.2' - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte': 5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) debug: 4.4.3(supports-color@7.2.0) svelte: 5.45.10 - vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) debug: 4.4.3(supports-color@7.2.0) svelte: 5.45.10 - vite: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(supports-color@7.2.0)(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(supports-color@7.2.0)(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) debug: 4.4.3(supports-color@7.2.0) deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 svelte: 5.45.10 - vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - vitefu: 1.1.1(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + vite: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vitefu: 1.1.1(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)))(svelte@5.45.10)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) debug: 4.4.3(supports-color@7.2.0) deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.21 svelte: 5.45.10 - vite: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - vitefu: 1.1.1(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) + vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) + vitefu: 1.1.1(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) transitivePeerDependencies: - supports-color @@ -24389,9 +24316,9 @@ snapshots: dependencies: '@types/unist': 3.0.3 - '@types/ioredis-mock@8.2.7(ioredis@5.9.2(supports-color@7.2.0))': + '@types/ioredis-mock@8.2.7(ioredis@5.9.2)': dependencies: - ioredis: 5.9.2(supports-color@7.2.0) + ioredis: 5.9.2 '@types/istanbul-lib-coverage@2.0.6': {} @@ -24939,14 +24866,6 @@ snapshots: optionalDependencies: vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - '@vitest/mocker@4.1.10(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))': - dependencies: - '@vitest/spy': 4.1.10 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - '@vitest/pretty-format@4.0.14': dependencies: tinyrainbow: 3.1.0 @@ -28024,17 +27943,17 @@ snapshots: dependencies: loose-envify: 1.4.0 - ioredis-mock@8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.9.2(supports-color@7.2.0)))(ioredis@5.9.2(supports-color@7.2.0)): + ioredis-mock@8.13.1(@types/ioredis-mock@8.2.7(ioredis@5.9.2))(ioredis@5.9.2): dependencies: '@ioredis/as-callback': 3.0.0 '@ioredis/commands': 1.5.0 - '@types/ioredis-mock': 8.2.7(ioredis@5.9.2(supports-color@7.2.0)) + '@types/ioredis-mock': 8.2.7(ioredis@5.9.2) fengari: 0.1.5 fengari-interop: 0.1.4(fengari@0.1.5) - ioredis: 5.9.2(supports-color@7.2.0) + ioredis: 5.9.2 semver: 7.8.4 - ioredis@5.9.2(supports-color@7.2.0): + ioredis@5.9.2: dependencies: '@ioredis/commands': 1.5.0 cluster-key-slot: 1.1.2 @@ -29777,60 +29696,6 @@ snapshots: - uploadthing - wrangler - nitro@3.0.260610-beta(@electric-sql/pglite@0.4.3)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(chokidar@5.0.0)(dotenv@17.4.2)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(giget@3.3.0)(jiti@2.7.0)(miniflare@4.20260617.1)(mysql2@3.15.3)(rollup@4.60.1)(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0))(wrangler@4.103.0): - dependencies: - consola: 3.4.2 - crossws: 0.4.6(srvx@0.11.17) - db0: 0.3.4(@electric-sql/pglite@0.4.3)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3) - env-runner: 0.1.14(miniflare@4.20260617.1)(wrangler@4.103.0) - h3: 2.0.1-rc.22(crossws@0.4.6(srvx@0.11.17)) - hookable: 6.1.1 - nf3: 0.3.17 - ocache: 0.1.5 - ofetch: 2.0.0-alpha.3 - ohash: 2.0.11 - rolldown: 1.1.5 - srvx: 0.11.17 - unenv: 2.0.0-rc.24 - unstorage: 2.0.0-alpha.7(aws4fetch@1.0.20)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.3)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3))(ofetch@2.0.0-alpha.3) - optionalDependencies: - dotenv: 17.4.2 - giget: 3.3.0 - jiti: 2.7.0 - rollup: 4.60.1 - vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - transitivePeerDependencies: - - '@azure/app-configuration' - - '@azure/cosmos' - - '@azure/data-tables' - - '@azure/identity' - - '@azure/keyvault-secrets' - - '@azure/storage-blob' - - '@capacitor/preferences' - - '@deno/kv' - - '@electric-sql/pglite' - - '@libsql/client' - - '@netlify/blobs' - - '@netlify/runtime' - - '@planetscale/database' - - '@upstash/redis' - - '@vercel/blob' - - '@vercel/functions' - - '@vercel/kv' - - aws4fetch - - better-sqlite3 - - chokidar - - drizzle-orm - - idb-keyval - - ioredis - - lru-cache - - miniflare - - mongodb - - mysql2 - - sqlite3 - - uploadthing - - wrangler - nitropack@2.13.1(@electric-sql/pglite@0.4.3)(aws4fetch@1.0.20)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3)(rolldown@1.1.5): dependencies: '@cloudflare/kv-asset-handler': 0.4.2 @@ -29865,7 +29730,7 @@ snapshots: h3: 1.15.5 hookable: 5.5.3 httpxy: 0.1.7 - ioredis: 5.9.2(supports-color@7.2.0) + ioredis: 5.9.2 jiti: 2.7.0 klona: 2.0.6 knitwork: 1.3.0 @@ -29967,7 +29832,7 @@ snapshots: h3: 1.15.5 hookable: 5.5.3 httpxy: 0.1.7 - ioredis: 5.9.2(supports-color@7.2.0) + ioredis: 5.9.2 jiti: 2.7.0 klona: 2.0.6 knitwork: 1.3.0 @@ -32927,7 +32792,7 @@ snapshots: optionalDependencies: aws4fetch: 1.0.20 db0: 0.3.4(@electric-sql/pglite@0.4.3)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3) - ioredis: 5.9.2(supports-color@7.2.0) + ioredis: 5.9.2 unstorage@1.17.4(aws4fetch@1.0.20)(db0@0.3.4(@electric-sql/pglite@0.4.3)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3))(ioredis@5.9.2): dependencies: @@ -32942,7 +32807,7 @@ snapshots: optionalDependencies: aws4fetch: 1.0.20 db0: 0.3.4(@electric-sql/pglite@0.4.3)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@cloudflare/workers-types@4.20260317.1)(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3) - ioredis: 5.9.2(supports-color@7.2.0) + ioredis: 5.9.2 unstorage@2.0.0-alpha.7(aws4fetch@1.0.20)(chokidar@5.0.0)(db0@0.3.4(@electric-sql/pglite@0.4.3)(better-sqlite3@12.11.1)(drizzle-orm@0.45.2(@electric-sql/pglite@0.4.3)(@opentelemetry/api@1.9.1)(@prisma/client@7.9.0(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3))(typescript@5.9.3))(better-sqlite3@12.11.1)(bun-types@1.3.14)(mysql2@3.15.3)(postgres@3.4.7)(prisma@7.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(better-sqlite3@12.11.1)(magicast@0.5.2)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)))(mysql2@3.15.3))(ofetch@2.0.0-alpha.3): optionalDependencies: @@ -33315,24 +33180,6 @@ snapshots: tsx: 4.21.0 yaml: 2.9.0 - vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0): - dependencies: - lightningcss: 1.32.0 - picomatch: 4.0.5 - postcss: 8.5.19 - rolldown: 1.1.5 - tinyglobby: 0.2.17 - optionalDependencies: - '@types/node': 24.10.3 - esbuild: 0.28.1 - fsevents: 2.3.3 - jiti: 2.7.0 - less: 4.6.6 - sass: 1.97.3 - terser: 5.44.1 - tsx: 4.21.0 - yaml: 2.9.0 - vitefu@1.1.1(vite@7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): optionalDependencies: vite: 7.3.3(@types/node@24.10.3)(jiti@2.7.0)(less@4.6.6)(lightningcss@1.32.0)(sass@1.101.0)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) @@ -33433,36 +33280,6 @@ snapshots: - msw optional: true - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@24.10.3)(happy-dom@20.0.11)(jsdom@27.3.0(postcss@8.5.19))(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)): - dependencies: - '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(vite@8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0)) - '@vitest/pretty-format': 4.1.10 - '@vitest/runner': 4.1.10 - '@vitest/snapshot': 4.1.10 - '@vitest/spy': 4.1.10 - '@vitest/utils': 4.1.10 - es-module-lexer: 2.0.0 - expect-type: 1.3.0 - magic-string: 0.30.21 - obug: 2.1.1 - pathe: 2.0.3 - picomatch: 4.0.5 - std-env: 4.0.0 - tinybench: 2.9.0 - tinyexec: 1.1.1 - tinyglobby: 0.2.17 - tinyrainbow: 3.1.0 - vite: 8.1.4(@types/node@24.10.3)(esbuild@0.28.1)(jiti@2.7.0)(less@4.6.6)(sass@1.97.3)(terser@5.44.1)(tsx@4.21.0)(yaml@2.9.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@opentelemetry/api': 1.9.1 - '@types/node': 24.10.3 - happy-dom: 20.0.11 - jsdom: 27.3.0(postcss@8.5.19) - transitivePeerDependencies: - - msw - vlq@1.0.1: {} vscode-uri@3.1.0: {} From 431f71e4a210da0226bb9e08599c9f03f60ea32c Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:07:10 +0000 Subject: [PATCH 8/8] ci: apply automated fixes --- examples/ts-code-mode-web/README.md | 8 ++++---- .../ts-code-mode-web/src/lib/create-isolate-driver.ts | 11 ++++++----- packages/ai-code-mode/src/create-code-mode-tool.ts | 6 ++---- packages/ai-isolate-quickjs-bun/src/isolate-driver.ts | 7 +++---- 4 files changed, 15 insertions(+), 17 deletions(-) diff --git a/examples/ts-code-mode-web/README.md b/examples/ts-code-mode-web/README.md index ff610bcd76..5acf6efe73 100644 --- a/examples/ts-code-mode-web/README.md +++ b/examples/ts-code-mode-web/README.md @@ -90,10 +90,10 @@ When set (or when the process is Bun), this example: Optional overrides: -| Env | Effect | -|---|---| -| `CODE_MODE_BUN=1` | Bun defaults + Nitro bun preset (also set by `pnpm dev:bun`) | -| `CODE_MODE_DEFAULT_VM=node\|quickjs\|quickjs-bun\|cloudflare` | Force default isolate regardless of Bun | +| Env | Effect | +| ------------------------------------------------------------- | ------------------------------------------------------------ | +| `CODE_MODE_BUN=1` | Bun defaults + Nitro bun preset (also set by `pnpm dev:bun`) | +| `CODE_MODE_DEFAULT_VM=node\|quickjs\|quickjs-bun\|cloudflare` | Force default isolate regardless of Bun | ## Build diff --git a/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts b/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts index edd4b12fc3..3f0915e876 100644 --- a/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts +++ b/examples/ts-code-mode-web/src/lib/create-isolate-driver.ts @@ -59,9 +59,7 @@ export async function createIsolateDriver( ): Promise { const cached = driverCache.get(vm) if (cached) { - console.info( - `[createIsolateDriver] reusing cached driver for vm=${vm}`, - ) + console.info(`[createIsolateDriver] reusing cached driver for vm=${vm}`) return cached } @@ -94,7 +92,8 @@ export async function createIsolateDriver( const { createQuickJSIsolateDriver } = await import('@tanstack/ai-isolate-quickjs') driver = createQuickJSIsolateDriver() - resolved = 'quickjs-wasm-fallback (requested quickjs-bun, no global Bun)' + resolved = + 'quickjs-wasm-fallback (requested quickjs-bun, no global Bun)' } break } @@ -130,7 +129,9 @@ export async function createIsolateDriver( } const runtime = - typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined' ? 'bun' : 'node' + typeof (globalThis as { Bun?: unknown }).Bun !== 'undefined' + ? 'bun' + : 'node' console.info( `[createIsolateDriver] vm=${vm} resolved=${resolved} serverRuntime=${runtime}`, ) diff --git a/packages/ai-code-mode/src/create-code-mode-tool.ts b/packages/ai-code-mode/src/create-code-mode-tool.ts index a4a113a32f..5d476f8414 100644 --- a/packages/ai-code-mode/src/create-code-mode-tool.ts +++ b/packages/ai-code-mode/src/create-code-mode-tool.ts @@ -210,8 +210,7 @@ export function createCodeModeTool( { success: false, error: { - message: - error instanceof Error ? error.message : String(error), + message: error instanceof Error ? error.message : String(error), name: 'TypeScriptError', ...(error instanceof Error && error.stack !== undefined && { stack: error.stack }), @@ -254,8 +253,7 @@ export function createCodeModeTool( { success: false, error: { - message: - error instanceof Error ? error.message : String(error), + message: error instanceof Error ? error.message : String(error), name: error instanceof Error ? error.name : 'CreateContextError', ...(error instanceof Error && diff --git a/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts b/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts index 2f4e75037e..6c6acd9392 100644 --- a/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts +++ b/packages/ai-isolate-quickjs-bun/src/isolate-driver.ts @@ -83,10 +83,9 @@ function resolveQuickjsBunEntry(): string { * Bun-only syntax / strict-mode edge cases. Building the importer via * `Function` keeps a true host `import()` (Bun loads the .ts entry + bun:ffi). */ -const nativeImport = new Function( - 'specifier', - 'return import(specifier)', -) as (specifier: string) => Promise +const nativeImport = new Function('specifier', 'return import(specifier)') as ( + specifier: string, +) => Promise /** * Dynamically import the `quickjs-bun` module namespace. Never a static import: