Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
73debce
chore(engine): vendor quickjs-ng v0.17.0 for code_execution's engine
Oct 18, 2026
94afb13
feat(code-execution): QuickJS-ng engine module and the binding a scri…
Oct 18, 2026
579bb2c
ci(engine): check the committed module against a rebuild of its sources
Oct 18, 2026
fc613f5
feat(code-execution): a result that doesn't fit takes the child's own…
Oct 18, 2026
32f3350
test(code-execution): both ends of the memory range load and run
Oct 18, 2026
437312b
fix(code-execution): a call's entry frees both functions if it can't …
Oct 19, 2026
8f9f427
fix(engine): vendor.sh replaces the copy only once it's whole; CI che…
Oct 19, 2026
bcd3041
fix(code-execution): an ending is serialized once; a thrown object wi…
Oct 19, 2026
7f04c8f
fix(code-execution): the engine holds results to a depth, ends a run …
Oct 19, 2026
ae0628f
fix(engine): vendor.sh applies patches and refuses over files it woul…
Oct 19, 2026
dec7786
fix(code-execution): an ending is read once even when reading fails, …
Oct 19, 2026
53c7413
fix(engine): vendor.sh refuses patches it would skip or drop, orders …
Oct 19, 2026
68e02b0
fix(code-execution): an ending past the engine's limit names the limi…
Oct 19, 2026
50b55da
fix(code-execution): QuickJS's stack limit is 192 KiB at most, so dee…
Oct 19, 2026
f66b1e0
fix(code-execution): a result's copy is freed once parsed, a stop aft…
Oct 19, 2026
115714e
fix(engine): vendor.sh sees every entry in engine/patches, checks eac…
Oct 19, 2026
5c691cb
fix(code-execution): an exception that escapes the script's promises …
Oct 19, 2026
c492521
fix(engine): vendor.sh refuses a listed file the patches change in ki…
Oct 19, 2026
1e3e806
fix(code-execution): a settle whose resolve call was stopped on its w…
Oct 19, 2026
efc9c49
fix(engine): vendor.sh cleans up whenever it exits, puts the copy bac…
Oct 19, 2026
297b8e7
fix(code-execution): a stop stays a stop, a throw as the run starts i…
Oct 19, 2026
bd62c6f
fix(engine): vendor.sh's restore command puts back what's committed, …
Oct 19, 2026
6f9c7bf
fix(code-execution): once a stop has come, a failure with no message …
Oct 19, 2026
0fd437a
fix(engine): vendor.sh's restore drops what the commit doesn't hold, …
Oct 19, 2026
6062d65
fix(code-execution): a stop ends a failure's read, and nothing after …
Oct 19, 2026
34f6bed
fix(engine): vendor.sh's leftover refusal says what the entries are a…
Oct 19, 2026
d4adf51
fix(code-execution): the script's code QuickJS runs after a stop can'…
Oct 19, 2026
4e78a43
Merge main into the engine's branch
Oct 20, 2026
d8e8971
feat(tools): ToolResult.refused says the dispatcher refused a call be…
Oct 19, 2026
987f63b
feat(code-execution): the isolate, a script's own process on the engi…
Oct 19, 2026
9a385b4
feat(code-execution)!: code_execution runs JavaScript in the isolate,…
Oct 19, 2026
1bf16d8
test(code-execution): allow macOS's own variable in the child's envir…
Oct 20, 2026
115c031
fix(code-execution): a return keeps its output's ends within the agen…
Oct 20, 2026
ebdbc5c
fix(code-execution): a stop of an ended script marks its ending befor…
Oct 20, 2026
1ed704c
build(code-execution): build the engine from quickjs-ng's pinned rele…
Oct 20, 2026
294e26d
fix(code-execution): an orphan whose script keeps a stop from landing…
Oct 20, 2026
64a5f60
fix(code-execution): a failure's part that a stop's code gives late i…
Oct 20, 2026
53e5000
fix(code-execution): the engine's refusals no longer say the script h…
Oct 20, 2026
52b611d
docs(code-execution): give the measured depths where a script's neste…
Oct 20, 2026
e88afe4
docs(code-execution): the changelog says where a return's output ends…
Oct 20, 2026
6555fb5
test(code-execution): take a yielded run's completion and its script'…
Oct 20, 2026
a0fe88a
fix(code-execution): the puppet stores a call's result under the cap …
Oct 20, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,51 @@ jobs:

- name: Packed release installs on its own
run: node scripts/pack-installs-alone.mjs

# create() refuses codeExecution on a Bun older than 1.4.0, reading the
# Bun it runs under: the settings file checks that here, under the Bun
# connectome-host's CI runs, which create() refuses.
- name: code_execution's settings under Bun 1.3.14, which create() refuses
run: bun test --timeout 60000 ./test/code-execution-isolate-settings.test.ts

# A Bun host runs a script's process on Bun too, from the package's source,
# so code_execution's files run there as well as under Node, each file in a
# process of its own, on both platforms, since the child's stdio and fd 3
# are where runtimes and platforms differ. code_execution needs Bun 1.4.0 or
# later (create() refuses it on an older one), so they run under 1.4.0, the
# oldest release it admits, and under 1.4.3.
code-execution-bun:
name: code_execution under Bun (${{ matrix.os }}, bun ${{ matrix.bun }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
bun: [1.4.0, 1.4.3]

steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false

- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 24

- name: Install dependencies
run: npm install --ignore-scripts

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version: ${{ matrix.bun }}

- name: code_execution's tests under Bun, from source
run: |
for f in test/code-execution-isolate-*.test.ts test/code-execution-engine.test.ts test/code-execution.test.ts; do
echo "::group::$f"
bun test --timeout 60000 "./$f" || exit 1
echo "::endgroup::"
done
52 changes: 52 additions & 0 deletions .github/workflows/engine.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Engine

# code_execution's engine module, src/code-execution/quickjs.wasm, is committed beside what it's
# built from: engine/build.sh, which fetches the pinned quickjs-ng release and applies the patches
# in engine/patches, and engine/interface.c, built with a pinned toolchain. This rebuilds the
# module and fails unless the bytes are the committed ones, so the module can't drift from its
# sources in either direction. It runs only when one of them changes, since the toolchain is a
# 143 MB download.
on:
push:
branches:
- main
paths:
- 'engine/**'
- 'src/code-execution/quickjs.wasm'
- '.github/workflows/engine.yml'
pull_request:
paths:
- 'engine/**'
- 'src/code-execution/quickjs.wasm'
- '.github/workflows/engine.yml'

permissions:
contents: read

jobs:
rebuild:
name: The committed engine module is what its sources build
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false

# binaryen's wasm-opt comes from npm and runs under Node.
- name: Setup Node.js
uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version: 24

- name: Rebuild the module
run: engine/build.sh "$RUNNER_TEMP/quickjs.wasm"

- name: Compare it with the committed module
run: |
sha256sum "$RUNNER_TEMP/quickjs.wasm" src/code-execution/quickjs.wasm
if ! cmp -s "$RUNNER_TEMP/quickjs.wasm" src/code-execution/quickjs.wasm; then
echo "::error::src/code-execution/quickjs.wasm isn't what engine/build.sh builds from these sources. Rebuild it with engine/build.sh and commit it with them."
exit 1
fi
13 changes: 13 additions & 0 deletions changelog.d/code-execution-isolate.breaking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
- **Agents with `code_execution`, and whoever writes their prompts:** scripts are JavaScript now, not Python, and each runs in a process of its own, in QuickJS compiled to WebAssembly, on the host's own runtime (Node or Bun). A script reaches nothing but its agent's tools: no network, files, timers, packages or environment.
- **Calling tools:** by exact name, `await tools["workspace--read"]({ path: "work/notes.md" })`, one argument, an object. The mangled Python names (`workspace__read`) are gone. `ALL_TOOLS` lists each tool's name, description and `inputSchema`. A name that isn't one of the agent's tools throws at once in the script, naming close matches, and nothing is sent. At most 16 of a script's calls are out at once: one past them waits until an earlier one's answer has reached the script, and is never refused, so a `Promise.all` over many calls is paced rather than refused. Calls that timed out count against the 16 the host holds until their tools end, so a call made while 16 of those still run waits, and one that waits past its timeout gets a `TimeoutError` saying it was never sent. A call whose tool gives no result within `toolCallTimeoutMs` and 30 s more, which the host stops waiting for then, keeps its place until the script ends, since its tool may still be running. A call's arguments may be up to 4 MiB of JSON holding at most 100,000 values: past either, the call throws `ArgumentsTooLarge`, and nothing is sent.
- **Results:** every tool answers one shape, `{ content, structuredContent, isError }`: `content` is what the model would read (a structured result's blocks, otherwise one text block), `structuredContent` the tool's own structured content or `null`, and `isError` true when the tool failed. A structured result's blocks are as its tool sent them, so images survive. A call refused before its tool ran (`ToolResult.refused`) throws an `Error` instead, `ToolRefused`, and nothing was done. A result that can't be converted to JSON, such as a cyclic object from an in-process module, comes as a failure in the same shape, its text naming the tool and saying the call completed. Before, a script got a string, the tool's text or `Error: …` for a failure, carrying `.is_error` and `.structured`, and a result with structured content got `{ content, structuredContent, isError, error? }`, with a failure's text in `error`.
- **`code_execution`'s own result** is an error (`isError`) when its script failed, crashed or was stopped at its limit, or the call was refused, with the text the model reads as its `error`; otherwise that text is its `data`, as a list of one text block. It carries no `structured`. Before, a script's result was a success whatever its `return_code`, and its `data` an object of `stdout`, `stderr`, `return_code` and, when the call's limit was capped, `time_limit_note`.
- **One kind of script:** a script runs in its agent's turn; past `yield_time_ms` (default 30 s, at most `maxYieldMs`, 10 min by default) it yields with its output so far and keeps running. Then `{"action": "wait", "script_id": …}` waits again, `{"action": "stop", …}` stops it and `{"action": "list"}` lists the agent's scripts, each with the wakes the host delivered for it. Each return shows the output since the previous one, its first and last 8,000 characters, cut alike to fit the agent's inline cap for tool results where the rest of the return leaves them room. The primary agent may leave a script running past its turn: `wake_agent()` is open to any running script, and a left script that fails, crashes or reaches its limit (24 h) wakes its agent; any other agent's scripts stop when its turn ends. The background log file is gone, its place taken by each return's output.
- **Refused inputs, each naming its replacement:** `background` (every script can yield and keep running), `time_limit_ms` (`yield_time_ms`) and the `cancel` action (`stop`).
- **Nothing is kept between scripts:** each starts fresh, so globals no longer persist until an idle reclaim. A script writes what must last with a tool.
- **Large results:** a result too large for the script's memory (past a third of it, or one that doesn't fit when it's parsed), or nested more than 2,000 deep, isn't passed to the script: the call's `ResultTooLarge` error says the tool ran, and a refusal the script can't take stays a refusal, with its start. Any other result reaches the script whole. Before, a text result was cut at 5,000,000 characters, and a structured one past that length was saved to the workspace, the script getting `{ isError, oversized: { chars, savedTo } }` in its place, or, when it couldn't be saved, a failure saying the tool may have completed.
- **Unchanged:** a script's calls go through the ordinary dispatcher as its agent's, with its allowlist and every gate; intermediate results stay out of the model's context; a call's `endTurn` ends the turn at the `code_execution` result watching the script; `wake_agent()` keeps its spacing and cap; RFC-007 observers see a script's calls as before.
- **Wakes:** a `wake_agent()` still waiting for its spacing when its script ends is dropped, however the script ended. Before, only a cancel dropped it, and a script that finished or died still woke its agent after the wait, saying it was still running. New: a wake's payload may be up to 64 KiB of JSON, and past it `wake_agent()` throws `WakeRefused`, with nothing sent.
- **Operators with `codeExecution` enabled:** the host no longer needs a Python interpreter. On Bun it needs Bun 1.4.0 or later: `create()` refuses `codeExecution` on an older Bun, since Bun 1.3.14 loses, at some starts, the pipe a script's process answers on, which crashes the script. `create()` refuses each setting the Python runner took, naming what to do: `pythonPath`, `idleReclaimMs`, `env` and `inheritEnv` are gone, since a script has no interpreter, nothing to reclaim and no environment, and `scriptTimeoutMs`, `maxScriptTimeoutMs`, `backgroundMaxLifetimeMs` and `maxBackgroundScripts` become `yieldMs`, `maxYieldMs`, `scriptLifetimeMs` and `maxRunningScripts`. New: `maxScriptChildren` (default 16), the script processes a host runs at once, and `memoryMaxBytes` (default 128 MiB, 4 MiB to 2 GiB in 64 KiB pages), each script's budget. A script's process stays within about its runtime's base plus 2.7 times `memoryMaxBytes`, plus what its calls take as they go out, up to about 200 MiB under Node and 410 MiB under Bun, and the host holds at most about 6 times `memoryMaxBytes`, plus 653 MiB, plus 128 KiB for each wake `maxWakesPerScript` allows, for a script, about 1.4 GiB at the defaults, for its 16 calls out at once, timed-out ones whose tools still run included, up to 16 more waiting for those, their answers, the frame from its process being read and its waiting wakes: size a host by both. `toolCallTimeoutMs`, `wakeMinIntervalMs` and `maxWakesPerScript` are unchanged. A connectome-host recipe that sets `codeExecution.pythonPath`, `env`, `scriptTimeoutMs` or `idleReclaimMs` is refused the same way.
- **What it promises:** `code_execution` adds nothing to what an agent's tools allow, provided the host runtime's WebAssembly engine, Bun's or Node's, and our glue are right. It claims no containment beyond that.
- **Library users:** `PyRunner`, `buildInjectedTools`, `PYTHON_RUNTIME_SOURCE` and the types `InjectedTool`, `ExecResult`, `PyRunnerOptions`, `ScriptToolCallHandler` and `ScriptToolResult` are no longer exported. `CodeExecutionConfig` has the new keys, and `buildCodeExecutionToolDefinition()` takes them, as `create()` does, where it took the Python runner's options. `HostModeStatus.backgroundScripts` counts scripts left running after a yield. A script's wake enters the agent's context with `metadata.source` `code-execution-script` (was `background-script`), tagged `script:wake` as before.
1 change: 0 additions & 1 deletion changelog.d/f55-code-execution-text.breaking.md

This file was deleted.

1 change: 0 additions & 1 deletion changelog.d/f55-code-execution-text.changed.md

This file was deleted.

1 change: 0 additions & 1 deletion changelog.d/fix-code-execution-output-to-host.breaking.md

This file was deleted.

Loading
Loading