Skip to content

feat(ai-isolate): add native QuickJS Code Mode isolate driver for Bun - #750

Merged
AlemTuzlak merged 9 commits into
TanStack:mainfrom
lithdew:quickjs-bun-code-mode
Aug 7, 2026
Merged

feat(ai-isolate): add native QuickJS Code Mode isolate driver for Bun#750
AlemTuzlak merged 9 commits into
TanStack:mainfrom
lithdew:quickjs-bun-code-mode

Conversation

@lithdew

@lithdew lithdew commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

Adds a new Code Mode isolate driver, @tanstack/ai-isolate-quickjs-bun, that runs QuickJS natively on Bun via bun:ffi (through quickjs-bun) rather than WebAssembly.

It implements the existing IsolateDriver contract from @tanstack/ai-code-mode, so it's a drop-in replacement for @tanstack/ai-isolate-quickjs on Bun servers:

import { createQuickJSBunIsolateDriver } from '@tanstack/ai-isolate-quickjs-bun'
import { createCodeModeTool } from '@tanstack/ai-code-mode'

const executeTypescript = createCodeModeTool({
  driver: createQuickJSBunIsolateDriver(),
  tools: [myTool],
})

Why

On Bun, the existing WASM driver (quickjs-emscripten) is both slower and, in my testing, unreliable for async host tool calls. Its asyncify bridge crashes the shared WASM module (memory access out of bounds) and hangs once a single execution makes ≥ 4 sequential awaited tool calls (reproduced on Node 22 and Bun 1.3.14). quickjs-bun maps the QuickJS C API directly through bun:ffi, giving each context its own native runtime with no asyncify and no shared-VM serialization.

Benchmarks

packages/ai-isolate-quickjs-bun/benchmarks/compare-with-wasm.ts, both drivers driven through the public IsolateDriver interface (Apple M-series, darwin/arm64; Bun 1.3.14 for the native driver, Node 22 for the WASM driver as its native habitat):

Scenario (fresh context per run) QuickJS Bun QuickJS WASM (Node)
Cold start (first context + run) ~130 ms ~20 ms
return 1 + 1 ~0.7 ms ~10 ms (≈14× WASM)
3 sequential tool calls ~0.9 ms see note ¹
8 sequential tool calls ~1.0 ms see note ¹
compute (fib(20)) ~5.0 ms see note ¹
return 1 + 1 (reused context) ~0.04 ms

¹ The WASM driver's asyncified host tool calls repeatedly crash the shared WASM module and hang subsequent executions (≥ 4 sequential awaited host calls per process), on both Node 22 and Bun 1.3.14. Sync-only workloads are unaffected. WASM wins cold start (one-time WASM instantiate vs TinyCC compile of the QuickJS sources); the native driver wins steady-state per-execution by ~14× on the trivial case.

Notes

The suites are gated with describe.skipIf(typeof Bun === 'undefined'), mirroring how @tanstack/ai-isolate-node skips when its native addon is unavailable — would like to know if it is fine to add a oven-sh/setup-bun job (pnpm --filter @tanstack/ai-isolate-quickjs-bun test:bun) so that the full test suite is ran in CI.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features
    • Added a Bun-native QuickJS isolate driver for Code Mode (@tanstack/ai-isolate-quickjs-bun, drop-in for Bun) using bun:ffi, with configurable timeout, memoryLimit, maxStackSize, and maxToolCalls (default 1000) plus normalized limit/timeout error behavior.
  • Documentation
    • Updated Code Mode and isolate-driver documentation, comparisons, READMEs, and skills to add the QuickJS Bun driver and guidance for choosing it on Bun servers.
  • Examples
    • Updated the Code Mode web example to add a quickjs-bun driver option.
  • Tests & Benchmarks
    • Added Bun-only isolation/escape and driver behavior tests, plus benchmarks comparing native Bun QuickJS vs WASM.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new Bun-native QuickJS isolate driver package, wires it into the example app and Code Mode docs, and includes validation, benchmarking, and release metadata updates.

Changes

Bun QuickJS Isolate Driver

Layer / File(s) Summary
Package bootstrap and exports
packages/ai-isolate-quickjs-bun/package.json, packages/ai-isolate-quickjs-bun/tsconfig.json, packages/ai-isolate-quickjs-bun/vite.config.ts, knip.json, packages/ai-isolate-quickjs-bun/src/index.ts
Adds the new package manifest, TypeScript/Vite config, workspace entry, and barrel exports for the Bun QuickJS isolate driver package.
Runtime, context, and driver implementation
packages/ai-isolate-quickjs-bun/src/error-normalizer.ts, packages/ai-isolate-quickjs-bun/src/isolate-driver.ts, packages/ai-isolate-quickjs-bun/src/isolate-context.ts
Implements Bun-only QuickJS loading, driver creation, per-context runtime setup, execution serialization, tool-call plumbing, console capture, disposal, timeout handling, and normalized error mapping.
Driver validation and sandbox tests
packages/ai-isolate-quickjs-bun/tests/isolate-driver.test.ts, packages/ai-isolate-quickjs-bun/tests/escape-attempts.test.ts
Adds Bun-gated tests covering execution behavior, tool calls, limits, logging, disposal, error normalization, runtime contract checks, and sandbox escape attempts.
Native vs WASM benchmark harness
packages/ai-isolate-quickjs-bun/benchmarks/compare-with-wasm.ts
Adds a benchmark runner comparing native Bun QuickJS and WASM QuickJS across guarded scenarios with hang handling.
Example app driver wiring
examples/ts-code-mode-web/package.json, examples/ts-code-mode-web/src/components/ToolSidebar.tsx, examples/ts-code-mode-web/src/lib/create-isolate-driver.ts, examples/ts-code-mode-web/vite.config.ts
Adds the new driver dependency, exposes it in the UI, wires dynamic driver creation, and updates Vite exclusions.
Documentation and release-note rollout
packages/ai-code-mode/README.md, packages/ai-code-mode/skills/ai-code-mode/SKILL.md, packages/ai-isolate-quickjs-bun/README.md, docs/code-mode/code-mode-isolates.md, docs/code-mode/code-mode.md, docs/comparison/vercel-ai-sdk.md, docs/config.json, .changeset/quickjs-bun-isolate-driver.md
Updates Code Mode docs, package README, comparison docs, navigation metadata, and release notes to describe the Bun-native QuickJS driver and its contract.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • TanStack/ai#697: Overlaps on the Code Execution / isolate-driver documentation and driver-comparison area.

Suggested reviewers

  • tombeckenham
  • KevinVandy

Poem

🐰 I hopped into Bun with QuickJS in tow,
Native sandboxes humming nice and low.
Timeouts, limits, and tools in a hop,
Logs stay clipped when the bytes won't stop.
A tiny carrot cheer — ship it, then go!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a native QuickJS isolate driver for Bun.
Description check ✅ Passed The description matches the template with Changes, Checklist, and Release Impact sections filled in and a generated changeset noted.
Docstring Coverage ✅ Passed Docstring coverage is 82.35% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@socket-security

socket-security Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​types/​bun@​1.3.141001004887100
Addednpm/​quickjs-bun@​0.1.27810010090100

View full report

@socket-security

socket-security Bot commented Jun 11, 2026

Copy link
Copy Markdown

All alerts resolved. Learn more about Socket for GitHub.

This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored.

View full report

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
examples/ts-code-mode-web/src/components/ToolSidebar.tsx (1)

106-111: 💤 Low value

Consider runtime detection for better UX.

The option is marked available: true unconditionally, so users on Node.js can select it but will encounter a runtime error when the driver attempts to create a context. While the description warns "requires running the server with Bun," detecting the runtime at page load and conditionally setting available: false on Node would prevent confusing errors.

🎨 Optional runtime detection pattern
  {
    id: 'quickjs-bun',
    name: 'QuickJS Bun',
    description: 'Native QuickJS engine (requires running the server with Bun)',
-   available: true,
+   available: typeof Bun !== 'undefined',
  },
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/ts-code-mode-web/src/components/ToolSidebar.tsx` around lines 106 -
111, The quickjs-bun item in ToolSidebar.tsx currently hard-codes available:
true, which allows Node clients to select it and hit runtime errors; change it
to compute availability at load time (e.g., derive a boolean like isBunRuntime
from a server-provided endpoint or a prop/initial state and set available:
isBunRuntime) so the entry for id 'quickjs-bun' is disabled when the
server/runtime is not Bun; update the ToolSidebar component to fetch or accept
the runtime indicator (or feature flag) and use that to set the available
property for the 'quickjs-bun' item and the UI disabled state.
packages/ai-isolate-quickjs-bun/src/isolate-driver.ts (1)

141-142: 💤 Low value

Consider caching the module import alongside the library.

importQuickJSBun() is called on every createContext() invocation. While JavaScript runtimes cache dynamic imports, you could align this with the libraryPromise pattern for consistency and to make the caching explicit.

♻️ Suggested refactor
-let libraryPromise: Promise<QuickJS> | undefined
+let modulePromise: Promise<QuickJSBunModule> | undefined
+let libraryPromise: Promise<QuickJS> | undefined

+async function loadQuickJSModule(): Promise<QuickJSBunModule> {
+  modulePromise ??= importQuickJSBun()
+  try {
+    return await modulePromise
+  } catch (error) {
+    modulePromise = undefined
+    throw error
+  }
+}

 async function loadQuickJSLibrary(): Promise<QuickJS> {
-  libraryPromise ??= importQuickJSBun().then((mod) => new mod.QuickJS())
+  libraryPromise ??= loadQuickJSModule().then((mod) => new mod.QuickJS())
   // ...
 }

Then in createContext:

-      const quickjs = await importQuickJSBun()
+      const quickjs = await loadQuickJSModule()
       const library = await loadQuickJSLibrary()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-isolate-quickjs-bun/src/isolate-driver.ts` around lines 141 -
142, importQuickJSBun() is being invoked on every createContext() call; make
this explicit and consistent with the existing libraryPromise pattern by caching
the dynamic import result. Add a top-level promise (e.g., quickjsModulePromise)
that stores importQuickJSBun(), use that cached promise inside createContext()
instead of calling importQuickJSBun() directly, and ensure you still await
loadQuickJSLibrary() (libraryPromise) as before so both the module and library
are only loaded once.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/code-mode/code-mode-isolates.md`:
- Around line 115-120: The markdown link in the QuickJS Bun Driver paragraph
uses backticks around the link text
(`[quickjs-bun](https://github.com/superpowerdotcom/quickjs-bun)`) which
prevents it rendering as a proper clickable link; update the text in the QuickJS
Bun Driver section (the line containing
`[quickjs-bun](https://github.com/superpowerdotcom/quickjs-bun)`) to either use
monospace label with a linked URL like via the
[`quickjs-bun`](https://github.com/superpowerdotcom/quickjs-bun) package or a
normal link via the
[quickjs-bun](https://github.com/superpowerdotcom/quickjs-bun) package so the
link renders correctly.

---

Nitpick comments:
In `@examples/ts-code-mode-web/src/components/ToolSidebar.tsx`:
- Around line 106-111: The quickjs-bun item in ToolSidebar.tsx currently
hard-codes available: true, which allows Node clients to select it and hit
runtime errors; change it to compute availability at load time (e.g., derive a
boolean like isBunRuntime from a server-provided endpoint or a prop/initial
state and set available: isBunRuntime) so the entry for id 'quickjs-bun' is
disabled when the server/runtime is not Bun; update the ToolSidebar component to
fetch or accept the runtime indicator (or feature flag) and use that to set the
available property for the 'quickjs-bun' item and the UI disabled state.

In `@packages/ai-isolate-quickjs-bun/src/isolate-driver.ts`:
- Around line 141-142: importQuickJSBun() is being invoked on every
createContext() call; make this explicit and consistent with the existing
libraryPromise pattern by caching the dynamic import result. Add a top-level
promise (e.g., quickjsModulePromise) that stores importQuickJSBun(), use that
cached promise inside createContext() instead of calling importQuickJSBun()
directly, and ensure you still await loadQuickJSLibrary() (libraryPromise) as
before so both the module and library are only loaded once.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: efa0ea2b-d53c-4c49-ac81-f8007e2d5a1d

📥 Commits

Reviewing files that changed from the base of the PR and between 984ac3c and ac5ba14.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (23)
  • .changeset/quickjs-bun-isolate-driver.md
  • docs/code-mode/code-mode-isolates.md
  • docs/code-mode/code-mode.md
  • docs/comparison/vercel-ai-sdk.md
  • docs/config.json
  • examples/ts-code-mode-web/package.json
  • examples/ts-code-mode-web/src/components/ToolSidebar.tsx
  • examples/ts-code-mode-web/src/lib/create-isolate-driver.ts
  • examples/ts-code-mode-web/vite.config.ts
  • knip.json
  • packages/ai-code-mode/README.md
  • packages/ai-code-mode/skills/ai-code-mode/SKILL.md
  • packages/ai-isolate-quickjs-bun/README.md
  • packages/ai-isolate-quickjs-bun/benchmarks/compare-with-wasm.ts
  • packages/ai-isolate-quickjs-bun/package.json
  • packages/ai-isolate-quickjs-bun/src/error-normalizer.ts
  • packages/ai-isolate-quickjs-bun/src/index.ts
  • packages/ai-isolate-quickjs-bun/src/isolate-context.ts
  • packages/ai-isolate-quickjs-bun/src/isolate-driver.ts
  • packages/ai-isolate-quickjs-bun/tests/escape-attempts.test.ts
  • packages/ai-isolate-quickjs-bun/tests/isolate-driver.test.ts
  • packages/ai-isolate-quickjs-bun/tsconfig.json
  • packages/ai-isolate-quickjs-bun/vite.config.ts

Comment thread docs/code-mode/code-mode-isolates.md
@tombeckenham tombeckenham self-assigned this Jun 16, 2026
@lithdew
lithdew force-pushed the quickjs-bun-code-mode branch from cb7abd2 to ab73851 Compare June 18, 2026 15:53
@lithdew

lithdew commented Jun 18, 2026

Copy link
Copy Markdown
Contributor Author

Rebased to the latest origin/main and fixed merge conflicts.

@lithdew
lithdew force-pushed the quickjs-bun-code-mode branch 3 times, most recently from 9b80eb7 to 8634ac7 Compare July 1, 2026 09:03
@nx-cloud

nx-cloud Bot commented Jul 1, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 431f71e

Command Status Duration Result
nx build @tanstack/ai-isolate-quickjs-bun ✅ Succeeded 15s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-07 08:12:08 UTC

@nx-cloud

nx-cloud Bot commented Jul 1, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 8634ac7

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1m 57s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-01 09:18:16 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 1, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@750

@tanstack/ai-acp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-acp@750

@tanstack/ai-angular

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-angular@750

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@750

@tanstack/ai-bedrock

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-bedrock@750

@tanstack/ai-byteplus

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-byteplus@750

@tanstack/ai-claude-code

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-claude-code@750

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@750

@tanstack/ai-code-mode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode@750

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-skills@750

@tanstack/ai-codex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-codex@750

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@750

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-durable-stream@750

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-elevenlabs@750

@tanstack/ai-event-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-event-client@750

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@750

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@750

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@750

@tanstack/ai-grok-build

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok-build@750

@tanstack/ai-groq

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-groq@750

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-cloudflare@750

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-node@750

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs@750

@tanstack/ai-isolate-quickjs-bun

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs-bun@750

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@750

@tanstack/ai-memory

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-memory@750

@tanstack/ai-mistral

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mistral@750

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@750

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@750

@tanstack/ai-opencode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-opencode@750

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@750

@tanstack/ai-persistence

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence@750

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@750

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@750

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@750

@tanstack/ai-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox@750

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-cloudflare@750

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-daytona@750

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-docker@750

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-local-process@750

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-sprites@750

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-vercel@750

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@750

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@750

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@750

@tanstack/ai-utils

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-utils@750

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@750

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@750

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@750

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@750

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@750

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@750

commit: 431f71e

lithdew and others added 2 commits July 2, 2026 04:08
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.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
@lithdew
lithdew force-pushed the quickjs-bun-code-mode branch from 8634ac7 to ec8c161 Compare July 1, 2026 20:12

@tombeckenham tombeckenham left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's good work generally. Thee's a few issues raised through the AI review you should address.

I've got 2 main concerns. I'm not sure about how we add the bun tests to the suite cleanly, and I couldn't properly get the example app to run in bun. Let me know how you'd address that.

- sandbox
- secure execution
---

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[bug] The YAML frontmatter opening --- (line 1) is never closed before the body starts at line 19. Every other Code Mode doc (code-mode.md, client-integration.md, etc.) closes frontmatter with --- on its own line. The diff removed the closing delimiter. Parsers (e.g. typedoc-plugin-frontmatter) will treat lines 2–34—including the comparison table and headings—as frontmatter or mis-parse the page.

Suggestion: Restore the closing --- immediately after the keywords block (after line 17), matching the other docs in docs/code-mode/.

Comment thread docs/code-mode/code-mode-isolates.md Outdated
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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] Broken inline-code formatting: `external_`* tool calls — the backtick closes before _, leaving a stray * and incorrect rendering.

Suggestion: Use `external_*` (or "external_* tool calls" without partial backticks).

| -------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `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. |

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The QuickJS Bun "Options" table documents memoryLimit, timeout, and maxStackSize but omits maxToolCalls, which is a first-class driver option (default 1000) documented in the package README, changeset, and QuickJSBunIsolateDriverConfig. Users reading the canonical isolate-driver doc will not discover this safety limit.

Suggestion: Add a maxToolCalls row to the Options table and mention it briefly in "How it works", consistent with packages/ai-isolate-quickjs-bun/README.md.

"clean": "premove ./build ./dist",
"lint:fix": "eslint ./src --fix",
"test:build": "publint --strict",
"test:bun": "bun test ./tests",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The substantive test suite (tests/*.test.ts, ~880 lines) is gated with describe.skipIf(typeof Bun === 'undefined') and only runnable via test:bun. Standard CI targets (test:lib / test:pr) use Vitest on Node and therefore exercise only the single "rejects createContext on Node.js" test. Escape-attempt, timeout, memory-limit, maxToolCalls, and concurrency behavior are unverified in CI.

I'm unsure whether we should add in bun to the main test suite. I'm thinking that you'll need a second CI workflow to run the bun tests separately. We'll need core approval to add that in.

id: 'quickjs-bun',
name: 'QuickJS Bun',
description: 'Native QuickJS engine (requires running the server with Bun)',
available: true,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The new quickjs-bun VM option is marked available: true unconditionally. When the example server runs under Node (the default pnpm workflow), selecting it fails at createContext with a runtime error. The description warns about Bun, but the UI still presents it as available—unlike the Node driver path in create-isolate-driver.ts, which falls back to QuickJS on failure.

Suggestion: Gate available on server runtime (e.g. env flag or server-side probe), or add a quickjs-bun fallback/error message in create-isolate-driver.ts similar to the Node driver pattern.

*
* 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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] "fully isolated ... from the host" overstates the boundary. Per-context heaps and scope isolation hold and are well-tested, but this driver runs QuickJS compiled by TinyCC and called through bun:ffidirectly in the host process address space, with no sandbox boundary. The memory "limit" is QuickJS's internal accounting (JS_SetMemoryLimit), not a hardware/OS/VM wall. Unlike Node isolated-vm (separate V8 isolate) or the WASM driver (WebAssembly linear-memory sandbox), a memory-safety bug in QuickJS or a mishandled FFI handle here is a host-process compromise, not a contained fault.

Suggestion: keep the accurate per-context-independence framing, but reword to state the trade-off honestly (native FFI trades WASM's/V8's containment boundary for speed — fits trusted-ish or otherwise-contained deployments, weaker fit where the sandbox wall itself is the security requirement). Same applies to the README's "with the same sandboxing guarantees" line — the guarantees are not the same, only the engine and scope surface are.

// the handle dumps to something else.
let argsJson = '{}'
try {
const dumped = vm.dump(argsHandle ?? vm.undefined)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] When vm.dump throws here (sandbox heap exhausted while materializing the args string), the catch swallows the JSException and the code falls through with argsJson = '{}' — and then the async IIFE below still calls binding.execute({}). So a host tool runs with empty args instead of the caller's real args (a real side effect with the wrong inputs for something like readFile/deleteRecords), and the underlying OOM is never surfaced or classified.

The "dump returned a non-string" case degrading to {} is fine (the wrapper guarantees a JSON string). But the throw case should not invoke the binding — record it via hostSettleError ??= this.toNormalizedError(error) (or settle the sandbox promise with the classified error) so the execution loop surfaces it and releases the VM if it was fatal.

return this.disposedResult()
}

this.logs.length = 0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] Reused-context state bleed: execute() resets host-side counters here but nothing drains the QuickJS job queue between runs. After a timeout/abort, the previous program's un-run promise reactions stay queued; on the next execute() the loop's executePendingJob() runs those stale jobs first — attributing their wall-clock, tool-call budget, and console.* output to the new execution.

Latent today (the main consumer creates a fresh context per call and disposes it), but this class explicitly supports multi-execute — that's the whole point of the execQueue serialization + per-run reset. Either drain/reset the VM job queue on entry, or document that a context is single-execute.

* dropped for the rest of the execution.
*/
const MAX_LOG_ENTRIES = 10_000
const MAX_LOG_BYTES = 1_000_000

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nit] MAX_LOG_BYTES is compared against msg.length in pushLog, but String.length is UTF-16 code units, not bytes — while the constant name and the doc comment say "bytes". Worst-case multi-byte content lets the host log buffer reach ~3–4 MB before truncation. Still bounded, so low risk. Rename to MAX_LOG_CHARS, or measure real byte length, to match the stated intent.

driver = createQuickJSIsolateDriver()
break
}
case 'quickjs-bun': {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[suggestion] The quickjs-bun driver case added here is effectively unreachable end-to-end, which makes the new sidebar option impossible to actually exercise:

  1. The server never passes the selected VM through. All nine server call sites hardcode createIsolateDriver('node') (api.execute-prompt.ts:31, api.product-codemode.ts:63, api.database-demo.ts, api.banking-demo.ts, api.reports.ts, api.report-event.ts, api.structured-output.ts, reports/*.ts). Selecting "QuickJS Bun" (or anything) in ToolSidebar changes nothing — the server always builds the Node isolated-vm driver.
  2. There's no way to run the server under Bun. dev is vite dev (Node), there's no bun script, and the README has no Bun instructions (its whole troubleshooting section is about isolated-vm on Node). But this driver requires the server process itself to be Bun.

So as shipped, the option is cosmetic. To make it usable: thread the selected IsolateVM from the request into getDriver() (keyed cache per VM), add a "dev:bun" script that runs the server under Bun, and document the Bun run path in the README. Otherwise the sidebar entry should be removed until the example can actually run it.

jherr and others added 3 commits August 2, 2026 09:43
Resolve conflicts in knip.json (keep both ai-memory and
ai-isolate-quickjs-bun entries) and examples/ts-code-mode-web/vite.config.ts
(adopt main's SERVER_ONLY_NATIVE/nitro() setup, add quickjs-bun).

Fix ai-isolate-quickjs-bun build under main's new tsconfig.base.json
`types: ["node"]` allowlist by re-adding "bun" to the package's
compilerOptions.types so the Bun global and bun:ffi resolve.
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
@jherr
jherr requested a review from a team as a code owner August 2, 2026 21:04
@jherr

jherr commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

@tombeckenham — I went back through all of your open threads on this branch and mapped each to where it now lands, to make a re-review easier. The branch is up to date with main (trivial merge, 4d044f10) and all reporting checks are green; the substantive Bun test suite is green locally (52 pass / 1 skip). Details below, grouped by kind.

Substantive correctness fixes (in code + covered by tests)

1. Fatal OOM/stack objects misclassified → exhausted VM reused (isolate-context.ts:518:620 region, r3528366410)
The recovered-object branch no longer returns early before the fatal-limit heuristics. It now recovers the user-facing name/message and routes them back through normalizeError so classification is uniform (isolate-context.ts:633). A real InternalError: out of memory object (not just the bare-null OOM) is now classified fatal, so fail() releases the VM instead of driving it on. This also fixes the timeout-parity nit you flagged (a sync evalCode interrupt now surfaces as TimeoutError, not InternalError: interrupted).

2. vm.dump throw ran the host tool with {} (isolate-context.ts:518, r3528366562)
The throw case no longer falls through to binding.execute({}). It records the classified error via this.hostSettleError ??= this.toNormalizedError(error) (:529), disposes the deferred, and settles — so the OOM is surfaced and the VM released rather than a host side effect running with the wrong args. The benign "dump returned a non-string" degrade-to-{} path is unchanged, as you noted it should be.

3. Reused-context job-queue bleed after timeout/abort (isolate-context.ts:189, r3528366636)
execute() now drains the QuickJS job queue on entry before the run starts: drainStaleJobs() (:186/:561), a bounded loop (MAX_STALE_JOB_DRAIN = 10_000) that runs out any leftover promise reactions from a prior program so their wall-clock, tool-call budget, and console.* output can't be attributed to the new execution.
Regression test added"does not leak a timed-out run's queued jobs into the next execution": a first run queues Promise.resolve().then(() => console.log('STALE_FROM_FIRST_RUN')) then busy-loops into a timeout; the test asserts the second run's logs do not contain the stale marker. Verified it fails without the drain (["SECOND_RUN","STALE_FROM_FIRST_RUN"]) and passes with it.

CI for the Bun-gated suite (package.json:42, r3527371410)

You were right that the ~880-line suite only ran the "rejects on Node.js" case under the Node PR job. I've added a separate, path-filtered Bun Tests workflow (.github/workflows/bun-test.yml): it installs Bun, builds the package's workspace deps via nx (@tanstack/ai-code-mode is a runtime peer dep imported as wrapCode), and runs test:bun. It's path-filtered to the package + ai-code-mode + the workflow file so it stays off unrelated PRs (keeping the runner-cost impact you raised contained). It currently shows action_required — that's the standard maintainer-approval gate on all workflows for a fork PR, not a config issue.

This is the one item I'd flag for a core decision: whether to (a) keep it as an informational check, or (b) make it a required status check for merge. I've wired it so either is a one-line settings change; I didn't presume to make it required.

Example sidebar option (ToolSidebar.tsx:111 + create-isolate-driver.ts:22, r3527371416, r3528379467)

Being straight with you here: I took the minimal, honest-gating route rather than the full end-to-end wiring. create-isolate-driver.ts now falls back to the WASM QuickJS driver with a warning when the server isn't running under Bun, so selecting the option degrades gracefully instead of failing at createContext, and the sidebar label says so.

I did not thread the selected IsolateVM through the nine hardcoded createIsolateDriver('node') server call sites, nor add a dev:bun script — so under the default pnpm dev (Node) the option still resolves to WASM rather than the native Bun driver end-to-end. That's a deliberate scope call to keep this PR focused on the driver itself. If you'd rather the example not present an option it can't fully exercise, I'm happy to either (a) fully wire it (per-VM getDriver() cache + dev:bun + README run path) as a follow-up, or (b) remove the sidebar entry until the example can run under Bun — your call.

Docs, metadata, wording (all fixed)

  • Frontmatter / formatting (code-mode-isolates.md, r3527371395, r3527371402): restored the closing ---; fixed `external_*` backticks.
  • maxToolCalls documented (r3527371406): added a row to the Options table (code-mode-isolates.md:148) and a mention in "How it works".
  • Supply-chain / pin (r3527371418): quickjs-bun pinned to exact 0.1.2 with a compatibility + Windows QUICKJS_BUN_NATIVE_LIBRARY note in the README and changeset.
  • Package metadata (r3527371422): added author/homepage/bugs/funding to match sibling drivers.
  • "fully isolated from the host" overstatement (r3528366488): reworded in both the driver doc-comment and README — it now states plainly that native FFI QuickJS runs in the host process address space (no OS/VM/WASM boundary; JS_SetMemoryLimit is internal accounting), and points to ai-isolate-node/WASM for a hard sandbox wall.
  • MAX_LOG_BYTES misnamed (r3528366746): renamed MAX_LOG_CHARS (isolate-context.ts:55) to match that it bounds UTF-16 code units.

Net: all the correctness threads are fixed with tests, the docs/metadata/wording nits are done, and the Bun suite now has CI. The two things that need your input rather than more code are (1) whether the Bun workflow should be a required check (needs core sign-off) and (2) the example-wiring scope call above. If those look reasonable, the last blocker is this review's changes-requested state — a re-review would clear it.


Attribution: this analysis, the review-response fixes, and this CI workflow were done by Claude (Claude Code) working in Jack Herrington's (@jherr) environment. The original driver is @lithdew's. Jack has not personally reviewed this line-by-line.

jherr and others added 3 commits August 2, 2026 14:33
… 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) <noreply@anthropic.com>
…emos

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).
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.

@tombeckenham tombeckenham left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've created a way to run the ts-code-mode-web example with bun. Run pnpm dev:bun or bun dev:bun.

@AlemTuzlak
AlemTuzlak merged commit b2b82e4 into TanStack:main Aug 7, 2026
10 checks passed
@github-actions github-actions Bot mentioned this pull request Aug 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants