Code Mode: One Program Beats Many Tool Rounds
Announcement calls it PTC, source calls it code mode: worker sandbox isolation, message protocol, and double bookkeeping
THE QUESTION THIS PAGE ANSWERS
ANSWER FIRSTWhat is the key idea behind “Code Mode: One Program Beats Many Tool Rounds”?
Announcement calls it PTC, source calls it code mode: worker sandbox isolation, message protocol, and double bookkeeping
Make the claim earn its place. Use this page as a decision aid, not a definition to memorize. Connect the idea to one real task, one observable result, and one failure that would change your mind.
Write one question you could answer with evidence after trying this idea.
A conclusion that sounds complete but leaves the key assumption untested.
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?
The handoffs inside “Try it first · One program vs multi-round calls”
“First, clear up the names.” shows that an Agent is not defined by the model alone. Each handoff between model, context, tools, state, permissions, and people affects both progress and recovery.
Write the state before adding capability
Starting from “Native tool calling is like handing the model a remote.”, split the workflow into starting state, next action, tool result, state update, and stop condition. Debugging then means finding the first lost piece of information or authority instead of saying vaguely that the model “got worse”.
A happy path is not reliability
Use “Program A is a sync hot loop while (true) {} ;” to replay one successful and one failed run. Record the context, tool result, and owner at each turn; the workflow is maintainable when a second person can follow it without the original builder.
From “Try it first · One program vs multi-round calls” to “Idea 1 · Let the model write a program — don’t make it a remote control”
“Try it first · One program vs multi-round calls” grounds the problem in “Same task: read 3 log files, aggregate stats, write 1 report Play Step Reset Traditional tool calls 0 sampling rounds 0 k tokens Model · sampling round 1 Read the first file logs/a.log first. read_file({ path…”. “Idea 1 · Let the model write a program — don’t make it a remote control” then moves it toward “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). Sear…”. Together, they show that the lesson is not just a conclusion to remember, but a claim with conditions.
Carry the judgment into the next situation
When analyzing an Agent, trace state, action, tool result, and next step in order. Each handoff should explain where information came from, who confirmed it, and where failure stops.
- “Try it first · One program vs multi-round calls”: Same task: read 3 log files, aggregate stats, write 1 report Play Step Reset Traditional tool calls 0 sampling rounds 0 k tokens Model · sampling round 1 Read the first file logs/a.log first. read_file({ path…
- “Idea 1 · Let the model write a program — don’t make it a remote control”: 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). Sear…
- “The closing point”: DSH’s approach is three lines of defense, plus one permission rule
The final “The closing point” brings the discussion to “DSH’s approach is three lines of defense, plus one permission rule”. The useful thing to carry forward is knowing which judgments must be revisited when input, scale, or risk changes.
I turned one judgment from this article into a small experiment I could run today. Knowing what to observe next is more useful than simply remembering the conclusion.
After reading this, I first looked for the conditions behind the idea instead of copying the method into a project. That order made the later trade-offs much clearer.
When this judgment reaches real work, which constraint should be added first? I am curious which step matters most between reading and the first practical attempt.
No discussion on this article yet.