第一部:先把 Pi 跑起来
从固定源码版本开始,沿 pi 命令的模式分派、启动组装、项目信任与资源加载,一直走到 createAgentSession() 返回可运行会话。

展开阅读路线与实验入口
这一部处理一个很朴素的问题:输入 pi 之后,真正可以接收消息的会话是怎样出现的。
Pi 的源码把模型 API、通用 Agent、终端 UI 和 Coding Agent 产品放在同一个 monorepo。目录名很容易让人产生一条想当然的链路:CLI 调 TUI,TUI 调 Agent,Agent 再调模型。实际启动并不是这样。pi 的进程入口先做少量环境初始化,然后把原始 argv 交给 main();TUI 要等会话 runtime 完整建立以后才会出现,print 和 RPC 则根本不需要 TUI。
读这一部之前,需要知道什么
不用先掌握所有 provider,也不用理解 Agent loop。只要能读 TypeScript 的 interface、class、async function,知道 stdin/stdout 的 TTY 状态会影响 CLI 行为,就可以跟上本部。我们只分析 earendil-works/pi 的固定开源版本,不拿 README 的概念图代替实现,也不把其他 fork 或后续 Harness v2 设计混进当前调用链。
所有源码证据锁定 v0.83.0 / 845d6ff1f6643aba440341cce877ce1c43ebbc39。固定版本不是为了让行号好看。Pi 的启动层同时涉及 CLI 参数、session 恢复、项目 trust、扩展 provider 注册和 mode runner;从不同 commit 各取一段,很容易拼出一条每个符号都存在、整体却从未运行过的路径。
实验也刻意收窄。本部使用 git show、git grep、git describe 读取固定 checkout,不直接运行 pi。这是因为启动 Pi 可能读取用户级设置、恢复 session,或者在 trusted project 中执行 extension。静态实验能证明定义、分支与调用点,不能证明模型 API 可用、某个 extension 安全,也不能替代真实进程 trace。这个证据边界会一直保留。
五章沿一条启动链前进
第 1 章先拆开两张图。workspace membership、build script 和 package dependencies 属于构建与发布图;cli.ts 调 main()、main() 调 service/session factory 才属于运行时图。随后给 pi-ai、pi-agent-core、pi-tui、pi-coding-agent 标出责任边界。
第 2 章处理最早的运行分叉。Pi 内部的 AppMode 有 interactive、print、json、rpc 四种;外部 --mode 参数却只有 text、json、rpc,print 由单独的 -p 或非 TTY 条件触发。JSON print 与 RPC 都输出 JSONL,但前者一次性消费 prompt,后者把 stdin 留给长驻双向命令协议。
第 3 章不再按文件顺序复述 main.ts,而是追 assembly ownership。恢复旧 session 可能改变有效 cwd,所以 Pi 必须先选定 SessionManager,再按那个 cwd 解析项目设置、资源、provider 和模型。AgentSessionServices 是基础设施集合,AgentSession 在其后创建,具体 I/O mode 又在 session runtime 之后绑定。
第 4 章进入最容易被一句“读取配置”带过的部分。全局 settings、项目 settings、CLI options 沿不同路径进入;未 trusted 的项目 settings 不参与合并。ResourceLoader 还会先以 untrusted 状态预载用户级与显式 CLI 资源,完成 trust 决定后再加载最终 extensions、skills、prompts、themes 和项目上下文。这里的 trust 管项目代码与资源加载,不是 bash sandbox。
第 5 章收束到 createAgentSession()。它补齐或复用 ModelRuntime、SettingsManager、SessionManager 与 ResourceLoader,恢复模型和 thinking level,计算工具集合,先构造底层 Agent,再构造 AgentSession。后者订阅 Agent 事件、建立工具 registry 和 ExtensionRunner,才交出能接收 prompt() 的会话。
flowchart LR
accTitle: 第一部阅读路径
accDescr: 从固定版本与 CLI 入口开始,依次经过模式选择、有效 cwd、信任与资源、会话构造,最后停在 AgentMessage 边界之前。
PIN["v0.83.0"] --> CLI["pi / main"]
CLI --> MODE["four AppModes"]
CLI --> CWD["effective session cwd"]
CWD --> TRUST["settings + trust + resources"]
TRUST --> SERVICES["cwd-bound services"]
SERVICES --> SESSION["Agent + AgentSession"]
SESSION --> NEXT["第 6 章:AgentMessage"]
本部做到哪里就停
这一部不会解释 provider payload、流式事件、工具批次、compaction 或 session tree。我们只证明运行前对象怎样被选中和组装。createAgentSession() 返回以后,调用方仍需提交 prompt;Agent 内部的 AgentMessage[] 也不能原样当成 provider 的 Message[]。第 6 章会从这个类型边界继续,进入一次模型请求真正发生之前的转换过程。
开始前只需运行这组只读校验:
repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
test "$(git -C "$repo" rev-parse HEAD)" = \
845d6ff1f6643aba440341cce877ce1c43ebbc39
test "$(git -C "$repo" describe --tags --exact-match HEAD)" = v0.83.0
两条命令静默通过,才进入第 1 章。
先固定版本,再谈 Pi 的架构
把 Pi 固定到 v0.83.0,区分 monorepo 的构建依赖与真实运行时调用,并建立 coding-agent、agent、ai、tui 四层源码地图。
读 Pi 最容易走偏的地方,是先把 packages/ 目录画成架构图。ai、agent、tui、coding-agent 的名字确实很清楚,但目录关系回答的是“代码如何组织和构建”,还没有回答“输入 pi 后谁调用谁”。这本书先固定版本,再给每一条运行时箭头找调用点。
先把所有证据钉在同一个 tag
本版只对应 v0.83.0,commit 是 845d6ff1f6643aba440341cce877ce1c43ebbc39。下面的命令只读取 Git 对象,不安装依赖,也不执行仓库里的脚本:
repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
test "$(git -C "$repo" rev-parse HEAD)" = \
845d6ff1f6643aba440341cce877ce1c43ebbc39
test "$(git -C "$repo" describe --tags --exact-match HEAD)" = v0.83.0
printf 'Pi source: %s @ %s\n' \
"$(git -C "$repo" describe --tags --exact-match HEAD)" \
"$(git -C "$repo" rev-parse --short=12 HEAD)"
预期最后输出 Pi source: v0.83.0 @ 845d6ff1f664。第一条 test 失败就应停止阅读;否则后面即使类型名相同,行号和控制流也可能已属于另一版。

终端先核对 tag、完整 commit 和 clean status,再按 packages/*/src
统计文件数量。它证明本册使用的证据 checkout 未混入其他版本,也直观展示 monorepo
的体量差异;文件数只描述代码分布,不能替代运行时调用链。
根目录声明了 workspace 成员,build 脚本也规定了若干 package 的构建先后。这些信息对复现构建有用,却不构成一次用户输入的动态时序。
coding-agent 的发布清单把 pi 映射到 dist/cli.js,同时从包根导出 SDK,并另行导出 rpc-entry。这是产品入口地图,不是完整调用链。
四个 package,各自只回答一类问题
pi-ai 负责模型、provider 与统一 LLM API;pi-agent-core 保存通用 Agent 状态并驱动回路;pi-tui 提供终端组件和差分渲染;pi-coding-agent 才把会话、工具、资源、配置和几种 I/O 模式装成 pi 产品。三个底层包都可以独立发布,不能因为 coding-agent/package.json 声明了它们,就写成“coding-agent 启动后依次调用 agent、ai、tui”。依赖声明只证明代码可以引用这些包。
真正的第一条运行时边出现在 cli.ts:它设置进程属性、配置 HTTP dispatcher,然后把 process.argv.slice(2) 交给 main()。从这里开始,箭头才有函数调用支撑。
flowchart LR
accTitle: Pi 第一版源码地图
accDescr: pi CLI 进入 coding-agent;coding-agent 组装 AgentSession,通用 Agent 通过模型运行时访问 provider;只有交互模式使用 TUI。
CLI["pi / cli.ts"] --> MAIN["coding-agent main.ts"]
MAIN --> SESSION["AgentSession"]
SESSION --> CORE["pi-agent-core / Agent"]
CORE --> AI["pi-ai / model + provider"]
MAIN -->|"interactive only"| TUI["pi-tui"]
可以再做一次只读定位,观察 package 依赖与调用点的差别:
repo="${PI_SOURCE_DIR:-/tmp/pi-handbook-qbQTcA}"
git -C "$repo" grep -n 'main(process.argv.slice(2))' v0.83.0 -- \
packages/coding-agent/src/cli.ts
git -C "$repo" grep -n 'createAgentSessionFromServices' v0.83.0 -- \
packages/coding-agent/src/main.ts
第一条落在进程入口,第二条落在启动组装的后半段。中间还隔着模式、cwd、配置、信任与资源。下一章先处理最早出现的分叉:同一个 pi 命令,为什么会变成四种运行方式。