What I learned building an opinionated and minimal coding agent我构建一个极简且有主见的编程代理(Coding Agent)的经验总结
2025-11-302025-11-30
Table of contents
In the past three years, I've been using LLMs for assisted coding. If you read this, you probably went through the same evolution: from copying and pasting code into ChatGPT, to Copilot auto-completions (which never worked for me), to Cursor, and finally the new breed of coding agent harnesses like Claude Code, Codex, Amp, Droid, and opencode that became our daily drivers in 2025.过去三年里,我一直使用大语言模型(LLM)辅助编程。如果你读到这里,大概也经历过同样的演变:从把代码复制粘贴到 ChatGPT,到 Copilot 的自动补全(对我来说从没好用过),再到 Cursor,最后是 2025 年成为我们日常主力工具的 Claude Code、Codex、Amp、Droid 和 opencode 等新一代编程代理框架。
I preferred Claude Code for most of my work. It was the first thing I tried back in April after using Cursor for a year and a half. Back then, it was much more basic. That fit my workflow perfectly, because I'm a simple boy who likes simple, predictable tools. Over the past few months, Claude Code has turned into a spaceship with 80% of functionality I have no use for. The system prompt and tools also change on every release, which breaks my workflows and changes model behavior. I hate that. Also, it flickers.在我的大部分工作中,我更偏爱 Claude Code。这是我在使用 Cursor 一年半后,于四月份尝试的第一个工具。那时它还非常基础,这完全契合我的工作流,因为我是一个喜欢简单、可预测工具的单纯的人。过去几个月里,Claude Code 变成了一艘庞大的“太空船”,其中 80% 的功能我都用不上。而且每次更新,系统提示词(System Prompt)和工具都会变动,这不仅破坏了我的工作流,还改变了模型的行为。我讨厌这样。此外,它还会闪烁。
I've also built a bunch of agents over the years, of various complexity. For example, Sitegeist, my little browser-use agent, is essentially a coding agent that lives inside the browser. In all that work, I learned that context engineering is paramount. Exactly controlling what goes into the model's context yields better outputs, especially when it's writing code. Existing harnesses make this extremely hard or impossible by injecting stuff behind your back that isn't even surfaced in the UI.这些年来,我也构建过许多不同复杂程度的代理。例如,我的小型浏览器操作代理 Sitegeist,本质上就是一个运行在浏览器内的编程代理。在所有这些工作中,我学到最重要的一点是:上下文工程(Context Engineering)至关重要。精确控制输入到模型上下文的内容,能产生更好的输出,尤其是在编写代码时。现有的框架在后台偷偷注入内容,且不在 UI 中展示,这使得精确控制变得极其困难甚至不可能。
Speaking of surfacing things, I want to inspect every aspect of my interactions with the model. Basically no harness allows that. I also want a cleanly documented session format I can post-process automatically, and a simple way to build alternative UIs on top of the agent core. While some of this is possible with existing harnesses, the APIs smell like organic evolution. These solutions accumulated baggage along the way, which shows in the developer experience. I'm not blaming anyone for this. If tons of people use your shit and you need some sort of backwards compatibility, that's the price you pay.说到展示内容,我希望检查与模型交互的每一个细节。基本上没有任何框架允许这样做。我还想要一种格式清晰、易于自动后处理的会话记录,以及一种在代理核心之上构建替代 UI 的简单方法。虽然现有框架部分实现了这些,但它们的 API 给人的感觉像是“有机进化”的产物。这些解决方案在演进过程中积累了沉重的历史包袱,这在开发者体验上体现得很明显。我并不责怪任何人,如果你的产品有大量用户,为了向后兼容性,这就是你必须付出的代价。
I've also dabbled in self-hosting, both locally and on DataCrunch. While some harnesses like opencode support self-hosted models, it usually doesn't work well. Mostly because they rely on libraries like the Vercel AI SDK, which doesn't play nice with self-hosted models for some reason, specifically when it comes to tool calling.我也尝试过自托管模型,无论是在本地还是在 DataCrunch 上。虽然 opencode 等框架支持自托管模型,但通常效果不佳。这主要是因为它们依赖于 Vercel AI SDK 等库,出于某种原因,这些库在处理自托管模型时表现不佳,特别是在工具调用(Tool Calling)方面。
So what's an old guy yelling at Claudes going to do? He's going to write his own coding agent harness and give it a name that's entirely un-Google-able, so there will never be any users. Which means there will also never be any issues on the GitHub issue tracker. How hard can it be?那么,一个对 Claude 喋喋不休的老家伙该怎么办呢?他决定编写自己的编程代理框架,并起一个完全无法被 Google 搜索到的名字,这样就不会有用户了。这意味着 GitHub 的 Issue 跟踪器里永远不会有任何问题。这能有多难?
To make this work, I needed to build:为了实现这个目标,我需要构建:
- pi-ai: A unified LLM API with multi-provider support (Anthropic, OpenAI, Google, xAI, Groq, Cerebras, OpenRouter, and any OpenAI-compatible endpoint), streaming, tool calling with TypeBox schemas, thinking/reasoning support, seamless cross-provider context handoffs, and token and cost tracking.pi-ai:一个统一的 LLM API,支持多提供商(Anthropic、OpenAI、Google、xAI、Groq、Cerebras、OpenRouter 以及任何兼容 OpenAI 的端点)、流式传输、基于 TypeBox 模式的工具调用、思考/推理支持、无缝的跨提供商上下文切换,以及 Token 和成本追踪。
- pi-agent-core: An agent loop that handles tool execution, validation, and event streaming.pi-agent-core:一个处理工具执行、验证和事件流的代理循环。
- pi-tui: A minimal terminal UI framework with differential rendering, synchronized output for (almost) flicker-free updates, and components like editors with autocomplete and markdown rendering.pi-tui:一个极简的终端 UI 框架,支持差异化渲染、同步输出以实现(几乎)无闪烁更新,并包含带有自动补全和 Markdown 渲染功能的编辑器等组件。
- pi-coding-agent: The actual CLI that wires it all together with session management, custom tools, themes, and project context files.pi-coding-agent:真正的 CLI 工具,它通过会话管理、自定义工具、主题和项目上下文文件将所有内容连接在一起。
My philosophy in all of this was: if I don't need it, it won't be built. And I don't need a lot of things.我秉持的哲学是:如果我不需要,我就不会去构建它。而我需要的东西并不多。
pi-ai and pi-agent-corepi-ai 和 pi-agent-core
I'm not going to bore you with the API specifics of this package. You can read it all in the README.md. Instead, I want to document the problems I ran into while creating a unified LLM API and how I resolved them. I'm not claiming my solutions are the best, but they've been working pretty well throughout various agentic and non-agentic LLM projects.我不会用这个包的 API 细节来烦你,你可以在 README.md 中阅读所有内容。相反,我想记录我在创建统一 LLM API 时遇到的问题以及我是如何解决它们的。我不敢说我的方案是最好的,但它们在各种代理和非代理 LLM 项目中运行得相当不错。
There. Are. Four. Ligh... APIs有。四。个。灯……API
There's really only four APIs you need to speak to talk to pretty much any LLM provider: OpenAI's Completions API, their newer Responses API, Anthropic's Messages API, and Google's Generative AI API.实际上,要与几乎任何 LLM 提供商进行交互,你只需要掌握四个 API:OpenAI 的 Completions API、他们较新的 Responses API、Anthropic 的 Messages API 以及 Google 的 Generative AI API。
They're all pretty similar in features, so building an abstraction on top of them isn't rocket science. There are, of course, provider-specific peculiarities you have to care for. That's especially true for the Completions API, which is spoken by pretty much all providers, but each of them has a different understanding of what this API should do. For example, while OpenAI doesn't support reasoning traces in their Completions API, other providers do in their version of the Completions API. This is also true for inference engines like llama.cpp, Ollama, vLLM, and LM Studio.它们的功能非常相似,因此在它们之上构建抽象层并非难事。当然,你需要处理一些特定于提供商的细节。对于 Completions API 尤其如此,虽然几乎所有提供商都支持它,但每个提供商对该 API 应该做什么的理解各不相同。例如,虽然 OpenAI 的 Completions API 不支持推理追踪(Reasoning Traces),但其他提供商在他们的版本中却支持。对于 llama.cpp、Ollama、vLLM 和 LM Studio 等推理引擎来说,情况也是如此。
For example, in openai-completions.ts:例如,在 openai-completions.ts 中:
- Cerebras, xAI, Mistral, and Chutes don't like the
storefieldCerebras、xAI、Mistral 和 Chutes 不支持 store 字段 - Mistral and Chutes use
max_tokensinstead ofmax_completion_tokensMistral 和 Chutes 使用 max_tokens 而不是 max_completion_tokens - Cerebras, xAI, Mistral, and Chutes don't support the
developerrole for system promptsCerebras、xAI、Mistral 和 Chutes 不支持系统提示词的 developer 角色 - Grok models don't like
reasoning_effortGrok 模型不支持 reasoning_effort - Different providers return reasoning content in different fields (
reasoning_contentvsreasoning)不同的提供商在不同的字段中返回推理内容(reasoning_content vs reasoning)
To ensure all features actually work across the gazillion of providers, pi-ai has a pretty extensive test suite covering image inputs, reasoning traces, tool calling, and other features you'd expect from an LLM API. Tests run across all supported providers and popular models. While this is a good effort, it still won't guarantee that new models and providers will just work out of the box.为了确保所有功能在海量提供商中都能正常工作,pi-ai 拥有相当广泛的测试套件,涵盖了图像输入、推理追踪、工具调用以及你对 LLM API 所期望的其他功能。测试运行在所有支持的提供商和主流模型上。虽然这付出了很大努力,但仍不能保证新的模型和提供商能直接开箱即用。
Another big difference is how providers report tokens and cache reads/writes. Anthropic has the sanest approach, but generally it's the Wild West. Some report token counts at the start of the SSE stream, others only at the end, making accurate cost tracking impossible if a request is aborted. To add insult to injury, you can't provide a unique ID to later correlate with their billing APIs and figure out which of your users consumed how many tokens. So pi-ai does token and cache tracking on a best-effort basis. Good enough for personal use, but not for accurate billing if you have end users consuming tokens through your service.另一个巨大的差异在于提供商如何报告 Token 和缓存读写。Anthropic 的方法最合理,但总体而言,这简直是“狂野西部”。有些在 SSE 流开始时报告 Token 计数,有些则只在结束时报告,如果请求被中止,精确的成本追踪将变得不可能。更糟糕的是,你无法提供唯一的 ID 来关联他们的账单 API,从而无法确定是哪位用户消耗了多少 Token。因此,pi-ai 尽最大努力进行 Token 和缓存追踪。这对于个人使用足够了,但如果你有通过你的服务消耗 Token 的最终用户,则无法实现精确计费。
Special shout out to Google who to this date seem to not support tool call streaming which is extremely Google.特别点名 Google,直到今天他们似乎仍不支持工具调用流式传输,这非常“Google”。
pi-ai also works in the browser, which is useful for building web-based interfaces. Some providers make this especially easy by supporting CORS, specifically Anthropic and xAI.pi-ai 也可以在浏览器中运行,这对于构建基于 Web 的界面很有用。一些提供商通过支持 CORS 使其变得非常简单,特别是 Anthropic 和 xAI。
Context handoff上下文切换
Context handoff between providers was a feature pi-ai was designed for from the start. Since each provider has their own way of tracking tool calls and thinking traces, this can only be a best-effort thing. For example, if you switch from Anthropic to OpenAI mid-session, Anthropic thinking traces are converted to content blocks inside assistant messages, delimited by <thinking></thinking> tags. This may or may not be sensible, because the thinking traces returned by Anthropic and OpenAI don't actually represent what's happening behind the scenes.上下文切换是 pi-ai 从一开始就设计的功能。由于每个提供商都有自己跟踪工具调用和思考追踪的方式,这只能是尽力而为。例如,如果你在会话中途从 Anthropic 切换到 OpenAI,Anthropic 的思考追踪会被转换为助手消息中的内容块,并由 <thinking></thinking> 标签分隔。这是否合理尚存争议,因为 Anthropic 和 OpenAI 返回的思考追踪实际上并不代表后台发生的真实情况。
These providers also insert signed blobs into the event stream that you have to replay on subsequent requests containing the same messages. This also applies when switching models within a provider. It makes for a cumbersome abstraction and transformation pipeline in the background.这些提供商还会将签名数据块插入到事件流中,你必须在包含相同消息的后续请求中重放这些数据。在同一提供商内切换模型时也是如此。这导致后台存在一个繁琐的抽象和转换管道。
I'm happy to report that cross-provider context handoff and context serialization/deserialization work pretty well in pi-ai:很高兴向大家报告,跨提供商的上下文切换以及上下文序列化/反序列化在 pi-ai 中运行得相当不错:
import { getModel, complete, Context } from '@mariozechner/pi-ai';
// Start with Claude
const claude = getModel('anthropic', 'claude-sonnet-4-5');
const context: Context = {
messages: []
};
context.messages.push({ role: 'user', content: 'What is 25 * 18?' });
const claudeResponse = await complete(claude, context, {
thinkingEnabled: true
});
context.messages.push(claudeResponse);
// Switch to GPT - it will see Claude's thinking as <thinking> tagged text
const gpt = getModel('openai', 'gpt-5.1-codex');
context.messages.push({ role: 'user', content: 'Is that correct?' });
const gptResponse = await complete(gpt, context);
context.messages.push(gptResponse);
// Switch to Gemini
const gemini = getModel('google', 'gemini-2.5-flash');
context.messages.push({ role: 'user', content: 'What was the question?' });
const geminiResponse = await complete(gemini, context);
// Serialize context to JSON (for storage, transfer, etc.)
const serialized = JSON.stringify(context);
// Later: deserialize and continue with any model
const restored: Context = JSON.parse(serialized);
restored.messages.push({ role: 'user', content: 'Summarize our conversation' });
const continuation = await complete(claude, restored);
We live in a multi-model world我们生活在一个多模型世界
Speaking of models, I wanted a typesafe way of specifying them in the getModel call. For that I needed a model registry that I could turn into TypeScript types. I'm parsing data from both OpenRouter and models.dev (created by the opencode folks, thanks for that, it's super useful) into models.generated.ts. This includes token costs and capabilities like image inputs and thinking support.说到模型,我想要一种类型安全的方式在 getModel 调用中指定它们。为此,我需要一个可以转换为 TypeScript 类型的模型注册表。我正在将来自 OpenRouter 和 models.dev(感谢 opencode 团队,这非常有用)的数据解析为 models.generated.ts。这包括 Token 成本以及图像输入和思考支持等功能。
And if I ever need to add a model that's not in the registry, I wanted a type system that makes it easy to create new ones. This is especially useful when working with self-hosted models, new releases that aren't yet on models.dev or OpenRouter, or trying out one of the more obscure LLM providers:如果我需要添加一个不在注册表中的模型,我想要一个易于创建新模型的类型系统。这在处理自托管模型、尚未在 models.dev 或 OpenRouter 上发布的新模型,或者尝试一些较冷门的 LLM 提供商时特别有用:
import { Model, stream } from '@mariozechner/pi-ai';
const ollamaModel: Model<'openai-completions'> = {
id: 'llama-3.1-8b',
name: 'Llama 3.1 8B (Ollama)',
api: 'openai-completions',
provider: 'ollama',
baseUrl: 'http://localhost:11434/v1',
reasoning: false,
input: ['text'],
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 32000
};
const response = await stream(ollamaModel, context, {
apiKey: 'dummy' // Ollama doesn't need a real key
});
Many unified LLM APIs completely ignore providing a way to abort requests. This is entirely unacceptable if you want to integrate your LLM into any kind of production system. Many unified LLM APIs also don't return partial results to you, which is kind of ridiculous. pi-ai was designed from the beginning to support aborts throughout the entire pipeline, including tool calls. Here's how it works:许多统一的 LLM API 完全忽略了提供中止请求的方法。如果你想将 LLM 集成到任何生产系统中,这是完全不可接受的。许多统一的 LLM API 也不返回部分结果,这简直荒谬。pi-ai 从一开始就设计为在整个管道中支持中止,包括工具调用。其工作原理如下:
import { getModel, stream } from '@mariozechner/pi-ai';
const model = getModel('openai', 'gpt-5.1-codex');
const controller = new AbortController();
// Abort after 2 seconds
setTimeout(() => controller.abort(), 2000);
const s = stream(model, {
messages: [{ role: 'user', content: 'Write a long story' }]
}, {
signal: controller.signal
});
for await (const event of s) {
if (event.type === 'text_delta') {
process.stdout.write(event.delta);
} else if (event.type === 'error') {
console.log(`${event.reason === 'aborted' ? 'Aborted' : 'Error'}:`, event.error.errorMessage);
}
}
// Get results (may be partial if aborted)
const response = await s.result();
if (response.stopReason === 'aborted') {
console.log('Partial content:', response.content);
}
Structured split tool results结构化拆分工具结果
Another abstraction I haven't seen in any unified LLM API is splitting tool results into a portion handed to the LLM and a portion for UI display. The LLM portion is generally just text or JSON, which doesn't necessarily contain all the information you'd want to show in a UI. It also sucks hard to parse textual tool outputs and restructure them for display in a UI. pi-ai's tool implementation allows returning both content blocks for the LLM and separate content blocks for UI rendering. Tools can also return attachments like images that get attached in the native format of the respective provider. Tool arguments are automatically validated using TypeBox schemas and AJV, with detailed error messages when validation fails:我在任何统一 LLM API 中都没见过另一个抽象,那就是将工具结果拆分为交给 LLM 的部分和用于 UI 显示的部分。交给 LLM 的部分通常只是文本或 JSON,并不一定包含你想在 UI 中展示的所有信息。解析文本工具输出并重构以用于 UI 显示非常痛苦。pi-ai 的工具实现允许同时返回用于 LLM 的内容块和用于 UI 渲染的单独内容块。工具还可以返回图像等附件,这些附件会以各自提供商的原生格式进行处理。工具参数会自动使用 TypeBox 模式和 AJV 进行验证,并在验证失败时提供详细的错误消息:
import { Type, AgentTool } from '@mariozechner/pi-ai';
const weatherSchema = Type.Object({
city: Type.String({ minLength: 1 }),
});
const weatherTool: AgentTool<typeof weatherSchema, { temp: number }> = {
name: 'get_weather',
description: 'Get current weather for a city',
parameters: weatherSchema,
execute: async (toolCallId, args) => {
const temp = Math.round(Math.random() * 30);
return {
// Text for the LLM
output: `Temperature in ${args.city}: ${temp}°C`,
// Structured data for the UI
details: { temp }
};
}
};
// Tools can also return images
const chartTool: AgentTool = {
name: 'generate_chart',
description: 'Generate a chart from data',
parameters: Type.Object({ data: Type.Array(Type.Number()) }),
execute: async (toolCallId, args) => {
const chartImage = await generateChartImage(args.data);
return {
content: [
{ type: 'text', text: `Generated chart with ${args.data.length} data points` },
{ type: 'image', data: chartImage.toString('base64'), mimeType: 'image/png' }
]
};
}
};
What's still lacking is tool result streaming. Imagine a bash tool where you want to display ANSI sequences as they come in. That's currently not possible, but it's a simple fix that will eventually make it into the package.目前还缺少的是工具结果流式传输。想象一下一个 bash 工具,你想在 ANSI 序列到达时实时显示它们。这目前还无法实现,但这是一个简单的修复,最终会加入到包中。
Partial JSON parsing during tool call streaming is essential for good UX. As the LLM streams tool call arguments, pi-ai progressively parses them so you can show partial results in the UI before the call completes. For example, you can display a diff streaming in as the agent rewrites a file.工具调用流式传输期间的部分 JSON 解析对于良好的用户体验至关重要。当 LLM 流式传输工具调用参数时,pi-ai 会逐步解析它们,以便在调用完成之前你就能在 UI 中看到部分结果。例如,当代理重写文件时,你可以实时显示 diff。
Minimal agent scaffold极简代理脚手架
Finally, pi-ai provides an agent loop that handles the full orchestration: processing user messages, executing tool calls, feeding results back to the LLM, and repeating until the model produces a response without tool calls. The loop also supports message queuing via a callback: after each turn, it asks for queued messages and injects them before the next assistant response. The loop emits events for everything, making it easy to build reactive UIs.最后,pi-ai 提供了一个处理完整编排的代理循环:处理用户消息、执行工具调用、将结果反馈给 LLM,并重复此过程,直到模型产生无需工具调用的响应。该循环还支持通过回调进行消息排队:在每一轮之后,它会询问是否有排队的消息,并在下一次助手响应之前注入它们。循环为所有操作发出事件,这使得构建响应式 UI 变得容易。
The agent loop doesn't let you specify max steps or similar knobs you'd find in other unified LLM APIs. I never found a use case for that, so why add it? The loop just loops until the agent says it's done. On top of the loop, however, pi-agent-core provides an Agent class with actually useful stuff: state management, simplified event subscriptions, message queuing with two modes (one-at-a-time or all-at-once), attachment handling (images, documents), and a transport abstraction that lets you run the agent either directly or through a proxy.代理循环不允许你指定最大步骤数或你在其他统一 LLM API 中可能找到的类似旋钮。我从没发现这有什么用,所以为什么要添加它呢?循环只是不断运行,直到代理说它完成了。不过,在循环之上,pi-agent-core 提供了一个具有实际有用功能的 Agent 类:状态管理、简化的事件订阅、两种模式(一次一个或一次全部)的消息队列、附件处理(图像、文档)以及允许你直接或通过代理运行代理的传输抽象。
Am I happy with pi-ai? For the most part, yes. Like any unifying API, it can never be perfect due to leaky abstractions. But it's been used in seven different production projects and has served me extremely well.我对 pi-ai 满意吗?在很大程度上,是的。像任何统一 API 一样,由于抽象泄漏,它永远不可能完美。但它已经在七个不同的生产项目中被使用,并且服务得非常好。
Why build this instead of using the Vercel AI SDK? Armin's blog post mirrors my experience. Building on top of the provider SDKs directly gives me full control and lets me design the APIs exactly as I want, with a much smaller surface area. Armin's blog gives you a more in-depth treatise on the reasons for building your own. Go read that.为什么要构建这个而不是使用 Vercel AI SDK?Armin 的博客文章反映了我的经历。直接在提供商 SDK 之上构建给了我完全的控制权,让我能以更小的表面积按我想要的方式设计 API。Armin 的博客对构建自己的 SDK 的原因进行了更深入的论述,去读读看吧。
pi-tuipi-tui
I grew up in the DOS era, so terminal user interfaces are what I grew up with. From the fancy setup programs for Doom to Borland products, TUIs were with me until the end of the 90s. And boy was I fucking happy when I eventually switched to a GUI operating system. While TUIs are mostly portable and easily streamable, they also suck at information density. Having said all that, I thought starting with a terminal user interface for pi makes the most sense. I could strap on a GUI later whenever I felt like I needed to.我在 DOS 时代长大,所以终端用户界面(TUI)是我成长的一部分。从 Doom 的精美设置程序到 Borland 的产品,TUI 伴随我直到 90 年代末。当我最终切换到 GUI 操作系统时,我真是高兴坏了。虽然 TUI 大多是可移植且易于流式传输的,但它们在信息密度方面表现很差。话虽如此,我认为为 pi 启动一个终端用户界面是最有意义的。以后如果需要,我随时可以再加一个 GUI。
So why build my own TUI framework? I've looked into the alternatives like Ink, Blessed, OpenTUI, and so on. I'm sure they're all fine in their own way, but I definitely don't want to write my TUI like a React app. Blessed seems to be mostly unmaintained, and OpenTUI is explicitly not production ready. Also, writing my own TUI framework on top of Node.js seemed like a fun little challenge.那么为什么要构建自己的 TUI 框架?我研究过 Ink、Blessed、OpenTUI 等替代方案。我相信它们各有各的好处,但我绝对不想像写 React 应用那样写 TUI。Blessed 似乎基本无人维护,而 OpenTUI 明确表示尚未准备好用于生产。此外,在 Node.js 之上编写自己的 TUI 框架似乎是一个有趣的小挑战。
Two kinds of TUIs两种 TUI
Writing a terminal user interface is not rocket science per se. You just have to pick your poison. There's basically two ways to do it. One is to take ownership of the terminal viewport (the portion of the terminal contents you can actually see) and treat it like a pixel buffer. Instead of pixels you have cells that contain characters with background color, foreground color, and styling like italic and bold. I call these full screen TUIs. Amp and opencode use this approach.编写终端用户界面本身并非难事。你只需要做出选择。基本上有两种方法。一种是接管终端视口(你实际能看到的终端内容部分)并将其视为像素缓冲区。只不过像素变成了包含背景色、前景色以及斜体和粗体等样式的字符单元。我称之为全屏 TUI。Amp 和 opencode 使用这种方法。
The drawback is that you lose the scrollback buffer, which means you have to implement custom search. You also lose scrolling, which means you have to simulate scrolling within the viewport yourself. While this is not hard to implement, it means you have to re-implement all the functionality your terminal emulator already provides. Mouse scrolling specifically always feels kind of off in such TUIs.缺点是你丢失了回滚缓冲区(Scrollback Buffer),这意味着你必须实现自定义搜索。你也失去了滚动,这意味着你必须自己模拟视口内的滚动。虽然实现起来不难,但这意味着你必须重新实现终端模拟器已经提供的所有功能。在这种 TUI 中,鼠标滚动总是感觉有点怪。
The second approach is to just write to the terminal like any CLI program, appending content to the scrollback buffer, only occasionally moving the "rendering cursor" back up a little within the visible viewport to redraw things like animated spinners or a text edit field. It's not exactly that simple, but you get the idea. This is what Claude Code, Codex, and Droid do.第二种方法是像任何 CLI 程序一样写入终端,将内容追加到回滚缓冲区,仅偶尔在可见视口内将“渲染光标”向上移动一点,以重绘动画加载器或文本编辑字段等内容。虽然没那么简单,但你明白意思了。这就是 Claude Code、Codex 和 Droid 的做法。
Coding agents have this nice property that they're basically a chat interface. The user writes a prompt, followed by replies from the agent and tool calls and their results. Everything is nicely linear, which lends itself well to working with the "native" terminal emulator. You get to use all the built-in functionality like natural scrolling and search within the scrollback buffer. It also limits what your TUI can do to some degree, which I find charming because constraints make for minimal programs that just do what they're supposed to do without superfluous fluff. This is the direction I picked for pi-tui.编程代理有一个很好的特性,它们本质上是一个聊天界面。用户输入提示词,随后是代理的回复、工具调用及其结果。一切都是线性的,这非常适合与“原生”终端模拟器配合使用。你可以使用回滚缓冲区内的自然滚动和搜索等所有内置功能。这也限制了 TUI 的功能,我觉得这很迷人,因为约束能促成极简的程序,只做它们该做的事,没有多余的修饰。这就是我为 pi-tui 选择的方向。
Retained mode UI保留模式 UI
If you've done any GUI programming, you've probably heard of retained mode vs immediate mode. In a retained mode UI, you build up a tree of components that persist across frames. Each component knows how to render itself and can cache its output if nothing changed. In an immediate mode UI, you redraw everything from scratch each frame (though in practice, immediate mode UIs also do caching, otherwise they'd fall apart).如果你做过 GUI 编程,你可能听说过保留模式(Retained Mode)与即时模式(Immediate Mode)。在保留模式 UI 中,你构建一个跨帧持久存在的组件树。每个组件都知道如何渲染自己,如果没有任何变化,它可以缓存其输出。在即时模式 UI 中,你每帧从头开始重绘所有内容(尽管实际上即时模式 UI 也会进行缓存,否则它们会崩溃)。
pi-tui uses a simple retained mode approach. A Component is just an object with a render(width) method that returns an array of strings (lines that fit the viewport horizontally, with ANSI escape codes for colors and styling) and an optional handleInput(data) method for keyboard input. A Container holds a list of components arranged vertically and collects all their rendered lines. The TUI class is itself a container that orchestrates everything.pi-tui 使用简单的保留模式方法。一个组件就是一个带有 render(width) 方法的对象,它返回一个字符串数组(水平适合视口的行,带有用于颜色和样式的 ANSI 转义码),以及一个可选的 handleInput(data) 方法用于键盘输入。容器包含垂直排列的组件列表,并收集它们所有渲染的行。TUI 类本身就是一个编排一切的容器。
When the TUI needs to update the screen, it asks each component to render. Components can cache their output: an assistant message that's fully streamed doesn't need to re-parse markdown and re-render ANSI sequences every time. It just returns the cached lines. Containers collect lines from all children. The TUI gathers all these lines and compares them to the lines it previously rendered for the previous component tree. It keeps a backbuffer of sorts, remembering what was written to the scrollback buffer.当 TUI 需要更新屏幕时,它会要求每个组件渲染。组件可以缓存其输出:完全流式传输的助手消息不需要每次都重新解析 Markdown 和重新渲染 ANSI 序列。它只需返回缓存的行。容器收集所有子项的行。TUI 汇总这些行,并将它们与之前为组件树渲染的行进行比较。它保留了一个类似后备缓冲区的东西,记录了写入回滚缓冲区的内容。
Then it only redraws what changed, using a method I call differential rendering. I'm very bad with names, and this likely has an official name.然后它只重绘改变的部分,使用我称之为“差异化渲染”(Differential Rendering)的方法。我不擅长起名,这可能有一个官方名称。
Differential rendering差异化渲染
Here's a simplified demo that illustrates what exactly gets redrawn.这是一个简化的演示,说明了到底重绘了什么。
The algorithm is simple:算法很简单:
- First render: Just output all lines to the terminal首次渲染:直接将所有行输出到终端
- Width changed: Clear screen completely and re-render everything (soft wrapping changes)宽度改变:完全清除屏幕并重新渲染所有内容(软换行会改变)
- Normal update: Find the first line that differs from what's on screen, move the cursor to that line, and re-render from there to the end正常更新:找到与屏幕上不同的第一行,将光标移动到该行,并从那里重绘到最后
There's one catch: if the first changed line is above the visible viewport (the user scrolled up), we have to do a full clear and re-render. The terminal doesn't let you write to the scrollback buffer above the viewport.有一个陷阱:如果第一个改变的行在可见视口上方(用户向上滚动了),我们必须进行完全清除并重新渲染。终端不允许你写入视口上方的回滚缓冲区。
To prevent flicker during updates, pi-tui wraps all rendering in synchronized output escape sequences (CSI ?2026h and CSI ?2026l). This tells the terminal to buffer all the output and display it atomically. Most modern terminals support this.为了防止更新期间闪烁,pi-tui 将所有渲染包装在同步输出转义序列(CSI ?2026h 和 CSI ?2026l)中。这告诉终端缓冲所有输出并原子地显示它。大多数现代终端都支持这一点。
How well does it work and how much does it flicker? In any capable terminal like Ghostty or iTerm2, this works brilliantly and you never see any flicker. In less fortunate terminal implementations like VS Code's built-in terminal, you will get some flicker depending on the time of day, your display size, your window size, and so on. Given that I'm very accustomed to Claude Code, I haven't spent any more time optimizing this. I'm happy with the little flicker I get in VS Code. I wouldn't feel at home otherwise. And it still flickers less than Claude Code.它效果如何,闪烁严重吗?在 Ghostty 或 iTerm2 等任何优秀的终端中,这效果极佳,你永远不会看到任何闪烁。在 VS Code 内置终端等不太理想的终端实现中,根据时间、显示大小、窗口大小等因素,你会遇到一些闪烁。鉴于我非常习惯 Claude Code,我没有花更多时间优化这一点。我对 VS Code 中那一点点闪烁感到满意。否则我反而会觉得不习惯。而且它仍然比 Claude Code 闪烁得少。
How wasteful is this approach? We store an entire scrollback buffer worth of previously rendered lines, and we re-render lines every time the TUI is asked to render itself. That's alleviated with the caching I described above, so the re-rendering isn't a big deal. We still have to compare a lot of lines with each other. Realistically, on computers younger than 25 years, this is not a big deal, both in terms of performance and memory use (a few hundred kilobytes for very large sessions). Thanks V8. What I get in return is a dead simple programming model that lets me iterate quickly.这种方法有多浪费?我们存储了整个回滚缓冲区大小的先前渲染行,并且每次要求 TUI 渲染自身时,我们都会重绘行。这通过我上面描述的缓存得到了缓解,所以重新渲染不是什么大问题。我们仍然需要比较很多行。实际上,在 25 年以内的计算机上,这在性能和内存使用(对于非常大的会话只需几百 KB)方面都不是问题。感谢 V8。我得到的回报是一个极其简单的编程模型,让我可以快速迭代。
pi-coding-agentpi-coding-agent
I don't need to explain what features you should expect from a coding agent harness. pi comes with most creature comforts you're used to from other tools:我不需要解释你应该期望编程代理框架具备哪些功能。pi 带有你从其他工具中习惯的大多数便利功能:
Runs on Windows, Linux, and macOS (or anything with a Node.js runtime and a terminal)运行于 Windows、Linux 和 macOS(或任何带有 Node.js 运行时和终端的环境)
Multi-provider support with mid-session model switching支持多提供商,支持会话中途切换模型
Session management with continue, resume, and branching支持继续、恢复和分支的会话管理
Project context files (AGENTS.md) loaded hierarchically from global to project-specific从全局到项目特定的分层加载项目上下文文件 (AGENTS.md)
Slash commands for common operations常用操作的斜杠命令
Custom slash commands as markdown templates with argument support支持参数的 Markdown 模板自定义斜杠命令
OAuth authentication for Claude Pro/Max subscriptionsClaude Pro/Max 订阅的 OAuth 认证
Custom model and provider configuration via JSON通过 JSON 进行自定义模型和提供商配置
Customizable themes with live reload支持实时重载的可自定义主题
Editor with fuzzy file search, path completion, drag & drop, and multi-line paste带有模糊文件搜索、路径补全、拖放和多行粘贴功能的编辑器
Message queuing while the agent is working代理工作时的消息排队
Image support for vision-capable models支持具有视觉能力的模型的图像
HTML export of sessions会话的 HTML 导出
Headless operation via JSON streaming and RPC mode通过 JSON 流和 RPC 模式的无头(Headless)运行
Full cost and token tracking完整的成本和 Token 追踪
If you want the full rundown, read the README. What's more interesting is where pi deviates from other harnesses in philosophy and implementation.如果你想要完整的概述,请阅读 README。更有趣的是 pi 在哲学和实现上与其他框架的不同之处。
Minimal system prompt极简系统提示词
Here's the system prompt:这是系统提示词:
You are an expert coding assistant. You help users with coding tasks by reading files, executing commands, editing code, and writing new files.
Available tools:
- read: Read file contents
- bash: Execute bash commands
- edit: Make surgical edits to files
- write: Create or overwrite files
Guidelines:
- Use bash for file operations like ls, grep, find
- Use read to examine files before editing
- Use edit for precise changes (old text must match exactly)
- Use write only for new files or complete rewrites
- When summarizing your actions, output plain text directly - do NOT use cat or bash to display what you did
- Be concise in your responses
- Show file paths clearly when working with files
Documentation:
- Your own documentation (including custom model setup and theme creation) is at: /path/to/README.md
- Read it when users ask about features, configuration, or setup, and especially if the user asks you to add a custom model or provider, or create a custom theme.
That's it. The only thing that gets injected at the bottom is your AGENTS.md file. Both the global one that applies to all your sessions and the project-specific one stored in your project directory. This is where you can customize pi to your liking. You can even replace the full system prompt if you want to. Compared to, for example, Claude Code's system prompt, Codex's system prompt, or opencode's model-specific prompts (the Claude one is a cut-down version of the original Claude Code prompt they copied).就是这样。底部唯一注入的是你的 AGENTS.md 文件。包括适用于所有会话的全局文件和存储在项目目录中的项目特定文件。你可以在这里根据自己的喜好自定义 pi。如果你愿意,甚至可以替换整个系统提示词。相比之下,看看 Claude Code、Codex 或 opencode 的系统提示词(Claude 版本是他们复制的原始 Claude Code 提示词的精简版)。
You might think this is crazy. In all likelihood, the models have some training on their native coding harness. So using the native system prompt or something close to it like opencode would be most ideal. But it turns out that all the frontier models have been RL-trained up the wazoo, so they inherently understand what a coding agent is. There does not appear to be a need for 10,000 tokens of system prompt, as we'll find out later in the benchmark section, and as I've anecdotally found out by exclusively using pi for the past few weeks. Amp, while copying some parts of the native system prompts, seems to also do just fine with their own prompt.你可能认为这很疯狂。很有可能,模型在其原生编程框架上进行了一些训练。因此,使用原生系统提示词或像 opencode 那样接近的东西是最理想的。但事实证明,所有的前沿模型都经过了大量的强化学习(RL),所以它们天生就理解什么是编程代理。似乎没有必要使用 10,000 个 Token 的系统提示词,我们稍后会在基准测试部分发现这一点,而且我过去几周独家使用 pi 的经验也证明了这一点。Amp 虽然复制了原生系统提示词的某些部分,但似乎使用他们自己的提示词也做得很好。
Minimal toolset极简工具集
Here are the tool definitions:这是工具定义:
read
Read the contents of a file. Supports text files and images (jpg, png,
gif, webp). Images are sent as attachments. For text files, defaults to
first 2000 lines. Use offset/limit for large files.
- path: Path to the file to read (relative or absolute)
- offset: Line number to start reading from (1-indexed)
- limit: Maximum number of lines to read
write
Write content to a file. Creates the file if it doesn't exist, overwrites
if it does. Automatically creates parent directories.
- path: Path to the file to write (relative or absolute)
- content: Content to write to the file
edit
Edit a file by replacing exact text. The oldText must match exactly
(including whitespace). Use this for precise, surgical edits.
- path: Path to the file to edit (relative or absolute)
- oldText: Exact text to find and replace (must match exactly)
- newText: New text to replace the old text with
bash
Execute a bash command in the current working directory. Returns stdout
and stderr. Optionally provide a timeout in seconds.
- command: Bash command to execute
- timeout: Timeout in seconds (optional, no default timeout)
There are additional read-only tools (grep, find, ls) if you want to restrict the agent from modifying files or running arbitrary commands. By default these are disabled, so the agent only gets the four tools above.如果你想限制代理修改文件或运行任意命令,还有额外的只读工具(grep, find, ls)。默认情况下这些是禁用的,所以代理只能获得上述四个工具。
As it turns out, these four tools are all you need for an effective coding agent. Models know how to use bash and have been trained on the read, write, and edit tools with similar input schemas. Compare this to Claude Code's tool definitions or opencode's tool definitions (which are clearly derived from Claude Code's, same structure, same examples, same git commit flow). Notably, Codex's tool definitions are similarly minimal to pi's.事实证明,这四个工具就是构建有效编程代理所需的全部。模型知道如何使用 bash,并且已经在具有类似输入模式的读、写和编辑工具上进行了训练。将此与 Claude Code 或 opencode 的工具定义进行比较(它们显然源自 Claude Code,结构相同,示例相同,git 提交流也相同)。值得注意的是,Codex 的工具定义与 pi 的类似,同样极简。
pi's system prompt and tool definitions together come in below 1000 tokens.pi 的系统提示词和工具定义加起来不到 1000 个 Token。
YOLO by default默认 YOLO(你只活一次)模式
pi runs in full YOLO mode and assumes you know what you're doing. It has unrestricted access to your filesystem and can execute any command without permission checks or safety rails. No permission prompts for file operations or commands. No pre-checking of bash commands by Haiku for malicious content. Full filesystem access. Can execute any command with your user privileges.pi 在完全的 YOLO 模式下运行,并假设你知道自己在做什么。它对你的文件系统拥有不受限制的访问权限,可以执行任何命令,无需权限检查或安全护栏。文件操作或命令没有权限提示。Haiku 不会对 bash 命令进行恶意内容预检查。完全的文件系统访问。可以使用你的用户权限执行任何命令。
If you look at the security measures in other coding agents, they're mostly security theater. As soon as your agent can write code and run code, it's pretty much game over. The only way you could prevent exfiltration of data would be to cut off all network access for the execution environment the agent runs in, which makes the agent mostly useless. An alternative is allow-listing domains, but this can also be worked around through other means.如果你查看其他编程代理中的安全措施,它们大多是“安全剧场”。一旦你的代理可以编写代码和运行代码,游戏基本上就结束了。防止数据外泄的唯一方法是切断代理运行环境的所有网络访问,但这会让代理基本没用。另一种方法是允许域名列表,但这也可以通过其他方式绕过。
Simon Willison has written extensively about this problem. His "dual LLM" pattern attempts to address confused deputy attacks and data exfiltration, but even he admits "this solution is pretty bad" and introduces enormous implementation complexity. The core issue remains: if an LLM has access to tools that can read private data and make network requests, you're playing whack-a-mole with attack vectors.Simon Willison 曾深入撰写过这个问题。他的“双 LLM”模式试图解决“困惑的代理人”(Confused Deputy)攻击和数据外泄问题,但即使他也承认“这个解决方案很糟糕”,并引入了巨大的实现复杂性。核心问题依然存在:如果 LLM 有权使用可以读取私有数据和发起网络请求的工具,你就是在玩一场针对攻击向量的“打地鼠”游戏。
Since we cannot solve this trifecta of capabilities (read data, execute code, network access), pi just gives in. Everybody is running in YOLO mode anyways to get any productive work done, so why not make it the default and only option?既然我们无法解决这三种能力的三角难题(读取数据、执行代码、网络访问),pi 就直接妥协了。反正每个人为了完成工作都在 YOLO 模式下运行,那么为什么不把它作为默认且唯一的选项呢?
By default, pi has no web search or fetch tool. However, it can use curl or read files from disk, both of which provide ample surface area for prompt injection attacks. Malicious content in files or command outputs can influence behavior. If you're uncomfortable with full access, run pi inside a container or use a different tool if you need (faux) guardrails.默认情况下,pi 没有网络搜索或获取工具。但是,它可以使用 curl 或从磁盘读取文件,这两者都为提示词注入攻击提供了充足的表面积。文件或命令输出中的恶意内容可能会影响行为。如果你对完全访问权限感到不舒服,请在容器内运行 pi,或者如果你需要(伪)护栏,请使用其他工具。
No built-in to-dos无内置待办事项
pi does not and will not support built-in to-dos. In my experience, to-do lists generally confuse models more than they help. They add state that the model has to track and update, which introduces more opportunities for things to go wrong.pi 不会也不会支持内置的待办事项。根据我的经验,待办事项列表通常会让模型感到困惑,而不是提供帮助。它们增加了模型必须跟踪和更新的状态,这引入了更多出错的机会。
If you need task tracking, make it externally stateful by writing to a file:如果你需要任务跟踪,通过写入文件使其在外部保持状态:
# TODO.md
- [x] Implement user authentication
- [x] Add database migrations
- [ ] Write API documentation
- [ ] Add rate limiting
The agent can read and update this file as needed. Using checkboxes keeps track of what's done and what remains. Simple, visible, and under your control.代理可以根据需要读取和更新此文件。使用复选框可以跟踪已完成和未完成的内容。简单、可见,且在你的控制之下。
No plan mode无计划模式
pi does not and will not have a built-in plan mode. Telling the agent to think through a problem together with you, without modifying files or executing commands, is generally sufficient.pi 不会也不会有内置的计划模式。告诉代理和你一起思考问题,而不修改文件或执行命令,通常就足够了。
If you need persistent planning across sessions, write it to a file:如果你需要在会话之间进行持久化计划,请将其写入文件:
# PLAN.md
## Goal
Refactor authentication system to support OAuth
## Approach
1. Research OAuth 2.0 flows
2. Design token storage schema
3. Implement authorization server endpoints
4. Update client-side login flow
5. Add tests
## Current Step
Working on step 3 - authorization endpoints
The agent can read, update, and reference the plan as it works. Unlike ephemeral planning modes that only exist within a session, file-based plans can be shared across sessions, and can be versioned with your code.代理在工作时可以读取、更新和引用该计划。与仅存在于会话中的短暂计划模式不同,基于文件的计划可以在会话之间共享,并可以随代码一起进行版本控制。
Funnily enough, Claude Code now has a Plan Mode that's essentially read-only analysis, and it will eventually write a markdown file to disk. And you can basically not use plan mode without approving a shit ton of command invocations, because without that, planning is basically impossible.有趣的是,Claude Code 现在有一个计划模式,本质上是只读分析,它最终会将 Markdown 文件写入磁盘。而且你基本上无法在不批准大量命令调用的情况下使用计划模式,因为没有这些,计划基本上是不可能的。
The difference with pi is that I have full observability of everything. I get to see which sources the agent actually looked at and which ones it totally missed. In Claude Code, the orchestrating Claude instance usually spawns a sub-agent and you have zero visibility into what that sub-agent does. I get to see the markdown file immediately. I can edit it collaboratively with the agent. In short, I need observability for planning and I don't get that with Claude Code's plan mode.pi 的区别在于我对一切都有完全的可观察性。我可以看到代理实际查看了哪些来源,以及它完全错过了哪些。在 Claude Code 中,编排的 Claude 实例通常会生成一个子代理,你对该子代理所做的一切完全不可见。我可以立即看到 Markdown 文件。我可以与代理协作编辑它。简而言之,我需要可观察性来进行计划,而 Claude Code 的计划模式无法提供这一点。
If you must restrict the agent during planning, you can specify which tools it has access to via the CLI:如果你必须在计划期间限制代理,可以通过 CLI 指定它可以访问哪些工具:
pi --tools read,grep,find,ls
This gives you read-only mode for exploration and planning without the agent modifying anything or being able to run bash commands. You won't be happy with that though.这为你提供了用于探索和计划的只读模式,代理无法修改任何内容或运行 bash 命令。不过你可能不会对此感到满意。
No MCP support不支持 MCP
pi does not and will not support MCP. I've written about this extensively, but the TL;DR is: MCP servers are overkill for most use cases, and they come with significant context overhead.pi 不会也不会支持 MCP。我已经深入写过这个问题,但简而言之:MCP 服务器对于大多数用例来说是杀鸡用牛刀,而且它们带来了巨大的上下文开销。
Popular MCP servers like Playwright MCP (21 tools, 13.7k tokens) or Chrome DevTools MCP (26 tools, 18k tokens) dump their entire tool descriptions into your context on every session. That's 7-9% of your context window gone before you even start working. Many of these tools you'll never use in a given session.流行的 MCP 服务器,如 Playwright MCP(21 个工具,13.7k Token)或 Chrome DevTools MCP(26 个工具,18k Token),会在每次会话中将其所有工具描述转储到你的上下文中。在你开始工作之前,你 7-9% 的上下文窗口就已经没了。其中许多工具你在给定的会话中永远不会用到。
The alternative is simple: build CLI tools with README files. The agent reads the README when it needs the tool, pays the token cost only when necessary (progressive disclosure), and can use bash to invoke the tool. This approach is composable (pipe outputs, chain commands), easy to extend (just add another script), and token-efficient.替代方案很简单:构建带有 README 文件的 CLI 工具。代理在需要工具时读取 README,仅在必要时支付 Token 成本(渐进式披露),并且可以使用 bash 来调用该工具。这种方法是可组合的(管道输出、链接命令)、易于扩展(只需添加另一个脚本),并且 Token 效率很高。
Here's how I add web search to pi:以下是我如何向 pi 添加网络搜索:
I maintain a collection of these tools at github.com/badlogic/agent-tools. Each tool is a simple CLI with a README that the agent reads on demand.我在 github.com/badlogic/agent-tools 维护着这些工具的集合。每个工具都是一个简单的 CLI,带有一个代理按需读取的 README。
If you absolutely must use MCP servers, look into Peter Steinberger's mcporter tool that wraps MCP servers as CLI tools.如果你绝对必须使用 MCP 服务器,请查看 Peter Steinberger 的 mcporter 工具,它将 MCP 服务器包装为 CLI 工具。
No background bash无后台 bash
pi's bash tool runs commands synchronously. There's no built-in way to start a dev server, run tests in the background, or interact with a REPL while the command is still running.pi 的 bash 工具同步运行命令。没有内置的方法来启动开发服务器、在后台运行测试或在命令仍在运行时与 REPL 交互。
This is intentional. Background process management adds complexity: you need process tracking, output buffering, cleanup on exit, and ways to send input to running processes. Claude Code handles some of this with their background bash feature, but it has poor observability (a common theme with Claude Code) and forces the agent to track running instances without providing a tool to query them. In earlier Claude Code versions, the agent forgot about all its background processes after context compaction and had no way to query them, so you had to manually kill them. This has since been fixed.这是故意的。后台进程管理增加了复杂性:你需要进程跟踪、输出缓冲、退出时清理以及向运行中的进程发送输入的方法。Claude Code 通过其后台 bash 功能处理了部分内容,但它的可观察性很差(Claude Code 的常见主题),并强制代理跟踪运行中的实例,而不提供查询它们的工具。在早期的 Claude Code 版本中,代理在上下文压缩后忘记了所有后台进程,并且无法查询它们,因此你必须手动杀死它们。这一点后来得到了修复。
Use tmux instead. Here's pi debugging a crashing C program in LLDB:改用 tmux 吧。这是 pi 在 LLDB 中调试崩溃的 C 程序:
How's that for observability? The same approach works for long-running dev servers, watching log output, and similar use cases. And if you wanted to, you could hop into that LLDB session above via tmux and co-debug with the agent. Tmux also gives you a CLI argument to list all active sessions. How nice.这种可观察性如何?同样的方法适用于长时间运行的开发服务器、查看日志输出和类似用例。如果你愿意,你可以通过 tmux 跳入上面的 LLDB 会话,与代理共同调试。Tmux 还为你提供了一个列出所有活动会话的 CLI 参数。多好。
There's simply no need for background bash. Claude Code can use tmux too, you know. Bash is all you need.根本不需要后台 bash。你知道,Claude Code 也可以使用 tmux。Bash 就是你所需要的一切。
No sub-agents无子代理
pi does not have a dedicated sub-agent tool. When Claude Code needs to do something complex, it often spawns a sub-agent to handle part of the task. You have zero visibility into what that sub-agent does. It's a black box within a black box. Context transfer between agents is also poor. The orchestrating agent decides what initial context to pass to the sub-agent, and you generally have little control over that. If the sub-agent makes a mistake, debugging is painful because you can't see the full conversation.pi 没有专门的子代理工具。当 Claude Code 需要做复杂的事情时,它通常会生成一个子代理来处理任务的一部分。你对该子代理所做的事情完全不可见。它是黑盒中的黑盒。代理之间的上下文传输也很差。编排代理决定传递给子代理的初始上下文,你通常对此几乎没有控制权。如果子代理犯了错,调试会很痛苦,因为你无法看到完整的对话。
If you need pi to spawn itself, just ask it to run itself via bash. You could even have it spawn itself inside a tmux session for full observability and the ability to interact with that sub-agent directly.如果你需要 pi 生成它自己,只需要求它通过 bash 运行它自己。你甚至可以让它在 tmux 会话中生成它自己,以获得完全的可观察性,并能够直接与该子代理交互。
But more importantly: fix your workflow, at least the ones that are all about context gathering. People use sub-agents within a session thinking they're saving context space, which is true. But that's the wrong way to think about sub-agents. Using a sub-agent mid-session for context gathering is a sign you didn't plan ahead. If you need to gather context, do that first in its own session. Create an artifact that you can later use in a fresh session to give your agent all the context it needs without polluting its context window with tool outputs. That artifact can be useful for the next feature too, and you get full observability and steerability, which is important during context gathering.但更重要的是:修复你的工作流,至少是那些关于上下文收集的工作流。人们在会话中使用子代理,认为他们节省了上下文空间,这确实是真的。但那是思考子代理的错误方式。在会话中途使用子代理进行上下文收集,表明你没有提前规划。如果你需要收集上下文,请先在它自己的会话中完成。创建一个工件,你可以在稍后的新会话中使用它,为你的代理提供它所需的所有上下文,而不会用工具输出污染它的上下文窗口。该工件对于下一个功能也很有用,并且你获得了完全的可观察性和可控性,这在上下文收集期间很重要。
Because despite popular belief, models are still poor at finding all the context needed for implementing a new feature or fixing a bug. I attribute this to models being trained to only read parts of files rather than full files, so they're hesitant to read everything. Which means they miss important context and can't see what they need to properly complete the task.因为与普遍看法相反,模型在寻找实现新功能或修复 Bug 所需的全部上下文方面表现依然很差。我认为这是因为模型在训练时往往只读取文件的部分内容,而非完整文件,因此它们在读取全部内容时显得犹豫不决。这意味着它们会遗漏重要的上下文,无法看到完成任务所需的关键信息。
Just look at the pi-mono issue tracker and the pull requests. Many get closed or revised because the agents couldn't fully grasp what's needed. That's not the fault of the contributors, which I truly appreciate because even incomplete PRs help me move faster. It just means we trust our agents too much.看看 pi-mono 的问题追踪器和拉取请求(PR)就知道了。许多 PR 被关闭或要求修改,原因都是智能体无法完全理解需求。这并不是贡献者的错,我非常感谢他们的付出,因为即使是不完整的 PR 也能帮我提高效率。这只能说明我们太信任这些智能体了。
I'm not dismissing sub-agents entirely. There are valid use cases. My most common one is code review: I tell pi to spawn itself with a code review prompt (via a custom slash command) and it gets the outputs.我并不是完全否定子智能体(sub-agents)。它们确实有合理的用例。我最常用的场景是代码审查:我告诉 pi 通过自定义斜杠命令启动自身并执行代码审查提示词,然后获取输出结果。
---
description: Run a code review sub-agent
---
Spawn yourself as a sub-agent via bash to do a code review: $@
Use `pi --print` with appropriate arguments. If the user specifies a model,
use `--provider` and `--model` accordingly.
Pass a prompt to the sub-agent asking it to review the code for:
- Bugs and logic errors
- Security issues
- Error handling gaps
Do not read the code yourself. Let the sub-agent do that.
Report the sub-agent's findings.
And here's how I use this to review a pull request on GitHub:以下是我如何利用这种方式在 GitHub 上审查拉取请求:
With a simple prompt, I can select what specific thing I want to review and what model to use. I could even set thinking levels if I wanted to. I can also save out the full review session to a file and hop into that in another pi session if I wanted. Or I can say this is an ephemeral session and it shouldn't be saved to disk. All of that gets translated into a prompt that the main agent reads and based on which it executes itself again via bash. And while I don't get full observability into the inner workings of the sub-agent, I get full observability on its output. Something other harnesses don't really provide, which makes no sense to me.通过简单的提示词,我可以选择具体要审查的内容以及使用的模型。如果需要,我甚至可以设置思维深度。我还可以将完整的审查会话保存到文件,以便在另一个 pi 会话中继续处理,或者将其标记为临时会话,不保存到磁盘。所有这些操作都会转化为提示词,由主智能体读取,并根据提示词再次通过 bash 执行自身。虽然我无法完全洞察子智能体的内部运作,但我可以完全掌控它的输出。其他工具框架往往做不到这一点,这让我感到费解。
Of course, this is a bit of a simulated use case. In reality, I would just spawn a new pi session and ask it to review the pull request, possibly pull it into a branch locally. After I see its initial review, I give my own review and then we work on it together until it's good. That's the workflow I use to not merge garbage code.当然,这算是一个模拟用例。在实际操作中,我会直接启动一个新的 pi 会话,要求它审查拉取请求,或者将其拉取到本地分支。在看到它的初步审查意见后,我会给出自己的反馈,然后我们一起协作直到满意为止。这就是我避免合并垃圾代码的工作流。
Spawning multiple sub-agents to implement various features in parallel is an anti-pattern in my book and doesn't work, unless you don't care if your codebase devolves into a pile of garbage.在我看来,为了并行实现各种功能而启动多个子智能体是一种反模式,根本行不通,除非你不在乎代码库是否会变成一堆垃圾。
Benchmarks基准测试
I make a lot of grandiose claims, but do I have numerical proof that all the contrarian things I say above actually work? I have my lived experience, but that's hard to transport in a blog post and you'd just have to believe me. So I created a Terminal-Bench 2.0 test run for pi with Claude Opus 4.5 and let it compete against Codex, Cursor, Windsurf, and other coding harnesses with their respective native models. Obviously, we all know benchmarks aren't representative of real-world performance, but it's the best I can provide you as a sort of proof that not everything I say is complete bullshit.我发表了很多宏大的言论,但上述那些反直觉的观点是否有数据支持?我有自己的亲身经验,但这很难在博文中传达,你们只能选择相信我。因此,我为 pi 创建了一个 Terminal-Bench 2.0 测试运行,使用 Claude Opus 4.5 模型,让它与 Codex、Cursor、Windsurf 以及其他带有原生模型的编码框架进行对比。显然,我们都知道基准测试并不能代表真实世界的性能,但这是我能提供给你们的最好证明,至少能说明我说的并非全是胡言乱语。
I performed a complete run with five trials per task, which makes the results eligible for submission to the leaderboard. I also started a second run that only runs during CET because I found that error rates (and consequently benchmark results) get worse once PST goes online. Here are the results for the first run:我针对每个任务进行了五次完整的试运行,使结果符合提交排行榜的条件。我还开始了第二次运行,仅在欧洲中部时间(CET)工作,因为我发现一旦太平洋标准时间(PST)进入工作时段,错误率(进而影响基准测试结果)就会上升。以下是第一次运行的结果:
And here's pi's placement on the current leaderboard as of December 2nd, 2025:这是截至 2025 年 12 月 2 日 pi 在当前排行榜上的排名:
And here's the results.json file I've submitted to the Terminal-Bench folks for inclusion in the leaderboard. The bench runner for pi can be found in this repository if you want to reproduce the results. I suggest you use your Claude plan instead of pay-as-you-go.这是我提交给 Terminal-Bench 团队以纳入排行榜的 results.json 文件。如果你想复现这些结果,可以在此仓库中找到 pi 的基准测试运行程序。建议使用 Claude 套餐,而不是按量付费。
Finally, here's a little glimpse into the CET-only run:最后,这是仅在 CET 时段运行的初步概览:
This is going to take another day or so to complete. I will update this blog post once that is done.这还需要一天左右的时间才能完成。完成后我会更新这篇博文。
Also note the ranking of Terminus 2 on the leaderboard. Terminus 2 is the Terminal-Bench team's own minimal agent that just gives the model a tmux session. The model sends commands as text to tmux and parses the terminal output itself. No fancy tools, no file operations, just raw terminal interaction. And it's holding its own against agents with far more sophisticated tooling and works with a diverse set of models. More evidence that a minimal approach can do just as well.还要注意排行榜上 Terminus 2 的排名。Terminus 2 是 Terminal-Bench 团队自己的极简智能体,它只为模型提供一个 tmux 会话。模型以文本形式向 tmux 发送命令,并自行解析终端输出。没有花哨的工具,没有文件操作,只有纯粹的终端交互。它在面对拥有更复杂工具的智能体时依然表现优异,并能兼容多种模型。这进一步证明了极简方案同样有效。
In summary总结
Benchmark results are hilarious, but the real proof is in the pudding. And my pudding is my day-to-day work, where pi has been performing admirably. Twitter is full of context engineering posts and blogs, but I feel like none of the harnesses we currently have actually let you do context engineering. pi is my attempt to build myself a tool where I'm in control as much as possible.基准测试结果很有趣,但实践才是检验真理的唯一标准。我的实践就是我的日常工作,而 pi 在其中表现出色。Twitter 上充斥着关于上下文工程的文章和博客,但我感觉目前我们拥有的工具框架中,没有一个真正让你能够进行上下文工程。pi 是我尝试构建的一个工具,让我能尽可能地掌控一切。
I'm pretty happy with where pi is. There are a few more features I'd like to add, like compaction or tool result streaming, but I don't think there's much more I'll personally need. Missing compaction hasn't been a problem for me personally. For some reason, I'm able to cram hundreds of exchanges between me and the agent into a single session, which I couldn't do with Claude Code without compaction.我对 pi 的现状非常满意。还有一些功能我想添加,比如压缩或工具结果流式传输,但我认为我个人不需要更多功能了。对我个人而言,缺少压缩功能并不是问题。出于某种原因,我能够将我和智能体之间的数百次交流塞进一个会话中,而这在没有压缩功能的 Claude Code 中是做不到的。
That said, I welcome contributions. But as with all my open source projects, I tend to be dictatorial. A lesson I've learned the hard way over the years with my bigger projects. If I close an issue or PR you've sent in, I hope there are no hard feelings. I will also do my best to give you reasons why. I just want to keep this focused and maintainable. If pi doesn't fit your needs, I implore you to fork it. I truly mean it. And if you create something that even better fits my needs, I'll happily join your efforts.话虽如此,我欢迎贡献。但正如我所有的开源项目一样,我倾向于采取独裁式管理——这是我多年来在大型项目中总结出的惨痛教训。如果我关闭了你提交的问题或 PR,希望你不要介意。我会尽力给出原因。我只是想保持项目的专注性和可维护性。如果 pi 不符合你的需求,我恳请你分叉(fork)它,我是认真的。如果你能创造出比它更符合我需求的东西,我会很乐意加入你的行列。
I think some of the learnings above transfer to other harnesses as well. Let me know how that goes for you.我认为上述的一些经验也适用于其他工具框架。期待听到你的反馈。