Pi 这个编码智能体,故意不做 plan mode 和子智能体

·10 分钟阅读·Evergreen Tools Team

Pi 是 Earendil 出的一个终端编码智能体,MIT 许可,10 月 1 日发布首个稳定版,之后 10 月 21 日跟了 v1.4.0。它的自我描述只有一句:「一个极简的、可扩展的智能体 harness,你可以把它改成你自己的」。然后它明确写了:Pi 自带强大的默认值,但刻意跳过 sub-agents 和 plan mode 这类功能——想要就自己让 Pi 写,或者装一个按你想法实现的包。我一开始以为这是功能没做完。读完它的包结构和四种驱动方式之后,我改主意了:这是一个立场,不是一个缺口。

终端窗口里显示的命令行输出

极简 harness 的四层扩展面:prompt template、theme、skill、extension。

装的时候,注意安装器做了一件不显眼的事

官方推荐用 curl 脚本安装,Windows 用 PowerShell。脚本会把所有依赖钉死版本,之后用 pi update 显式升级。这事听起来很无聊,直到你调试一个「同一个提示词两次运行结果不同」的问题,才发现是 harness 在两次之间悄悄变了。

npm 路线也存在,但明确写着不钉死传递依赖。另外要加 --ignore-scripts:官方文档说正常安装不需要依赖的生命周期脚本,那允许它们就是给自己增加不必要的攻击面。

要求 Node.js 22.19 或更新。Nix 用户可以走 nix run github:earendil-works/pi/stable 从源码构建,可复现。

在 CI 里务必把版本钉死。这是「构建能复现」和「构建某个周二莫名其妙挂掉」之间的全部区别。

# Install, and note what the installer actually does: it pins dependencies.
# That sounds boring until you have debugged a harness that changed under you
# between two runs of the same prompt.

curl -fsSL https://pi.dev/install.sh | sh      # macOS / Linux
# powershell -c "irm https://pi.dev/install.ps1 | iex"   # Windows

pi --version
pi update          # the pinned path, update explicitly

# The npm route exists but does not pin transitive dependencies:
#   npm install -g --ignore-scripts @earendil-works/pi-coding-agent
#
# Requirement: Node.js 22.19 or newer. The macOS/Linux/Windows installers can
# install Node if it is missing. `--ignore-scripts` is not paranoia: Pi states
# it does not need dependency lifecycle scripts for a normal install, so
# allowing them is unnecessary surface area.
#
# Nix users get a reproducible build from source:
#   nix run github:earendil-works/pi/stable
#   nix profile add github:earendil-works/pi/stable

# Then, in the project you want it to work on:
cd /path/to/project
pi
# Inside, run /login to connect a subscription or an API key.

# For CI, pin the version. This is the whole difference between a build that
# reproduces and a build that fails on a Tuesday for no reason.
npm install -g @earendil-works/[email protected] --ignore-scripts
pi --version | grep -q "1.4.0" || { echo "pi version drifted" >&2; exit 1; }

不做 plan mode 和子智能体,是一个架构立场

官方的说法很直白:自带强大的默认值,但跳过 sub-agents 和 plan mode。想要就自己让 Pi 写,或者装一个包实现你想要的方式。

为什么我觉得这是立场而不是缺口?因为 plan mode 和子智能体编排本质上是策略决定——智能体可以在无人看管的状态下做多少事、并行放在哪一层。把这些硬编码进去,等于每个团队都继承某一个团队对这个问题的答案。

它给的是四层扩展面,从轻到重:prompt template(可复用的指令文本)、theme(终端外观,零行为风险)、skill(智能体能调用的能力)、extension(挂进 harness 的代码)。一个 Pi package 把它们打包,通过 npm 或 git 分发。

这个心智模型就是「智能体行为的 npm」,而它带来同一个后果:你的智能体行为现在是一个需要版本管理的依赖。

# The design statement is one sentence: "a minimal, extensible agent harness
# that you can make your own." Pi ships powerful defaults but deliberately
# skips sub-agents and plan mode. Ask it to build what you want, or install a
# package that does it your way.
#
# That is a real architectural position, not a gap in the roadmap. Plan mode
# and sub-agent orchestration are policy decisions: how much the agent may do
# unattended, and where parallelism lives. Hard-coding them means every team
# inherits one team's answer.
#
# Four extension surfaces, in increasing order of effort:
#
#   prompt-template  reusable instruction text
#   theme            terminal appearance; zero behavioural risk
#   skill            a capability the agent can invoke
#   extension        code that hooks the harness
#
# A Pi package bundles them and is shared over npm or git. The mental model is
# npm for agent behaviour, and it has the same consequence: your agent's
# behaviour is now a dependency you version.

mkdir -p ~/.pi/packages/team-conventions

cat > ~/.pi/packages/team-conventions/package.json <<'JSON'
{
  "name": "pi-pkg-team-conventions",
  "version": "0.1.0",
  "pi": { "promptTemplates": ["templates"], "skills": ["skills"] }
}
JSON

mkdir -p ~/.pi/packages/team-conventions/templates
cat > ~/.pi/packages/team-conventions/templates/review-diff.md <<'MD'
Review the current diff against these rules, in order:

1. Does every changed file appear in the task description? List any that do not.
2. Is there a test that fails without this change? Name it, or say "none".
3. Any new dependency? If yes, stop and ask.
4. Any edit to an applied migration? If yes, stop and refuse.

Output: a verdict line, then at most five bullet findings. No summary paragraph.
MD

# Why this is the interesting bet: conventions become installable artefacts
# instead of an onboarding doc. A new repository gets your review rules with
# one install, and updating them is a version bump, not a Slack announcement.

四种驱动方式,选错是最常见的集成错误

交互式(人在键盘前)、print/JSON(一次调用、机器可读、可脚本化)、RPC(一个长驻进程被另一个程序驱动)、TypeScript SDK(在 harness 之上建应用)。

大部分团队先上手 TUI,然后发现没法测,然后围绕 print 模式把一切重写一遍。这个弯路很值得提前避开。

print 模式属于 CI 和定时任务。JSON 模式属于「由另一个程序决定下一步做什么」的流水线。

有一个失败模式必须提前设计:智能体什么都没做,但 exit code 是 0。所以永远断言内容,不要只看退出码。空结果在一个非空仓库里是一个信号,不是成功。

# Four ways to drive it, and picking the wrong one is the usual integration
# mistake. Most teams start with the TUI, discover they cannot test it, and
# then rewrite everything around print mode.
#
#   1. interactive    a human at a keyboard
#   2. print / JSON   one shot, machine readable, scriptable
#   3. RPC            a long-lived process driven by another program
#   4. TypeScript SDK build an application on top of the harness
#
# Print mode is the one that belongs in CI and in cron jobs. JSON mode is the
# one that belongs in a pipeline where something else decides what to do next.

# One-shot, human readable
pi "Summarise the failing test in test/api/search.spec.ts and propose a fix"

# One-shot, JSON, for a script that parses the result
pi --json "List every file that imports the deprecated config module" \
  > /tmp/imports.json

# The failure mode to design around: agents that exit 0 while doing nothing.
# Always assert on content, never on exit status alone.
python3 - <<'PY'
import json, subprocess, sys

def run(prompt):
    p = subprocess.run(["pi", "--json", prompt], capture_output=True, text=True, timeout=600)
    try:
        return json.loads(p.stdout)
    except json.JSONDecodeError:
        print("non-JSON output, harness probably errored:", p.stdout[:200], file=sys.stderr)
        return None

out = run("Which modules import the deprecated config module? Return file paths only.")
if not out:
    sys.exit(1)
files = out.get("files") or []
print(f"found {len(files)} importers")
if not files:
    # Empty result on a non-empty repository is a signal, not a success.
    print("WARNING: zero importers returned; verify the prompt or the repo path", file=sys.stderr)
    sys.exit(2)
PY
屏幕上密集排列的代码

print / JSON 模式属于 CI;交互式 TUI 属于探索。别搞反。

RPC 和 SDK:当 harness 是一个组件而不是一个工具

Pi 是 monorepo,它把内部库也作为包暴露出来:chord(带类型的宿主运行时,支持热重载插件、契约、复制状态、JSON 链路上的 RPC)、pi-telemetry(厂商中立的遥测契约、参考适配器、一致性测试)、pi-ai(统一的多 provider LLM API)。

pi-ai 是里面最实用的那个。provider 之间的差异是任何智能体项目里最没意思的部分,而它恰好是所有人都在重写的那部分。

在自己写包装层的时候,我建议强制两件事:maxTurns 永远要设(不受约束的智能体就是不受约束的账单),以及文件系统允许清单(「不要改源文件」是一句请求,不是一个权限边界)。

遥测这点比听起来重要。如果 harness 发出的是中立契约,你换厂商不用重写面板;如果它发的是 provider 专属的 span,那你的可观测性就和模型选择绑死了。

# RPC and the SDK are for when the harness is a component rather than a
# tool. The reason to care: Pi's monorepo exposes its libraries as packages,
# so the pieces are usable separately.
#
#   @earendil-works/chord         typed host runtime for hot-reloadable plugins,
#                                 contracts, replicated state, RPC over JSON links
#   @earendil-works/pi-telemetry  vendor-neutral telemetry contracts + adapters
#   @earendil-works/pi-ai         unified multi-provider LLM API
#
# That last one is quietly the most useful. Provider differences are the least
# interesting part of any agent project, and it is the part everyone rebuilds.

# SDK shape, per the official docs (see packages/coding-agent/docs/sdk.md):
cat > probe.mjs <<'JS'
import { PiAgent } from "@earendil-works/pi-coding-agent";

const agent = await PiAgent.create({
  cwd: process.cwd(),
  // model/provider comes from your /login, or from pi-ai config
});

const result = await agent.run(
  "Add a regression test for the empty-cart total. Do not change source files.",
  { maxTurns: 12 }
);

console.log(result.summary);
console.log("tool calls:", result.toolCalls?.length ?? 0);

// Two properties worth enforcing in your own wrapper:
//   1. maxTurns, always. An unconstrained agent is an unconstrained bill.
//   2. a filesystem allowlist. "Do not change source files" is a request,
//      not a permission boundary.
await agent.close();
JS
node probe.mjs

# Telemetry matters more than it sounds. If the harness emits a neutral
# contract, you can swap vendors without rewriting dashboards. If it emits
# provider-specific spans, your observability is coupled to your model choice.

极简的代价:约束要自己建

极简意味着更少的策略默认值,也就意味着更多的围栏是你自己的责任。这才是真正的取舍,不是「功能少所以轻」。

三条必须自己补的:一是被强制执行而不是被请求的权限边界;二是能让运行失败的 diff 大小上限;三是留在工作树之外、关于实际执行了什么的记录。

我的做法是:先 git worktree 加 --detach 开一份独立工作树,agent 只在这份副本里干活,退出时删掉;进沙箱之前把 AWS、GitHub、npm 的密钥从环境里 unset 掉——一个能读文件的编码智能体会读到你的 shell 导出的一切;运行时同时上时间上限和轮次上限,谁先到谁结束;结束后看 git diff --stat,不看它自己说了什么。

最后说回那个立场。如果你要的是「开箱就能无人看管跑一整夜」,Pi 不是这个定位,它甚至没打算让你这么用。如果你要的是「一个能被塞进我自己流程里、行为可版本化的组件」,那它省下来的复杂度是实打实的。这两个需求不一样,选错了会很痛苦。

#!/usr/bin/env bash
# Running a minimal harness safely. Minimal means fewer policy defaults, so
# more of the containment is yours to build. That is the actual trade.
set -euo pipefail

REPO="${1:?usage: run-pi.sh /path/to/repo}"

echo "== version pin =="
pi --version

echo "== containment: work on a copy, not the original =="
WORK="$(mktemp -d)/repo"
git -C "$REPO" worktree add --detach "$WORK" HEAD
trap 'git -C "$REPO" worktree remove --force "$WORK" 2>/dev/null || true' EXIT
cd "$WORK"

echo "== containment: no credentials in the sandbox =="
# A coding agent reads files. If your shell has AWS keys exported, so does it.
unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY GITHUB_TOKEN NPM_TOKEN || true

echo "== run with hard limits =="
# Time limit AND turn limit. Whichever trips first ends the run.
pi --json "Run the test suite and fix only the failures in src/pricing/" \
  > /tmp/pi-out.json 2>/tmp/pi-err.log || echo "pi exited non-zero"

echo "== review what changed, not what it said =="
git add -A
git diff --cached --stat
CHANGED=$(git diff --cached --numstat | awk '{s+=$1+$2} END {print s+0}')
echo "changed lines: $CHANGED"
[ "$CHANGED" -le 400 ] || echo "WARN: diff exceeds 400 lines"

echo "== keep the evidence =="
cp /tmp/pi-out.json /tmp/pi-err.log . 2>/dev/null || true
echo "worktree: $WORK (removed on exit)"

# The three things a minimal harness does not give you, and you must supply:
#   1. a permission boundary that is enforced, not requested
#   2. a diff size ceiling that fails the run
#   3. a record of what was actually executed, kept outside the worktree
机房中成排的服务器机柜

极简的代价:权限边界、diff 上限、执行记录,三样都得自己建。

📌 常见问题 FAQ

Pi 是什么?谁做的?什么许可?

Pi 是一个极简、可扩展的终端编码智能体 harness,由 Earendil(earendil-works 组织)维护,MIT 许可。2026 年 10 月 1 日发布首个稳定版 Pi 1.0,之后持续发布,10 月 21 日为 v1.4.0。代码在 GitHub 的 earendil-works/pi,文档在 pi.dev。

它真的没有 plan mode 和子智能体吗?

官方 README 原文是「Pi ships with powerful defaults but skips features like sub-agents and plan mode」,并建议需要的话让 Pi 自己写,或安装按你想要方式实现的包。所以是没有内置,不是无法实现。

四种运行方式分别适合什么场景?

交互式适合人在键盘前探索;print / JSON 模式适合 CI、定时任务和需要被脚本解析的流水线;RPC 适合由另一个长驻程序驱动;TypeScript SDK 适合在 harness 之上构建自己的应用。

安装时为什么建议加 --ignore-scripts?

官方说明 Pi 在正常 npm 安装下不需要依赖的生命周期脚本,因此禁用它们不会破坏安装,同时减少供应链攻击面。另外官方推荐的 curl / PowerShell 安装器会钉死所有依赖版本,比 npm 路线更可复现。

极简 harness 需要自己补哪些安全措施?

至少三条:用 git worktree 在独立副本上操作并在结束时清理;进入沙箱前清除环境里的云厂商和代码托管密钥;同时设置时间上限与轮次上限,并在运行后用 git diff --stat 检查实际改动,而不是相信智能体的自述。