Перейти к основному содержимому

Pi 1.0: The Minimal Harness, and the Codemode Trick That Fixes MCP Token Bloat

Продвинутый

Pi 1.0 shipped on 1 October 2026 and sat at the top of Hacker News the next morning with over 1,500 points. The pitch is almost aggressively boring: "a minimal, extensible agent harness that you can make your own." MIT licensed, TypeScript, one terminal command.

The pitch undersells it. Underneath the TUI, Pi ships the most complete answer anyone has published to the problem every agent harness hits at month three: MCP servers destroy your context window, and nobody wants to admit it. Pi spent a year publicly refusing to support MCP, then shipped it — by not putting the tools in the prompt at all. The mechanism is documented, measurable, and copyable whether or not you ever run Pi.

This page is about that mechanism, the numbers behind it, and the three design decisions in Pi that will change how you think about your own harness.

What you'll learn
  • Understand codemode: a QuickJS/WASM sandbox where nested tool calls never enter the model's context — only the script's output does
  • Learn Pi's four MCP exposure modes and why the default (codemode) declares nothing to the model at all
  • See the exact budgets: 3,000 tokens for inlined tool declarations, 20 KB before an MCP result gets gutted, 1 MiB of bash output a script can see that the model cannot
  • Copy the prompt-cache trick: append a changed system-prompt section to the transcript instead of mutating tool definitions
  • Know the deliberate omissions (no sub-agents, no plan mode, no per-call approval) and the trust gap that loads a cloned repo's AGENTS.md without asking

The one-paragraph version​

Pi is a model-agnostic terminal agent from Earendil (which also builds Radius and Lefos). The repo — github.com/earendil-works/pi — is a TypeScript monorepo of thirteen packages that you can use independently: a unified LLM API across ~30 providers, an agent loop, a TUI library, a standalone MCP client, a WASM sandbox, and an experimental durable runtime. It needs Node.js 22.19+. It deliberately ships without sub-agents and without plan mode, and it does not ask for approval before every tool call. What it ships instead is context engineering: codemode, a JavaScript sandbox the model writes scripts for, where tool calls happen outside the transcript and only the script's distilled output comes back.

Why this matters even if you never install it​

Every harness starts the same way. You add three MCP servers because they look useful. Each one dumps twenty tool definitions into the system prompt. Now every single model call in every session pays for sixty tool schemas, most of which will never be called, and your prompt cache breaks every time a server reconnects and the tool list shifts.

AILmanac has measured this before — see What MCP servers really cost you in tokens. Pi's answer is structurally different from "use fewer servers."

Codemode: the actual mechanism​

The codemode tool takes raw JavaScript source as its input — not JSON, not a fenced code block. Pi runs it as the body of an async function inside a QuickJS VM compiled to WebAssembly, so top-level await and return work.

The sandbox's only capability is calling injected tools. There are no timers, no fetch, no process, no require, no modules, no nested WebAssembly. (eval and Function work, but they only produce more code inside the same VM.) Remote image URLs are rejected; only base64 data: URLs and image blocks get through.

The consequence is the whole point:

Nested tool calls never enter the LLM context. Only the script's output and return value do.

So a script can fan out ten tool calls in parallel, filter 400 KB of results down to four lines, and the model sees four lines. The ten calls, their arguments, and their raw results are never transcript entries.

Guided walkthrough1 of 5
  1. Inside a script, every callable tool is a method on `tools`. Characters that are not valid in a JavaScript identifier become underscores, so the MCP tool `mcp__dev-radius__search` is reachable as `tools.mcp__dev_radius__search`. The model does not need these declared in its prompt to call them.

Enable codemode without any MCP server, then try it

# Pi activates codemode automatically when an MCP server with
# "codemode" exposure connects. To have it always on, add it to
# ~/.pi/agent/settings.json alongside the default tools:
#
#   { "defaultTools": ["+codemode"] }

pi

# Then ask for something that would normally flood the context, e.g.:
#   "Use codemode: run the test suite, and return only the names of
#    failing tests plus the first assertion line of each."
#
# The full test output never becomes a transcript entry.

The bash number worth remembering​

A direct bash call shows the model a result capped at 2,000 lines or 50 KB. Called from inside a codemode script, the same tool resolves to a structured value whose output field holds up to 1 MiB — and when it is longer than that, it keeps the first and last 512 KiB around an omission marker, sets truncated, and names the full output in full_output_path.

That is a 20× bigger window on command output for the script, which then decides what the model is allowed to see. Same for MCP: text results over 20 KB reach the model with their middle removed around a …N chars truncated… marker, while a codemode script receives the complete CallToolResult including structuredContent and isError.

The four exposure modes​

This is the table to copy into your own design notes. Every MCP server in Pi has an exposure, and it decides how the model can reach that server's tools. Each tool is registered as mcp__<server>__<tool>.

ExposureWhat the model seesUse it for
codemode (default)Nothing. Not declared, and not even listed in the codemode description. Scripts find it with searchTools(), describeTool() or ALL_TOOLS.General servers, especially when scripts should combine or filter calls
deferredNothing, until tool_search loads a match for the next model call. Then it is declared and stays declared on that branch.Large servers whose tools should be called directly after discovery
directDeclared like a built-in tool, and also callable from codemode.Small, frequently used tool sets
hiddenRegistered but unreachable.Servers or tools that should stay unavailable

toolExposure overrides the server-level setting per tool. Keys are exact tool names or * patterns; exact names beat patterns, and among patterns the first match wins. So a hidden server can expose exactly one tool, or a deferred server can keep its destructive tools unreachable:

Per-tool exposure in .pi/mcp.json

{
"mcpServers": {
  "github": {
    "exposure": "deferred",
    "toolExposure": {
      "search_code": "direct",
      "get_*": "codemode",
      "delete_*": "hidden"
    }
  }
}
}

Two details that are easy to miss. First, codemode and deferred tools are reachable through either indirect mechanism — scripts can call them and tool_search can load them — and codemode calls do not depend on the active tool set, so they still work after a branch switch, a resume, or a fork. Second, Pi warns once if neither codemode nor tool_search is active, because then your non-direct tools are simply uncallable.

The prompt-cache trick​

Here is the single most copyable idea on this page.

Pi connects every enabled MCP server in the background when a session starts. Servers with codemode or deferred tools get listed in an mcp_servers section of the system prompt — one line each, from the configured description or, once connected, from the server's own instructions.

That section changes as servers connect. A naive harness would rewrite the system prompt or the tool declarations, which invalidates every cached prefix behind it. Pi instead:

appends the new section to the conversation instead of changing tool declarations, so earlier messages stay cached.

A changed prefix costs you the whole cache. A changed suffix costs you nothing. Pi turns a prompt mutation into a transcript append, which is why the codemode tool description itself is also designed not to change while servers connect — deferred-exposure tools are deliberately left out of it.

The first prompt waits up to 10 seconds only for servers with direct tools, because those must be declared in the request. Everything else is waited for lazily: a script waits for the servers it names, and waits for all of them only if it calls searchTools() or reads ALL_TOOLS.

If you are building on Anthropic's API, the same reasoning applies to your own prefixes — see Prompt caching and Mid-conversation tool changes, which is exactly the capability Pi 1.0 calls "mid-conversation system messages."

The budgets, in one table​

ThingValueWhere
Inlined tool declarations in the codemode description3,000 estimated tokens (characters ÷ 4); 0 lists only namespacescodemode.inlineBudget
Default max_output_tokens for a script's output10,000codemode options line
searchTools() default result count8 (BM25 ranking)codemode globals
MCP text result before the middle is removed20 KBMCP tool results
bash output the model sees directly2,000 lines or 50 KBbuilt-in tool
bash output a script seesup to 1 MiB (first + last 512 KiB if longer)codemode
One store value / all store values262,144 / 1,048,576 characters of JSONcodemode store
Concurrent classifier or image-model calls per script4codemode models
MCP HTTP retries on 408/429/5xx2MCP transport
Tool-name limit providers accept64 characters of [A-Za-z0-9_-]pi-mcp

With codemode.mode set to "only" instead of the default "on", built-in and extension tools are hidden from the model entirely and listed in the codemode description instead — the model reaches everything through scripts. That is the most extreme version of the idea, and it is one settings key away.

Non-LLM models inside the sandbox​

The models global in a codemode script can run classifier and image models with the session's credentials. Chat models are listed but cannot be run from a script — that boundary is deliberate.

The classifier interface takes a state object plus a map of questions, and answers all of them in one call. Questions come in three shapes: choice (pick one label, with criteria mapping each label to its meaning), score (an ordered scale described lowest-first), and bool. Answers come back with probabilities and a confidence, not prose.

This is the same System One / decision-model category AILmanac covers in System One decision models: Jev, Kev and the open clones and Train your own decision model. Pi ships TypeSafe as a first-class provider, and the docs note that model IDs differ per provider — typesafe/jev-latest versus openrouter/typesafe/jev-1.13 — so you resolve them with models.getAvailableOfType("classifier") rather than hardcoding.

The pattern this unlocks: your frontier model writes a script that pulls 150 issues from an MCP server, classifies each one with a cheap calibrated classifier at four-way concurrency, and returns the six that matter. The 150 issues never touch the expensive model's context.

Watch out
  • `classify()` and `generateImages()` do not throw on provider errors — check `stopReason` and `errorMessage` yourself, or you will silently treat failures as results.
  • Classifier and image usage inside a script is added to the codemode tool result and counts toward session cost. Cheap per call does not mean free at 150 calls.
  • Image generation can take minutes. A short `timeout_ms` on a script that generates images will kill it mid-flight.

Cache warming, and the $0.05 rule​

Pi has a cacheWarming setting with three values — "off", "streaming" (the default, warming during active runs) and "idle" (also between runs).

The interesting part is the gate. Cache warming runs only when the model declares a cache lifetime and Pi estimates at least $0.05 in avoided cache-miss cost. Refresh usage counts toward session totals but does not enter model context. /session shows the next decision, and an extension can override it via a cache_warming_decision hook.

A hardcoded dollar threshold inside an open-source harness is an unusually honest piece of engineering: warming a cache is itself a billable request, so below some break-even it is a tax rather than an optimisation. If you have built your own warming loop and never computed that threshold, you are probably paying it. The economics are the same ones in Prompt caching economics.

Configuration, and what replaces versus appends​

User-level configuration lives in the agent directory, default ~/.pi/agent (override with PI_CODING_AGENT_DIR). Project configuration lives in .pi/ under the working directory and loads only after project trust is granted.

PathWhat it does
<agent-dir>/settings.jsonPreferences, defaults, resource paths, package declarations
<agent-dir>/mcp.json, models.json, auth.jsonGlobal MCP servers, compatible endpoints and overrides, saved credentials
<agent-dir>/SYSTEM.mdReplaces Pi's default system prompt
<agent-dir>/APPEND_SYSTEM.mdAdds to Pi's system prompt
<agent-dir>/AGENTS.md — or CLAUDE.mdUser instructions applied across working directories
<agent-dir>/extensions/, skills/, prompts/, themes/User resources; prompts/ become slash commands
.pi/SYSTEM.md, .pi/APPEND_SYSTEM.mdProject equivalents; the trusted project file wins over the agent-directory one — same-name files are not combined

Two things worth internalising. Pi reads CLAUDE.md as a context file, so an existing Claude Code repo is already configured for Pi — the portability point from Coding agent CLIs compared holds in both directions. And AGENTS.override.md replaces AGENTS.md or CLAUDE.md only in the same directory; it does not suppress context files inherited from the agent directory or from ancestor directories.

Built-in tools are read, bash, powershell, edit, write, grep, find and ls, of which only read, bash, edit and write are on by default. The built-in extensions are builtin:mcp, builtin:llama.cpp, builtin:codemode and builtin:tool-search — note that llama.cpp is a first-class built-in, so local weights are not an afterthought here.

The security model, stated plainly​

Pi's own security doc is refreshingly blunt, and the three sentences below are the ones to take seriously before you point it at a repo you did not write.

Watch out
  • Pi does not ask for approval before every tool call. Extensions, package installers and language servers run as child processes with the same permissions as the account that started Pi. Extensions are TypeScript modules loaded into the Pi process — they are not sandboxed, and the codemode VM protects the model's output path, not your machine.
  • Project trust is not a startup boundary. Pi reads the project `sessionDir` setting before it resolves trust, so declining trust cannot undo that lookup. A bare `.pi` directory does not trigger a trust prompt at all.
  • Context-file discovery does not require project trust. A cloned repo's `AGENTS.md` or `CLAUDE.md` is loaded into the system prompt without asking you — which is a prompt-injection surface, not a configuration convenience.
  • The docs say it outright: watching the transcript, using project trust, and reviewing changes do not create a security boundary. The boundary is a container, a VM, or a dedicated OS user.

If that last point is new to you, read Coding agents under attack and Securing local and hybrid agents next. One small mitigation Pi gets right: the npm install path is documented as npm install -g --ignore-scripts @earendil-works/pi-coding-agent, and Pi does not require dependency lifecycle scripts for a normal install.

What Pi deliberately does not have​

MissingPi's position
Sub-agentsNot shipped. Ask Pi to build what you want, or install a package.
Plan modeNot shipped.
Per-call approval promptsNot the model. Isolation is the boundary, not confirmation dialogs.
An official MCP SDK dependencypi-mcp is a standalone client with no dependency on the official SDK or other pi packages.
An open contribution funnelIssues and PRs from new contributors are closed automatically; maintainers review closed submissions daily. Read CONTRIBUTING.md first.

The omissions are the product. A harness that refuses features until they prove themselves is a harness whose context budget stays legible — which is the same argument as Why agents burn tokens, made by subtraction.

For the opposite trade-off — a harness that optimises turn count rather than per-turn context — compare Async-first agent harnesses, whose published Terminal-Bench 4.0 run finished in 28 turns against Pi's 44.

Pi Durable, in one breath​

@earendil-works/pi-durable is flagged experimental — the API changes without notice between releases — and it is the most architecturally interesting package in the repo. Conversations, model turns, tool calls and your own state are committed to storage before anything is shown; if the process dies mid-turn, reopening the storage picks up where it stopped.

The vocabulary is worth stealing even if you never install it: a conversation is a transcript of immutable entries (pi.user, pi.assistant, pi.tool-result, pi.system, pi.reset); documents are typed JSON state stored next to the transcript (pi.agent for model choices, pi.live for the running generation, pi.inbox for queued submissions, pi.usage for spend); a task is a durable state machine that checkpoints at every step, so pi.generation owns the pi.tool tasks of its tool calls and a restarted process continues from the last checkpoint. And the model sees only the entries from the newest reset onward — reset is a transcript entry, not a deletion.

Compare with Agent memory architectures and Long-running agent harnesses.

Should you switch?​

Honest answer: probably not wholesale, and that is fine — the ideas are the deliverable.

  • Try Pi if you want one harness across many providers, you run local weights (llama.cpp is built in), or your MCP surface has outgrown your context window.
  • Stay where you are if you depend on sub-agents, plan mode, or per-call approval prompts. Pi does not have them and is not apologetic about it.
  • Steal regardless: the exposure ladder (codemode / deferred / direct / hidden), the suffix-append trick for changing prompt sections, and a measured break-even threshold before warming any cache.

Check yourself

0/6
  1. In Pi's codemode, what reaches the model's context from a script that makes ten tool calls?
  2. A server is configured with the default `codemode` exposure. What does the model see in its prompt?
  3. When an MCP server connects mid-session and its summary line changes, what does Pi do to protect the prompt cache?
  4. Pi's cache warming is gated on a specific condition. Which one?
  5. A codemode script times out after completing seven of its ten tool calls. What is the state of the system?
  6. Which of these loads WITHOUT a project-trust prompt in Pi?
Нажмите Enter или пробел, чтобы перевернуть карточку. Используйте стрелки влево и вправо для перехода между карточками.Показан термин.
1 / 14

Sources & further reading​

Next​