Code Mode: One Program Beats Many Tool Rounds
The model writes a small program; run_code sends it into a worker sandbox; five tool calls finish in one round. This lesson covers the two ideas behind that design.
First, clear up the names. The product announcement calls it PTC — programmatic tool calling — and the preset metadata even says name: PTC 模式 (Source: apps/cli/config/agent-presets/code/preset.yml line 1). Search the source for PTC and you’ll find nothing. Internally it’s code mode end to end: config mode: code, tool name run_code. Two names, one mechanism.
Native tool calling is like handing the model a remote. Press once, read one file; result comes back, press again. Every press is a full sampling round — the model must re-read the growing context before deciding the next step. Reading 3 log files and writing a report is already 5 rounds; scale to 50 files and both budget and patience burn out. The cost bottleneck isn’t the tools — it’s the one-sample-per-step rhythm.
In one sampling round, the model writes a small TypeScript program and calls await tools.name(args) as many times as it wants — loops and branches fine. The program finishes the work in the sandbox; intermediates stay in sandbox variables; only print and return content go back to the model. The tool description says it plainly: “Only what you print or return comes back — curate it.” (Source: packages/core/tools/src/code-mode.ts line 52).
That line has a source: the design-intent comment at the top of the preset file says “five round trips becomes one” — this lesson’s title comes from there:
# The `code` agent preset: the standard coding agent, presented as Code Mode.
#
# Everything in `standard` is here unchanged. What is added is the `tool-presentation`
# row: instead of one tool call per action, the model writes a TypeScript
# program against a generated SDK and `run_code` executes it, so a sequence
# that would be five round trips becomes one.
deepseek-harness-master repo; verified against apps/cli/config/agent-presets/code/agent.cordis.yml, verified on 2026-08-13. Code blocks keep the original source text.The second half of the comment plants a seed: this preset only changes how tools are presented — the registry itself stays with the host (Source: same file lines 8–11). Idea 2’s permission follow-through starts from that arrangement.
This is an old lesson from decades of network programming: round trips are expensive, batching is cheap. Databases have bulk writes; RPC frameworks accumulate batches. One model sampling round costs far more than one network hop, so collapsing N trips into one pays even bigger. Rewrite DSH in another language someday — the same math still holds.
Idea 1 leaves a premise unsolved: the sandbox runs code the model just wrote — nobody reviewed a line. Run it in the host process for convenience, and failure modes are plentiful: scoop API keys from env vars; a while (true) eats all memory and takes the host down with it; the program can forge messages and impersonate tool results to fool upper layers. So from day one the host must assume the other side means harm — once that stance holds, the rest is engineering.
DSH’s approach is three lines of defense, plus one permission rule.
First: resources are welded shut. Every run spins up a fresh worker and throws it away. Launch flags slam the doors: env vars cleared so the program gets no host credentials; inherited loader flags cut off; heap limit welded — if the program blows the heap, the worker exits (Source: packages/code-runtime/code-runtime-worker-thread/src/index.ts lines 378–387).
Second: communication accepts only structured messages. Host and worker share one message port — no shared memory. Upstream messages are only call, log, output-limit, and done. Every inbound message is shape-checked first, then rebuilt field by field into a clean copy; garbage is silently dropped (Source: same file lines 142–165). The worker-side tools namespace is built with a null prototype, so forged names like __proto__ touch nothing (Source: bootstrap.ts lines 324–326).
Third: time and bytes are double-booked — numbers the worker self-reports don’t count. On time, two ledgers: computeMs polls the worker’s measured busy time so hot loops can’t hide, and idle waits on slow tools aren’t unfairly billed; maxWallMs is the backstop — either budget hits its limit and forces kill (Source: index.ts lines 534–545). On bytes, the worker pre-checks before send; the host re-books with OutputLedger on receive (Source: index.ts lines 169–229). Nobody gets away with reporting a pretty number alone.
Then the permission rule: once the program is in the sandbox, approval doesn’t loosen an inch. Every await tools.xxx is wrapped by the host as a sub-dispatch, carrying the parent call’s token, through the same pre-execute approval cascade as native mode. Sub-call ids look like callId:code:n, traced end to end in events (Source: packages/core/tools/src/code-mode.ts lines 545, 477, 470). Tools the program can bind are exactly those declared in the System Prompt; restricted tools simply vanish from the list (Source: same file lines 601–608). Conversely, under mode: code, trying to bypass run_code and fire native calls is rejected as UNKNOWN_TOOL before the policy pipeline (Source: docs/subsystems/tools.zh.md). The entrance narrowed; the permission door didn’t change.
DSH is clear-eyed about this isolation. The README opens with:
“This is isolation, not a security boundary: its trust stance is intentionally bash-equivalent… but it offers isolation bash doesn’t: a separate isolate, empty env, heap ceiling, and forced kill.”
Source: packages/code-runtime/code-runtime-worker-thread/README.zh.md; ellipsis marks an original citation omissionA distrust boundary is a universal shape in security design — it has nothing to do with worker_threads as a specific tech. Swap in containers, a V8 isolate, or another language’s subprocess and you still do these four: spin a clean environment, cap resources, talk over a narrow interface with per-message validation, and keep ledgers on both ends. Browsers vs pages, OSes vs processes — same idea. Model code is just a new name on the list; the treatment stays the same.
Claude Code
No equivalent. The bash tool is a general escape hatch — the model can write a script and run it — but APIs like Read and Edit aren’t exposed as programmable bindings to scripts, and actions inside a script don’t go through each tool’s own pipeline. Anthropic’s official blog “Code execution with MCP” proposes the same idea; as of the verification date, Claude Code doesn’t ship a built-in run_code-class mechanism.
Grok Build
No such mechanism, based on public evidence. A full-repo search of the local grok-build-main tree for run_code and code mode only hits a telemetry event name that collides literally. Its tool system combines native calls with toolset presets — no channel for the model to write a program that the sandbox orchestrates via tool APIs.
Codex CLI & Cloudflare
The idea rhymes, but this lesson didn’t verify either codebase — just the facts: DSH’s design notes explicitly cite Cloudflare’s blog; the core observation is that models write code better than they fire tool calls in a chain (Source: .agents/notes/implemented/feature/2026-06-15-code-mode.zh.md). Codex CLI has similar public discussion; again, idea only.
Two hostile programs — which budget does each hit?
Program A is a sync hot loop while (true) {}; program B is await new Promise(() => {}) that never resolves. Using Idea 2’s third defense, reason it out: are A and B killed by computeMs or maxWallMs? Why can’t A dodge billing by hanging a pending tool call?