More than a year ago I wrote a few posts here that recommended people not to load custom tools into their context (or MCP servers) but to just use more scripts. Most importantly I wrote that Code Is All You Need and I wrote about that MCP needs code. With Pi 1.0 we now added MCP support via Codemode which in some ways is a long time coming, but then also maybe somewhat surprising to some. So I want to share some updated thoughts on this blog on what this all means.
一年多前我在这里写过几篇,劝大家别把自定义工具(或 MCP server)装进上下文,直接多用脚本就好。最重要的两篇是《Code Is All You Need》和《MCP needs code》。现在 Pi 1.0 经由 Codemode 加入了 MCP 支持——某种意义上这是姗姗来迟,但对某些人来说也许有点意外。所以我想在这篇里更新一下我对这一切的看法。
What Are Tools · 工具是什么
When a harness like Pi provides tools for an LLM to call, it does so by supplying some tool definitions which then translate into some token structure on the server side. Whether a model is encouraged to call a tool is the result of the reinforcement learning process. Something I wrote about before if you want to learn more.
当 Pi 这样的 harness 给 LLM 提供工具时,它提供的是一些工具定义,然后在服务端翻译成某种 token 结构。模型是否倾向调用某个工具,是强化学习过程的结果——细节我之前写过(《Better Models: Worse Tools》)。
One of the reasons we strongly lean towards CLI and bash is because it allows easy composition of calls, and because the model also learns how the file system works when it's trained. So when it invokes a tool like `echo foo > /tmp/test.txt` the model also learns that after that tool call, there is now a file called `test.txt` in `/tmp`.
我们强烈偏向 CLI 和 bash 的原因之一,是它让调用组合变得容易;而且模型在训练时也学会了文件系统怎么运作——当它调用 `echo foo > /tmp/test.txt` 这样的工具时,它同时学到:这条调用之后,`/tmp` 里会多出一个叫 `test.txt` 的文件。
However bash has one fundamental limitation which is that it can only compose programs that run. And there are some things, which are not programs, but native tools to the LLM and they sort of have to be.
但 bash 有一个根本局限:它只能组合「能跑的程序」。有些东西不是程序,而是 LLM 的原生工具——而且多多少少必须如此。
The most obvious example here is `read` or `view_image`. If a multimodal model needs to read an image, it cannot use `cat` for that because the harness needs to inject the actual image payload into the protocol of the LLM.
最明显的例子是 `read` 或 `view_image`:多模态模型要读一张图时,没法用 `cat`——因为 harness 必须把真实的图像载荷注入 LLM 的协议里。
Another quite vivid example are sub agents. In order to spawn and orchestrate sub agents, it's tricky to avoid tools that are provided by the harness. While in theory the agent could provide a CLI tool that talks to the outer harness via environment variables and Unix sockets, it's a rather crude process. It however has another issue, and that is where the code runs.
另一个鲜活的例子是 sub agent。要生成并编排 sub agent,很难绕开 harness 提供的工具。理论上 agent 可以提供一个 CLI 工具、靠环境变量和 Unix socket 跟外层 harness 通信,但那是个相当粗糙的过程。而且它还有另一个问题——代码到底跑在哪。
Brains vs Hands · 脑与手
To better understand that, it's important to think a bit more about where all the bits and pieces run. There really usually are two different systems involved. The first is the brain, the harness: it runs on one machine. It's trusted. The second is *often* the same machine, but it's really where the tools are executing: the hands. In Pi we now call this the execution environment, but you can think of it as the target of all the operations.
要理解这一点,得先想清楚各个零件都跑在哪。通常涉及两个不同的系统:第一个是脑,也就是 harness——它跑在一台机器上,是可信的。第二个*经常*是同一台机器,但它的本质是工具执行的地方:手。Pi 现在称之为执行环境(execution environment),你可以把它理解为所有操作的目标。
Crucially what is important for us, is that there is a dividing line between the harness brain and the target environment that runs bash and executes the tools.
对我们来说关键在于:harness 之脑与「跑 bash、执行工具」的目标环境之间,存在一条分界线。
And splitting this in half has some really important consequences. For a start it means that they are running on different file systems and they have different levels of trust. If you for instance use a sandboxing solution like Gondolin your bash stuff will be sandboxed just fine, but the harness itself will not be.
把这两半劈开有一些非常重要的后果。首先,它们跑在不同的文件系统上,信任级别不同。比如你用 Gondolin 这样的沙箱方案,你的 bash 会被沙箱包得很好——但 harness 本身不在沙箱里。
Orchestrating The Harness · 编排 harness
Which brings us to what Codemode really does: it's a way for the LLM to express and orchestrate complex operations on the harness side, but not the execution environment side. Codemode runs in the harness, in its own sandbox. In case of Pi it's running in QuickJS within a WASM runtime with intentional limitations: no network, no file system, no timers, limited RAM. The only way is to call more tools. You could also imagine that Codemode could run Scheme or some other language as well.
这就引出了 Codemode 真正做的事:它是 LLM 在 **harness 侧**(而非执行环境侧)表达并编排复杂操作的方式。Codemode 跑在 harness 里、跑在它自己的沙箱里。在 Pi 里,它跑在 WASM 运行时内的 QuickJS 中,带着故意的限制:无网络、无文件系统、无定时器、内存受限。唯一的出路是调用更多工具。你也可以想象 Codemode 跑 Scheme 或别的语言。
If you are not familiar with Codemode, it's basically just a way to issue tool calls from within some language, in our case JavaScript. That allows you to compose those calls without necessarily going through the LLM's context. Credit for naming goes to our friends at Cloudflare who coined it.
如果你不熟悉 Codemode:它本质上就是「在某种语言里发起工具调用」的方式——在我们的场景里是 JavaScript。这让你可以组合这些调用,而不必经过 LLM 的上下文。命名的功劳归 Cloudflare 的朋友们。
For instance if you issue a bash call as a regular tool call in the LLM, then we only throw the trailing 2000 lines into the context and if the agent wants more, it needs to look at the overflow file itself. If however the agent issues that invocation via Codemode, then the Codemode side gets larger outputs sent structurally.
举例来说:如果 agent 用常规工具调用发起 bash,我们只把末尾 2000 行放进上下文,agent 想看更多就得自己去翻 overflow 文件。但如果 agent 经由 Codemode 发起这次调用,Codemode 侧就能结构化地拿到更大的输出。
Most importantly, because Codemode is JavaScript the agent can express concurrent operations and basic workflows. A common way in which you see agents now use this, is to first probe at 5-10 items from some tool response to see what it looks like, and to then write a Codemode script that processes the next n items.
最重要的是:因为 Codemode 是 JavaScript,agent 可以表达并发操作和基础工作流。如今常见的用法是:先从工具响应里探测 5-10 项看看长什么样,然后写一个 Codemode 脚本去处理接下来的 n 项。
Codemode also allows you to throw state into the transcript! That means that one Codemode invocation can stash away data, that the next call in the session can load again. And remember: this is on the harness host, not the sandbox.
Codemode 还允许你把状态扔进 transcript!也就是说,一次 Codemode 调用可以把数据存起来,同 session 里的下一次调用可以再读回来。记住:这存在 harness 主机上,不在沙箱里。
In case of Pi, Codemode also allows you to issue calls that naturally do not make any sense in Pi's traditional interface. For instance if you want to generate images with an image model or you want to classify some text with a one shot classifier model, those Pi APIs are exposed via Codemode, but not via regular tools where they would just waste context.
在 Pi 里,Codemode 还能发起那些在传统接口下「天然说不通」的调用。比如用图像模型生图、或用一次性分类器模型给文本分类——这些 Pi API 经由 Codemode 暴露,而不做成常规工具(做成常规工具只会白白浪费上下文)。
Note that Codemode is by default only enabled in Pi when MCP is enabled, but you can turn it on with `"defaultTools": ["+codemode"]` in the settings. Just ask Pi to enable it for you.
注意:Pi 里 Codemode 默认只在启用 MCP 时开启;你也可以在设置里用 `"defaultTools": ["+codemode"]` 打开——直接让 Pi 帮你开就行。
Generating Images · 生成图像
Let's start simple with image generation. Image generation is a feature that Pi supports in the AI SDK core, but it's not a tool that the agent can use. In the past the only way to use image models has been to write a bespoke extension or to have the agent run node itself and use the internal image APIs. However because we expose quite a few of the internal model APIs within Codemode, it means that the agent can use it:
从简单的开始:生图。图像生成是 Pi 在 AI SDK 核心里支持的功能,但它不是 agent 能用的工具。过去用图像模型的唯一办法是写定制扩展,或让 agent 自己跑 node 调内部图像 API。而现在我们在 Codemode 里暴露了相当多内部模型 API,agent 就能直接用:
const [painter] = await models.getAvailableOfType("image");
const result = await models.generateImages(painter, {
input: [{ type: "text", text: "A cute little puppy sitting on a grassy " +
"lawn, soft natural light, photorealistic" }],
});
if (result.stopReason !== "stop") return result.errorMessage;
for (const block of result.output) {
if (block.type === "image") image(block);
else text(block.text);
}Note that the call to `image()` sends the image back as image content to the LLM. On the harness side it feeds it directly into both the agent, as well as onto disk as a temporary artifact in case the agent wants to be able to pass that image back to bash.
注意 `image()` 这个调用会把图像作为 image content 发回给 LLM。在 harness 侧,它一边直接喂给 agent,一边落盘成临时工件——万一 agent 想把这张图传回给 bash 用。
Calling MCP Servers · 调用 MCP server
Lastly, Codemode obviously is great for calling MCP servers. And because we do not actually expose any of the MCP tools to the LLM, the agent first uses provided APIs to issue a tool search within Codemode to discover what it might be able to do with the connected servers. This form of progressive discovery makes the whole MCP business work well enough for a lot of use cases today.
最后,Codemode 显然很适合调 MCP server。而且因为我们**并不把任何 MCP 工具暴露给 LLM**,agent 会先用提供的 API 在 Codemode 里发起 tool search,探索连上的 server 能干什么。这种渐进发现的形式,让 MCP 这摊事在今天对很多用例都够好用了。
Here for instance you can see the agent reach for the Sentry MCP straight away, even without discovering the tools, presumably because it has learned during the RL process already about what the Sentry MCP looks like. But it learns from what we inject into the system prompt, that the Sentry server is available to begin with. It's not completely guessing here.
比如下面这个例子里,agent 没做发现就直接伸手够 Sentry MCP——多半是因为它在 RL 过程中早就学过 Sentry MCP 长什么样。但它「知道 Sentry server 存在」这件事,来自我们注入 system prompt 的声明。它并不是在纯瞎猜。
const orgs = await tools.mcp__sentry__find_organizations({});
const { organizations } = orgs.structuredContent;
const results = await Promise.allSettled(organizations.map(org =>
tools.mcp__sentry__find_projects({
organizationSlug: org.slug,
regionUrl: org.regionUrl,
})
));
return organizations.map((org, i) => {
const r = results[i];
if (r.status !== "fulfilled") return { org: org.slug, error: String(r.reason) };
if (r.value.isError) return { org: org.slug, error: r.value.content };
return {
org: org.slug,
projects: r.value.structuredContent.projects.map(p => p.slug),
};
});Modern MCP Is A Fight · 现代 MCP 是一场搏斗
I really don't want to talk too much about MCP here, but MCP is in fact a protocol that greatly benefits from Codemode. The problem in parts is that MCP in practice often targets harnesses that do not (yet?) use Codemode. But the tide is shifting. In the meantime, a temporary crutch has been to do what Cloudflare did, and do Codemode within the MCP server. But now we have Codemode in Codemode which is pretty bad. It means double JSON escaping, easy for smaller models to get confused by and the inner code cannot call the outer tools.
这里我不想多谈 MCP,但 MCP 确实是一个能从 Codemode 获益巨大的协议。问题部分在于:MCP 实践中往往面向「还没用上 Codemode 的 harness」。风向在变。在过渡期,一个临时拐杖是学 Cloudflare——把 Codemode 做进 MCP server 里。但这就成了 Codemode 套 Codemode,相当糟糕:双重 JSON 转义、小模型很容易被绕晕、而且内层代码调不了外层工具。
MCP Desires · MCP 的愿望清单
So to end things off: how well does Codemode work with MCP today? Well … not amazingly well. That's because MCP servers are not really targeting harnesses that use Codemode yet (though at this point I think most harnesses support it).
收个尾:Codemode 今天跟 MCP 配合得怎么样?嗯……没那么惊艳。因为 MCP server 还不是为「用 Codemode 的 harness」设计的(虽然我觉得此刻大多数 harness 都已支持)。
For this to work well some recommendations: **Structured content:** Codemode wants calls to return some nicely formatted JSON. So that needs to come back from the server, and many don't do that yet. The `outputSchema` system in MCP is great for that. **Consistent results:** an interesting failure case is when an MCP server does not return consistent data. For instance because it tries to token optimize things depending on how many items are in the result set. This can cause an initial probe with 5 items to succeed, but then fail when the server returns the maximum batch size. **Large binary data:** today MCP does not yet support large binary data so quite a few use cases that are really interesting do not work well at all yet. You end up with all kinds of weird workarounds such as pre-signed URLs to allow file uploads then to happen through non MCP channels. **Composable tool search:** the MCP server might know better than the MCP client which tool is appropriate for a task. But there is no good mechanism today that allows a harness to fan out tool searches across multiple MCP servers. It's all emergent behavior and it does not scale well to multiple active servers.
要配合好,有几条建议。**结构化内容**:Codemode 希望调用返回整洁的 JSON——这得由 server 返回,很多还没做;MCP 的 `outputSchema` 机制正合适。**结果形状一致**:一个有趣的失败模式是 server 返回的数据不有一致性——比如按结果集大小做 token 优化,结果探测 5 项时成功、返回最大批量时失败。**大二进制数据**:MCP 至今不支持大二进制,很多真正有趣的用例根本跑不好,只能靠预签名 URL 这类怪招让上传绕开 MCP 通道。**可组合的 tool search**:哪个工具适合哪个任务,MCP server 可能比 client 更清楚——但今天没有好机制让 harness 把 tool search 扇出到多个 server;全靠涌现行为,多 server 活跃时扩展性很差。
Future of Codemode · Codemode 的未来
So where does this leave us? Is this a reversal of what I wrote a year ago where I encouraged CLIs? I don't think so. In fact, the MCP ecosystem from my perspective picked up on exactly what we pointed out a year ago works: code. But Codemode goes beyond MCP in that it can act as a capable mechanism within the harness to express more freedom for the agent.
那么我们身处何地?这是对我一年前鼓励 CLI 的立场反悔吗?我不这么认为。事实上,在我看来 MCP 生态恰恰吸收了我们一年前指出的有效的东西:代码。而 Codemode 走得比 MCP 更远——它作为 harness 内的一套有力机制,为 agent 表达了更多自由。
There are however also some things that we still need to figure out. For one, durability with Codemode is trickier. We might have to adopt some ideas from durable workflow engines here to snapshot invocations. Or maybe, something like Starlark is a better composition language than JavaScript given its deterministic nature. Images, binary data and just the inability of this pattern to work with smaller models is also something that needs to be fleshed out. So it's for sure not a perfect solution yet, but it's quite a useful pattern that I expect us to leverage more.
当然还有些待解的问题。其一,Codemode 的 durable(持久化)更棘手——也许得借鉴 durable workflow 引擎的思路给调用做快照。又或者,鉴于其确定性,Starlark 之类也许比 JavaScript 更适合作组合语言。图像、二进制数据、以及这个模式对小模型的无能为力,也都待补。所以它肯定还不是完美方案,但它是一个相当有用的模式,我预计我们会用得越来越多。