DeepSeek Harness · Code Mode

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.

Course goalAfter reading, you can explain two things: why having the model write a program is faster and cheaper than firing tool calls one by one; and when model-written code runs in a sandbox, how the host protects itself and why permissions don’t loosen an inch.
Try it first · One program vs multi-round calls
Same task: read 3 log files, aggregate stats, write 1 report
Traditional tool calls0 sampling rounds0k tokens
Model · sampling round 1Read the first file logs/a.log first.
read_file({ path: "logs/a.log" })
412 lines returned in full — the whole chunk enters context
Model · sampling round 2Now read logs/b.log.
read_file({ path: "logs/b.log" })
398 lines returned in full — the whole chunk enters context
Model · sampling round 3Still need logs/c.log.
read_file({ path: "logs/c.log" })
441 lines returned in full — the whole chunk enters context
Model · sampling round 4Compute stats from the three full texts in context, write the report.
write_file({ path: "report.md", ... })
Write succeeded
Model · sampling round 5Aggregation done — emit the final answer.
Task done · 5 sampling rounds total; every round resends a growing context
Code Mode(PTC)0 sampling rounds0k tokens
Model · sampling round 1Write a program and let the sandbox orchestrate these five steps.
Sample program (5 lines, course example — not a source quote)
let total = 0, errors = 0 // 统计值,全程留在沙箱里for (const p of ['a.log', 'b.log', 'c.log']) { // 三个文件,循环里读 total += 统计(await tools.read_file({ path: p })) }await tools.write_file({ path: 'report.md', … }) // 写报告,照走审批return { total, errors } // 只有这一行回到模型
worker sandbox · runs in isolation; intermediate results never return to the model
await tools.read_file("logs/a.log")Forwarded by the host; still goes through the approval pipeline
await tools.read_file("logs/b.log")Result stays in a sandbox variable
await tools.read_file("logs/c.log")Result stays in a sandbox variable
Accumulate stats inside the loopPure compute, zero round trips
await tools.write_file("report.md")Still the same permission pipeline
return { total, errors }Only this value plus logs leave the sandbox
Returns once { total: 1251, errors: 17 } · still sampling round 1
Hit Play to see how the same task runs in both modes.
Sampling rounds: 5 vs 1Left: each action costs one sampling round; right: one sampling round writes the program, and the sandbox runs every action for it.
Throughput sketch: ~18.3k vs ~2.6kLeft: every round resends a growing context; right: context holds only the program and a one-shot return value.
Where intermediates goLeft: all three full file texts land in context; right: they stay in sandbox variables — only what you print or return comes back.
Teaching sketch: round counts and token figures are course estimates to show structural differences between the two modes; the program on the right is a 5-line course example, not a source quote.
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). 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.

What problem it solves

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.

What the idea is

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

Remote-control mode (native tool calls) Model Tool 5 round trips — each needs a sampling round, and context keeps growing Program mode (Code Mode) Model Only 1 sampling round worker sandbox Calls 5 tools in a loop; intermediates stay in variables One program Only print and return come back
Teaching diagram: the same five-step task — 5 round trips above, 1 below.
Five round trips become one.

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:

apps/cli/config/agent-presets/code/agent.cordis.ymllines 1–6 excerpt
# 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.
Source snapshot note: Based on the local 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.

Why it lasts

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 2 · When running someone else’s code, assume it’s hostile
What problem it solves

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.

What the idea is

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.

Model Sees only logs and return values Host process (inspect, approve, bookkeep) Inspect: strip types, check the binding allowlist Reject bad programs outright — worker never starts Approve: same tool pipeline as native mode Each sub-call carries the parent token; pre-execute first Bookkeep: cross-check time and bytes on both ends computeMs、maxWallMs、OutputLedger worker sandbox Program (model-written) Fresh every run, discarded after tools namespace Only allowlisted tools Resource ceilings Empty env, heap ceiling, forced kill Program reply call Logs + return value Only structured messages between host and worker; every inbound message is shape-checked then rebuilt field by field
Teaching diagram: nodes and edges explain source relationships; content is course-adapted.

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 omission
Why it lasts

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

Side-by-side · Who has an equivalent

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.

Based on local study materials search

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.

Based on local source search · 2026-08-13

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.

Source not verified · public accounts only
Classroom Exercise
01

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?

Takeaway: Code Mode (announcement name PTC) trades one sampling round for N round trips: the model writes a program, the sandbox runs it, intermediates never enter context. The sandbox is isolation — not a security boundary — and protects itself with empty env, heap ceiling, dual budgets, and double bookkeeping. Permissions don’t loosen because you’re in a sandbox; every sub-call still walks the same approval cascade.