π Pi Agent 源码学习课程 阶段一 · 3/14

阶段一 · 准备与全景 预计 2~3 小时 依赖:第 1、2 讲

第 3 讲

安装与使用初体验

从 /login 到多模式运行:先把 Pi 当工具用起来,拿到「现象层」的第一手体感,再进源码。

STEP 1 — INSTALL install.sh / npm pi --version cd 项目; pi STEP 2 — FIRST-RUN CONFIG /login /model /thinking STEP 3 — FOUR INTERFACES interactive (TUI) pi -p "..." --mode json --mode rpc

图 3-0 · Pi 的首次启动链路:安装(三种方式任选其一)→ 进入项目目录启动 pi → /login 配置凭据 → /model 选模型 → /thinking 调思考等级;随后四种接口共享同一套会话与模型配置。

学习目标

  • 掌握 Pi 的三种安装方式(官方脚本 / npm / Nix)及各自的更新机制与适用场景;
  • 走完首次配置闭环:/login 登录厂商 → /model 选模型 → /thinking 调思考等级,并知道凭据落在 ~/.pi/agent/auth.json;
  • 理解交互模式的完整工作流:提交 prompt、观察工具调用、在正确时机使用 steering(中途插话)与 follow-up(追加任务),以及 abort 取消;
  • 记住七个会话命令:/tree /fork /clone /compact /export /resume /session;
  • 对比四种运行接口(interactive / print / JSON / RPC)的输入输出形态与适用边界;
  • 建立 ~/.pi/agent/ 用户级配置目录与项目级 .pi/ 的心智地图。

§3.1安装:三种方式对比

Pi 是一个跑在终端里的编码代理(coding agent)。动手读源码之前,先用官方推荐的方式把它装好——能跑起来的系统才值得读。三种安装方式的核心差异不在命令本身,而在依赖是否被锁定与由谁负责更新。

表 3-1 · 三种安装方式对比(依据 quickstart.md 整理)
方式命令依赖处理更新机制适合谁
官方安装脚本
macOS / Linux / Windows
curl -fsSL https://pi.dev/install.sh | sh
powershell -c "irm https://pi.dev/install.ps1 | iex"
锁定全部依赖 pi update 自更新;重跑脚本可卸载 默认推荐:日常使用者
npm 全局安装 npm install -g --ignore-scripts @earendil-works/pi-coding-agent 不锁定传递依赖;要求 Node.js ≥ 22.19 由 npm 管理;正常安装不需要依赖生命周期脚本 已有 Node 工具链、想统一管理全局包的开发者
Nix
macOS / Linux
nix profile add github:earendil-works/pi/stable 从源码构建,可按 tag 锁定版本 nix profile upgrade pi;pi update 不可用 Nix 用户;需要可复现安装
packages/coding-agent/docs/quickstart.mdbash / powershell
# macOS / Linux 官方安装脚本
curl -fsSL https://pi.dev/install.sh | sh

# Windows 官方安装脚本
powershell -c "irm https://pi.dev/install.ps1 | iex"

# npm 方式(不锁定传递依赖,要求 Node.js 22.19 或更新)
npm install -g --ignore-scripts @earendil-works/pi-coding-agent

# Nix 方式(源码构建;旧版本 Nix 用 nix profile install)
nix profile add github:earendil-works/pi/stable

# 验证安装
pi --version

三个容易忽略的细节:第一,官方脚本同时承担安装与更新——重跑 install.sh 会进入菜单,可选择卸载;第二,npm 安装特意加了 --ignore-scripts,因为 Pi 正常安装不需要依赖包的生命周期脚本,关掉它反而更安全;第三,三种方式互不清理——卸载任何一种都不会删除 ~/.pi/agent/ 下的配置、凭据、会话与已安装的 Pi 包。

§3.2首次配置:/login → /model → /thinking

安装完成后的第一次 pi 启动还什么都不能做——Pi 只是个「挽具」(harness),它需要一个可访问的大模型。先分清两个词:模型(model)生成 Pi 的回复;厂商(provider)是你访问该模型所用的服务或账号。

packages/coding-agent/docs/quickstart.mdtext
# 1. 进入 Pi 要工作的目录(目录决定文件发现、指令加载与会话分组)
cd /path/to/folder
pi

# 2. 在 Pi 交互界面内依次执行:
/login      # 选择厂商,按提示完成订阅账号登录或存储 API Key
/model      # (可选)切换到其他可用模型
/thinking   # 调整当前模型的思考(reasoning)强度

/login 支持两类凭据:走 OAuth 订阅账号(打开浏览器授权),或直接存 API Key。无论哪种,凭据都保存在 ~/.pi/agent/auth.json——这也是后续安全话题里最敏感的一个文件。不想进会话也可以在终端里完成:pi auth login <provider>(--api-key 存密钥,--no-browser 走设备码等非浏览器方式,适合 SSH 环境)。

/thinking 的等级从低到高是 off、minimal、low、medium、high、xhigh、max,但会被钳制(clamp)在模型能力范围内——给不支持的模型设 max 不会报错,只会取它支持的上限。交互模式里按 Shift+Tab 循环切换;编辑器边框即当前思考等级的指示器。命令行同样能预设:pi --model sonnet:high 里的 :high 后缀,或 --thinking <level>(后者会覆盖模型后缀)。

  • /model:打开模型选择器;快捷键 Ctrl+L 打开同一选择器。
  • /thinking:选择当前模型使用多少推理能力;Shift+Tab 循环切换。
  • /login 与 /logout:管理厂商凭据的存取。

配置完成后给 Pi 一个真实任务即可。Quickstart 文档给的三个示例任务值得照抄一遍——它们分别覆盖「读文件 + 写文件」「仓库结构分析」「多文件对比」三种典型工作流:

packages/coding-agent/docs/quickstart.mdprompt
Summarize @meeting-notes.md and save the action items to action-items.md.

Explain how this repository is structured and how to run its checks.

Compare @previous.csv with @current.csv and summarize the important changes.

§3.3交互工作流:prompt、steering、follow-up、abort

交互模式(interactive TUI)的界面由三块组成:transcript(对话区,显示你的 prompt、Pi 的回复、每次工具调用、结果与错误)、editor(输入区)与 footer(当前目录、会话、模型、上下文用量与累计费用)。工作流的心脏是一个问题:模型正在干活时,你说的话什么时候生效?

输入侧:prompt 怎么写

  • Enter 发送;Shift+Enter 换行;Ctrl+G 在配置的外部编辑器里写长 prompt;
  • 输入 @ 搜索并引用文件(Tab 补全路径);粘贴或拖入图片;
  • !git status:执行终端命令并把输出计入对话;!!cmd:执行但不把输出发给模型。

观察侧:Pi 怎么干活

Pi 会可视化每一次工具调用与结果——读了哪个文件、跑了什么命令、改了哪段代码,全部实时显示。Ctrl+O 展开/收起工具输出,Ctrl+T 显示/隐藏思考块。注意官方文档的明确声明:Pi 不会在每次工具调用前征求确认,所以重要工作要靠版本控制或备份兜底,不可信任务要进沙箱。

控制侧:三种「插话」的进入时机

这是本讲最值得画图的地方。你输入的消息有三种去向,官方 usage.md 的表述分别是:以 Enter 发送的消息「等待当前响应及其工具调用结束后,指导下一次响应」(steering);以 Alt+Enter 发送的 follow-up「等待 Pi 完成当前任务」;Escape 则停止当前任务,且排队中的消息会退回编辑器。

t prompt 你的输入 (Enter) Turn 1 assistant 回复 + 工具调用 Turn 2 基于结果继续请求模型 任务结束 (agent_settled) Enter · steering 当前响应+工具调用结束后,指导下一次响应 Alt+Enter · follow-up 等待整个任务(含所有 turn)完成后进入 Escape · abort 任意时刻立即取消;排队消息退回编辑器(Alt+Up 也可手动取回)
图 3-1 · 交互控制时间线。steering 与 follow-up 的差别不是「谁更早」,而是边界画在哪:steering 的边界是一个 turn(assistant 回复 + 它触发的工具调用),follow-up 的边界是整个任务(可能包含多个 turn 与自动重试、压缩恢复)。对应 JSON 事件流:agent_end 只关一个底层运行,agent_settled 才表示不会再自动继续。
表 3-2 · 改变方向(依据 usage.md)
意图操作
调整当前任务输入消息并按 Enter(steering)
在当前任务之后追加工作输入消息并按 Alt+Enter(follow-up)
把排队消息取回编辑器Alt+Up(Windows Terminal 有保留键位,见 terminal-setup.md)
停止当前任务Escape(abort;排队消息自动返回编辑器)

§3.4会话命令:树、分叉、克隆与压缩

Pi 会话不是一条直线,而是一棵条目树(entry tree):回到历史某点提问,不会抹掉你离开的那个分支。理解这一点后,七个命令的分工就清晰了。

表 3-3 · 会话命令速查(依据 sessions.md / usage.md)
命令作用何时用
/tree在当前会话文件内的树上移动;选中 user 消息会把文本放回编辑器,改写后提交即产生新分支;离开分支时可生成摘要挂在进入的分支上相关备选方案希望留在同一会话里
/fork从更早的某条 user 消息创建新会话,原会话不动备选方案要成为独立工作
/clone把当前活动分支复制成一个新会话想要当前状态的独立副本
/compact手动压缩:生成摘要并保留近期消息,不删除原始条目;可附指示要求保留特定主题上下文接近模型上限前主动瘦身
/export把当前会话导出为 HTML 或 JSONL归档、评审、离线分享
/resume打开会话选择器(可搜索、重命名、删除);会回到会话自己的工作目录执行换回任意历史会话
/session显示当前会话文件、ID、消息数、token 用量与费用排查「我到底在哪个会话里」

配套的 CLI 入口:pi --continue 恢复当前目录最近一次会话;pi --resume 打开选择器;pi --fork <path|id> 直接分叉。会话默认持久化在 ~/.pi/agent/sessions/,按工作目录分组——每个会话记录创建时的工作目录且永不更改,恢复时 Pi 会在那个目录里运行,工具与 @path 引用都从那里解析。--no-session 则得到一个退出即消失、无法恢复的内存会话。

§3.5四种接口:interactive / print / JSON / RPC

同一套会话与模型配置,有四种消费方式。选型只看两个维度:谁来读输出(人还是程序),以及谁控制生命周期(一次跑完还是长期驻留)。源码层面它们就在 src/modes/ 下各占一个模块。

表 3-4 · 四种运行接口对比(依据 cli.md / json.md / rpc.md)
接口启动输入输出生命周期典型场景
interactive pi(stdin/stdout 均为终端时) TUI 编辑器、斜杠命令、@ 文件 transcript + 工具调用可视化 + footer 长期会话,人工驱动 日常编码、调试、探索
print pi --print "...";任一流被重定向且未选 JSON/RPC 时自动进入 参数 prompt、管道 stdin(前置到首条 prompt) 最终 assistant 文本 → stdout 跑完即退 脚本、CI、一次性问答
JSON pi --mode json "..." > events.jsonl 同 print 严格 JSONL:先 session header,后事件流 跑完即退 程序消费完整事件、观测管道
RPC pi --mode rpc stdin 逐行 JSON 命令(拒绝 @file 参数) stdout:命令 response + 会话事件(无 session header) 长期驻留子进程,直到 shutdown IDE、自定义 UI、跨语言集成

print 与 JSON:同一 prompt 的两种输出

packages/coding-agent/docs/cli.mdsh
pi
pi --print "Summarize this repository"
git diff | pi --print "Review this change"
pi --mode json "Inspect this repository" > events.jsonl

print 模式适合「只要答案」:pi --print "Summarize this repository" 只把最终回复写到 stdout;管道场景 git diff | pi --print "Review this change" 会把 stdin 内容前置到 prompt。要看过程就换 JSON 模式——每一行是一个以 LF 结尾的 JSON 对象,首行是 session header,随后是 agent/turn/message/tool 事件:

packages/coding-agent/docs/json.md(事件序列示例,节选)jsonl
{"type":"session","version":3,"id":"uuid","timestamp":"2024-12-03T14:00:00.000Z","cwd":"/path"}
{"type":"agent_start"}
{"type":"turn_start"}
{"type":"message_start","message":{"role":"user","content":"Review this repository","timestamp":1733234401000}}
{"type":"message_end","message":{"role":"user","content":"Review this repository","timestamp":1733234401000}}
{"type":"message_start","message":{"role":"assistant","content":[],"stopReason":"pending","...":"..."}}
{"type":"message_update","usage":{"...":"..."},"assistantMessageEvent":{"type":"text_delta","contentIndex":0,"delta":"Hello"}}
{"type":"message_end","message":{"role":"assistant","...":"..."}}
{"type":"turn_end","message":{"role":"assistant","...":"..."},"toolResults":[]}
{"type":"agent_end","messages":[{"...":"..."}],"willRetry":false}
{"type":"agent_settled","aborted":false}

两个关键语义:agent_end 只表示一个底层运行结束——自动重试、溢出恢复、压缩重试、steering、follow-up 仍可能继续;agent_settled 才表示 Pi 对这次会话级运行没有剩余的自动工作。文档还给出一个即用的过滤示例:

packages/coding-agent/docs/json.mdbash
# 打印一次性运行里已完成的消息
pi --mode json "List files" 2>/dev/null | jq -c 'select(.type == "message_end")'

RPC:把 Pi 变成受控子进程

RPC 模式通过 stdin/stdout 的 JSONL 记录实现双向控制:stdin 写命令,stdout 收response 与会话事件。命令可带字符串 id,响应原样回带用于关联——因为命令处理是异步的,不能按响应顺序对号入座。

packages/coding-agent/docs/rpc.mdjsonl
# 启动
pi --mode rpc --no-session

# stdin:查询状态 → stdout:带相同 id 的 response
{"id":"req-1","type":"get_state"}
{"id":"req-1","type":"response","command":"get_state","success":true,"data":{"...":"..."}}

# stdin:发送 prompt;success=true 只表示「已接受」,不代表模型跑完
{"id":"req-2","type":"prompt","message":"Review this repository"}
{"id":"req-2","type":"response","command":"prompt","success":true,"data":{"disposition":"started"}}

# 之后继续消费事件流,直到 agent_settled

注意 prompt 的响应语义:success:true 只说明 prompt 被接受、排队或处理(disposition:"started" 表示已启动运行),模型何时跑完要看后续事件;若 disposition:"handled" 则该 prompt 没有启动运行,不必等 agent_settled。帧解析也有一条官方警告:不要用 Node.js 的 readline——它会把 JSON 字符串内合法的 U+2028/U+2029 也当行边界,必须按字节/LF 切分,并持续读取 stdout 以免管道缓冲填满卡住进程。

源码速览:modes/ 目录

四种接口在仓库里各有落点。现在只需记住地图,第 8 讲与第 13 讲会深入:

packages/coding-agent/src/modes/(目录节选)tree
modes/
├── index.ts                # 模式分发入口
├── interactive/            # 交互 TUI(interactive-mode.ts、tui-renderer.ts)
│   ├── components/         # footer / model-selector / thinking-selector /
│   │                       # tree-selector / login-dialog / tool-execution …
│   └── theme/              # dark.json / light.json / theme-controller
├── print-mode.ts           # print 模式
├── json-event.ts           # JSON 事件流的线格式转换
└── rpc/                    # rpc-mode.ts / rpc-client.ts / rpc-types.ts / jsonl.ts

§3.6配置地图:~/.pi/agent/ 与项目级 .pi/

用户级配置全部住在agent 目录(默认 ~/.pi/agent,可用环境变量 PI_CODING_AGENT_DIR 重定位);项目级配置住在工作目录的 .pi/ 下,且在项目获得信任后才加载。第 3 讲先建立地图,每个文件在后续讲次逐一展开。

图 3-2 · agent 目录结构(依据 configuration.md 的 Agent directory 与 Project .pi directory 两表)。注意 sessionDir 是唯一在信任判定前读取的例外。

§3.7动手实践

动手实践 · 三项任务
  1. 完整工具调用链观察:完成安装与 /login 后,找一个有真实 bug 的小仓库,让 Pi 修复它。全程不要打断,观察 transcript 里每一次 read / bash / edit 的参数与结果,用 Ctrl+O 展开输出,记录「一次任务里 Pi 自己发起了几轮工具调用」。
  2. print 对 JSON:用同一个 prompt 分别执行 pi --print "Explain how this repository is structured" 与 pi --mode json "Explain how this repository is structured" > events.jsonl,对比 stdout 差异;在 JSONL 里数一数 turn_start 与 tool_execution_start 的次数。
  3. 分支体验:在会话中问两个问题后执行 /tree,选中第一条 user 消息、改写提问并提交,观察新分支的诞生;再用 /fork 从同一节点分叉,最后 /resume 对比两个会话文件。

§3.8常见误区

误区一:npm 安装「失败」是因为禁用了 scripts

--ignore-scripts 不是妥协,而是官方推荐写法——Pi 的正常 npm 安装不需要依赖包的生命周期脚本。真正要注意的是它不锁定传递依赖,且要求 Node ≥ 22.19;追求确定性请用官方安装脚本或 Nix。

误区二:以为 steering 会立刻打断模型

steering 不是中断,是排队:消息等到当前响应及其工具调用都结束后才进入,用于指导下一次响应。想立刻停手是 Escape(abort)的职责;想在整个任务后追加是 Alt+Enter(follow-up)的职责。

误区三:以为 /fork 会改动原会话,或 /compact 会删历史

/fork 与 /clone 都是新建会话,原会话文件不动;/tree 才是同一文件内的分支移动。/compact 生成摘要并保留近期消息,但从不删除原始条目——它改变的是发给模型的视图,不是持久化日志。

误区四:用 Node readline 解析 JSON/RPC 流

JSONL 帧只按 LF 切分,而 readline 会把 JSON 字符串里合法的 U+2028/U+2029 也当边界。正确做法是按字节读、按 LF 切、可选去掉前置 CR;同时要持续读取 stdout,否则管道缓冲填满会卡住 Pi。

§3.9自测问答

1. steering 与 follow-up 的进入时机有何差异?

steering(Enter 发送)等到当前响应及其工具调用结束后进入,用于指导下一次响应——边界是一个 turn。follow-up(Alt+Enter 发送)等到 Pi 完成整个当前任务后进入——边界是整个运行(可能含多个 turn、自动重试与压缩恢复)。

2. JSON 模式输出的每一行是什么?首行和结束判断有什么讲究?

每行是一个以 LF 结尾的 JSON 对象(严格 JSONL 帧)。首行是 session header({"type":"session","version":3,...}),随后是 agent/turn/message/tool 等事件。结束判断看 agent_settled 而非 agent_end:前者表示没有剩余自动工作(含重试、压缩恢复、排队消息),后者只结束一个底层运行。

3. 会话默认存在哪里?按什么分组?恢复会话时在哪个目录运行?

默认在 ~/.pi/agent/sessions/,按工作目录分组。每个会话记录创建时的工作目录且永不更改;pi --continue 恢复当前目录的最近会话,而 pi --resume / --session / /import 会在会话自己的工作目录里运行(可能与当前目录不同)。

4. 三种安装方式各自的更新机制是什么?

官方安装脚本:依赖全部锁定,用 pi update 自更新,重跑安装脚本可选择卸载。npm:由 npm 管理全局包,要求 Node ≥ 22.19,不锁定传递依赖。Nix:nix profile upgrade pi 更新(可按 tag 锁定),pi update 对 Nix 安装不可用。

5. /tree、/fork、/clone 三者的边界差异?

/tree 在同一个会话文件内的条目树上移动,适合相关的备选方案共存;/fork 从更早的 user 消息创建新会话,适合备选方案独立成工作;/clone 把当前活动分支复制成新会话,适合对当前状态做独立副本。三者都不破坏被离开的分支。

6. RPC 模式里 prompt 命令返回 success:true 意味着什么?@file 参数能用吗?

只表示 prompt 被接受、排队或处理(看 data.disposition,如 "started"),不代表模型工作完成,还要继续消费事件直到 agent_settled;若 disposition 为 "handled" 则没有启动运行。RPC 模式拒绝 @file 参数,文件内容应通过 prompt 命令传递。


— 第 3 讲完 · 下一讲进入模型层 —