← ursb.me
FIELD NOTE / 10 Pi Agent · Harness 工程 Pi Agent · Harness Engineering 2026

一条用户消息在
Pi Agent里的一生

The life of one user message
inside Pi Agent.

CLI → AgentSession → agentLoop → pi-ai → tools → JSONL → TUI diff

CLI → AgentSession → agentLoop → pi-ai → tools → JSONL → TUI diff

你在终端里敲下一行「读取 README.md,用一句话告诉我这个项目做什么」之后,这串文字要穿过 26 道工序、跨 6 个 npm 包、在 3 层消息模型里变形,最后才变成屏幕上滚动的 token 和磁盘上的一行 JSONL。对照 earendil-works/pi 生产源码与 hahhforest/pi-textbook 的 15 个 checkpoint,把每一步的类层级、事件流、owner 分工、可视化全摊开。

After you type «read README.md and tell me what this project does in one sentence», that string passes through 26 stations, crosses six npm packages, morphs across three message layers, and only then becomes scrolling tokens on screen and one JSONL line on disk. Cross-reading earendil-works/pi production source with hahhforest/pi-textbook's 15 checkpoints — every step: class hierarchy, event flow, owner roles, and diagrams.

一条用户消息 · 26 个站 One user message · 26 stations ▸ live trace
回车Enter JSONLJSONL
OVERVIEW · 森林图 OVERVIEW · Forest map

先见森林,再见树木

See the forest before the trees

全文 26 章不是平铺的百科条目,而是一条固定主线 prompt从终端回车追到 JSONL 落盘的流水线。下面这张图是望远镜:告诉你 Phase 在哪、章节密度在哪、心脏区(runLoop + tools)在哪。读任何一章前,先看这里的 compass 条(每章顶部)确认自己在全局中的位置。

All 26 chapters are not a flat encyclopedia — they are a pipeline tracing one fixed through-line prompt from Enter to JSONL on disk. The diagram below is the telescope: where phases sit, where chapter density peaks, where the heart (runLoop + tools) lives. Before any chapter, check the compass bar at the top to locate yourself in the whole.

FIG 森林图:8 个 Phase 把 26 章串成一条线;Phase II(心脏)是 runLoop 与工具执行最密集的区域。 Forest map: 8 phases string 26 chapters; Phase II (Heart) is the densest runLoop + tool region.
标题层级约定:每章仅一个 h2 章标题;正文小节为 C07.1 式编号;「深度细读」块内为 C07.4+,FAQ / Case Study 不再占用标题层级。 Heading convention: one h2 chapter title; body sections numbered C07.1; deep-dive block uses C07.4+; FAQ / Case Study use labels, not headings.
CHAPTER 01 · 引子

Harness 不是 Product

Harness is not a product

agent 产品战争里的第三条路

a third path in the agent product war

模块Module
哲学层Philosophy
Package
pi-monopi-mono
线程Thread
主线未进入Through-line not entered
输出Output
心智模型Mental model

2025–2026 年的 coding agent 市场被两条路线撕成两半:密封产品(Claude Code、Cursor、Windsurf)和可组合 harness(Pi、OpenCode、aider 变体)。我们的主线 prompt——读取 README.md,用一句话告诉我这个项目做什么——在两种架构里走的路径完全不同:产品把中间层焊死在二进制里;harness 把每一站写成可测试的 TypeScript。

The 2025–2026 coding-agent market splits into sealed products (Claude Code, Cursor, Windsurf) and composable harnesses (Pi, OpenCode, aider variants). Our through-line prompt — read README.md and tell me what this project does in one sentence — takes completely different paths: products weld the middle layers into a binary; harnesses write every station as testable TypeScript.

产品思维Product mindset
用户买的是体验闭环——从安装到第一次 commit 零配置Users buy a closed loop — zero config from install to first commit
Harness 思维Harness mindset
开发者买的是可观测编排——每一层都有源码和测试Developers buy observable orchestration — every layer has source and tests
Pi 的赌注Pi's bet
终端原生 + JSONL 会话树 + TypeScript 扩展 > IDE 锁定Terminal-native + JSONL session tree + TS extensions > IDE lock-in
代价The cost
没有 MCP 协议层、没有子 agent、没有 Plan 模式——故意不做No MCP layer, no sub-agents, no plan mode — intentionally omitted
架构公式ARCHITECTURE
Product = UX × Integrations × Policy Harness = AgentLoop × Tools × Session × Extensions
Pi 选择右边那条等式Pi chooses the right-hand equation
Claude CodeCursorPi
定位密封 CLI 产品密封 IDE 产品开源 harness
会话格式专有专有JSONL 树
扩展MCP + 插件VS Code 生态TypeScript hooks
可 fork✓ pi-mono
主线可读黑盒黑盒agent-loop.ts 逐行
SOURCE  ·  packages/coding-agent/src/core/sdk.ts sdk
/** CreateAgentSessionOptions — harness entry contract */ export interface CreateAgentSessionOptions { cwd?: string; model?: Model<any>; tools?: string[]; resourceLoader?: ResourceLoader; sessionManager?: SessionManager; }

C01.1 为什么「最小」是特性而不是缺陷Why "minimal" is a feature, not a bug

密封产品的复杂度藏在黑盒里:你不知道 steering 消息如何插队、compaction 的精确阈值、tool 并行策略。Pi 把这三件事写进 agent-loop.tsagent-session.ts,用单元测试锁住行为。你可以 fork pi-mono 换工具、写扩展注入 lint、用 --mode rpc 嵌进 CI。

Sealed products hide complexity. Pi writes steering, compaction, and tool parallelism into agent-loop.ts and agent-session.ts, locked by tests. Fork pi-mono, swap tools, inject lint via extensions, embed with --mode rpc.

6
npm 包npm packages
agent-core · ai · tui · coding-agent · protocol · serveragent-core · ai · tui · coding-agent · protocol · server
26
工序站stations
一条用户消息穿越的完整链路full path of one user message
15
checkpointcheckpoints
pi-textbook 教学里程碑pi-textbook teaching milestones
Claude Code 和 Cursor 解决普通开发者的问题;Pi 解决想理解 agent 内部机制的人的问题。 Claude Code and Cursor serve average developers; Pi serves people who want to understand agent internals.
产品卖的是答案。
Harness 卖的是能问出下一个问题的显微镜。
Products sell answers.
Harnesses sell the microscope to ask the next question.
Field Note · 10
📘 pi-textbook checkpoint 00:一次 README 读取的七个里程碑(prologue.ts)· 生产映射:packages/coding-agent/src/main.ts · packages/agent/src/agent-loop.ts 📘 pi-textbook checkpoint 00: 一次 README 读取的七个里程碑 (prologue.ts) · production: packages/coding-agent/src/main.ts · packages/agent/src/agent-loop.ts
FIG Harness vs Product 光谱:密封产品在左,可组合 harness 在右;Pi 落在终端原生、JSONL 可读的一侧。 Harness vs product spectrum: sealed products left, composable harness right; Pi sits on the terminal-native, JSONL-readable side.
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 00一次 README 读取的七个里程碑prologue.tspackages/coding-agent/src/main.ts · packages/agent/src/agent-loop.ts
cp 13Composition Rootcomposition-rootpackages/coding-agent/src/core/agent-session.ts · sdk.ts
CASE STUDY
从 Claude Code 迁移到 PiMigrating from Claude Code to Pi

Claude Code 把编排藏在产品里;Pi 把编排写在 agent-loop.ts。迁移的第一步不是换 CLI,而是跑通 pi-textbook checkpoint 00 的七里程碑,建立事件心智模型。

Claude Code hides orchestration in the product; Pi writes it in agent-loop.ts. Migration starts not with a new CLI but checkpoint 00's seven milestones — building an event mental model.

DEPTH 深度细读 · C01.4+Deep dive · C01.4+

C01.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/agent/src/agent-loop.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/agent/src/agent-loop.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C01.5 关键函数与数据流key functions and data flow

agent-loop.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in agent-loop.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C01.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/agent/src/agent-loop.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/agent/src/agent-loop.ts.

C01.7 读完后应能回答的 5 个问题 · Harness 不是 ProductFive questions you should answer after reading · Harness 不是 Product

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 00

Station 00:harness 心智,尚未 read。

Station 00: harness mindset; read not yet.

CASE STUDY
迁移到 PiMigrate to Pi

跑 checkpoint 00 再对照 AgentSession.prompt()。

Run checkpoint 00 then compare AgentSession.prompt().

SOURCE  ·  packages/agent/src/agent-loop.ts loop
/** Transforms to Message[] only at the LLM call boundary. */ export function agentLoop(...)
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 00一次 README 读取的七个里程碑prologue.tspackages/coding-agent/src/main.ts · packages/agent/src/agent-loop.ts
cp 13Composition Rootcomposition-rootpackages/coding-agent/src/core/agent-session.ts · sdk.ts
附录Appendix 常见问题FAQ
QPi 是产品还是库?Product or library?

CLI + 可嵌入库。

CLI plus embeddable libs.

Q为何不 fork Claude Code?Why not fork Claude Code?

闭源,无法审计 agentLoop。

Closed source.

Q读完 C01?After C01?

克隆 pi 与 pi-textbook。

Clone pi and pi-textbook.

C01.8 打开源码:packages/agent/src/agent-loop.tsOpen source: packages/agent/src/agent-loop.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/agent/src/agent-loop.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/agent/src/agent-loop.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 02 · 引子

一条消息的 22 站

22 stations for one message

从回车到 JSONL 的全景图

from Enter to JSONL, the full map

模块Module
全景Panorama
Package
跨包Cross-package
线程Thread
user→tooluser→tool
输出Output
七里程碑Seven milestones

按下回车之后,读取 README.md,用一句话告诉我这个项目做什么 不是直接发给 LLM——它要先变成 AgentMessage,穿过 AgentSession.prompt(),触发 agentLoop(),在 runLoop 的双环里转两圈 model stream,中间插一次 read 工具,最后才在 TUI 里滚动、在 JSONL 里落盘。

After Enter, read README.md and tell me what this project does in one sentence does not go straight to the LLM — it becomes an AgentMessage, passes through AgentSession.prompt(), triggers agentLoop(), spins two model streams in runLoop's twin loops with a read tool in between, then scrolls in the TUI and lands on disk as JSONL.

C02.1 七个里程碑 · pi-textbook 序章Seven milestones · pi-textbook prologue

pi-textbook 用离线 ScriptedModel 固定了主线 prompt 的七步 trace。生产代码 agent-loop.ts 产生语义等价但事件更细的事件流。

pi-textbook fixes a seven-step trace for the through-line prompt with offline ScriptedModel. Production agent-loop.ts emits semantically equivalent but finer-grained events.

#typeownerdetail
01user_messageuser读取 README.md,用一句话告诉我…
02model_startmodelturn=1
03assistant_messagemodelstopReason=toolUse · call_1
04tool_startloopread(call_1)
05tool_resulttoolREADME fixture
06model_startmodelturn=2
07assistant_messagemodelstopReason=stop
FIG 七个里程碑:owner 分工——user 定目标,model 推理,loop 调度,tool 返回环境事实。 Seven milestones: owner roles — user sets goal, model reasons, loop dispatches, tool returns environment facts.

C02.2 22 站全景(前 22 站)22-station panorama (first 22)

FIG 22 站全景:从 CLI 解析到 compaction 检查的完整蛇形时间线,颜色按 Phase 编码。 22-station panorama: snake timeline from CLI parse to compaction check, color-coded by phase.
01
CLI 解析cli.ts argv
intake
02
main 启动main.ts boot
intake
03
Sessionsession factory
intake
04
AGENTS.mdcontext stack
intake
05
prompt 入队prompt queued
heart
06
agent_startagent_start
heart
07
turn_startturn_start
heart
08
convertToLlm→ Message[]
llm
09
streamSimplepi-ai stream
llm
10
text_deltatoken stream
llm
11
toolcall_δtool assembly
llm
12
toolUseread request
heart
13
executeTooltool dispatch
heart
14
read READMEfilesystem I/O
tool
15
tool_resulttool result
tool
16
stream #2turn 2 model
llm
17
assistant stopfinal answer
llm
18
message_endmessage_end
heart
19
JSONL appendJSONL write
persist
20
TUI diffdiff render
terminal
21
extensionsextension hooks
terminal
22
compactioncontext check
persist
FIG 22 站按 Phase 分组:入口四站、心脏四站、LLM 三站、终端两站、持久化两站——持住这张地图读完全文。 22 stations by phase: intake 4, heart 4, LLM 3, terminal 2, persistence 2 — hold this map through the article.
agent_start
循环开始 · 订阅者收到首事件Loop begins · subscribers receive first event
turn_start
新 turn · 可能含 steering 注入New turn · may include steering injection
message_start
用户消息或 tool result 进入 transcriptUser message or tool result enters transcript
message_update
流式 delta · TUI 差分渲染的燃料Streaming delta · fuel for TUI differential render
message_end
消息定稿 · SessionManager 落盘Message finalized · SessionManager persists
tool_execution_start
read 开始 · cwd 相对路径解析read begins · cwd-relative path resolution
tool_execution_end
read 结束 · 结果截断策略生效read ends · truncation policy applied
turn_end
本 turn 结束 · 检查 follow-up 队列Turn ends · check follow-up queue
agent_end
循环退出 · 返回 newMessages[]Loop exits · returns newMessages[]
EVTAgentEvent 流 · 主线 promptAgentEvent stream · through-line prompt
FIG 事件泳道图:同一主线在 user/model/loop/tool 四条泳道上的时序。 Event swimlane: through-line timing across user/model/loop/tool lanes.
owner=userowner=user
只有用户消息user messages only
owner=modelowner=model
model_start + assistant_messagemodel_start + assistant_message
owner=loopowner=loop
tool_start · 调度决策tool_start · dispatch decisions
owner=toolowner=tool
tool_result · 环境事实tool_result · environment facts
SOURCE  ·  pi-textbook/workshop/src/demo/prologue.ts prologue
const README_FIXTURE = "# tiny-pi\n用于学习 Agent 内核的 TypeScript 项目。"; // 07 assistant_message stopReason=stop → 最终回答 appendTrace(trace, { owner: "model", type: "assistant_message", ... });
从 C02 到 C26,每一章解释两站之间的过渡。持住这条 22 站骨架,细节才不会丢。 From C02 to C26, each chapter explains one transition between stations. Hold this 22-station skeleton and the details won't drift.
七个里程碑是望远镜。
二十六个章节是显微镜。
Seven milestones are the telescope.
Twenty-six chapters are the microscope.
Field Note · 10
DEPTH 深度细读 · C02.4+Deep dive · C02.4+

C02.4 回车到第一次 streamEnter to first stream

站 01–06:shell 回车 → cli.ts → main.ts → createAgentSession() → prompt() 将「读取 README.md,用一句话告诉我这个项目做什么」变为 AgentMessage 并写 JSONL。

Stations 01–06: Enter → cli.ts → main.ts → createAgentSession() → prompt() turns «read README.md and tell me what this project does in one sentence» into AgentMessage + JSONL.

agentLoop emit agent_start/turn_start 后 runLoop 内环调用 streamAssistantResponse——第一次 owner=model 边界。

agentLoop emits agent_start/turn_start; inner runLoop calls streamAssistantResponse — first owner=model boundary.

典型第一次 stream:stopReason=toolUse,toolCall type=read path=README.md(agent-loop.ts 第203行 filter toolCall)。

Typical first stream: stopReason=toolUse, read README.md (agent-loop.ts line 203 filters toolCall).

owner=loop 站 07–11:executeToolCalls → packages/coding-agent/src/core/tools/read.ts createReadTool。

owner=loop 07–11: executeToolCalls → createReadTool in read.ts.

C02.5 工具结果到第二次 streamTo second stream

toolResult push 后 convertToLlm(messages) 转 Message[];第二次 streamAssistantResponse。

After toolResult, convertToLlm to Message[]; second streamAssistantResponse.

stopReason=stop 产出最终回答;message_update 驱动 TUI;message_end 写 JSONL。

stopReason=stop yields answer; message_update drives TUI; message_end writes JSONL.

turn_end 携带 toolResults;无 follow-up 时 agent_end。

turn_end carries toolResults; agent_end without follow-up.

C02.6 七里程碑映射Milestone map

01 user_message↔01–04;03 assistant(toolUse)↔06;04–05 tool↔07–12;07 stop↔15–18。

01 user↔01–04; 03 toolUse↔06; 04–05 tool↔07–12; 07 stop↔15–18.

站 19–22:扩展 hook、compaction 检查、TUI flush、leafId——短任务可能 noop。

19–22: extension hooks, compaction, TUI flush, leafId — may noop on short task.

C02.7 如何读后续章Read later chapters

C06↔03–05;C07↔05–08;C10↔09–11;C14↔14–16。

C06↔03–05; C07↔05–08; C10↔09–11; C14↔14–16.

验收:pi --verbose 事件序列应对齐 prologue.ts 类型顺序。

Acceptance: pi --verbose event types mirror prologue.ts order.

C02.8 读完后应能回答的 5 个问题 · 一条消息的 22 站Five questions you should answer after reading · 一条消息的 22 站

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/coding-agent/src/main.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/coding-agent/src/main.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/coding-agent/src/main.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/coding-agent/src/main.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 01

站 01:「读取 README.md,用一句话告诉我这个项目做什么」进入 cli.ts。

Station 01: «read README.md and tell me what this project does in one sentence» enters cli.ts.

SOURCE  ·  pi-textbook/workshop/src/demo/prologue.ts prologue
appendTrace(trace, { type: "user_message" }); appendTrace(trace, { type: "tool_start" });
SOURCE  ·  packages/agent/src/agent-loop.ts loop
while (true) { // outer: follow-up while (hasMoreToolCalls || pendingMessages.length) {
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 00一次 README 读取的七个里程碑prologue.tspackages/coding-agent/src/main.ts · packages/agent/src/agent-loop.ts
附录Appendix 常见问题FAQ
Q22 vs 26?22 vs 26?

章含背景 C03–C05 与全景 C24–C25。

Chapters include background and landscape.

Qowner 从哪来?owner?

prologue.ts trace 约定。

prologue.ts trace convention.

Q如何验证?Verify?

对照 coding-agent package.json 版本。

Match coding-agent version.

C02.9 打开源码:packages/coding-agent/src/main.tsOpen source: packages/coding-agent/src/main.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/coding-agent/src/main.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/coding-agent/src/main.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 03 · BACKGROUND

pi-mono 家谱

The pi-mono family tree

Mario Zechner 与 earendil-works

Mario Zechner and earendil-works

模块Module
模块Module
Package
package.jsonpackage.json
线程Thread
背景Background
输出Output
package.jsonpackage.json

pi-mono 是 Mario Zechner(Badlogic Games,libGDX 作者)在 earendil-works 组织下的 monorepo。它不是「又一个 ChatGPT 套壳」——而是一套可 fork 的 agent harness,npm 发布六个包。

pi-mono is Mario Zechner's (Badlogic Games, libGDX author) monorepo under earendil-works. Not another ChatGPT wrapper — a forkable agent harness published as six npm packages.

C03.1 仓库拓扑Repository topology

SOURCE  ·  pi-mono/package.json · workspaces root
"workspaces": [ "packages/agent", "packages/ai", "packages/tui", "packages/coding-agent", "packages/protocol", ]
Package职责 / Role主线 touchpoint
@earendil-works/pi-agent-coreagentLoop · types · StreamFnC07–C13
@earendil-works/pi-aiModels · providers · EventStreamC14–C16
@earendil-works/pi-tuiTerminal · diff render · EditorC17–C18
@earendil-works/coding-agentCLI · AgentSession · toolsC06–C08
@earendil-works/pi-protocolRPC JSONL 协议C25
pi-server远程会话 CBORC25

C03.2 与 pi-textbook 的关系Relationship to pi-textbook

pi-textbook 把生产代码降维成 15 个 checkpoint 的可运行 workshop。每个 checkpoint 对应 pi-mono 里一个真实目录,但剥离了 OAuth、40+ provider、TUI 差分渲染等生产复杂度。

pi-textbook reduces production code into 15 runnable workshop checkpoints. Each maps to a real pi-mono directory, stripped of OAuth, 40+ providers, TUI diff rendering, etc.

Checkpoint教学焦点生产文件
00一次 README 读取的七个里程碑packages/coding-agent/src/main.ts · packages/agent/src/agent-loop.ts
01TypeScript 生存集
02EventStreampackages/ai/src/utils/event-stream.ts
03Message IRpackages/agent/src/types.ts · packages/coding-agent/src/core/messages.ts
04ScriptedModel
05Provider Adapterpackages/ai/src/api/*.ts · packages/ai/src/models.ts
06Tool Contractpackages/agent/src/agent-loop.ts · packages/coding-agent/src/core/tools/
07Agent Looppackages/agent/src/agent-loop.ts
2015
libGDXlibGDX era
Mario 的游戏引擎背景影响 TUI 性能偏执Mario's game-engine background shapes TUI perf obsession
40+
providersproviders
pi-ai 支持的模型提供商数量model providers supported by pi-ai
v3
sessionsession version
JSONL CURRENT_SESSION_VERSIONJSONL CURRENT_SESSION_VERSION
读 pi-mono 时建议开两个窗口:生产仓库 + pi-textbook 对应 checkpoint 的 workshop 测试。 When reading pi-mono, keep two windows: production repo + pi-textbook workshop test for the matching checkpoint.
FIG 六层蛋糕:coding-agent 组装下层;agent-core 不 import pi-ai/compat,由宿主注入 StreamFn。 Six-layer cake: coding-agent composes below; agent-core does not import pi-ai/compat — host injects StreamFn.
FIG pi-textbook 15 checkpoint 与生产代码的映射关系(节选)。 pi-textbook 15 checkpoints mapped to production code (excerpt).
DEPTH 深度细读 · C03.4+Deep dive · C03.4+

C03.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,pi-mono/package.json 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», pi-mono/package.json defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C03.5 关键函数与数据流key functions and data flow

package.json 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in package.json are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C03.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 pi-mono/package.json 的具体行。

Under --verbose, stderr event types trace to lines in pi-mono/package.json.

C03.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C03.8 读完后应能回答的 5 个问题 · pi-mono 家谱Five questions you should answer after reading · pi-mono 家谱

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

SOURCE  ·  pi-mono/package.json src
// pi-mono 家谱 · pi-mono/package.json // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 13Composition Rootcomposition-rootpackages/coding-agent/src/core/agent-session.ts · sdk.ts
附录Appendix 常见问题FAQ
Qpi-mono 家谱最关键文件?Key file?

pi-mono/package.json

pi-mono/package.json.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C03.9 打开源码:package.jsonOpen source: package.json

本章主线 prompt 经过此模块时,请在 pi-mono 打开 package.json,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open package.json in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 04 · BACKGROUND

六层蛋糕

The six-layer cake

agent-core · ai · tui · coding-agent

agent-core · ai · tui · coding-agent

模块Module
模块Module
Package
agentagent
线程Thread
背景Background
输出Output
package.jsonpackage.json

主线 prompt 读取 README.md,用一句话告诉我这个项目做什么 从 L6 进入,在 L5 循环,L4 调模型,L3 渲染,L2/L1 只在 RPC 模式参与。

Through-line prompt enters at L6, loops in L5, calls model at L4, renders at L3; L2/L1 only in RPC mode.

C04.1 六层蛋糕Six-layer cake

FIG 六层蛋糕:coding-agent 组装下层;agent-core 不 import pi-ai/compat,由宿主注入 StreamFn。 Six-layer cake: coding-agent composes below; agent-core does not import pi-ai/compat — host injects StreamFn.
01
L6 · coding-agentL6 · coding-agent — CLI · AgentSession · tools · extensions
02
L5 · agent-coreL5 · agent-core — agentLoop · AgentMessage · events
03
L4 · pi-aiL4 · pi-ai — Models · streamSimple · providers
04
L3 · pi-tuiL3 · pi-tui — TUI · Editor · Markdown · diff
05
L2 · protocolL2 · protocol — JSONL RPC 帧
06
L1 · serverL1 · server — 远程会话
依赖方向DEPENDENCY
coding-agent → agent-core → pi-ai pi-tui ← coding-agent (UI only)
上层组装下层,不反向依赖Upper layers compose lower; no reverse deps
SOURCE  ·  packages/agent/package.json agent
"name": "@earendil-works/pi-agent-core", "exports": { ".": "./src/index.ts" }
SOURCE  ·  packages/ai/package.json ai
"name": "@earendil-works/pi-ai",

C04.2 边界纪律Boundary discipline

AgentMessage 在整个 agent-core 内流通;只在 streamAssistantResponse 调用点通过 convertToLlm 变成 pi-ai 的 Message[]。这条边界是 pi-textbook checkpoint 03 的核心教训。

AgentMessage flows through agent-core; only at streamAssistantResponse does convertToLlm produce pi-ai Message[]. Core lesson of textbook checkpoint 03.

可替换?例子
agent-core✓ fork自定义 runLoop
pi-ai换 provider adapter
coding-agent toolsregisterTool
JSONL formatversion 字段演进
📘 pi-textbook checkpoint 03:Message IR(types.ts)· 生产映射:packages/agent/src/types.ts · packages/coding-agent/src/core/messages.ts 📘 pi-textbook checkpoint 03: Message IR (types.ts) · production: packages/agent/src/types.ts · packages/coding-agent/src/core/messages.ts
📘 pi-textbook checkpoint 13:Composition Root(composition-root)· 生产映射:packages/coding-agent/src/core/agent-session.ts · sdk.ts 📘 pi-textbook checkpoint 13: Composition Root (composition-root) · production: packages/coding-agent/src/core/agent-session.ts · sdk.ts
DEPTH 深度细读 · C04.4+Deep dive · C04.4+

C04.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/agent/package.json 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/agent/package.json defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C04.5 关键函数与数据流key functions and data flow

package.json 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in package.json are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C04.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/agent/package.json 的具体行。

Under --verbose, stderr event types trace to lines in packages/agent/package.json.

C04.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C04.8 读完后应能回答的 5 个问题 · 六层蛋糕Five questions you should answer after reading · 六层蛋糕

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

SOURCE  ·  packages/agent/package.json src
// 六层蛋糕 · packages/agent/package.json // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 03Message IRtypes.tspackages/agent/src/types.ts · packages/coding-agent/src/core/messages.ts
cp 13Composition Rootcomposition-rootpackages/coding-agent/src/core/agent-session.ts · sdk.ts
附录Appendix 常见问题FAQ
Q六层蛋糕最关键文件?Key file?

packages/agent/package.json

packages/agent/package.json.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C04.9 打开源码:packages/agent/package.jsonOpen source: packages/agent/package.json

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/agent/package.json,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/agent/package.json in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 05 · BACKGROUND

故意不做的功能

Intentionally missing features

No MCP · No sub-agents · No plan mode

No MCP · No sub-agents · No plan mode

模块Module
哲学Philosophy
Package
coding-agent/README.mdcoding-agent/README.md
线程Thread
背景Background
输出Output
边界声明Boundary manifesto

Pi 的 README 明确列出故意不做的功能:No MCP · No sub-agents · No plan mode。这不是遗漏——是 harness 哲学的边界声明。

Pi's README explicitly lists intentionally omitted features: No MCP · No sub-agents · No plan mode. Not oversights — boundary statements of harness philosophy.

功能Claude CodePi替代路径
MCP一等协议TypeScript Extension API
Sub-agents内置fork session + RPC
Plan mode专用 UIthinking level + 用户消息
IDE 集成原生终端 only外部编辑器
权限弹窗GUI终端确认tool 级别 policy

C05.1 为什么 No MCPWhy no MCP

MCP 把工具发现协议化,但 Pi 选择 registerTool + npm/git 扩展包——类型安全、可测试、与 agent loop 同进程。代价是生态不互通 Claude Desktop 的 MCP 市场。

MCP protocolizes tool discovery; Pi chooses registerTool + npm/git extension packages — type-safe, testable, same process as agent loop. Cost: no interchange with Claude Desktop's MCP marketplace.

C05.2 为什么 No sub-agentsWhy no sub-agents

子 agent 本质是嵌套 agentLoop + 独立 context。Pi 用 session fork(JSONL parentSession)+ RPC 模式 达到类似效果,但不隐藏嵌套层。

Sub-agents are nested agentLoop + isolated context. Pi achieves similar via session fork (JSONL parentSession) + RPC mode without hiding the nesting.

「故意不做」= 把复杂度推给扩展作者,而不是推给终端用户。 «Intentionally omitted» = push complexity to extension authors, not terminal users.
少即是多:
每一层省略的功能,都是一层可观测性。
Less is more:
every omitted feature is a layer of observability gained.
Field Note · 10
DEPTH 深度细读 · C05.4+Deep dive · C05.4+

C05.4 为什么 No MCPWhy no MCP

packages/coding-agent/README.md 第 498 行:No MCP。Mario 的博客解释:MCP 把工具发现协议化,但 Pi 选择 registerTool + Skills + npm/git 扩展包。

packages/coding-agent/README.md line 498: No MCP. Mario's blog: MCP protocolizes tool discovery; Pi chooses registerTool + Skills + npm/git extension packages.

对主线 prompt 的影响:模型调用 read 是 coding-agent 内置 tool,经 createReadTool 同进程执行——无需 MCP server 握手。

Through-line impact: model calls built-in read via createReadTool in-process — no MCP handshake.

代价:无法直接接入 Claude Desktop MCP 市场。收益:tool schema 在 TypeScript 中定义,validateToolArgumentsagent-loop.ts 执行前校验。

Cost: no Claude Desktop MCP marketplace. Payoff: TypeScript tool schemas; validateToolArguments before execution.

扩展路径:examples/extensions/ 可添加 MCP;plan-mode/ 示例展示 extension 实现产品功能。

Extension path: examples/extensions/ can add MCP; plan-mode/ shows product features via extensions.

C05.5 为什么 No sub-agentsWhy no sub-agents

README 第 500 行:可用 tmux spawn 多个 pi 实例,或用 extension 自建,或安装第三方 pi package。

README line 500: spawn pi via tmux, build with extensions, or install third-party pi packages.

Pi 的替代是 session forkSessionManager.forkparentSession)+ RPC 模式--mode rpc)。嵌套层不隐藏——JSONL 树可 diff。

Pi's alternative: session fork + RPC mode. Nesting visible in diffable JSONL tree.

主线 README 任务不需要子 agent:一次 read + 一次总结,单 agentLoop 足够。

README through-line needs no sub-agent: one read + one summary suffices.

packages/clientacquireSession exclusive lease 防止并发写同一会话。

packages/client acquireSession exclusive lease prevents concurrent session writes.

C05.6 为什么 No plan modeWhy no plan mode

README 第 504 行:把计划写进文件、用 extension 实现、或安装 package。examples/extensions/plan-mode/ 提供 /planCtrl+Alt+P

README line 504: write plans to files, use extensions, or install packages. plan-mode/ provides /plan and Ctrl+Alt+P.

Pi 的 thinking level(ThinkingLevelSchema in protocol/schemas.ts)是模型侧推理,不是 UI plan 模式。

Pi's thinking level in protocol/schemas.ts is model-side reasoning, not UI plan mode.

对主线 prompt:模型可直接 toolUse read,无需先进入 plan 只读阶段。

Through-line: model can toolUse read directly — no plan-only phase.

C05.7 故意不做 = 可观测性预算Omissions = observability budget

每省略一个产品功能,就少一层黑盒。Pi 用 --verbose 事件 + 开源 agent-loop.ts 补偿 IDE 集成缺失。

Each omitted feature removes a black box. Pi compensates with --verbose events and open agent-loop.ts.

对比 Claude Code Plan 模式:用户看不见 plan 状态机;Pi plan-mode extension 仍走 ExtensionRunner 事件。

vs Claude Code Plan: users cannot see state machine; Pi plan-mode extension still emits via ExtensionRunner.

C05.8 主线 prompt 在「故意不做」语境下Through-line under omissions

主线只需 read + summarize——恰好是 Pi 默认 tool 集的最小闭环。

Through-line needs only read + summarize — Pi default tool set minimal loop.

若 fork Pi 加 MCP:改动 ExtensionRunner + tool registry,不必 fork agent-loop.ts

Forking to add MCP: change ExtensionRunner + tool registry, not agent-loop.ts.

C05.9 读完后应能回答的 5 个问题 · 故意不做的功能Five questions you should answer after reading · 故意不做的功能

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

SOURCE  ·  packages/coding-agent/README.md readme
**No MCP.** Build CLI tools with READMEs ... **No sub-agents.** Spawn pi instances via tmux ... **No plan mode.** Write plans to files ...
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 12Resources + Extensionsresources-extensionspackages/coding-agent/src/core/extensions/ · resource-loader.ts
cp 13Composition Rootcomposition-rootpackages/coding-agent/src/core/agent-session.ts · sdk.ts
附录Appendix 常见问题FAQ
QPi 永远不会有 MCP 吗?Will Pi never have MCP?

核心不做一等协议;社区 extension 可加。

Not first-class in core; community extensions can add it.

Qsession fork 和 sub-agent 有何不同?fork vs sub-agent?

fork 显式 JSONL 树 + parentSession。

fork has explicit JSONL tree + parentSession.

Qplan-mode 示例在哪?plan-mode example?

examples/extensions/plan-mode/

examples/extensions/plan-mode/.

C05.10 打开源码:packages/coding-agent/README.mdOpen source: packages/coding-agent/README.md

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/coding-agent/README.md,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/coding-agent/README.md in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 06 · INTAKE

CLI 启动链

CLI boot chain

cli.ts → main.ts → createAgentSession

cli.ts → main.ts → createAgentSession

模块Module
模块Module
Package
coding-agentcoding-agent
线程Thread
站 06St. 06
输出Output
cli.tscli.ts

你在 shell 里输入 pinpx @earendil-works/coding-agent,启动链从 cli.ts 解析 argv,到 main.ts 选择 interactive/print/rpc 模式,最终调用 createAgentSession()

You type pi or npx @earendil-works/coding-agent; boot chain parses argv in cli.ts, main.ts picks interactive/print/rpc mode, then createAgentSession().

01
cli.tsargv · --model · --mode rpc
02
main.tsmode dispatch · theme · keybindings
03
createAgentSessionAgentSession + ExtensionRunner
04
interactive modeTUI loop · Editor
SOURCE  ·  packages/coding-agent/src/cli.ts cli
import { main } from "./main.ts"; main(process.argv.slice(2)).catch(...);
SOURCE  ·  packages/coding-agent/src/main.ts main
export async function main(args: string[]) { const mode = parseMode(args); // interactive | print | rpc const session = await createAgentSession({ cwd, model, ... }); }
--verbose--verbose
打印 AgentEvent 到 stderrlog AgentEvents to stderr
--mode rpc--mode rpc
JSONL stdin/stdout 协议JSONL stdin/stdout protocol
--continue--continue
加载 leafId 会话load leafId session

C06.1 Case study:第一次启动Case study: first boot

冷启动时 ModelRegistry 读取 models.generated.tsResourceLoader 扫描 AGENTS.md 栈,SessionManager 创建新 JSONL 文件。用户尚未输入主线 prompt,但 harness 已就绪。

On cold boot ModelRegistry reads models.generated.ts, ResourceLoader scans AGENTS.md stack, SessionManager creates new JSONL. User hasn't typed the through-line prompt yet, but harness is ready.

📘 pi-textbook checkpoint 13:Composition Root(composition-root)· 生产映射:packages/coding-agent/src/core/agent-session.ts · sdk.ts 📘 pi-textbook checkpoint 13: Composition Root (composition-root) · production: packages/coding-agent/src/core/agent-session.ts · sdk.ts
DEPTH 深度细读 · C06.4+Deep dive · C06.4+

C06.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/coding-agent/src/cli.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/coding-agent/src/cli.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C06.5 关键函数与数据流key functions and data flow

cli.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in cli.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C06.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/coding-agent/src/cli.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/coding-agent/src/cli.ts.

C06.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C06.8 读完后应能回答的 5 个问题 · CLI 启动链Five questions you should answer after reading · CLI 启动链

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/coding-agent/src/cli.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/coding-agent/src/cli.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/coding-agent/src/cli.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/coding-agent/src/cli.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 06

站 06:packages/coding-agent/src/cli.ts 处理主线。

Station 06: packages/coding-agent/src/cli.ts on through-line.

SOURCE  ·  packages/coding-agent/src/cli.ts src
// CLI 启动 · packages/coding-agent/src/cli.ts // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 13Composition Rootcomposition-rootpackages/coding-agent/src/core/agent-session.ts · sdk.ts
附录Appendix 常见问题FAQ
QCLI 启动最关键文件?Key file?

packages/coding-agent/src/cli.ts

packages/coding-agent/src/cli.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C06.9 打开源码:packages/coding-agent/src/cli.tsOpen source: packages/coding-agent/src/cli.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/coding-agent/src/cli.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/coding-agent/src/cli.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 07 · INTAKE

AgentSession 编排

AgentSession orchestration

唯一的中枢调度器

the single orchestration hub

模块Module
模块Module
Package
coding-agentcoding-agent
线程Thread
站 07St. 07
输出Output
agent-session.tsagent-session.ts

AgentSession 是 Pi 的唯一中枢调度器——所有模式(interactive、print、rpc)共享它。主线 prompt 通过 prompt() 进入,内部调用 agentLoop() 并订阅事件写 JSONL。

AgentSession is Pi's single orchestration hub — all modes share it. Through-line prompt enters via prompt(), internally calls agentLoop() and subscribes to events for JSONL persistence.

SOURCE  ·  packages/coding-agent/src/core/agent-session.ts session
/** AgentSession - Core abstraction for agent lifecycle */ export class AgentSession { async prompt(text: string): Promise<void> { ... } private async runLoop(...): Promise<void> { ... } }
session_start
会话加载/创建 · 扩展收到 session_startSession load/create · extensions get session_start
turn_start
用户 prompt 触发新 turnUser prompt triggers new turn
message_update
流式内容 · TUI 订阅Streaming content · TUI subscribes
turn_end
turn 完成 · 检查 auto-compactTurn complete · check auto-compact
EVTAgentEvent 流 · 主线 promptAgentEvent stream · through-line prompt

C07.1 AgentSession 职责矩阵AgentSession responsibility matrix

职责类/模块主线时刻
编排 agentLoopagent-session.tsprompt() 调用时
持久化SessionManager每个 message_end
扩展ExtensionRunner全程 hook
压缩compaction/token 超阈值
模型切换ModelRegistrymodel_change entry

C07.2 与 agent-core 的分工Split with agent-core

AgentSession 拥有 I/O 和持久化;agentLoop 是纯函数式循环,不知道 JSONL 和 TUI 的存在。测试 agent loop 不需要启动终端。

AgentSession owns I/O and persistence; agentLoop is a pure loop unaware of JSONL and TUI. Testing agent loop needs no terminal.

📘 pi-textbook checkpoint 07:Agent Loop(agent-loop.ts)· 生产映射:packages/agent/src/agent-loop.ts 📘 pi-textbook checkpoint 07: Agent Loop (agent-loop.ts) · production: packages/agent/src/agent-loop.ts
📘 pi-textbook checkpoint 09:Stateful Agent(stateful-agent)· 生产映射:packages/agent/src/agent.ts 📘 pi-textbook checkpoint 09: Stateful Agent (stateful-agent) · production: packages/agent/src/agent.ts
pi-textbook checkpoint 09 Stateful Agent 讲 steer/follow-up/abort——生产代码在 AgentSession + agent-loop 的 getSteeringMessages / queueFollowUp。 Textbook checkpoint 09 covers steer/follow-up/abort — production code in AgentSession + agent-loop getSteeringMessages / queueFollowUp.
SOURCE  ·  packages/coding-agent/src/core/agent-session.ts walkthrough
1 async prompt(text: string) // 用户主线入口 / through-line entry 2 const userMsg = createUserMessage(text) // 构造 AgentMessage / build AgentMessage 3 agentLoop([userMsg], ctx, config, signal, streamFn) // 委托 agent-core / delegate to agent-core 4 for await (const event of stream) // 事件泵 → JSONL + TUI / event pump → JSONL + TUI
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 09Stateful Agentstateful-agentpackages/agent/src/agent.ts
cp 13Composition Rootcomposition-rootpackages/coding-agent/src/core/agent-session.ts · sdk.ts
DEPTH 深度细读 · C07.4+Deep dive · C07.4+

C07.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/coding-agent/src/core/agent-session.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/coding-agent/src/core/agent-session.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C07.5 关键函数与数据流key functions and data flow

agent-session.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in agent-session.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C07.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/coding-agent/src/core/agent-session.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/coding-agent/src/core/agent-session.ts.

C07.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C07.8 读完后应能回答的 5 个问题 · AgentSession 编排Five questions you should answer after reading · AgentSession 编排

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/coding-agent/src/core/agent-session.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/coding-agent/src/core/agent-session.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/coding-agent/src/core/agent-session.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/coding-agent/src/core/agent-session.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 07

站 07:packages/coding-agent/src/core/agent-session.ts 处理主线。

Station 07: packages/coding-agent/src/core/agent-session.ts on through-line.

SOURCE  ·  packages/coding-agent/src/core/agent-session.ts src
// AgentSession · packages/coding-agent/src/core/agent-session.ts // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 09Stateful Agentstateful-agentpackages/agent/src/agent.ts
cp 13Composition Rootcomposition-rootpackages/coding-agent/src/core/agent-session.ts · sdk.ts
附录Appendix 常见问题FAQ
QAgentSession最关键文件?Key file?

packages/coding-agent/src/core/agent-session.ts

packages/coding-agent/src/core/agent-session.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C07.9 prompt() 到 agentLoop 的逐步 traceStep-by-step trace from prompt() to agentLoop

AgentSession.prompt(text) 首先构造 createUserMessage(text),类型为 AgentMessage 而非 pi-ai 的 Message。这是 checkpoint 03 与 09 的分水岭:在 agent-core 内永远操作 AgentMessage,只在 streamAssistantResponse 边界调用 convertToLlm

AgentSession.prompt(text) first builds createUserMessage(text) as AgentMessage, not pi-ai Message. Checkpoint 03/09 watershed: AgentMessage inside agent-core; convertToLlm only at streamAssistantResponse.

随后 runLoop 把 userMsg 推入 pending queue,emit turn_startmessage_start。若 SessionManager 已绑定,同一事件流会触发 JSONL append——因此 TUI 与磁盘是同一事件的两个订阅者,不是两条路径。

Then runLoop pushes userMsg to pending queue, emits turn_startmessage_start. With SessionManager bound, the same stream triggers JSONL append — TUI and disk are two subscribers to one event bus, not two paths.

主线 prompt 的第一圈 model stream 在 agent-loop.ts 的 inner loop 启动;当 stopReason=toolUse 时,outer loop 不退出,而是进入 tool batch。读 README 的 read 在此执行——cwd 相对路径由 coding-agent 的 tool registry 解析。

First model stream for the through-line starts in agent-loop's inner loop; when stopReason=toolUse, outer loop continues into tool batch. read for README runs here — cwd-relative paths resolved by coding-agent tool registry.

SOURCE  ·  packages/coding-agent/src/core/agent-session.ts walkthrough
async prompt(text: string) { const userMsg = createUserMessage(text); await this.runLoop([userMsg]); }
CHAPTER 08 · INTAKE

AGENTS.md 上下文栈

The AGENTS.md context stack

从 ~/.pi 到项目根的指令叠加

instructions stacked from ~/.pi to project root

模块Module
模块Module
Package
coding-agentcoding-agent
线程Thread
站 08St. 08
输出Output
resource-loader.tsresource-loader.ts

在主线 prompt 到达模型之前,ResourceLoader 把从 ~/.pi/AGENTS.md 到项目根目录的指令文件叠加进 system context。每一层目录的 AGENTS.md 追加规则。

Before the through-line prompt reaches the model, ResourceLoader stacks instruction files from ~/.pi/AGENTS.md to project root into system context.

01
~/.pi/AGENTS.md全局 harness 指令
02
~/.pi/PROJECT.md用户级项目偏好
03
./AGENTS.md仓库级规则
04
./subdir/AGENTS.md子目录覆盖(若存在)
SOURCE  ·  packages/coding-agent/src/core/resource-loader.ts loader
export class ResourceLoader { loadResources(cwd): ResourceBundle { ... } // merges AGENTS.md chain into system prompt }
叠加顺序Stack order
从全局到局部,后加载的优先级更高global → local, later wins
frontmatterfrontmatter
YAML 头可指定 tool 白名单YAML header can whitelist tools
刷新Refresh
文件变更可触发 reload(扩展 hook)file change can trigger reload via extension hook

C08.1 主线 prompt 时的 context 长什么样What context looks like at through-line prompt

模型看到的不是裸的「读取 README」——而是 system: [AGENTS.md 栈] + user: 读取 README.md…。工具 schema 也在 system 或 tools 参数里。

Model sees not bare «read README» but system: [AGENTS.md stack] + user: read README.md…. Tool schemas live in system or tools param.

📘 pi-textbook checkpoint 12:Resources + Extensions(resources-extensions)· 生产映射:packages/coding-agent/src/core/extensions/ · resource-loader.ts 📘 pi-textbook checkpoint 12: Resources + Extensions (resources-extensions) · production: packages/coding-agent/src/core/extensions/ · resource-loader.ts
DEPTH 深度细读 · C08.4+Deep dive · C08.4+

C08.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/coding-agent/src/core/resource-loader.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/coding-agent/src/core/resource-loader.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C08.5 关键函数与数据流key functions and data flow

resource-loader.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in resource-loader.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C08.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/coding-agent/src/core/resource-loader.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/coding-agent/src/core/resource-loader.ts.

C08.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C08.8 读完后应能回答的 5 个问题 · AGENTS.md 上下文栈Five questions you should answer after reading · AGENTS.md 上下文栈

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 08

站 08:packages/coding-agent/src/core/resource-loader.ts 处理主线。

Station 08: packages/coding-agent/src/core/resource-loader.ts on through-line.

SOURCE  ·  packages/coding-agent/src/core/resource-loader.ts src
// AGENTS.md 栈 · packages/coding-agent/src/core/resource-loader.ts // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 12Resources + Extensionsresources-extensionspackages/coding-agent/src/core/extensions/ · resource-loader.ts
附录Appendix 常见问题FAQ
QAGENTS.md 栈最关键文件?Key file?

packages/coding-agent/src/core/resource-loader.ts

packages/coding-agent/src/core/resource-loader.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C08.9 打开源码:packages/coding-agent/src/core/resource-loader.tsOpen source: packages/coding-agent/src/core/resource-loader.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/coding-agent/src/core/resource-loader.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/coding-agent/src/core/resource-loader.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 09 · HEART

两层消息模型

Two-layer message model

AgentMessage ≠ Message

AgentMessage ≠ Message

模块Module
模块Module
Package
agentagent
线程Thread
站 09St. 09
输出Output
types.tstypes.ts

Pi 维护两层消息模型AgentMessage(agent-core 内部,可含自定义 role)和 Message(pi-ai LLM API,严格 user/assistant/tool)。convertToLlm 是唯一转换点。

Pi maintains two message layers: AgentMessage (agent-core, custom roles) and Message (pi-ai LLM API, strict user/assistant/tool). convertToLlm is the sole conversion point.

类型谁能看例子
TranscriptAgentMessage[]agent loop · JSONLuser · assistant · toolResult · custom
LLM APIMessage[]pi-ai providersuser · assistant · tool_result
TUIRenderedMessage终端组件Markdown · tool 卡片
SOURCE  ·  packages/agent/src/types.ts types
export type AgentMessage = | UserMessage | AssistantMessage | ToolResultMessage | CustomMessage;
SOURCE  ·  packages/coding-agent/src/core/messages.ts convert
export function convertToLlm(messages: AgentMessage[]): Message[]

C09.1 三层 canonical transcriptThree-layer canonical transcript

pi-textbook checkpoint 03 强调:transcript 是唯一真相源;LLM 请求是投影;TUI 是另一个投影。fork 会话时只复制 AgentMessage 链。

Textbook checkpoint 03: transcript is the single source of truth; LLM request is a projection; TUI is another. Session fork copies AgentMessage chain only.

投影PROJECTION
AgentMessage[] — canonical convertToLlm() → Message[] renderMessage() → TUI cells
主线 prompt 在三层各出现一次Through-line prompt appears once per layer
FIG 三层投影:transcript 是唯一真相源;LLM 与 TUI 各取所需字段。 Three projections: transcript is single source of truth; LLM and TUI each take needed fields.
📘 pi-textbook checkpoint 03:Message IR(types.ts)· 生产映射:packages/agent/src/types.ts · packages/coding-agent/src/core/messages.ts 📘 pi-textbook checkpoint 03: Message IR (types.ts) · production: packages/agent/src/types.ts · packages/coding-agent/src/core/messages.ts
SOURCE TRACE · C09 主线 prompt「读取 README.md,用一句话告诉我这个项目做什么」在消息 IR 层的变形 Through-line prompt message IR transformation

本章 trace 只盯住一件事:同一条用户输入在 agent-core 内永远是 AgentMessage,直到 streamAssistantResponse 调用 convertToLlm 才投影成 pi-ai 的 Message[]。主线 prompt 的 user 消息、read 的 toolResult、最终 assistant 回答——三者形状不同,但 JSONL 里存的全是 AgentMessage 变体。

This trace tracks one thing: the same user input stays AgentMessage inside agent-core until streamAssistantResponse calls convertToLlm to project into pi-ai Message[]. User message, read toolResult, final assistant — different shapes, all AgentMessage variants in JSONL.

SOURCE  ·  packages/agent/src/types.ts types
149 export interface AgentLoopConfig extends SimpleStreamOptions { 150 model: Model<any>; 151 152 /** 153 * Converts AgentMessage[] to LLM-compatible Message[] before each LLM call. 154 * 155 * Each AgentMessage must be converted to a UserMessage, AssistantMessage, or ToolResultMessage 156 * that the LLM can understand. AgentMessages that cannot be converted (e.g., UI-only notifications, 157 * status messages) should be filtered out. 158 * 159 * Contract: must not throw or reject. Return a safe fallback value instead. 160 * Throwing interrupts the low-level agent loop without producing a normal event sequence. 161 * 162 * @example 163 * ```typescript 164 * convertToLlm: (messages) => messages.flatMap(m => { 165 * if (m.role === "custom") { 166 * // Convert custom message to user message 167 * return [{ role: "user", content: m.content, timestamp: m.timestamp }]; 168 * } 169 * if (m.role === "notification") { 170 * // Filter out UI-only messages 171 * return []; 172 * } 173 * // Pass through standard LLM messages 174 * return [m]; 175 * }) 176 * ``` 177 */ 178 convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>; ◀ trace convertToLlm:AgentMessage[] → Message[],每次 LLM 调用前执行convertToLlm: AgentMessage[] → Message[] before each LLM call 179 180 /** 181 * Optional transform applied to the context before "src-str">`convertToLlm`. 182 * 183 * Use this for operations that work at the AgentMessage level: 184 * - Context window management (pruning old messages) 185 * - Injecting context from external sources 186 * 187 * Contract: must not throw or reject. Return the original messages or another 188 * safe fallback value instead. 189 * 190 * @example 191 * ```typescript 192 * transformContext: async (messages) => { 193 * if (estimateTokens(messages) > MAX_TOKENS) { 194 * return pruneOldMessages(messages); 195 * } 196 * return messages; 197 * } 198 * ``` 199 */ 200 transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>; ◀ trace transformContext:在 convert 之前做 compaction / 剪枝transformContext: compaction/prune before convert

AgentLoopConfig 的 JSDoc 把契约写得很硬:convertToLlm must not throw——抛错会打断低层循环且不会产生正常 event 序列。这就是为什么 compaction 和 custom message 过滤都在 Message 层做投影,而不是在 agent-loop 里 try/catch 糊过去。

AgentLoopConfig JSDoc is strict: convertToLlm must not throw — throws break the low-level loop without a normal event sequence. Compaction and custom message filtering project at Message layer, not try/catch in agent-loop.

SOURCE  ·  packages/coding-agent/src/core/messages.ts convert
140 /** 141 * Transform AgentMessages (including custom types) to LLM-compatible Messages. 142 * 143 * This is used by: 144 * - Agent's transormToLlm option (for prompt calls and queued messages) 145 * - Compaction's generateSummary (for summarization) 146 * - Custom extensions and tools 147 */ 148 export function convertToLlm(messages: AgentMessage[]): Message[] { ◀ trace coding-agent 的生产 convertToLlm 实现production convertToLlm in coding-agent 149 return messages 150 .map((m): Message | undefined => { 151 switch (m.role) { 152 case "bashExecution": ◀ trace switch(m.role):custom / bashExecution / branchSummary 在此折叠switch(m.role): custom/bash/branch fold here 153 // Skip messages excluded from context (!! prefix) 154 if (m.excludeFromContext) { 155 return undefined; 156 } 157 return { 158 role: "user", 159 content: [{ type: "text", text: bashExecutionToText(m) }], 160 timestamp: m.timestamp, 161 }; 162 case "custom": { 163 const content = typeof m.content === "string" ? [{ type: "text" as const, text: m.content }] : m.content; 164 return { 165 role: "user", 166 content, 167 timestamp: m.timestamp, 168 }; 169 } 170 case "branchSummary": 171 return { 172 role: "user", 173 content: [{ type: "text" as const, text: BRANCH_SUMMARY_PREFIX + m.summary + BRANCH_SUMMARY_SUFFIX }], 174 timestamp: m.timestamp, 175 }; 176 case "compactionSummary": 177 return { 178 role: "user", 179 content: [ 180 { type: "text" as const, text: COMPACTION_SUMMARY_PREFIX + m.summary + COMPACTION_SUMMARY_SUFFIX }, 181 ], 182 timestamp: m.timestamp, 183 }; 184 case "user": ◀ trace user/assistant/toolResult 原样透传——主线三角色user/assistant/toolResult pass through — through-line trio 185 case "assistant": 186 case "toolResult": 187 return m; 188 default: 189 // biome-ignore lint/correctness/noSwitchDeclarations: fine 190 const _exhaustiveCheck: never = m; 191 return undefined; 192 } 193 }) 194 .filter((m) => m !== undefined); 195 }
主线 prompt 消息投影时间线Through-line message projection timeline
#location主线时刻 / through-line moment
1AgentSession.prompt()user AgentMessage: «读取 README.md,用一句话告…»
2context.messages[]canonical transcript 追加 user
3transformContext?compaction 可能在此剪枝旧消息
4convertToLlm()→ [{role:user, content:…}] 送 pi-ai
5turn-1 assistanttoolUse(read) 仍在 AgentMessage 形状
6toolResultread README 内容 · role=toolResult
7turn-2 convertuser+assistant+toolResult → Message[]
8turn-2 assistantstopReason=stop · 最终回答
SOURCE  ·  packages/agent/src/agent-loop.ts stream
281 async function streamAssistantResponse( 282 context: AgentContext, 283 config: AgentLoopConfig, 284 signal: AbortSignal | undefined, 285 emit: AgentEventSink, 286 streamFunction: StreamFn, 287 ): Promise<AssistantMessage> { 288 // Apply context transform if configured (AgentMessage[] → AgentMessage[]) transformContext 边界:仍是 AgentMessage[]transformContext boundary: still AgentMessage[] 289 let messages = context.messages; 290 if (config.transformContext) { 291 messages = await config.transformContext(messages, signal); 292 } 293 294 // Convert to LLM-compatible messages (AgentMessage[] → Message[]) ◀ trace ★ 唯一 convert 调用点★ sole convert call site 295 const llmMessages = await config.convertToLlm(messages); ◀ trace llmMessages 进入 streamFunctionllmMessages enters streamFunction 296 297 // Build LLM context 298 const llmContext: Context = { 299 systemPrompt: context.systemPrompt, 300 messages: llmMessages, 301 tools: context.tools, 302 }; 303 304 // Resolve API key (important for expiring tokens) 305 const resolvedApiKey = 306 (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey; 307 308 const response = await streamFunction(config.model, llmContext, { 309 ...config, 310 apiKey: resolvedApiKey, 311 signal, 312 });

注意 streamAssistantResponse 注释原文:「This is where AgentMessage[] gets transformed to Message[] for the LLM.」——读 pi-mono 时搜索这句话,比搜索 convertToLlm 更快定位心脏。主线 prompt 第一次 convert 发生在 turn-1 model stream 前;第二次在 turn-2(tool result 已入 context)前。

Note streamAssistantResponse comment: «This is where AgentMessage[] gets transformed to Message[] for the LLM.» Search this phrase in pi-mono to locate the heart faster than convertToLlm. First convert before turn-1; second before turn-2 after tool result enters context.

SOURCE  ·  packages/agent/src/types.ts agentmsg
318 } 319 320 /** 321 * AgentMessage: Union of LLM messages + custom messages. 322 * This abstraction allows apps to add custom message types while maintaining 323 * type safety and compatibility with the base LLM messages. 324 */ 325 export type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages]; ◀ trace AgentMessage = Message | CustomAgentMessages 合并AgentMessage = Message | CustomAgentMessages merge 326 327 /** 328 * Public agent state. 329 * 330 * "src-str">`tools` and "src-str">`messages` use accessor properties so implementations can copy
SOURCE  ·  packages/agent/src/types.ts events-type
421 /** 422 * Events emitted by the Agent for UI updates. 423 * 424 * "src-str">`agent_end` is the last event emitted for a run, but awaited "src-str">`Agent.subscribe()` 425 * listeners for that event are still part of run settlement. The agent becomes 426 * idle only after those listeners finish. 427 */ 428 export type AgentEvent = ◀ trace AgentEvent:message_* 事件携带 AgentMessage 快照AgentEvent: message_* carries AgentMessage snapshot 429 // Agent lifecycle 430 | { type: "agent_start" } 431 | { type: "agent_end"; messages: AgentMessage[] } 432 // Turn lifecycle - a turn is one assistant response + any tool calls/results 433 | { type: "turn_start" } 434 | { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] } 435 // Message lifecycle - emitted for user, assistant, and toolResult messages 436 | { type: "message_start"; message: AgentMessage } 437 // Only emitted for assistant messages during streaming 438 | { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent } ◀ trace message_update 仅 assistant 流式阶段message_update only during assistant streaming 439 | { type: "message_end"; message: AgentMessage } 440 // Tool execution lifecycle 441 | { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any } 442 | { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any } 443 | { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean };
练习:在 pi-textbook checkpoint 03 给 transcript 加一条 role:custom 消息,观察 convertToLlm 是否过滤、JSONL 是否仍完整保存。 Exercise: in textbook cp03 add a role:custom message; observe convertToLlm filter vs JSONL full save.
DEPTH 深度细读 · C09.4+Deep dive · C09.4+

C09.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/agent/src/types.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/agent/src/types.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C09.5 关键函数与数据流key functions and data flow

types.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in types.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C09.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/agent/src/types.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/agent/src/types.ts.

C09.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C09.8 读完后应能回答的 5 个问题 · 两层消息模型Five questions you should answer after reading · 两层消息模型

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 09

站 09:packages/agent/src/types.ts 处理主线。

Station 09: packages/agent/src/types.ts on through-line.

SOURCE  ·  packages/agent/src/types.ts src
// 消息模型 · packages/agent/src/types.ts // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 03Message IRtypes.tspackages/agent/src/types.ts · packages/coding-agent/src/core/messages.ts
附录Appendix 常见问题FAQ
Q消息模型最关键文件?Key file?

packages/agent/src/types.ts

packages/agent/src/types.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C09.9 打开源码:packages/agent/src/types.tsOpen source: packages/agent/src/types.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/agent/src/types.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/agent/src/types.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 10 · HEART

runLoop 双环

The runLoop twin loops

outer follow-up · inner tool batch

outer follow-up · inner tool batch

模块Module
模块Module
Package
agentagent
线程Thread
站 10St. 10
输出Output
agent-loop.tsagent-loop.ts

runLoop 是 agent 的心脏:外环处理 follow-up 队列(用户在你以为结束时又发了一条),内环处理 tool batch 和 steering 注入。

runLoop is the agent heart: outer loop handles follow-up queue; inner loop handles tool batch and steering injection.

SOURCE  ·  packages/agent/src/agent-loop.ts loop
// Outer loop: continues when queued follow-up messages arrive while (true) { // Inner loop: process tool calls and steering while (hasMoreToolCalls || pendingMessages.length > 0) { const message = await streamAssistantResponse(...); } }

C10.1 主线 prompt 的两圈 model streamTwo model streams for through-line prompt

FIG runLoop 双环:外环处理 follow-up;内环处理 tool batch 与 steering 注入。 runLoop twin loops: outer handles follow-up; inner handles tool batch and steering injection.
01
Turn 1user → assistant(toolUse: read)
02
Tool batchexecuteTool(read) → toolResult
03
Turn 2context+result → assistant(stop)
外环退出条件Outer exit
无 follow-up 且内环完成no follow-up and inner loop done
内环继续条件Inner continue
stopReason=toolUse 或 pending steeringstopReason=toolUse or pending steering
streamFn 注入streamFn inject
测试用 ScriptedModel 替换ScriptedModel for tests
turn_start
每圈 model 前before each model round
message_update
text_delta / toolcall_deltastreaming deltas
turn_end
assistant 定稿后after assistant finalized
EVTAgentEvent 流 · 主线 promptAgentEvent stream · through-line prompt
📘 pi-textbook checkpoint 07:Agent Loop(agent-loop.ts)· 生产映射:packages/agent/src/agent-loop.ts 📘 pi-textbook checkpoint 07: Agent Loop (agent-loop.ts) · production: packages/agent/src/agent-loop.ts
外环是耐心。
内环是纪律。
Outer loop is patience.
Inner loop is discipline.
Field Note · 10
SOURCE TRACE · C10 runLoop 双环:主线 prompt 两圈 model stream 完整源码 trace runLoop twin loops: full source trace for two model streams

runLoopagent-loop.ts:155)是 Pi 与 Claude Code 最大架构差异所在:不是「一次 completion」,而是外环 follow-up × 内环 tool batch。主线 prompt 固定走两圈内环迭代(turn-1 toolUse + turn-2 stop),外环通常只转一圈。

runLoop (agent-loop.ts:155) is Pi's biggest architectural difference from Claude Code: not one completion but outer follow-up × inner tool batch. Through-line prompt runs two inner iterations (turn-1 toolUse + turn-2 stop); outer loop usually one revolution.

SOURCE  ·  packages/agent/src/agent-loop.ts runloop
155 async function runLoop( 156 initialContext: AgentContext, 157 newMessages: AgentMessage[], 158 initialConfig: AgentLoopConfig, 159 signal: AbortSignal | undefined, 160 emit: AgentEventSink, 161 streamFunction: StreamFn, 162 ): Promise<void> { 163 let currentContext = initialContext; 164 let config = initialConfig; 165 let firstTurn = true; 166 // Check for steering messages at start (user may have typed while waiting) 167 let pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || []; 启动时先 drain steering 队列drain steering queue at start 168 169 // Outer loop: continues when queued follow-up messages arrive after agent would stop ◀ trace 外环:follow-up 到达时 agent 本可停止却继续outer: continue when follow-up arrives 170 while (true) { 171 let hasMoreToolCalls = true; 172 173 // Inner loop: process tool calls and steering messages 174 while (hasMoreToolCalls || pendingMessages.length > 0) { ◀ trace 内环条件:还有 tool 或 pending steeringinner: more tools or pending steering 175 if (!firstTurn) { 176 await emit({ type: "turn_start" }); 177 } else { 178 firstTurn = false; 179 } 180 181 // Process pending messages (inject before next assistant response) 182 if (pendingMessages.length > 0) { steering 注入:message_start/end 无 streamingsteering inject: message_start/end, no streaming 183 for (const message of pendingMessages) { 184 await emit({ type: "message_start", message }); 185 await emit({ type: "message_end", message }); 186 currentContext.messages.push(message); 187 newMessages.push(message); 188 } 189 pendingMessages = []; 190 } 191 192 // Stream assistant response 193 const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFunction); ◀ trace ★ streamAssistantResponse:每圈内环一次 model★ streamAssistantResponse: one model per inner round 194 newMessages.push(message); 195 196 if (message.stopReason === "error" || message.stopReason === "aborted") { 197 await emit({ type: "turn_end", message, toolResults: [] }); 198 await emit({ type: "agent_end", messages: newMessages }); 199 return; 200 } 201 202 // Check for tool calls 203 const toolCalls = message.content.filter((c) => c.type === "toolCall"); 从 assistant content 提取 toolCallextract toolCall from assistant content 204 205 const toolResults: ToolResultMessage[] = []; 206 hasMoreToolCalls = false; 207 if (toolCalls.length > 0) { 208 // A &quot;length&quot; stop means the output was cut off by the token limit, so 209 // every tool call in the message may carry truncated arguments. Fail 210 // them all instead of executing potentially borked calls. 211 const executedToolBatch = stopReason=length → 批量 fail tool,不执行stopReason=length → fail all tools, don't execute 212 message.stopReason === "length" 213 ? await failToolCallsFromTruncatedMessage(toolCalls, emit) 214 : await executeToolCalls(currentContext, message, config, signal, emit); ◀ trace ★ executeToolCalls:read README 在此★ executeToolCalls: read README here 215 toolResults.push(...executedToolBatch.messages); 216 hasMoreToolCalls = !executedToolBatch.terminate; 217 218 for (const result of toolResults) { 219 currentContext.messages.push(result); 220 newMessages.push(result); 221 } 222 } 223 224 await emit({ type: "turn_end", message, toolResults }); turn_end:TUI 可在此刷新 tool 卡片turn_end: TUI refreshes tool cards 225
主线 prompt · runLoop 状态机Through-line prompt · runLoop state machine
#location主线时刻 / through-line moment
T0outer iter=1, inner iter=1pending=[] · stream turn-1
T1inner iter=1 endstopReason=toolUse · toolCalls=[read]
T2executeToolCallstool_execution_* · README content
T3inner iter=2context+=toolResult · stream turn-2
T4inner iter=2 endstopReason=stop · 最终回答
T5inner exithasMoreToolCalls=false · pending=[]
T6outer check follow-up无 follow-up → break
T7agent_endreturn newMessages[]
SOURCE  ·  packages/agent/src/agent-loop.ts stream-emit
314 let partialMessage: AssistantMessage | null = null; 315 let addedPartial = false; 316 317 for await (const event of response) { 消费 pi-ai EventStreamconsume pi-ai EventStream 318 switch (event.type) { 319 case "start": start → message_start(partial)start → message_start(partial) 320 partialMessage = event.partial; 321 context.messages.push(partialMessage); 322 addedPartial = true; 323 await emit({ type: "message_start", message: { ...partialMessage } }); 324 break; 325 326 case "text_start": 327 case "text_delta": 328 case "text_end": 329 case "thinking_start": 330 case "thinking_delta": 331 case "thinking_end": 332 case "toolcall_start": 333 case "toolcall_delta": 334 case "toolcall_end": 335 if (partialMessage) { 336 partialMessage = event.partial; 337 context.messages[context.messages.length - 1] = partialMessage; 338 await emit({ ◀ trace ★ message_update:每个 text/toolcall delta★ message_update: each text/toolcall delta 339 type: "message_update", 340 assistantMessageEvent: event, 341 message: { ...partialMessage }, 342 }); 343 } 344 break; 345 346 case "done": 347 case "error": { 348 const finalMessage = await response.result(); 349 if (addedPartial) { 350 context.messages[context.messages.length - 1] = finalMessage; 351 } else { 352 context.messages.push(finalMessage); 353 } 354 if (!addedPartial) { 355 await emit({ type: "message_start", message: { ...finalMessage } }); 356 } 357 await emit({ type: "message_end", message: finalMessage }); ◀ trace done → message_end(final)done → message_end(final) 358 return finalMessage; 359 } 360 }

内环第二次迭代时,currentContext.messages 已含:user prompt、turn-1 assistant(toolUse)、toolResult(README)。convertToLlm 把 toolResult 折叠成 provider 特定 tool_result 块——Anthropic 与 OpenAI 形状不同,但 agent-loop 不感知,这是 pi-ai adapter 的职责(C14)。

Second inner iteration: currentContext.messages has user, turn-1 assistant(toolUse), toolResult(README). convertToLlm folds toolResult to provider-specific blocks — agent-loop doesn't care, pi-ai adapter's job (C14).

SOURCE  ·  packages/agent/src/agent-loop.ts outer-exit
247 if ( shouldStopAfterTurn:扩展可强制提前 agent_endshouldStopAfterTurn: extensions can force early agent_end 248 await config.shouldStopAfterTurn?.({ 249 message, 250 toolResults, 251 context: currentContext, 252 newMessages, 253 }) 254 ) { 255 await emit({ type: "agent_end", messages: newMessages }); 256 return; 257 } 258 259 pendingMessages = (await config.getSteeringMessages?.()) || []; 内环结束后再次 poll steeringpoll steering again after inner loop 260 } 261 262 // Agent would stop here. Check for follow-up messages. ◀ trace 外环 follow-up 检查点outer follow-up checkpoint 263 const followUpMessages = (await config.getFollowUpMessages?.()) || []; 264 if (followUpMessages.length > 0) { ◀ trace 有 follow-up → pendingMessages → continue 外环follow-up → pendingMessages → continue outer 265 // Set as pending so inner loop processes them 266 pendingMessages = followUpMessages; 267 continue; 268 } 269 270 // No more messages, exit 271 break; 272 } 273 274 await emit({ type: "agent_end", messages: newMessages }); ◀ trace 无消息 → agent_endno messages → agent_end 275 }
SOURCE  ·  packages/agent/src/agent-loop.ts length-fail
381 async function failToolCallsFromTruncatedMessage( ◀ trace length 截断时拒绝执行所有 tool calllength truncation rejects all tool calls 382 toolCalls: AgentToolCall[], 383 emit: AgentEventSink, 384 ): Promise<ExecutedToolCallBatch> { 385 const messages: ToolResultMessage[] = []; 386 for (const toolCall of toolCalls) { 387 await emit({ 388 type: "tool_execution_start", 389 toolCallId: toolCall.id, 390 toolName: toolCall.name, 391 args: toolCall.arguments, 392 }); 393 const finalized: FinalizedToolCallOutcome = { 394 toolCall, 395 result: createErrorToolResult( 396 "src-str">`Tool call "${toolCall.name}" was not executed: the response hit the output token limit, so its arguments may be truncated. Re-issue the tool call with complete arguments.`, 错误 tool result 文案:要求模型重新发起完整参数error tool result: ask model to re-issue full args 397 ), 398 isError: true, 399 }; 400 await emitToolExecutionEnd(finalized, emit); 401 const toolResultMessage = createToolResultMessage(finalized); 402 await emitToolResultMessage(toolResultMessage, emit); 403 messages.push(toolResultMessage); 404 } 405 return { messages, terminate: false };

C10 心脏止于 streamFunction 调用点——再往下就是 C14 的 pi-ai 层。下面这条桥接 trace 把同一次 turn-1 model stream从 agent-loop 追到 Anthropic SSE,是理解「双环」与「provider 抽象」接缝的关键。

C10 heart ends at streamFunction — below that is C14 pi-ai. This bridge trace follows the same turn-1 model stream from agent-loop to Anthropic SSE; the seam between twin loops and provider abstraction.

C10 → C14 跨层调用链(主线 turn-1)C10 → C14 cross-layer call chain (through-line turn-1)
#location主线时刻 / through-line moment
1agent-loop.ts:308streamFunction(model, llmContext, options)
2agent.ts:222Agent.streamFunction ← sdk streamFn
3sdk.ts:322modelRuntime.streamSimple(model, context, …)
4model-runtime.ts:638prepareRequest → provider.streamSimple
5models.ts:693applyAuth → apiKey/headers/baseUrl
6compat.ts:275streamSimple → builtinProvider / resolveApiProvider
7anthropic-messages.ts:584SSE → stream.push(start|toolcall_delta|done)
8agent-loop.ts:317for await event → message_update → emit
SOURCE  ·  packages/agent/src/agent-loop.ts bridge-call
297 // Build LLM context 298 const llmContext: Context = { llmContext:systemPrompt + messages + toolsllmContext: systemPrompt + messages + tools 299 systemPrompt: context.systemPrompt, 300 messages: llmMessages, 301 tools: context.tools, 302 }; 303 304 // Resolve API key (important for expiring tokens) 305 const resolvedApiKey = getApiKey:OAuth token 可能在此刷新getApiKey: OAuth token may refresh here 306 (config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey; 307 308 const response = await streamFunction(config.model, llmContext, { ◀ trace ★ 跨层边界:agent-core 不再感知 provider★ cross-layer boundary: agent-core unaware of provider 309 ...config, 310 apiKey: resolvedApiKey, 311 signal, 312 });
SOURCE  ·  packages/coding-agent/src/core/sdk.ts default-stream
33 // Preserve the pre-0.81 fallback for extensions that construct Agent instances 34 // or invoke low-level agent loops without supplying streamFn. Agent core remains 35 // provider-agnostic and does not import pi-ai/compat itself. 36 setDefaultStreamFn(streamSimple); ◀ trace 模块加载时注入默认 streamFn=streamSimplemodule load injects default streamFn=streamSimple
SOURCE  ·  packages/agent/src/stream-fn.ts stream-fn
5 /** 6 * Configure the fallback used by Agent and low-level loops when callers omit streamFn. 7 * 8 * Hosts that provide a default model runtime can install its stream function here 9 * without making pi-agent-core depend on a provider catalog or compatibility layer. 10 */ 11 export function setDefaultStreamFn(streamFn: StreamFn | undefined): void { ◀ trace setDefaultStreamFn:agent-core 与 pi-ai 解耦点setDefaultStreamFn: agent-core / pi-ai decoupling 12 defaultStreamFn = streamFn; 13 } 14 15 export function getDefaultStreamFn(): StreamFn { 16 if (!defaultStreamFn) { 17 throw new Error("No default stream function configured. Pass streamFn explicitly or call setDefaultStreamFn()."); 未配置时抛错——extensions 必须显式传 streamFnthrows if unset — extensions must pass streamFn 18 } 19 return defaultStreamFn;
SOURCE  ·  packages/coding-agent/src/core/sdk.ts sdk-streamfn
304 agent = new Agent({ 305 initialState: { 306 systemPrompt: "", 307 model, 308 thinkingLevel, 309 tools: [], 310 }, 311 convertToLlm: convertToLlmWithBlockImages, 312 streamFn: async (model, context, options) => { Agent 构造时覆盖默认 streamFnAgent ctor overrides default streamFn 313 const providerRetrySettings = settingsManager.getProviderRetrySettings(); 314 const httpIdleTimeoutMs = settingsManager.getHttpIdleTimeoutMs(); 315 // SDKs treat timeout=0 as 0ms (immediate timeout), not &quot;no timeout&quot;. 316 // Use max int32 to effectively disable the timeout. 317 const effectiveTimeoutMs = httpIdleTimeoutMs === 0 ? 2147483647 : httpIdleTimeoutMs; 318 const timeoutMs = options?.timeoutMs ?? providerRetrySettings.timeoutMs ?? effectiveTimeoutMs; 319 const websocketConnectTimeoutMs = 320 options?.websocketConnectTimeoutMs ?? settingsManager.getWebSocketConnectTimeoutMs(); 321 const headerRunner = extensionRunnerRef.current; 322 return modelRuntime.streamSimple(model, context, { ◀ trace ★ modelRuntime.streamSimple:重试/timeout/header 在此★ modelRuntime.streamSimple: retry/timeout/headers here 323 ...options, 324 timeoutMs, 325 websocketConnectTimeoutMs, 326 maxRetries: options?.maxRetries ?? providerRetrySettings.maxRetries, 327 maxRetryDelayMs: options?.maxRetryDelayMs ?? providerRetrySettings.maxRetryDelayMs, 328 transformHeaders: async (requestHeaders) => { transformHeaders:扩展 before_provider_headerstransformHeaders: extension before_provider_headers 329 const headers = mergeProviderAttributionHeaders( 330 model, 331 settingsManager, 332 options?.sessionId, 333 requestHeaders, 334 ); 335 return headerRunner?.hasHandlers("before_provider_headers") 336 ? headerRunner.emitBeforeProviderHeaders(headers ?? {}) 337 : (headers ?? {}); 338 }, 339 }); 340 },
SOURCE  ·  packages/coding-agent/src/core/model-runtime.ts runtime-simple
573 private async prepareRequest<TOptions extends ProviderRequestOptions & ModelsRequestTransforms>( 574 model: Model<Api>, 575 options: TOptions | undefined, 576 ): Promise<{ 577 provider: Provider; 578 model: Model<Api>; 579 options: Omit<TOptions, "transformHeaders"> & ProviderRequestOptions; 580 }> { 581 const provider = this.models.getProvider(model.provider); 582 if (!provider) throw new ModelsError("provider", "src-str">`Unknown provider: ${model.provider}`); 583 const resolution = await this.getAuth(model, { prepareRequest:getAuth + merge headersprepareRequest: getAuth + merge headers 584 apiKey: options?.apiKey, 585 env: options?.env, 586 signal: options?.signal, 587 }); 588 if (!resolution) throw new ModelsError("auth", "src-str">`Provider is not configured: ${model.provider}`); 589 590 const { transformHeaders, ...rawProviderOptions } = options ?? {}; 591 const providerOptions = rawProviderOptions as Omit<TOptions, "transformHeaders"> & ProviderRequestOptions; 592 let headers = mergeHeaders(resolution.auth.headers, providerOptions.headers); 593 if (transformHeaders) headers = await transformHeaders(headers ?? {}); 594 const env = 595 resolution.env || providerOptions.env 596 ? { ...(resolution.env ?? {}), ...(providerOptions.env ?? {}) } 597 : undefined; 598 return { 599 provider, 600 model: resolution.auth.baseUrl ? { ...model, baseUrl: resolution.auth.baseUrl } : model, 601 options: { 602 ...providerOptions, 603 apiKey: providerOptions.apiKey ?? resolution.auth.apiKey, 604 headers, 605 env, 606 } as Omit<TOptions, "transformHeaders"> & ProviderRequestOptions, 607 }; 608 } 609 610 stream<TApi extends Api>( 611 model: Model<TApi>, 612 context: Context, 613 options?: ModelsApiStreamOptions<TApi>, 614 ): AssistantMessageEventStream { 615 return lazyStream(model, async () => { 616 const prepared = await this.prepareRequest( 617 model, 618 options as (StreamOptions & ModelsRequestTransforms) | undefined, 619 ); 620 return prepared.provider.stream( 621 prepared.model as Model<TApi>, 622 context, 623 prepared.options as ApiStreamOptions<TApi>, 624 ); 625 }); 626 } 627 628 complete<TApi extends Api>( 629 model: Model<TApi>, 630 context: Context, 631 options?: ModelsApiStreamOptions<TApi>, 632 ): Promise<AssistantMessage> { 633 return this.stream(model, context, options).result(); 634 } 635 636 streamSimple(model: Model<Api>, context: Context, options?: ModelsSimpleStreamOptions): AssistantMessageEventStream { ◀ trace ModelRuntime.streamSimple 入口ModelRuntime.streamSimple entry 637 return lazyStream(model, async () => { 638 const prepared = await this.prepareRequest(model, options); 639 return prepared.provider.streamSimple(prepared.model, context, prepared.options as SimpleStreamOptions); ◀ trace 委托 provider.streamSimpledelegate to provider.streamSimple 640 });
对照 pi-textbook cp07 agent-loop.test.ts:搜索 two turntoolUse,测试里的 event 序列应与 --verbose stderr 同构。 Cross-read textbook cp07 agent-loop.test.ts: search two turn or toolUse; test event sequence should match --verbose stderr.
SOURCE  ·  packages/agent/src/agent-loop.ts walkthrough
1 while (true) { // 外环:follow-up / outer: follow-up 2 while (hasMoreToolCalls || pendingMessages.length) // 内环:tool+steer / inner: tool+steer 3 await streamAssistantResponse(...) // 调 LLM / call LLM 4 await executeToolCalls(...) // 执行 read 等 / execute read etc
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 07Agent Loopagent-loop.tspackages/agent/src/agent-loop.ts
DEPTH 深度细读 · C10.4+Deep dive · C10.4+

C10.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/agent/src/agent-loop.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/agent/src/agent-loop.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C10.5 关键函数与数据流key functions and data flow

agent-loop.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in agent-loop.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C10.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/agent/src/agent-loop.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/agent/src/agent-loop.ts.

C10.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C10.8 读完后应能回答的 5 个问题 · runLoop 双环Five questions you should answer after reading · runLoop 双环

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 10

站 10:packages/agent/src/agent-loop.ts 处理主线。

Station 10: packages/agent/src/agent-loop.ts on through-line.

SOURCE  ·  packages/agent/src/agent-loop.ts src
while (true) { // outer follow-up while (hasMoreToolCalls || pendingMessages.length) {
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 07Agent Loopagent-loop.tspackages/agent/src/agent-loop.ts
附录Appendix 常见问题FAQ
QrunLoop 双环最关键文件?Key file?

packages/agent/src/agent-loop.ts

packages/agent/src/agent-loop.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C10.9 双环状态机:用主线 prompt 走一遍Twin-loop state machine walked with through-line prompt

外环条件:while (true) 检查 follow-up 队列与 steering 注入。主线 prompt 首次进入时 follow-up 为空,但 steering 可能在 tool 执行中被用户插入——这是 Pi 与「一次性 completion API」的本质差异。

Outer loop: while (true) checks follow-up queue and steering. First through-line entry has empty follow-up, but steering may inject during tool execution — Pi's essential difference from one-shot completion APIs.

内环条件:while (hasMoreToolCalls || pendingMessages.length)。第一圈 model 返回 toolUse(read) 后,hasMoreToolCalls 为真,内环继续而不回到用户。第二圈 model 收到 tool result 后 stopReason=stop,内环退出,外环检查无 follow-up 后 agent_end。

Inner loop: while (hasMoreToolCalls || pendingMessages.length). After turn-1 toolUse(read), inner loop continues. Turn-2 stop ends inner loop; outer loop exits with agent_end when no follow-up.

理解双环的最快方法:在 pi-textbook checkpoint 07 的测试里给 ScriptedModel 固定两轮响应,对照 --verbose stderr 的 event 顺序——应看到两次 model_start 夹一次 tool_execution_*

Fastest way to grok twin loops: fix two-turn responses in textbook cp07's ScriptedModel, compare with --verbose stderr — expect two model_start bracketing one tool_execution_*.

SOURCE  ·  packages/agent/src/agent-loop.ts loop
while (true) { // outer: follow-up while (hasMoreToolCalls || pendingMessages.length) { // inner await streamAssistantResponse(...); await executeToolCalls(...); } }
CHAPTER 11 · HEART

Steering 与 Follow-up

Steering and follow-up

运行中插队与结束后续

mid-run injection and post-stop follow-up

模块Module
agent-coreagent-core
Package
agent.ts + agent-loop.tsagent.ts + agent-loop.ts
线程Thread
站 10–11Stations 10–11
输出Output
steeringQueue / followUpQueuesteeringQueue / followUpQueue

Steering:运行中用户插入新消息(内环下一轮前注入)。Follow-up:agent 即将停止时队列里的后续消息(外环重启)。

Steering: user inserts mid-run (injected before next inner round). Follow-up: queued messages when agent would stop (outer loop restarts).

SOURCE  ·  packages/agent/src/agent-loop.ts steer
let pendingMessages = (await config.getSteeringMessages?.()) || []; if (pendingMessages.length > 0) { for (const message of pendingMessages) { ... } }
机制触发时机典型场景
Steering内环每轮前「别读 README 了,先 ls」
Follow-up外环 would-stop批量任务第二条命令
AbortAbortSignalCtrl+C · 扩展取消

C11.1 Case study:read 中途改主意Case study: change mind mid-read

用户发出主线 prompt 后,模型开始 toolUse read;用户在 TUI 里输入 steering「先列出目录」。内环在 executeTool 前收到 pendingMessages,注入 user 消息,可能改变 tool 选择。

After through-line prompt, model starts toolUse read; user steers «list directory first». Inner loop receives pendingMessages before executeTool, may change tool choice.

📘 pi-textbook checkpoint 09:Stateful Agent(stateful-agent)· 生产映射:packages/agent/src/agent.ts 📘 pi-textbook checkpoint 09: Stateful Agent (stateful-agent) · production: packages/agent/src/agent.ts
SOURCE TRACE · C11 Steering / Follow-up 双队列源码 trace Steering / Follow-up dual-queue source trace

Steering 与 Follow-up 不是 UI 花活——它们在类型系统里是 AgentLoopConfig 的两个可选钩子:getSteeringMessagesgetFollowUpMessages。AgentSession 维护 _steeringMessages / _followUpMessages 字符串队列,在 config 工厂里桥接到 agent-loop。

Steering and follow-up aren't UI gimmicks — they're optional hooks on AgentLoopConfig: getSteeringMessages and getFollowUpMessages. AgentSession maintains string queues bridged to agent-loop via config factory.

SOURCE  ·  packages/agent/src/types.ts config-hooks
230 context: PrepareNextTurnContext, 231 ) => AgentLoopTurnUpdate | undefined | Promise<AgentLoopTurnUpdate | undefined>; 232 233 /** 234 * Returns steering messages to inject into the conversation mid-run. 235 * 236 * Called after the current assistant turn finishes executing its tool calls, unless "src-str">`shouldStopAfterTurn` exits first. 237 * If messages are returned, they are added to the context before the next LLM call. 238 * Tool calls from the current assistant message are not skipped. 239 * 240 * Use this for "steering" the agent while it's working. 241 * 242 * Contract: must not throw or reject. Return [] when no steering messages are available. 243 */ 244 getSteeringMessages?: () => Promise<AgentMessage[]>; ◀ trace getSteeringMessages:内环每轮前 pollgetSteeringMessages: poll before each inner round 245 246 /** 247 * Returns follow-up messages to process after the agent would otherwise stop. 248 * 249 * Called when the agent has no more tool calls and no steering messages. 250 * If messages are returned, they're added to the context and the agent 251 * continues with another turn. 252 * 253 * Use this for follow-up messages that should wait until the agent finishes. 254 * 255 * Contract: must not throw or reject. Return [] when no follow-up messages are available. 256 */ 257 getFollowUpMessages?: () => Promise<AgentMessage[]>; ◀ trace getFollowUpMessages:外环 would-stop 时 pollgetFollowUpMessages: poll at outer would-stop 258 259 /** 260 * Tool execution mode.
SOURCE  ·  packages/agent/src/agent-loop.ts inject
166 // Check for steering messages at start (user may have typed while waiting) 167 let pendingMessages: AgentMessage[] = (await config.getSteeringMessages?.()) || []; 循环开头:await getSteeringMessagesloop start: await getSteeringMessages 168 169 // Outer loop: continues when queued follow-up messages arrive after agent would stop 170 while (true) { 171 let hasMoreToolCalls = true; 172 173 // Inner loop: process tool calls and steering messages 174 while (hasMoreToolCalls || pendingMessages.length > 0) { 175 if (!firstTurn) { 176 await emit({ type: "turn_start" }); 177 } else { 178 firstTurn = false; 179 } 180 181 // Process pending messages (inject before next assistant response) 182 if (pendingMessages.length > 0) { ◀ trace pending 非空:逐条 push 到 context,无 LLMpending non-empty: push to context, no LLM 183 for (const message of pendingMessages) { 184 await emit({ type: "message_start", message }); 185 await emit({ type: "message_end", message }); message_end 后立即进入 streamAssistantResponsemessage_end then streamAssistantResponse 186 currentContext.messages.push(message); 187 newMessages.push(message); 188 } 189 pendingMessages = []; 190 }
SOURCE  ·  packages/agent/src/agent-loop.ts followup
258 259 pendingMessages = (await config.getSteeringMessages?.()) || []; 内环结束后再 poll steeringpoll steering after inner loop 260 } 261 262 // Agent would stop here. Check for follow-up messages. 263 const followUpMessages = (await config.getFollowUpMessages?.()) || []; ◀ trace ★ follow-up:外环唯一重启条件★ follow-up: sole outer restart condition 264 if (followUpMessages.length > 0) { 265 // Set as pending so inner loop processes them 266 pendingMessages = followUpMessages; ◀ trace follow-up 变成 pending,continue 外环follow-up becomes pending, continue outer 267 continue; 268 }
Case:read 中途 steering「先 ls」Case: steer «ls first» mid-read
#location主线时刻 / through-line moment
1turn-1 toolUse(read)模型已决定读 README
2executeTool 前getSteeringMessages 返回 user「先 ls」
3pending 注入context += steer user · 无 model
4stream turn-1b模型可能改调 bash/ls
5队列 UIAgentSession splice steering 队列
SOURCE  ·  packages/coding-agent/src/core/agent-session.ts session-queue
620 /** Internal handler for agent events - shared by subscribe and reconnect */ 621 private _handleAgentEvent = async (event: AgentEvent): Promise<void> => { 622 // When a user message starts, check if it&#x27;s from either queue and remove it BEFORE emitting 623 // This ensures the UI sees the updated queue state 624 if (event.type === "message_start" && event.message.role === "user") { ◀ trace message_start(user) 时从队列移除on message_start(user) remove from queue 625 this._overflowRecoveryAttempted = false; 626 const messageText = contentText(event.message.content, ""); 627 if (messageText) { 628 // Check steering queue first 629 const steeringIndex = this._steeringMessages.indexOf(messageText); ◀ trace 先匹配 steering 队列match steering queue first 630 if (steeringIndex !== -1) { 631 this._steeringMessages.splice(steeringIndex, 1); 632 this._emitQueueUpdate(); 633 } else { 634 // Check follow-up queue 635 const followUpIndex = this._followUpMessages.indexOf(messageText); ◀ trace 再匹配 follow-up 队列then follow-up queue 636 if (followUpIndex !== -1) { 637 this._followUpMessages.splice(followUpIndex, 1); 638 this._emitQueueUpdate(); 639 } 640 } 641 } 642 } 643 644 // Emit to extensions first 645 await this._emitExtensionEvent(event); 646 647 // Notify all listeners 648 this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event); _emit 给 TUI 监听器_emit to TUI listeners

关键时序:_handleAgentEventmessage_start(user)先于 emit 修改队列状态——保证 TUI 看到 steering 消息时侧边栏队列已更新。这是「事件驱动 UI」与「轮询队列」的接缝,也是 fork Pi 时最容易漏掉的细节。

Key timing: _handleAgentEvent mutates queue before emit on message_start(user) — TUI sees updated sidebar when steering message appears. Seam between event-driven UI and queue polling; easy to miss when forking Pi.

实验:interactive 模式下模型 toolUse(read) 后立刻输入 steering,观察 inner loop 是否多一轮 model stream。 Experiment: after toolUse(read) in interactive mode, steer immediately; observe extra inner model round.
DEPTH 深度细读 · C11.4+Deep dive · C11.4+

C11.4 内环插队语义inner-loop injection

runLoop 内环每次迭代末尾调用 getSteeringMessages(agent-loop.ts 259 行)。返回的 AgentMessage[] 在下一次 streamAssistantResponse 之前 push 进 context。

Inner loop calls getSteeringMessages (line 259). AgentMessage[] pushed before next streamAssistantResponse.

agent.ts 475–480 行:steeringQueue.drain();skipInitialSteeringPoll 避免首轮重复 drain。

agent.ts 475–480: steeringQueue.drain(); skipInitialSteeringPoll avoids double drain.

AgentSession 1545 行 getSteeringMessages() 暴露只读副本给 TUI。

AgentSession line 1545 exposes read-only copy for TUI.

场景:read 执行中用户输入「先 ls」——steering 在 tool batch 完成后、下次 LLM 前注入。

Scenario: while read runs, user types «ls first» — steering injects after tool batch.

C11.5 外环续聊语义outer-loop continuation

内环结束后外环检查 getFollowUpMessages(263 行)。非空则 pendingMessages 并 continue 外环。

Outer loop checks getFollowUpMessages (line 263). Non-empty continues outer loop.

agent.ts 482 行 followUpQueue.drain()。AgentSession._queueFollowUp(1407 行)路由后续用户输入。

agent.ts 482 followUpQueue.drain(); AgentSession._queueFollowUp routes post-turn input.

与 steering 区别:follow-up 在 agent would stop 时;steering 在 still running 时。

vs steering: follow-up when would stop; steering while still running.

C11.6 AgentSession 与 Agent 的分工AgentSession vs Agent

AgentSession 维护 _steeringMessages / _followUpMessages;构造 AgentLoopConfig 时绑定 drain。

AgentSession maintains queues; binds drain when building AgentLoopConfig.

Agent 类封装有状态队列,供 SDK 与测试使用。

Agent class wraps stateful queues for SDK and tests.

interactive-mode.ts 4303 行合并队列状态渲染 footer。

interactive-mode.ts line 4303 merges queue state in footer.

C11.7 QueueMode 与并发QueueMode and concurrency

types.ts 导出 QueueMode 控制排队策略。

types.ts exports QueueMode for queue policy.

agent-session-concurrent.test.ts 验证 extension steer。

concurrent test verifies extension steering.

abort:AgentSession.abort() 1561 行调用 agent.abort()。

Abort: AgentSession.abort() line 1561 calls agent.abort().

C11.8 主线 prompt 下的 steering/follow-upOn through-line

默认 README 任务不触发 steering;测试 mock getSteeringMessages 观察内环注入。

Default README task does not steer; mock getSteeringMessages to test injection.

C11.9 读完后应能回答的 5 个问题 · Steering 与 Follow-upFive questions you should answer after reading · Steering 与 Follow-up

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 10

站 10–11:getSteeringMessages / getFollowUpMessages 桥接 AgentSession 与 runLoop。

Stations 10–11: steering/follow-up bridge AgentSession and runLoop.

SOURCE  ·  packages/agent/src/agent-loop.ts loop
pendingMessages = (await config.getSteeringMessages?.()) || []; const followUpMessages = (await config.getFollowUpMessages?.()) || [];
SOURCE  ·  packages/agent/src/agent.ts agent
getSteeringMessages: async () => this.steeringQueue.drain(), getFollowUpMessages: async () => this.followUpQueue.drain(),
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 09Stateful Agentstateful-agentpackages/agent/src/agent.ts
附录Appendix 常见问题FAQ
Qsteering 和 follow-up 能否同时排队?Both queue?

可以;pendingMessageCount 1540 行返回两者之和。

Yes; pendingMessageCount sums both.

Qsteering 会打断 bash 吗?Interrupt bash?

不立即;在当前 tool batch 完成后注入。

Not immediately; after tool batch.

Qextension 如何注入 steering?Extension steer?

通过 ExtensionActions queue 方法。

Via ExtensionActions queue methods.

C11.10 打开源码:packages/agent/src/agent-loop.tsOpen source: packages/agent/src/agent-loop.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/agent/src/agent-loop.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/agent/src/agent-loop.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 12 · HEART

Tool 执行管线

Tool execution pipeline

parallel vs sequential · length 截断

parallel vs sequential · length truncation

模块Module
模块Module
Package
coding-agentcoding-agent
线程Thread
站 12St. 12
输出Output
read.tsread.ts

模型返回 toolUse 后,executeToolsparallelsequential 策略调度。read/write/edit/bash 各有 executor;结果超长时 truncateToolResult 截断。

After model returns toolUse, executeTools dispatches per parallel or sequential policy. read/write/edit/bash have executors; oversized results hit truncateToolResult.

SOURCE  ·  packages/coding-agent/src/core/tools/index.ts tools
export const CODING_TOOLS = [readTool, writeTool, editTool, bashTool, ...];
SOURCE  ·  packages/agent/src/agent-loop.ts exec
async function executeToolCalls(...) // validateToolArguments → execute → normalize images
Tool并行安全?主线用例
readREADME.md
bash需确认
write/edit✗ sequential

C12.1 read README 的完整路径Full path of read README

01
validateToolArguments{ "path": "README.md" }
02
resolvePath(cwd)绝对路径
03
readFileSyncUTF-8 内容
04
truncate if needed默认 max length
05
toolResult AgentMessagetoolCallId=call_1
📘 pi-textbook checkpoint 06:Tool Contract(tool-contract)· 生产映射:packages/agent/src/agent-loop.ts · packages/coding-agent/src/core/tools/ 📘 pi-textbook checkpoint 06: Tool Contract (tool-contract) · production: packages/agent/src/agent-loop.ts · packages/coding-agent/src/core/tools/
📘 pi-textbook checkpoint 08:Coding Tools(coding-tools)· 生产映射:packages/coding-agent/src/core/tools/index.ts 📘 pi-textbook checkpoint 08: Coding Tools (coding-tools) · production: packages/coding-agent/src/core/tools/index.ts
SOURCE TRACE · C12 read README 工具管线 · 从 toolUse 到 toolResult read README tool pipeline · toolUse to toolResult

主线 prompt 的第一次 tool call 几乎总是 read({"path":"README.md"})executeToolCalls 根据 toolExecution 配置和 per-tool executionMode 选择并行或串行;read 默认并行安全。

Through-line's first tool call is usually read({"path":"README.md"}). executeToolCalls picks parallel vs sequential from toolExecution config and per-tool executionMode; read is parallel-safe by default.

SOURCE  ·  packages/agent/src/agent-loop.ts dispatch
411 async function executeToolCalls( 412 currentContext: AgentContext, 413 assistantMessage: AssistantMessage, 414 config: AgentLoopConfig, 415 signal: AbortSignal | undefined, 416 emit: AgentEventSink, 417 ): Promise<ExecutedToolCallBatch> { 418 const toolCalls = assistantMessage.content.filter((c) => c.type === "toolCall"); 从 assistant content filter toolCallfilter toolCall from assistant content 419 const hasSequentialToolCall = toolCalls.some( 任一 tool executionMode=sequential → 串行any tool sequential → sequential path 420 (tc) => currentContext.tools?.find((t) => t.name === tc.name)?.executionMode === "sequential", 421 ); 422 if (config.toolExecution === "sequential" || hasSequentialToolCall) { ◀ trace config.toolExecution 全局覆盖config.toolExecution global override 423 return executeToolCallsSequential(currentContext, assistantMessage, toolCalls, config, signal, emit); 424 } 425 return executeToolCallsParallel(currentContext, assistantMessage, toolCalls, config, signal, emit); ◀ trace read 单 call 时 parallel/sequential 等价single read: parallel≈sequential 426 }
SOURCE  ·  packages/agent/src/agent-loop.ts sequential
433 async function executeToolCallsSequential( 434 currentContext: AgentContext, 435 assistantMessage: AssistantMessage, 436 toolCalls: AgentToolCall[], 437 config: AgentLoopConfig, 438 signal: AbortSignal | undefined, 439 emit: AgentEventSink, 440 ): Promise<ExecutedToolCallBatch> { 441 const finalizedCalls: FinalizedToolCallOutcome[] = []; 442 const messages: ToolResultMessage[] = []; 443 444 for (const toolCall of toolCalls) { 逐 toolCall:tool_execution_startper toolCall: tool_execution_start 445 await emit({ ◀ trace 446 type: "tool_execution_start", 447 toolCallId: toolCall.id, 448 toolName: toolCall.name, 449 args: toolCall.arguments, 450 }); 451 452 const preparation = await prepareToolCall(currentContext, assistantMessage, toolCall, config, signal); prepareToolCall:参数校验 + 扩展 hookprepareToolCall: validate + extension hook 453 let finalized: FinalizedToolCallOutcome; 454 if (preparation.kind === "immediate") { 455 finalized = { 456 toolCall, 457 result: preparation.result, 458 isError: preparation.isError, 459 }; 460 } else { 461 const executed = await executePreparedToolCall(preparation, signal, emit); ◀ trace executePreparedToolCall:真正执行 readexecutePreparedToolCall: actually run read 462 finalized = await finalizeExecutedToolCall( 463 currentContext, 464 assistantMessage, 465 preparation, 466 executed, 467 config, 468 signal, 469 ); 470 } 471 472 await emitToolExecutionEnd(finalized, emit); 473 const toolResultMessage = createToolResultMessage(finalized); ◀ trace createToolResultMessage → message_start/endcreateToolResultMessage → message_start/end 474 await emitToolResultMessage(toolResultMessage, emit); 475 finalizedCalls.push(finalized);
SOURCE  ·  packages/coding-agent/src/core/tools/read.ts read-exec
209 export function createReadToolDefinition( createReadToolDefinition:schema + executecreateReadToolDefinition: schema + execute 210 cwd: string, 211 options?: ReadToolOptions, 212 ): ToolDefinition<typeof readSchema, ReadToolDetails | undefined> { 213 const autoResizeImages = options?.autoResizeImages ?? true; 214 const ops = options?.operations ?? defaultReadOperations; 215 return { 216 name: "read", 217 label: "read", 218 description: "src-str">`Read the contents of a file. Supports text files and images (jpg, png, gif, webp, bmp). Images are sent as attachments. For text files, output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`, DEFAULT_MAX_LINES / MAX_BYTES 截断说明在 descriptiontruncation limits in description 219 promptSnippet: readToolSystemPromptContribution.snippet, 220 promptGuidelines: [...readToolSystemPromptContribution.guidelines], 221 parameters: readSchema, 222 constrainedSampling: getExperimentalToolSampling(), 223 async execute( ◀ trace execute 入口:Promise + AbortSignalexecute entry: Promise + AbortSignal 224 _toolCallId, 225 { path, offset, limit }: { path: string; offset?: number; limit?: number }, 226 signal?: AbortSignal, 227 _onUpdate?, 228 ctx?, 229 ) { 230 return new Promise<{ content: (TextContent | ImageContent)[]; details: ReadToolDetails | undefined }>(
SOURCE  ·  packages/coding-agent/src/core/tools/read.ts read-body
243 (async () => { 244 try { 245 const absolutePath = await resolveReadPathAsync(path, cwd); ◀ trace resolveReadPathAsync:cwd 相对 → 绝对resolveReadPathAsync: cwd-relative → absolute 246 if (aborted) return; 247 // Check if file exists and is readable. 248 await ops.access(absolutePath); fs access 检查可读fs access readability check 249 if (aborted) return; 250 const mimeType = ops.detectImageMimeType ? await ops.detectImageMimeType(absolutePath) : undefined; 251 let content: (TextContent | ImageContent)[]; 252 let details: ReadToolDetails | undefined; 253 const nonVisionImageNote = getNonVisionImageNote(ctx?.model); 254 if (mimeType) { 255 // Read image as binary. 256 const buffer = await ops.readFile(absolutePath); 257 const processed = await processImage(buffer, mimeType, { autoResizeImages }); 258 if (!processed.ok) { 259 let textNote = "src-str">`Read image file [${mimeType}]\n${processed.message}`; 260 if (nonVisionImageNote) textNote += "src-str">`\n${nonVisionImageNote}`; 261 content = [{ type: "text", text: textNote }]; 262 } else { 263 let textNote = "src-str">`Read image file [${processed.mimeType}]`; 264 if (processed.hints.length > 0) textNote += "src-str">`\n${processed.hints.join("\n")}`; 265 if (nonVisionImageNote) textNote += "src-str">`\n${nonVisionImageNote}`; 266 content = [ 267 { type: "text", text: textNote }, 268 { type: "image", data: processed.data, mimeType: processed.mimeType }, 269 ]; 270 } 271 } else { 272 // Read text content. 273 const buffer = await ops.readFile(absolutePath); 文本分支:buffer.toString utf-8text branch: buffer.toString utf-8 274 const textContent = buffer.toString("utf-8"); 275 const allLines = textContent.split("\n"); 276 const totalFileLines = allLines.length; 277 // Apply offset if specified. Convert from 1-indexed input to 0-indexed array access. 278 const startLine = offset ? Math.max(0, offset - 1) : 0; 279 const startLineDisplay = startLine + 1; 280 // Check if offset is out of bounds. 281 if (startLine >= allLines.length) { 282 throw new Error("src-str">`Offset ${offset} is beyond end of file (${allLines.length} lines total)`); 283 } 284 let selectedContent: string; 285 let userLimitedLines: number | undefined; 286 // If limit is specified by the user, honor it first. Otherwise truncateHead decides. 287 if (limit !== undefined) { 288 const endLine = Math.min(startLine + limit, allLines.length); 289 selectedContent = allLines.slice(startLine, endLine).join("\n"); 290 userLimitedLines = endLine - startLine; 291 } else { 292 selectedContent = allLines.slice(startLine).join("\n"); 293 } 294 // Apply truncation, respecting both line and byte limits. 295 const truncation = truncateHead(selectedContent); ◀ trace ★ truncateHead:超长按行/字节截断★ truncateHead: line/byte truncation 296 let outputText: string; 297 if (truncation.firstLineExceedsLimit) { 298 // First line alone exceeds the byte limit. Point the model at a bash fallback. 299 const firstLineSize = formatSize(Buffer.byteLength(allLines[startLine], "utf-8")); 300 outputText = "src-str">`[Line ${startLineDisplay} is ${firstLineSize}, exceeds ${formatSize(DEFAULT_MAX_BYTES)} limit. Use bash: sed -n '${startLineDisplay}p' ${path} | head -c ${DEFAULT_MAX_BYTES}]`; 301 details = { truncation }; 302 } else if (truncation.truncated) { 303 // Truncation occurred. Build an actionable continuation notice. 304 const endLineDisplay = startLineDisplay + truncation.outputLines - 1; 305 const nextOffset = endLineDisplay + 1; 306 outputText = truncation.content; 307 if (truncation.truncatedBy === "lines") { 308 outputText += "src-str">`\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}. Use offset=${nextOffset} to continue.]`; 截断提示:offset=N 继续读truncation hint: offset=N to continue 309 } else { 310 outputText += "src-str">`\n\n[Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines} (${formatSize(DEFAULT_MAX_BYTES)} limit). Use offset=${nextOffset} to continue.]`; 311 } 312 details = { truncation }; 313 } else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) { 314 // User-specified limit stopped early, but the file still has more content. 315 const remaining = allLines.length - (startLine + userLimitedLines); 316 const nextOffset = startLine + userLimitedLines + 1; 317 outputText = "src-str">`${truncation.content}\n\n[${remaining} more lines in file. Use offset=${nextOffset} to continue.]`; 318 } else { 319 // No truncation and no remaining user-limited content. 320 outputText = truncation.content; 321 } 322 content = [{ type: "text", text: outputText }]; ◀ trace 返回 TextContent[] 给 agent-loopreturn TextContent[] to agent-loop 323 }
read(README.md) 逐步read(README.md) step by step
#location主线时刻 / through-line moment
1validateToolArguments{"path":"README.md"}
2resolveReadPathAsynccwd/README.md → absolute
3fsReadFileUTF-8 buffer
4truncateHead≤2000 lines / 256KB
5tool_execution_endisError=false
6AgentMessagerole=toolResult · toolCallId=call_1
7inner loop iter 2context.messages += result

若 README 超大,truncateHead 会在 tool result 末尾附加 [Showing lines … Use offset=N to continue.]——模型在 turn-2 可能只读到文件头部。主线 prompt「一句话概括」通常不需要全文;这是 Pi 故意用截断换上下文窗口的设计取舍。

If README is huge, truncateHead appends continuation hints — turn-2 model may only see the head. Through-line «one sentence summary» rarely needs full file; truncation trades context window for completeness.

DEPTH 深度细读 · C12.4+Deep dive · C12.4+

C12.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/coding-agent/src/core/tools/read.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/coding-agent/src/core/tools/read.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C12.5 关键函数与数据流key functions and data flow

read.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in read.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C12.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/coding-agent/src/core/tools/read.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/coding-agent/src/core/tools/read.ts.

C12.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C12.8 读完后应能回答的 5 个问题 · Tool 执行管线Five questions you should answer after reading · Tool 执行管线

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/coding-agent/src/core/tools/read.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/coding-agent/src/core/tools/read.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/coding-agent/src/core/tools/read.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/coding-agent/src/core/tools/read.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 12

站 12:packages/coding-agent/src/core/tools/read.ts 处理主线。

Station 12: packages/coding-agent/src/core/tools/read.ts on through-line.

SOURCE  ·  packages/coding-agent/src/core/tools/read.ts src
// Tool 管线 · packages/coding-agent/src/core/tools/read.ts // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 06Tool Contracttool-contractpackages/agent/src/agent-loop.ts · packages/coding-agent/src/core/tools/
cp 08Coding Toolscoding-toolspackages/coding-agent/src/core/tools/index.ts
附录Appendix 常见问题FAQ
QTool 管线最关键文件?Key file?

packages/coding-agent/src/core/tools/read.ts

packages/coding-agent/src/core/tools/read.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C12.9 打开源码:packages/coding-agent/src/core/tools/read.tsOpen source: packages/coding-agent/src/core/tools/read.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/coding-agent/src/core/tools/read.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/coding-agent/src/core/tools/read.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 13 · HEART

事件总线

The event bus

message_update 如何驱动 TUI

how message_update drives the TUI

模块Module
模块Module
Package
coding-agentcoding-agent
线程Thread
站 13St. 13
输出Output
agent-session.tsagent-session.ts

agent-core 通过 AgentEvent tagged union 广播状态变化。message_update 携带 text_deltatoolcall_delta——TUI 差分渲染的唯一输入。

agent-core broadcasts state via AgentEvent tagged union. message_update carries text_delta or toolcall_delta — sole input for TUI differential render.

事件订阅者副作用
agent_startAgentSession 订阅者TUI / JSONL / Extension
turn_startAgentSession 订阅者TUI / JSONL / Extension
message_startAgentSession 订阅者TUI / JSONL / Extension
message_updateAgentSession 订阅者TUI / JSONL / Extension
message_endAgentSession 订阅者TUI / JSONL / Extension
tool_execution_startAgentSession 订阅者TUI / JSONL / Extension
tool_execution_updateAgentSession 订阅者TUI / JSONL / Extension
tool_execution_endAgentSession 订阅者TUI / JSONL / Extension
turn_endAgentSession 订阅者TUI / JSONL / Extension
agent_endAgentSession 订阅者TUI / JSONL / Extension
SOURCE  ·  packages/agent/src/types.ts events
export type AgentEvent = | { type: "message_update"; message: ...; delta: ... } | { type: "tool_execution_start"; ... };

C13.1 主线一轮的事件顺序Event order for one through-line turn

简化版:agent_start → turn_start → message_start(user) → message_end(user) → message_update×N → message_end(assistant) → tool_execution_* → message_update×M → message_end(assistant) → turn_end → agent_end。

Simplified: agent_start → turn_start → message_start(user) → … → agent_end.

FIG 事件泳道图:同一主线在 user/model/loop/tool 四条泳道上的时序。 Event swimlane: through-line timing across user/model/loop/tool lanes.
📘 pi-textbook checkpoint 01:TypeScript 生存集(events.ts)· 生产映射: 📘 pi-textbook checkpoint 01: TypeScript 生存集 (events.ts) · production:
📘 pi-textbook checkpoint 02:EventStream(event-stream.ts)· 生产映射:packages/ai/src/utils/event-stream.ts 📘 pi-textbook checkpoint 02: EventStream (event-stream.ts) · production: packages/ai/src/utils/event-stream.ts
SOURCE TRACE · C13 AgentEvent 总线:主线一轮完整事件序列 AgentEvent bus: full event sequence for one through-line turn

AgentEvent 是 agent-core 与 coding-agent/TUI 的唯一契约。agent-loop 只 call emit(event);AgentSession 订阅后 fan-out 到 JSONL、TUI、ExtensionRunner。主线 prompt 一轮大约产生 25–40 个事件(含 message_update delta)。

AgentEvent is the sole contract between agent-core and coding-agent/TUI. agent-loop only emit(event); AgentSession fans out to JSONL, TUI, ExtensionRunner. One through-line turn emits ~25–40 events (including message_update deltas).

SOURCE  ·  packages/agent/src/types.ts event-union
421 /** 422 * Events emitted by the Agent for UI updates. 423 * 424 * "src-str">`agent_end` is the last event emitted for a run, but awaited "src-str">`Agent.subscribe()` 425 * listeners for that event are still part of run settlement. The agent becomes 426 * idle only after those listeners finish. 427 */ 428 export type AgentEvent = 429 // Agent lifecycle 430 | { type: "agent_start" } agent_start / agent_end 包裹整次 runLoopagent_start/agent_end wrap runLoop 431 | { type: "agent_end"; messages: AgentMessage[] } 432 // Turn lifecycle - a turn is one assistant response + any tool calls/results 433 | { type: "turn_start" } turn_start / turn_end 包裹每圈 model+toolsturn_start/turn_end wrap each model+tools round 434 | { type: "turn_end"; message: AgentMessage; toolResults: ToolResultMessage[] } 435 // Message lifecycle - emitted for user, assistant, and toolResult messages 436 | { type: "message_start"; message: AgentMessage } message_start/end:user/assistant/toolResult 共用message_start/end: shared by user/assistant/toolResult 437 // Only emitted for assistant messages during streaming 438 | { type: "message_update"; message: AgentMessage; assistantMessageEvent: AssistantMessageEvent } ◀ trace ★ message_update:仅 assistant 流式★ message_update: assistant streaming only 439 | { type: "message_end"; message: AgentMessage } 440 // Tool execution lifecycle 441 | { type: "tool_execution_start"; toolCallId: string; toolName: string; args: any } ◀ trace tool_execution_*:read 生命周期tool_execution_*: read lifecycle 442 | { type: "tool_execution_update"; toolCallId: string; toolName: string; args: any; partialResult: any } 443 | { type: "tool_execution_end"; toolCallId: string; toolName: string; result: any; isError: boolean };
主线 prompt 事件序列(简化)Through-line event sequence (simplified)
#location主线时刻 / through-line moment
1agent_startrunLoop 进入
2turn_startturn-1 开始
3message_startuser prompt
4message_enduser 定稿 → JSONL append
5message_startassistant partial
6message_update×Ntoolcall_delta 组装 read(args)
7message_endstopReason=toolUse
8tool_execution_startread · call_1
9tool_execution_endREADME 内容
10message_start/endtoolResult 消息
11turn_endturn-1 完成
12turn_startturn-2
13message_update×Mtext_delta 最终回答
14message_endstopReason=stop
15turn_endturn-2 完成
16agent_endnewMessages 返回
SOURCE  ·  packages/agent/src/agent-loop.ts emit-loop
326 case "text_start": 327 case "text_delta": 328 case "text_end": 329 case "thinking_start": 330 case "thinking_delta": 331 case "thinking_end": 332 case "toolcall_start": toolcall_delta → message_updatetoolcall_delta → message_update 333 case "toolcall_delta": 334 case "toolcall_end": 335 if (partialMessage) { 336 partialMessage = event.partial; 337 context.messages[context.messages.length - 1] = partialMessage; 338 await emit({ ◀ trace emit 携带 assistantMessageEvent 原样emit carries assistantMessageEvent as-is 339 type: "message_update", 340 assistantMessageEvent: event, 341 message: { ...partialMessage }, TUI 靠 message 快照差分渲染TUI diffs from message snapshot 342 }); 343 } 344 break;
SOURCE  ·  packages/coding-agent/src/core/agent-session.ts session-persist
644 // Emit to extensions first 645 await this._emitExtensionEvent(event); 扩展先收到事件(可改写 message_end)extensions receive first (can rewrite message_end) 646 647 // Notify all listeners 648 this._emit(event.type === "agent_end" ? { ...event, willRetry: this._willRetryAfterAgentEnd(event) } : event); fan-out 到 session 监听器fan-out to session listeners 649 650 // Handle session persistence 651 if (event.type === "message_end") { ◀ trace ★ message_end → SessionManager.appendMessage★ message_end → SessionManager.appendMessage 652 // Check if this is a custom message from extensions 653 if (event.message.role === "custom") { 654 // Persist as CustomMessageEntry 655 this.sessionManager.appendCustomMessageEntry( 656 event.message.customType, 657 event.message.content, 658 event.message.display, 659 event.message.details, 660 ); 661 } else if ( 662 event.message.role === "user" || 663 event.message.role === "assistant" || 664 event.message.role === "toolResult" 665 ) { 666 // Regular LLM message - persist as SessionMessageEntry 667 this.sessionManager.appendMessage(event.message); ◀ trace user/assistant/toolResult 写 JSONLuser/assistant/toolResult to JSONL 668 } 669 // Other message types (bashExecution, compactionSummary, branchSummary) are persisted elsewhere 670 671 // Track assistant message for auto-compaction (checked on agent_end) 672 if (event.message.role === "assistant") { assistant message_end 触发 compaction 检查标记assistant message_end sets compaction check flag 673 this._lastAssistantMessage = event.message; 674 675 const assistantMsg = event.message as AssistantMessage; 676 if (assistantMsg.stopReason !== "error" && assistantMsg.stopReason !== "length") { 677 this._overflowRecoveryAttempted = false; 678 } 679 680 // Reset retry counter immediately on successful assistant response 681 // This prevents accumulation across multiple LLM calls within a turn 682 if (assistantMsg.stopReason !== "error" && this._retryAttempt > 0) { 683 this._emit({ 684 type: "auto_retry_end", 685 success: true, 686 attempt: this._retryAttempt, 687 }); 688 this._retryAttempt = 0; 689 } 690 }

「message_update 驱动 TUI,message_end 驱动 JSONL」——不是两条管道,而是同一 emit 的两个订阅者消费不同阶段。TUI 在 update 时重绘;SessionManager 只在 end 时落盘,避免每个 delta 写一行 JSONL。

«message_update drives TUI, message_end drives JSONL» — not two pipes but two subscribers to same emit at different stages. TUI redraws on update; SessionManager persists only on end, avoiding per-delta JSONL lines.

SOURCE  ·  packages/coding-agent/src/core/agent-session.ts extension-fanout
738 private async _emitExtensionEvent(event: AgentEvent): Promise<void> { 739 if (event.type === "agent_start") { 740 this._turnIndex = 0; 741 await this._extensionRunner.emit({ type: "agent_start" }); 742 } else if (event.type === "agent_end") { 743 await this._extensionRunner.emit({ type: "agent_end", messages: event.messages }); 744 } else if (event.type === "turn_start") { 745 const extensionEvent: TurnStartEvent = { 746 type: "turn_start", 747 turnIndex: this._turnIndex, 748 timestamp: Date.now(), 749 }; 750 await this._extensionRunner.emit(extensionEvent); 751 } else if (event.type === "turn_end") { 752 const extensionEvent: TurnEndEvent = { 753 type: "turn_end", 754 turnIndex: this._turnIndex, 755 message: event.message, 756 toolResults: event.toolResults, 757 }; 758 await this._extensionRunner.emit(extensionEvent); 759 this._turnIndex++; 760 } else if (event.type === "message_start") { 761 const extensionEvent: MessageStartEvent = { 762 type: "message_start", 763 message: event.message, 764 }; 765 await this._extensionRunner.emit(extensionEvent); 766 } else if (event.type === "message_update") { ◀ trace message_update → ExtensionRunnermessage_update → ExtensionRunner 767 const extensionEvent: MessageUpdateEvent = { 768 type: "message_update", 769 message: event.message, 770 assistantMessageEvent: event.assistantMessageEvent, 771 }; 772 await this._extensionRunner.emit(extensionEvent); 773 } else if (event.type === "message_end") { 774 const extensionEvent: MessageEndEvent = { ◀ trace message_end → emitMessageEnd 可替换消息message_end → emitMessageEnd can replace message 775 type: "message_end", 776 message: event.message, 777 }; 778 const replacement = await this._extensionRunner.emitMessageEnd(extensionEvent); 779 if (replacement) { 780 // Untyped extension handlers can return messages with null/missing content; 781 // normalize so it never enters agent state or session history. 782 const normalized = 783 (replacement.role === "user" || 784 replacement.role === "assistant" || 785 replacement.role === "toolResult" || 786 replacement.role === "custom") && 787 replacement.content == null 788 ? ({ ...replacement, content: [] } as AgentMessage) 789 : replacement; 790 this._replaceMessageInPlace(event.message, normalized); 791 } 792 } else if (event.type === "tool_execution_start") { 793 const extensionEvent: ToolExecutionStartEvent = { tool_execution_start 扩展可见tool_execution_start visible to extensions 794 type: "tool_execution_start", 795 toolCallId: event.toolCallId, 796 toolName: event.toolName, 797 args: event.args, 798 }; 799 await this._extensionRunner.emit(extensionEvent); 800 } else if (event.type === "tool_execution_update") { 801 const extensionEvent: ToolExecutionUpdateEvent = { 802 type: "tool_execution_update", 803 toolCallId: event.toolCallId, 804 toolName: event.toolName, 805 args: event.args, 806 partialResult: event.partialResult, 807 }; 808 await this._extensionRunner.emit(extensionEvent); 809 } else if (event.type === "tool_execution_end") { 810 const extensionEvent: ToolExecutionEndEvent = { 811 type: "tool_execution_end", 812 toolCallId: event.toolCallId, 813 toolName: event.toolName, 814 result: event.result, 815 isError: event.isError, 816 }; 817 await this._extensionRunner.emit(extensionEvent); 818 }
pi --verbose 2>events.log 跑主线 prompt,grep message_update events.log | wc -l 应与 TUI 刷新次数同量级。 Run pi --verbose 2>events.log on through-line; grep message_update events.log | wc -l should match TUI refresh magnitude.
DEPTH 深度细读 · C13.4+Deep dive · C13.4+

C13.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/coding-agent/src/core/agent-session.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/coding-agent/src/core/agent-session.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C13.5 关键函数与数据流key functions and data flow

agent-session.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in agent-session.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C13.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/coding-agent/src/core/agent-session.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/coding-agent/src/core/agent-session.ts.

C13.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C13.8 读完后应能回答的 5 个问题 · 事件总线Five questions you should answer after reading · 事件总线

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 13

站 13:packages/coding-agent/src/core/agent-session.ts 处理主线。

Station 13: packages/coding-agent/src/core/agent-session.ts on through-line.

SOURCE  ·  packages/coding-agent/src/core/agent-session.ts src
// 事件总线 · packages/coding-agent/src/core/agent-session.ts // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 09Stateful Agentstateful-agentpackages/agent/src/agent.ts
附录Appendix 常见问题FAQ
Q事件总线最关键文件?Key file?

packages/coding-agent/src/core/agent-session.ts

packages/coding-agent/src/core/agent-session.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C13.9 打开源码:packages/agent/src/types.tsOpen source: packages/agent/src/types.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/agent/src/types.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/agent/src/types.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 14 · LLM

pi-ai 提供商抽象

pi-ai provider abstraction

Models · createProvider · streamSimple

Models · createProvider · streamSimple

模块Module
模块Module
Package
aiai
线程Thread
站 14St. 14
输出Output
models.tsmodels.ts

pi-ai 抽象 40+ provider:Models 目录 · createProvider 工厂 · streamSimple 统一流式入口。agent-loop 只依赖 StreamFn 签名。

pi-ai abstracts 40+ providers: Models catalog · createProvider factory · streamSimple unified streaming entry. agent-loop depends only on StreamFn signature.

SOURCE  ·  packages/ai/src/stream.ts stream
export async function streamSimple(model, context, options)
SOURCE  ·  packages/ai/src/models.ts models
export const Models = { ... }; // models.generated.ts
Provider 族认证流式协议
OpenAI-compatibleAPI keySSE
AnthropicAPI keySSE events
OAuth (Kimi etc.)OAuth tokenprovider-specific
📘 pi-textbook checkpoint 05:Provider Adapter(provider-adapter.ts)· 生产映射:packages/ai/src/api/*.ts · packages/ai/src/models.ts 📘 pi-textbook checkpoint 05: Provider Adapter (provider-adapter.ts) · production: packages/ai/src/api/*.ts · packages/ai/src/models.ts
SOURCE TRACE · C14 pi-ai 提供商抽象:streamSimple 如何接到 agent-loop pi-ai provider abstraction: how streamSimple connects to agent-loop

pi-ai(packages/ai)是 Pi 的 LLM 适配层。streamSimple 是 agent-core 与 40+ provider 之间的唯一窄接口:输入 Model + Context + options,输出 AssistantMessageEventStream。主线 prompt turn-1 的 toolcall_delta 组装 read({"path":"README.md"}) 就发生在此层。

pi-ai (packages/ai) is Pi's LLM adapter layer. streamSimple is the sole narrow interface between agent-core and 40+ providers: Model + Context + options in, AssistantMessageEventStream out. Through-line turn-1 toolcall_delta assembling read({"path":"README.md"}) happens here.

SOURCE  ·  packages/ai/src/compat.ts stream-simple
275 export function streamSimple<TApi extends Api>( ◀ trace compat 层 streamSimple 入口compat layer streamSimple entry 276 model: Model<TApi>, 277 context: Context, 278 options?: SimpleStreamOptions, 279 ): AssistantMessageEventStream { 280 const builtinProvider = getBuiltinProviderForModel(model); builtin provider 优先(anthropic/openai/…)builtin provider first (anthropic/openai/…) 281 if (builtinProvider) { 282 if (model.provider.startsWith("cloudflare-") && !hasResolvedCloudflareAuth(options)) { 283 return compatModels.streamSimple(model, context, options); 284 } 285 return builtinProvider.streamSimple(model, context, withEnvApiKey(model, options)); 286 } 287 const provider = resolveApiProvider(model.api); ◀ trace 自定义 provider 走 resolveApiProvidercustom providers via resolveApiProvider 288 return provider.streamSimple(model, context, withEnvApiKey(model, options)); 289 }
SOURCE  ·  packages/ai/src/models.ts provider-if
97 export interface Provider<TApi extends Api = Api> { 98 readonly id: string; 99 readonly name: string; 100 101 readonly baseUrl?: string; 102 readonly headers?: ProviderHeaders; 103 104 /** 105 * Required: at least one of "src-str">`apiKey`/"src-str">`oauth`. Every provider has auth 106 * semantics — even providers with only ambient credentials (env vars, AWS 107 * profiles, ADC files) and keyless local servers provide "src-str">`apiKey` auth 108 * whose "src-str">`resolve()` reports whether the provider is configured. 109 * "src-str">`Models.getAuth()` returns undefined when the provider is unconfigured. 110 */ 111 readonly auth: ProviderAuth; ◀ trace Provider.auth:每个 provider 必有认证语义Provider.auth: every provider has auth semantics 112 113 /** 114 * Current known models, sync. Static providers return their catalog; 115 * dynamic providers return the list as of the last "src-str">`refreshModels()` 116 * (empty before the first). Must not throw; "src-str">`Models` treats a throwing 117 * implementation as having no models. 118 */ 119 getModels(): readonly Model<TApi>[]; ◀ trace getModels():静态目录或 refresh 后动态列表getModels(): static catalog or post-refresh dynamic list 120 121 /** 122 * Dynamic providers only: restore "src-str">`context.stored` and optionally fetch a newer list using 123 * the effective credential. Implementations retain their previous list on failure, publish 124 * persistence and synchronous state changes through "src-str">`context.publish()`, and honor the 125 * shared abort signal for blocking work. 126 */ 127 refreshModels?(context: RefreshModelsContext): Promise<void>; 128 129 /** 130 * Optional provider policy for credential-specific model availability. 131 * "src-str">`getModels()` remains the complete synchronous catalog; "src-str">`Models.getAvailable()` 132 * applies this filter after confirming that provider auth is configured. 133 */ 134 filterModels?(models: readonly Model<TApi>[], credential: Credential | undefined): readonly Model<TApi>[]; 135 136 stream<T extends TApi>( 137 model: Model<T>, 138 context: Context, 139 options?: ApiStreamOptions<T>, 140 ): AssistantMessageEventStream;
SOURCE  ·  packages/ai/src/models.ts apply-auth
636 private async applyAuth<TOptions extends ProviderRequestOptions & ModelsRequestTransforms>( 637 model: Model<Api>, 638 options: TOptions | undefined, 639 ): Promise<{ 640 requestModel: Model<Api>; 641 requestOptions: Omit<TOptions, "transformHeaders"> & ProviderRequestOptions; 642 }> { 643 this.requireProvider(model); requireProvider:model.provider → Provider 实例requireProvider: model.provider → Provider instance 644 const resolution = await this.getAuth(model, { ◀ trace getAuth:credential / env / OAuth 解析getAuth: credential / env / OAuth resolution 645 apiKey: options?.apiKey, 646 env: options?.env, 647 signal: options?.signal, 648 }); 649 if (!resolution) { 650 throw new ModelsError("auth", "src-str">`Provider is not configured: ${model.provider}`); 651 } 652 const auth = resolution.auth; 653 654 // Explicit request options win per-field; the Models-only transform runs last. 655 const apiKey = options?.apiKey ?? auth.apiKey; ◀ trace apiKey = options.apiKey ?? auth.apiKeyapiKey = options.apiKey ?? auth.apiKey 656 let headers = mergeHeaders(auth.headers, options?.headers); 657 if (options?.transformHeaders) headers = await options.transformHeaders(headers ?? {}); 658 const env = resolution.env || options?.env ? { ...(resolution.env ?? {}), ...(options?.env ?? {}) } : undefined; 659 const requestModel = auth.baseUrl ? { ...model, baseUrl: auth.baseUrl } : model; baseUrl overlay:代理/自定义 endpointbaseUrl overlay: proxy/custom endpoint 660 const { transformHeaders: _transformHeaders, ...providerOptions } = options ?? {}; 661 const requestOptions = { ...providerOptions, apiKey, headers, env } as Omit<TOptions, "transformHeaders"> & 662 ProviderRequestOptions; 663 664 return { requestModel, requestOptions }; 665 }
SOURCE  ·  packages/ai/src/models.ts models-stream
690 streamSimple(model: Model<Api>, context: Context, options?: ModelsSimpleStreamOptions): AssistantMessageEventStream { ◀ trace Models.streamSimple:lazyStream + applyAuthModels.streamSimple: lazyStream + applyAuth 691 return lazyStream(model, async () => { 692 const provider = this.requireProvider(model); 693 const { requestModel, requestOptions } = await this.applyAuth(model, options); 694 return provider.streamSimple(requestModel, context, requestOptions as SimpleStreamOptions); ◀ trace provider.streamSimple 真正发 HTTPprovider.streamSimple actually sends HTTP 695 }); 696 } 697 698 async completeSimple( 699 model: Model<Api>, 700 context: Context, 701 options?: ModelsSimpleStreamOptions, 702 ): Promise<AssistantMessage> { 703 return this.streamSimple(model, context, options).result(); 704 }
主线 turn-1 · streamSimple 数据流Through-line turn-1 · streamSimple data flow
#location主线时刻 / through-line moment
inContext.messagesuser + system + tools schema
authapplyAuthANTHROPIC_API_KEY / OAuth bearer
httpanthropic-messagesPOST /v1/messages stream:true
outEventStreamstart → toolcall_delta×N → done(toolUse)
upagent-loop:317message_update → TUI tool 卡片
SOURCE  ·  packages/ai/src/models.ts create-provider
739 export interface CreateProviderOptions<TApi extends Api = Api> { 740 id: string; 741 /** Display name. Default: "src-str">`id`. */ 742 name?: string; 743 baseUrl?: string; 744 headers?: ProviderHeaders; 745 /** Required — every provider has auth semantics, even ambient/keyless ones. */ 746 auth: ProviderAuth; auth 必填——即使 ambient/keyless providerauth required — even ambient/keyless providers 747 /** Static baseline model list (empty for purely dynamic providers). */ 748 models: readonly Model<TApi>[]; 749 /** Fetch a dynamic model overlay. createProvider restores and publishes it transactionally. */ 750 fetchModels?: (context: RefreshModelsContext) => Promise<readonly Model<TApi>[]>; 751 filterModels?: (models: readonly Model<TApi>[], credential: Credential | undefined) => readonly Model<TApi>[]; 752 /** Single implementation, or map keyed by "src-str">`model.api` for mixed-API providers. */ 753 api: ProviderStreams | Partial<Record<TApi, ProviderStreams>>; 754 } 755 756 /** 757 * Builds a provider from parts. Built-in provider factories and models.json api 字段:单实现或按 model.api 分派api field: single impl or dispatch by model.api 758 * custom providers both go through this. A single "src-str">`api` streams all models; 759 * an "src-str">`api` map dispatches on "src-str">`model.api`, and a model whose api has no entry 760 * produces a stream error. 761 */ 762 export function createProvider<TApi extends Api = Api>(input: CreateProviderOptions<TApi>): Provider<TApi> { ◀ trace createProvider:内置与 models.json 自定义共用createProvider: built-in and models.json custom share this 763 const baselineModels = input.models; 764 let dynamicModels: readonly Model<TApi>[] = []; 765 const fetchModels = input.fetchModels; 766 const currentModels = (): readonly Model<TApi>[] => { 767 const merged = [...baselineModels]; 768 for (const model of dynamicModels) { 769 const index = merged.findIndex((entry) => entry.id === model.id); 770 if (index >= 0) merged[index] = model; 771 else merged.push(model); 772 } 773 return merged; 774 }; 775 const single = 776 typeof (input.api as ProviderStreams).stream === "function" ? (input.api as ProviderStreams) : undefined; 777 const byApi = single ? undefined : (input.api as Partial<Record<string, ProviderStreams>>); 778 779 const apiFor = (model: Model<Api>): ProviderStreams | undefined => single ?? byApi?.[model.api]; ◀ trace apiFor(model):按 model.api 选 stream 实现apiFor(model): pick stream impl by model.api 780 781 const dispatch = ( 782 model: Model<Api>, 783 run: (streams: ProviderStreams) => AssistantMessageEventStream, 784 ): AssistantMessageEventStream => { 785 const streams = apiFor(model); 786 if (!streams) { 787 return lazyStream(model, async () => { 788 throw new ModelsError("stream", "src-str">`Provider ${input.id} has no API implementation for "${model.api}"`); 789 }); 790 } 791 return run(streams); 792 };
SOURCE  ·  packages/ai/src/providers/anthropic.ts anthropic-prov
9 function anthropicApiKeyAuth(): ApiKeyAuth { 10 return { 11 name: "Anthropic API key", 12 login: async (interaction) => { 13 interaction.signal.throwIfAborted(); 14 const key = await interaction.prompt({ type: "secret", message: "Enter Anthropic API key" }); 15 interaction.signal.throwIfAborted(); 16 return { type: "api_key", key }; 17 }, 18 resolve: async ({ ctx, credential, signal }) => { resolve:credential → env ANTHROPIC_* 回退resolve: credential → env ANTHROPIC_* fallback 19 signal.throwIfAborted(); 20 if (credential?.key) { 21 return { auth: { apiKey: credential.key }, env: credential.env, source: "stored credential" }; 22 } 23 24 const authToken = await ctx.env(ANTHROPIC_AUTH_TOKEN_ENV); 25 signal.throwIfAborted(); 26 if (authToken) { 27 return { 28 auth: { headers: { Authorization: "src-str">`Bearer ${authToken}` } }, 29 source: ANTHROPIC_AUTH_TOKEN_ENV, 30 }; 31 } 32 33 for (const envVar of [ANTHROPIC_OAUTH_TOKEN_ENV, ANTHROPIC_API_KEY_ENV]) { 34 const apiKey = await ctx.env(envVar); 35 signal.throwIfAborted(); 36 if (apiKey) return { auth: { apiKey }, source: envVar }; 37 } 38 return undefined; 39 }, 40 }; 41 } 42 43 export function anthropicProvider(): Provider<"anthropic-messages"> { 44 return createProvider({ ◀ trace anthropicProvider() 工厂anthropicProvider() factory 45 id: "anthropic", 46 name: "Anthropic", 47 baseUrl: "https://api.anthropic.com&quot;, 48 auth: { 49 apiKey: anthropicApiKeyAuth(), 50 oauth: lazyOAuth({ 51 name: "Anthropic (Claude Pro/Max)", 52 isSubscription: true, 53 load: loadAnthropicOAuth, 54 }), 55 }, 56 models: Object.values(ANTHROPIC_MODELS), 57 api: anthropicMessagesApi(), ◀ trace api: anthropicMessagesApi()api: anthropicMessagesApi() 58 });

createProvider 把 auth、models、api 三块粘成不可再分的 Provider 对象。Models 类持有 provider map,ModelRuntime(coding-agent)在其上叠加 credential store、extension provider、availability snapshot——但HTTP/SSE 协议细节永远不下沉到 agent-loop

createProvider glues auth, models, api into one Provider. Models holds the provider map; ModelRuntime adds credential store, extension providers, availability — but HTTP/SSE protocol never sinks into agent-loop.

SOURCE  ·  packages/ai/src/models.ts provider-dispatch
794 const provider: Provider<TApi> = { ◀ trace Provider 对象:stream/streamSimple 分派Provider object: stream/streamSimple dispatch 795 id: input.id, 796 name: input.name ?? input.id, 797 baseUrl: input.baseUrl, 798 headers: input.headers, 799 auth: input.auth, 800 getModels: currentModels, 801 refreshModels: fetchModels refreshModels:动态 provider 拉取模型列表refreshModels: dynamic provider fetches model list 802 ? async (context) => { 803 if (context.stored) { 804 const restored = context.stored.models 805 .filter((model) => model.provider === input.id) 806 .map((model) => model as Model<TApi>); 807 if ( 808 !(await context.publish({ 809 update: () => { 810 dynamicModels = restored; 811 }, 812 })) 813 ) { 814 return; 815 } 816 } 817 if (!context.allowNetwork || context.signal.aborted) return; 818 const refreshed = await fetchModels(context); 819 if (context.signal.aborted) return; 820 await context.publish({ 821 persist: { models: refreshed, checkedAt: Date.now() }, 822 update: () => { 823 dynamicModels = refreshed; 824 }, 825 }); 826 } 827 : undefined, 828 filterModels: input.filterModels, 829 stream: (model, context, options) => dispatch(model, (streams) => streams.stream(model, context, options)), 830 streamSimple: (model, context, options) =>
练习:在 pi-textbook 用 registerFauxProvider 替换 streamSimple,观察 agent-loop 是否仍收到相同形状的 toolcall_delta。 Exercise: in pi-textbook replace streamSimple with registerFauxProvider; observe agent-loop still gets same-shaped toolcall_delta.
DEPTH 深度细读 · C14.4+Deep dive · C14.4+

C14.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/ai/src/models.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/ai/src/models.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C14.5 关键函数与数据流key functions and data flow

models.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in models.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C14.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/ai/src/models.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/ai/src/models.ts.

C14.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C14.8 读完后应能回答的 5 个问题 · pi-ai 提供商抽象Five questions you should answer after reading · pi-ai 提供商抽象

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/ai/src/api/stream-simple.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/ai/src/api/stream-simple.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/ai/src/api/stream-simple.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/ai/src/api/stream-simple.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 14

站 14:packages/ai/src/models.ts 处理主线。

Station 14: packages/ai/src/models.ts on through-line.

SOURCE  ·  packages/ai/src/models.ts src
// pi-ai 提供商 · packages/ai/src/models.ts // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 05Provider Adapterprovider-adapter.tspackages/ai/src/api/*.ts · packages/ai/src/models.ts
附录Appendix 常见问题FAQ
Qpi-ai 提供商最关键文件?Key file?

packages/ai/src/models.ts

packages/ai/src/models.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C14.9 streamSimple 如何吃掉 convertToLlm 的输出How streamSimple consumes convertToLlm output

convertToLlm(messages: AgentMessage[]) 产出 pi-ai 的 Message[],其中 tool result 已折叠为 provider 特定格式。Anthropic 与 OpenAI 的 tool 块形状不同——pi-ai 的 adapter 层负责这一转换,agent-core 不应 import provider 细节。

convertToLlm(messages: AgentMessage[]) yields pi-ai Message[] with tool results folded to provider format. Anthropic vs OpenAI tool blocks differ — pi-ai adapters handle this; agent-core must not import provider details.

streamSimple(model, messages, tools) 返回 EventStream:先过程项(text_delta、toolcall_delta),后终态(done + usage)。TUI 只订阅 message_update;SessionManager 在 message_end 落盘——两者消费同一 stream 的不同阶段。

streamSimple(model, messages, tools) returns EventStream: process items first (text_delta, toolcall_delta), terminal state last (done + usage). TUI subscribes to message_update; SessionManager persists at message_end.

CHAPTER 15 · LLM

流式 EventStream

Streaming EventStream

text_delta · toolcall_delta · done

text_delta · toolcall_delta · done

模块Module
pi-aipi-ai
Package
event-stream.tsevent-stream.ts
线程Thread
站 15St. 15
输出Output
text_delta → donetext_delta → done

EventStream<T> 同时交付过程项(delta)和终态(done)。TUI 订阅过程项;agent-loop 等待终态 AssistantMessage。

EventStream<T> delivers both progress items (deltas) and terminal state (done). TUI subscribes to progress; agent-loop awaits terminal AssistantMessage.

SOURCE  ·  packages/ai/src/utils/event-stream.ts es
export class EventStream<TEvent, TResult> { push(event: TEvent): void; end(result: TResult): void; }
事件消费者主线时刻
text_deltaTUI Markdown最终回答流式显示
toolcall_deltaTUI tool 卡片read 参数组装
doneagent-loopstopReason 判定
📘 pi-textbook checkpoint 02:EventStream(event-stream.ts)· 生产映射:packages/ai/src/utils/event-stream.ts 📘 pi-textbook checkpoint 02: EventStream (event-stream.ts) · production: packages/ai/src/utils/event-stream.ts
SOURCE TRACE · C15 流式 EventStream:text_delta / toolcall_delta 协议全 trace Streaming EventStream: full text_delta / toolcall_delta protocol trace

pi-ai 与 agent-core 之间的流式契约是 AssistantMessageEvent 联合类型 + AssistantMessageEventStream 队列。agent-loopfor await (event of response) 消费的正是这个协议——不是 provider 原始 SSE。主线 turn-1 的 read toolUse 由一串 toolcall_start → toolcall_delta×N → toolcall_end 组装。

The streaming contract between pi-ai and agent-core is AssistantMessageEvent union + AssistantMessageEventStream queue. agent-loop's for await (event of response) consumes this protocol — not raw provider SSE. Through-line turn-1 read toolUse is assembled from toolcall_start → toolcall_delta×N → toolcall_end.

SOURCE  ·  packages/ai/src/types.ts event-protocol
527 /** 528 * Event protocol for AssistantMessageEventStream. 529 * 530 * Streams should emit "src-str">`start` before partial updates, then terminate with either: 531 * - "src-str">`done` carrying the final successful AssistantMessage, or 532 * - "src-str">`error` carrying the final AssistantMessage with stopReason "error" or "aborted" 533 * and errorMessage. 534 */ 535 export type AssistantMessageEvent = AssistantMessageEvent 联合类型定义AssistantMessageEvent union definition 536 | { type: "start"; partial: AssistantMessage } start:携带 partial AssistantMessagestart: carries partial AssistantMessage 537 | { type: "text_start"; contentIndex: number; partial: AssistantMessage } 538 | { type: "text_delta"; contentIndex: number; delta: string; partial: AssistantMessage } text_delta:turn-2 最终回答的增量text_delta: turn-2 final answer increments 539 | { type: "text_end"; contentIndex: number; content: string; partial: AssistantMessage } 540 | { type: "thinking_start"; contentIndex: number; partial: AssistantMessage } 541 | { type: "thinking_delta"; contentIndex: number; delta: string; partial: AssistantMessage } 542 | { type: "thinking_end"; contentIndex: number; content: string; partial: AssistantMessage } 543 | { type: "toolcall_start"; contentIndex: number; partial: AssistantMessage } 544 | { type: "toolcall_delta"; contentIndex: number; delta: string; partial: AssistantMessage } ◀ trace ★ toolcall_delta:turn-1 read args 增量 JSON★ toolcall_delta: turn-1 read args incremental JSON 545 | { type: "toolcall_end"; contentIndex: number; toolCall: ToolCall; partial: AssistantMessage } 546 | { 547 type: "done"; ◀ trace done:stopReason=stop|toolUse|length|deferreddone: stopReason=stop|toolUse|length|deferred 548 reason: Extract<StopReason, "stop" | "length" | "toolUse" | "deferred">; 549 message: AssistantMessage; 550 } 551 | { type: "error"; reason: Extract<StopReason, "aborted" | "error">; error: AssistantMessage }; error:aborted|error + errorMessageerror: aborted|error + errorMessage
SOURCE  ·  packages/ai/src/utils/event-stream.ts event-stream
1 import type { AssistantMessage, AssistantMessageEvent } from "../types.ts"; 2 3 // Generic event stream class for async iteration 4 export class EventStream<T, R = T> implements AsyncIterable<T> { EventStream 泛型:AsyncIterable + result()EventStream generic: AsyncIterable + result() 5 private queue: T[] = []; 6 private waiting: ((value: IteratorResult<T>) => void)[] = []; 7 private done = false; 8 private finalResultPromise: Promise<R>; 9 private resolveFinalResult!: (result: R) => void; 10 private isComplete: (event: T) => boolean; 11 private extractResult: (event: T) => R; 12 13 constructor(isComplete: (event: T) => boolean, extractResult: (event: T) => R) { 14 this.isComplete = isComplete; 15 this.extractResult = extractResult; 16 this.finalResultPromise = new Promise((resolve) => { 17 this.resolveFinalResult = resolve; 18 }); 19 } 20 21 push(event: T): void { ◀ trace push:完成事件 resolve finalResultpush: complete event resolves finalResult 22 if (this.done) return; 23 24 if (this.isComplete(event)) { 25 this.done = true; 26 this.resolveFinalResult(this.extractResult(event)); 27 } 28 29 // Deliver to waiting consumer or queue it 等待消费者或入队wait for consumer or enqueue 30 const waiter = this.waiting.shift(); 31 if (waiter) { 32 waiter({ value: event, done: false }); 33 } else { 34 this.queue.push(event); 35 } 36 } 37 38 end(result?: R): void { end():强制结束迭代器end(): force iterator completion 39 this.done = true; 40 if (result !== undefined) { 41 this.resolveFinalResult(result); 42 } 43 // Notify all waiting consumers that we&#x27;re done 44 while (this.waiting.length > 0) { 45 const waiter = this.waiting.shift()!; 46 waiter({ value: undefined as any, done: true }); 47 } 48 } 49 50 async *[Symbol.asyncIterator](): AsyncIterator<T> { async iterator:背压友好async iterator: backpressure-friendly 51 while (true) { 52 if (this.queue.length > 0) { 53 yield this.queue.shift()!; 54 } else if (this.done) { 55 return; 56 } else { 57 const result = await new Promise<IteratorResult<T>>((resolve) => this.waiting.push(resolve)); 58 if (result.done) return; 59 yield result.value; 60 } 61 } 62 } 63 64 result(): Promise<R> { ◀ trace result():await 最终 AssistantMessageresult(): await final AssistantMessage 65 return this.finalResultPromise; 66 } 67 }
SOURCE  ·  packages/ai/src/utils/event-stream.ts assistant-stream
69 export class AssistantMessageEventStream extends EventStream<AssistantMessageEvent, AssistantMessage> { ◀ trace AssistantMessageEventStream 子类AssistantMessageEventStream subclass 70 constructor() { 71 super( 72 (event) => event.type === "done" || event.type === "error", ◀ trace isComplete:done 或 errorisComplete: done or error 73 (event) => { 74 if (event.type === "done") { extractResult:done→message, error→errorextractResult: done→message, error→error 75 return event.message; 76 } else if (event.type === "error") { 77 return event.error; 78 } 79 throw new Error("Unexpected event type for final result"); 80 }, 81 ); 82 } 83 } 84 85 /** Factory function for AssistantMessageEventStream (for use in extensions) */ 86 export function createAssistantMessageEventStream(): AssistantMessageEventStream { 工厂:extensions 可构造假流factory: extensions can build fake streams 87 return new AssistantMessageEventStream(); 88 }
主线 turn-1 · toolcall_delta 时间线Through-line turn-1 · toolcall_delta timeline
#location主线时刻 / through-line moment
1toolcall_startcontent block tool_use 开始
2toolcall_delta{"path"
3toolcall_delta:"README.md"}
4toolcall_delta} 片段…
5toolcall_endarguments 解析完成 · id=toolu_…
6donestopReason=toolUse
7agent-loopmessage_update×N → executeToolCalls
SOURCE  ·  packages/ai/src/api/anthropic-messages.ts sse-start
575 const response = await retryProviderRequest( 576 () => client.messages.create({ ...params, stream: true }, requestOptions).asResponse(), client.messages.create stream:trueclient.messages.create stream:true 577 { 578 maxRetries: options?.maxRetries, 579 maxRetryDelayMs: options?.maxRetryDelayMs, 580 signal: options?.signal, 581 }, 582 ); 583 await options?.onResponse?.({ status: response.status, headers: headersToRecord(response.headers) }, model); 584 stream.push({ type: "start", partial: output }); ◀ trace stream.push startstream.push start 585 586 type Block = (ThinkingContent | TextContent | (ToolCall & { partialJson: string })) & { index: number }; 587 const blocks = output.content as Block[]; 588 589 for await (const event of iterateAnthropicEvents(response, options?.signal)) { 590 if (event.type === "message_start") { 591 output.responseId = event.message.id; 592 output.model = event.message.model; 593 const fallbackCost = 594 output.model === model.id 595 ? undefined 596 : model.compat?.allowedFallbackModels?.find( 597 (fallback) => fallback.provider === model.provider && fallback.model === output.model, 598 )?.cost; 599 usageModel = fallbackCost ? { ...model, id: output.model, cost: fallbackCost } : model; 600 // Capture initial token usage from message_start event 601 // This ensures we have input token counts even if the stream is aborted early 602 output.usage.input = event.message.usage.input_tokens || 0; 603 output.usage.output = event.message.usage.output_tokens || 0; 604 output.usage.cacheRead = event.message.usage.cache_read_input_tokens || 0; 605 output.usage.cacheWrite = event.message.usage.cache_creation_input_tokens || 0; 606 output.usage.cacheWrite1h = event.message.usage.cache_creation?.ephemeral_1h_input_tokens || 0; 607 // Anthropic doesn&#x27;t provide total_tokens, compute from components 608 output.usage.totalTokens = 609 output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite; 610 calculateCost(usageModel, output.usage); 611 } else if (event.type === "content_block_start") { content_block_start:text/thinking/tool_usecontent_block_start: text/thinking/tool_use 612 if (event.content_block.type === "text") { 613 const block: Block = { 614 type: "text", 615 text: event.content_block.text ?? "", 616 index: event.index, 617 }; 618 output.content.push(block); 619 stream.push({ type: "text_start", contentIndex: output.content.length - 1, partial: output }); 620 } else if (event.content_block.type === "thinking") { 621 const block: Block = { 622 type: "thinking", 623 thinking: event.content_block.thinking ?? "", 624 thinkingSignature: event.content_block.signature ?? "", 625 index: event.index, 626 }; 627 output.content.push(block); 628 stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output }); 629 } else if (event.content_block.type === "redacted_thinking") { 630 const block: Block = { 631 type: "thinking", 632 thinking: "[Reasoning redacted]", 633 thinkingSignature: event.content_block.data, 634 redacted: true, 635 index: event.index, 636 }; 637 output.content.push(block); 638 stream.push({ type: "thinking_start", contentIndex: output.content.length - 1, partial: output }); 639 } else if (event.content_block.type === "tool_use") { tool_use block → toolcall_starttool_use block → toolcall_start 640 const block: Block = { 641 type: "toolCall", 642 id: event.content_block.id, 643 name: isOAuth 644 ? fromClaudeCodeName(event.content_block.name, context.tools) 645 : event.content_block.name, 646 arguments: (event.content_block.input as Record<string, any>) ?? {}, 647 partialJson: "", 648 index: event.index, 649 }; 650 output.content.push(block); 651 stream.push({ type: "toolcall_start", contentIndex: output.content.length - 1, partial: output }); ◀ trace ★ toolcall_start push★ toolcall_start push 652 }
SOURCE  ·  packages/ai/src/api/anthropic-messages.ts sse-delta
653 } else if (event.type === "content_block_delta") { 654 if (event.delta.type === "text_delta") { 655 const index = blocks.findIndex((b) => b.index === event.index); 656 const block = blocks[index]; 657 if (block && block.type === "text") { 658 block.text += event.delta.text; 659 stream.push({ 660 type: "text_delta", 661 contentIndex: index, 662 delta: event.delta.text, 663 partial: output, 664 }); 665 } 666 } else if (event.delta.type === "thinking_delta") { 667 const index = blocks.findIndex((b) => b.index === event.index); 668 const block = blocks[index]; 669 if (block && block.type === "thinking") { 670 block.thinking += event.delta.thinking; 671 stream.push({ 672 type: "thinking_delta", 673 contentIndex: index, 674 delta: event.delta.thinking, 675 partial: output, 676 }); 677 } 678 } else if (event.delta.type === "input_json_delta") { input_json_delta → toolcall_deltainput_json_delta → toolcall_delta 679 const index = blocks.findIndex((b) => b.index === event.index); 680 const block = blocks[index]; 681 if (block && block.type === "toolCall") { 682 block.partialJson += event.delta.partial_json; partialJson 累加 + parseStreamingJsonpartialJson accumulate + parseStreamingJson 683 block.arguments = parseStreamingJson(block.partialJson); 684 stream.push({ ◀ trace ★ 每个 JSON 片段 push toolcall_delta★ each JSON fragment pushes toolcall_delta 685 type: "toolcall_delta", 686 contentIndex: index, 687 delta: event.delta.partial_json, 688 partial: output, 689 }); 690 } 691 } else if (event.delta.type === "signature_delta") { 692 const index = blocks.findIndex((b) => b.index === event.index); 693 const block = blocks[index]; 694 if (block && block.type === "thinking") { 695 block.thinkingSignature = block.thinkingSignature || ""; 696 block.thinkingSignature += event.delta.signature; 697 } 698 } 699 } else if (event.type === "content_block_stop") { 700 const index = blocks.findIndex((b) => b.index === event.index); 701 const block = blocks[index]; 702 if (block) { 703 delete (block as any).index; 704 if (block.type === "text") { 705 stream.push({ 706 type: "text_end", 707 contentIndex: index, 708 content: block.text, 709 partial: output, 710 }); 711 } else if (block.type === "thinking") { 712 stream.push({ 713 type: "thinking_end", 714 contentIndex: index, 715 content: block.thinking, 716 partial: output, 717 }); 718 } else if (block.type === "toolCall") { content_block_stop → toolcall_endcontent_block_stop → toolcall_end 719 block.arguments = parseStreamingJson(block.partialJson); 720 // Finalize in-place and strip the scratch buffer so replay only 721 // carries parsed arguments. 722 delete (block as { partialJson?: string }).partialJson; 723 stream.push({ ◀ trace delete partialJson,只保留 argumentsdelete partialJson, keep arguments only 724 type: "toolcall_end", 725 contentIndex: index, 726 toolCall: block, 727 partial: output, 728 }); 729 }
SOURCE  ·  packages/ai/src/api/anthropic-messages.ts sse-done
731 } else if (event.type === "message_delta") { 732 if (event.delta.stop_reason) { message_delta:stop_reason → stopReasonmessage_delta: stop_reason → stopReason 733 output.rawStopReason = event.delta.stop_reason; 734 const stopReasonResult = mapStopReason(event.delta.stop_reason, event.delta.stop_details); 735 output.stopReason = stopReasonResult.stopReason; 736 if (stopReasonResult.errorMessage) { 737 output.errorMessage = stopReasonResult.errorMessage; 738 } 739 } 740 // Only update usage fields if present (not null). 741 // Preserves input_tokens from message_start when proxies omit it in message_delta. 742 if (event.usage) { 743 if (event.usage.input_tokens != null) { 744 output.usage.input = event.usage.input_tokens; 745 } 746 if (event.usage.output_tokens != null) { 747 output.usage.output = event.usage.output_tokens; 748 } 749 if (event.usage.cache_read_input_tokens != null) { 750 output.usage.cacheRead = event.usage.cache_read_input_tokens; 751 } 752 if (event.usage.cache_creation_input_tokens != null) { 753 output.usage.cacheWrite = event.usage.cache_creation_input_tokens; 754 } 755 // Anthropic reports reasoning tokens in `output_tokens_details.thinking_tokens` on the 756 // final message_delta usage (a subset of output_tokens). SDK 0.91.1 omits the field from 757 // its Usage type, so read it through a narrow cast. Verified against the live API. 758 const thinkingTokens = (event.usage as { output_tokens_details?: { thinking_tokens?: number } }) 759 .output_tokens_details?.thinking_tokens; 760 if (thinkingTokens != null) { 761 output.usage.reasoning = thinkingTokens; 762 } 763 } 764 // Anthropic doesn&#x27;t provide total_tokens, compute from components 765 output.usage.totalTokens = 766 output.usage.input + output.usage.output + output.usage.cacheRead + output.usage.cacheWrite; 767 calculateCost(usageModel, output.usage); 768 } 769 } 770 771 if (options?.signal?.aborted) { 772 throw new Error("Request was aborted"); 773 } 774 775 if (output.stopReason === "pending") { 776 throw new Error("Anthropic stream ended without a stop reason"); 777 } 778 if (output.stopReason === "aborted" || output.stopReason === "error") { 779 throw new Error(output.errorMessage || "An unknown error occurred"); 780 } 781 782 stream.push({ type: "done", reason: output.stopReason, message: output }); ◀ trace stream.push donestream.push done 783 stream.end(); 784 } catch (error) { 785 for (const block of output.content) { 786 delete (block as { index?: number }).index; 787 // partialJson is only a streaming scratch buffer; never persist it. 788 delete (block as { partialJson?: string }).partialJson; 789 } 790 output.stopReason = options?.signal?.aborted ? "aborted" : "error"; 791 output.errorMessage = error instanceof Error ? error.message : JSON.stringify(error); 792 stream.push({ type: "error", reason: output.stopReason, error: output }); catch → error event + endcatch → error event + end 793 stream.end(); 794 } 795 })(); 796 797 return stream;
SOURCE  ·  packages/agent/src/agent-loop.ts agent-consume
314 let partialMessage: AssistantMessage | null = null; 315 let addedPartial = false; 316 317 for await (const event of response) { for await response:消费 pi-ai EventStreamfor await response: consume pi-ai EventStream 318 switch (event.type) { 319 case "start": start → message_startstart → message_start 320 partialMessage = event.partial; 321 context.messages.push(partialMessage); 322 addedPartial = true; 323 await emit({ type: "message_start", message: { ...partialMessage } }); 324 break; 325 326 case "text_start": 327 case "text_delta": 328 case "text_end": 329 case "thinking_start": 330 case "thinking_delta": 331 case "thinking_end": 332 case "toolcall_start": ◀ trace toolcall_delta → message_updatetoolcall_delta → message_update 333 case "toolcall_delta": 334 case "toolcall_end": 335 if (partialMessage) { 336 partialMessage = event.partial; 337 context.messages[context.messages.length - 1] = partialMessage; 338 await emit({ 339 type: "message_update", 340 assistantMessageEvent: event, 341 message: { ...partialMessage }, 342 }); 343 } 344 break; 345 346 case "done": 347 case "error": { 348 const finalMessage = await response.result(); 349 if (addedPartial) { 350 context.messages[context.messages.length - 1] = finalMessage; 351 } else { 352 context.messages.push(finalMessage); 353 } 354 if (!addedPartial) { 355 await emit({ type: "message_start", message: { ...finalMessage } }); 356 } 357 await emit({ type: "message_end", message: finalMessage }); ◀ trace done → message_end(final)done → message_end(final) 358 return finalMessage; 359 } 360 }

设计要点:partial 字段在每个 event 里携带当前累积状态的 AssistantMessage 快照——TUI 不需要自己拼 delta;agent-loop 把 event 原样 emit 给 AgentSession,由订阅者决定是差分渲染还是等 message_end 落盘。

Design note: partial in each event carries a cumulative AssistantMessage snapshot — TUI need not assemble deltas; agent-loop emits events as-is to AgentSession; subscribers diff-render or persist on message_end.

--verbose 跑主线 prompt,stderr 里 toolcall_delta 行数应与 Anthropic SSE input_json_delta 包数同量级。 Run through-line with --verbose; stderr toolcall_delta count should match Anthropic SSE input_json_delta packets.
DEPTH 深度细读 · C15.4+Deep dive · C15.4+

C15.4 EventStream 核心语义EventStream core semantics

packages/ai/src/utils/event-stream.ts:泛型 EventStream,构造时传入 isComplete 与 extractResult。

packages/ai/src/utils/event-stream.ts: generic EventStream with isComplete and extractResult at construction.

push(event):若 isComplete(event) 为真,设 done=true 并 resolveFinalResult;否则入队或交给 waiting consumer。

push(event): if isComplete, set done and resolveFinalResult; else queue or deliver to waiting consumer.

AsyncIterable 协议:消费者 for-await 时,队列空且未 done 则挂起在 waiting 数组。

AsyncIterable: for-await suspends on waiting array when queue empty and not done.

AssistantMessageEventStream 子类:isComplete 当 type===done 或 error;extractResult 返回 message 或抛错。

AssistantMessageEventStream: isComplete on done/error; extractResult returns message or throws.

C15.5 主线 prompt 上的流事件类型Stream events on through-line

第一次 stream(toolUse):text_delta 可能为空或极短;toolcall_delta 累积 read 参数;done 时 stopReason=toolUse。

First stream (toolUse): short text_delta; toolcall_delta accumulates read args; done with stopReason=toolUse.

第二次 stream(stop):text_delta 逐 chunk 推送最终总结;done 时 stopReason=stop。

Second stream (stop): text_delta chunks for summary; done with stopReason=stop.

streamAssistantResponse 把 LLM EventStream 事件映射为 AgentEvent message_update。

streamAssistantResponse maps LLM EventStream events to AgentEvent message_update.

C15.6 agent-loop 侧的 EventStreamAgent-side EventStream

createAgentStream(agent-loop.ts 145 行):isComplete 当 type===agent_end;extractResult 返回 messages 数组。

createAgentStream (line 145): isComplete on agent_end; extractResult returns messages array.

agentLoop 返回 EventStream;AgentSession for-await 消费并写 JSONL。

agentLoop returns EventStream; AgentSession for-await consumes and writes JSONL.

end(result) 手动 resolve——用于 abort 或错误路径提前结束。

end(result) manually resolves — for abort or error early termination.

C15.7 背压与 TUI 消费Backpressure and TUI consumption

EventStream 无界队列:高速 token 时 TUI diff 渲染可能落后;TuiMainScreen firstChanged/lastChanged 优化重绘区间。

Unbounded queue: TUI diff may lag on fast tokens; TuiMainScreen firstChanged/lastChanged optimizes redraw.

message_update 携带 partial content;message_end 携带完整 AssistantMessage。

message_update carries partial content; message_end carries full AssistantMessage.

C15.8 与 textbook checkpoint 02 对照vs textbook checkpoint 02

textbook event-stream.ts 教学「过程项与终态同时交付」——生产代码在 streamSimple 与 agentLoop 两层复用同一模式。

Textbook checkpoint 02 teaches simultaneous partial and terminal delivery — production reuses pattern in streamSimple and agentLoop.

测试:用 ScriptedModel 注入固定 delta 序列,断言 message_update 次数与顺序。

Test: ScriptedModel injects fixed delta sequence; assert message_update count and order.

C15.9 读完后应能回答的 5 个问题 · 流式 EventStreamFive questions you should answer after reading · 流式 EventStream

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 15

站 15:EventStream push text_delta/toolcall_delta;done 触发 stopReason 分支。

Station 15: EventStream pushes deltas; done triggers stopReason branch.

SOURCE  ·  packages/ai/src/utils/event-stream.ts es
export class EventStream<T, R = T> push(event: T): void { if (this.isComplete(event)) { this.done = true; ... }
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 02EventStreamevent-stream.tspackages/ai/src/utils/event-stream.ts
附录Appendix 常见问题FAQ
QEventStream 会丢事件吗?Drop events?

push 在 done 后忽略;正常路径不丢。

push ignores after done; normal path does not drop.

Qagent_end 与 done 区别?agent_end vs done?

agent_end 是 AgentEvent;done 是 LLM AssistantMessageEvent。

agent_end is AgentEvent; done is LLM AssistantMessageEvent.

Q如何 mock 流?Mock stream?

ScriptedModel 或自定义 StreamFn 返回预置 EventStream。

ScriptedModel or custom StreamFn returning preset EventStream.

C15.10 打开源码:packages/ai/src/utils/event-stream.tsOpen source: packages/ai/src/utils/event-stream.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/ai/src/utils/event-stream.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/ai/src/utils/event-stream.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 16 · LLM

认证与模型目录

Auth and model catalog

40+ providers · OAuth · models.generated.ts

40+ providers · OAuth · models.generated.ts

模块Module
pi-aipi-ai
Package
models.generated.tsmodels.generated.ts
线程Thread
站 16St. 16
输出Output
Model + AuthResultModel + AuthResult

models.generated.ts 由脚本从模型目录生成;OAuth 流程把 token 存 agent dir。无 key 时 formatNoApiKeyFoundMessage 给可操作的引导。

models.generated.ts is script-generated from model catalog; OAuth stores tokens in agent dir. Without keys, formatNoApiKeyFoundMessage gives actionable guidance.

SOURCE  ·  packages/coding-agent/src/core/auth-guidance.ts auth
export function formatNoApiKeyFoundMessage(...)
ModelRegistryModelRegistry
解析 model id → Model 对象resolve model id → Model object
thinking levelthinking level
clamped per model capabilityclamped per model capability
retryretry
isRetryableAssistantError + backoffisRetryableAssistantError + backoff
SOURCE TRACE · C16 认证与模型目录:从 models.generated 到 findInitialModel Auth and model catalog: models.generated to findInitialModel

Pi 启动时必须先回答两个问题:用哪个 model?凭据从哪来? models.generated.ts 聚合 40+ provider 的静态目录;ModelRuntime 叠加 credential store 与 availability;findInitialModel 按 CLI → scoped → settings → first-available 优先级选型。没有 key 时 auth-guidance.ts 给出 /login 指引。

Pi boot must answer: which model? and where credentials come from? models.generated.ts aggregates 40+ provider static catalogs; ModelRuntime adds credential store and availability; findInitialModel picks CLI → scoped → settings → first-available. No key → auth-guidance.ts points to /login.

SOURCE  ·  packages/ai/src/models.generated.ts models-gen
1 // This file is auto-generated by scripts/generate-models.ts ◀ trace 自动生成:npm run generate-modelsauto-generated: npm run generate-models 2 // Do not edit manually - run &#x27;npm run generate-models&#x27; to update 3 4 import { AMAZON_BEDROCK_MODELS } from "./providers/amazon-bedrock.models.ts"; 每 provider 一个 *.models.tsone *.models.ts per provider 5 import { ANT_LING_MODELS } from "./providers/ant-ling.models.ts"; 6 import { ANTHROPIC_MODELS } from "./providers/anthropic.models.ts"; 7 import { AZURE_OPENAI_RESPONSES_MODELS } from "./providers/azure-openai-responses.models.ts"; 8 import { BASETEN_MODELS } from "./providers/baseten.models.ts"; 9 import { CEREBRAS_MODELS } from "./providers/cerebras.models.ts"; 10 import { CLOUDFLARE_AI_GATEWAY_MODELS } from "./providers/cloudflare-ai-gateway.models.ts"; 11 import { CLOUDFLARE_WORKERS_AI_MODELS } from "./providers/cloudflare-workers-ai.models.ts"; 12 import { DEEPSEEK_MODELS } from "./providers/deepseek.models.ts"; 13 import { FIREWORKS_MODELS } from "./providers/fireworks.models.ts"; 14 import { GITHUB_COPILOT_MODELS } from "./providers/github-copilot.models.ts"; 15 import { GOOGLE_MODELS } from "./providers/google.models.ts"; 16 import { GOOGLE_VERTEX_MODELS } from "./providers/google-vertex.models.ts"; 17 import { GROQ_MODELS } from "./providers/groq.models.ts"; 18 import { HUGGINGFACE_MODELS } from "./providers/huggingface.models.ts"; 19 import { KIMI_CODING_MODELS } from "./providers/kimi-coding.models.ts"; 20 import { MINIMAX_MODELS } from "./providers/minimax.models.ts"; 21 import { MINIMAX_CN_MODELS } from "./providers/minimax-cn.models.ts"; 22 import { MISTRAL_MODELS } from "./providers/mistral.models.ts"; 23 import { MOONSHOTAI_MODELS } from "./providers/moonshotai.models.ts"; 24 import { MOONSHOTAI_CN_MODELS } from "./providers/moonshotai-cn.models.ts"; 25 import { NVIDIA_MODELS } from "./providers/nvidia.models.ts"; 26 import { OPENAI_MODELS } from "./providers/openai.models.ts"; 27 import { OPENAI_CODEX_MODELS } from "./providers/openai-codex.models.ts"; 28 import { OPENCODE_MODELS } from "./providers/opencode.models.ts"; 29 import { OPENCODE_GO_MODELS } from "./providers/opencode-go.models.ts"; 30 import { OPENROUTER_MODELS } from "./providers/openrouter.models.ts"; 31 import { QWEN_TOKEN_PLAN_MODELS } from "./providers/qwen-token-plan.models.ts"; 32 import { QWEN_TOKEN_PLAN_CN_MODELS } from "./providers/qwen-token-plan-cn.models.ts"; 33 import { QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS } from "./providers/qwen-token-plan-individual.models.ts"; 34 import { TOGETHER_MODELS } from "./providers/together.models.ts"; 35 import { VERCEL_AI_GATEWAY_MODELS } from "./providers/vercel-ai-gateway.models.ts"; 36 import { XAI_MODELS } from "./providers/xai.models.ts"; 37 import { XIAOMI_MODELS } from "./providers/xiaomi.models.ts"; 38 import { XIAOMI_TOKEN_PLAN_AMS_MODELS } from "./providers/xiaomi-token-plan-ams.models.ts"; 39 import { XIAOMI_TOKEN_PLAN_CN_MODELS } from "./providers/xiaomi-token-plan-cn.models.ts"; 40 import { XIAOMI_TOKEN_PLAN_SGP_MODELS } from "./providers/xiaomi-token-plan-sgp.models.ts"; 41 import { ZAI_MODELS } from "./providers/zai.models.ts"; 42 import { ZAI_CODING_CN_MODELS } from "./providers/zai-coding-cn.models.ts"; 43 44 export const MODELS: { ◀ trace MODELS 聚合对象类型MODELS aggregate object type 45 readonly "amazon-bedrock": typeof AMAZON_BEDROCK_MODELS;
SOURCE  ·  packages/ai/src/models.generated.ts models-map
84 } = { 85 "amazon-bedrock": AMAZON_BEDROCK_MODELS, 86 "ant-ling": ANT_LING_MODELS, 87 "anthropic": ANTHROPIC_MODELS, ◀ trace anthropic: ANTHROPIC_MODELSanthropic: ANTHROPIC_MODELS 88 "azure-openai-responses": AZURE_OPENAI_RESPONSES_MODELS, 89 "baseten": BASETEN_MODELS, 90 "cerebras": CEREBRAS_MODELS, 91 "cloudflare-ai-gateway": CLOUDFLARE_AI_GATEWAY_MODELS, 92 "cloudflare-workers-ai": CLOUDFLARE_WORKERS_AI_MODELS, 93 "deepseek": DEEPSEEK_MODELS, 94 "fireworks": FIREWORKS_MODELS, 95 "github-copilot": GITHUB_COPILOT_MODELS, 96 "google": GOOGLE_MODELS, 97 "google-vertex": GOOGLE_VERTEX_MODELS, 98 "groq": GROQ_MODELS, 99 "huggingface": HUGGINGFACE_MODELS, 100 "kimi-coding": KIMI_CODING_MODELS, 101 "minimax": MINIMAX_MODELS, 102 "minimax-cn": MINIMAX_CN_MODELS, 103 "mistral": MISTRAL_MODELS, 104 "moonshotai": MOONSHOTAI_MODELS, 105 "moonshotai-cn": MOONSHOTAI_CN_MODELS, 106 "nvidia": NVIDIA_MODELS, openai / openai-codexopenai / openai-codex 107 "openai": OPENAI_MODELS, 108 "openai-codex": OPENAI_CODEX_MODELS, 109 "opencode": OPENCODE_MODELS, 110 "opencode-go": OPENCODE_GO_MODELS, 111 "openrouter": OPENROUTER_MODELS, openrouter / moonshotai / …openrouter / moonshotai / … 112 "qwen-token-plan": QWEN_TOKEN_PLAN_MODELS, 113 "qwen-token-plan-cn": QWEN_TOKEN_PLAN_CN_MODELS, 114 "qwen-token-plan-individual": QWEN_TOKEN_PLAN_INDIVIDUAL_MODELS, 115 "together": TOGETHER_MODELS, 116 "vercel-ai-gateway": VERCEL_AI_GATEWAY_MODELS, 117 "xai": XAI_MODELS, 118 "xiaomi": XIAOMI_MODELS, 119 "xiaomi-token-plan-ams": XIAOMI_TOKEN_PLAN_AMS_MODELS, 120 "xiaomi-token-plan-cn": XIAOMI_TOKEN_PLAN_CN_MODELS, 121 "xiaomi-token-plan-sgp": XIAOMI_TOKEN_PLAN_SGP_MODELS, 122 "zai": ZAI_MODELS, 123 "zai-coding-cn": ZAI_CODING_CN_MODELS, 124 };
SOURCE  ·  packages/ai/src/providers/anthropic.models.ts anthropic-models
1 // This file is auto-generated by scripts/generate-models.ts 2 // Do not edit manually - run &#x27;npm run generate-models&#x27; to update 3 4 import values from "./data/anthropic.json" with { type: "json" }; 从 anthropic.json flattenflatten from anthropic.json 5 import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts"; 6 7 export const ANTHROPIC_MODELS: ModelCatalog<typeof values, "anthropic"> = ◀ trace ANTHROPIC_MODELS 目录常量ANTHROPIC_MODELS catalog constant 8 flattenModelCatalog("anthropic", values);
SOURCE  ·  packages/ai/src/providers/anthropic.ts auth-resolve
18 resolve: async ({ ctx, credential, signal }) => { 19 signal.throwIfAborted(); 20 if (credential?.key) { ◀ trace stored credential 优先stored credential first 21 return { auth: { apiKey: credential.key }, env: credential.env, source: "stored credential" }; 22 } 23 24 const authToken = await ctx.env(ANTHROPIC_AUTH_TOKEN_ENV); ANTHROPIC_AUTH_TOKEN envANTHROPIC_AUTH_TOKEN env 25 signal.throwIfAborted(); 26 if (authToken) { 27 return { 28 auth: { headers: { Authorization: "src-str">`Bearer ${authToken}` } }, 29 source: ANTHROPIC_AUTH_TOKEN_ENV, 30 }; 31 } 32 33 for (const envVar of [ANTHROPIC_OAUTH_TOKEN_ENV, ANTHROPIC_API_KEY_ENV]) { ◀ trace OAuth token / API key env 回退链OAuth token / API key env fallback chain 34 const apiKey = await ctx.env(envVar); 35 signal.throwIfAborted(); 36 if (apiKey) return { auth: { apiKey }, source: envVar }; 37 } 38 return undefined; 39 }, 40 };
SOURCE  ·  packages/coding-agent/src/core/auth-guidance.ts auth-guidance
6 export function getProviderLoginHelp(): string { 7 return [ getProviderLoginHelp:/login + docs 链接getProviderLoginHelp: /login + docs links 8 "Use /login to log into a provider via OAuth or API key. See:", 9 "src-str">` ${join(getDocsPath(), "providers.md")}`, 10 "src-str">` ${join(getDocsPath(), "models.md")}`, 11 ].join("\n"); 12 } 13 14 export function formatNoModelsAvailableMessage(): string { ◀ trace formatNoModelsAvailableMessageformatNoModelsAvailableMessage 15 return "src-str">`No models available. ${getProviderLoginHelp()}`; 16 } 17 18 export function formatNoModelSelectedMessage(): string { 19 return "src-str">`No model selected.\n\n${getProviderLoginHelp()}\n\nThen use /model to select a model.`; 20 } 21 22 export function formatNoApiKeyFoundMessage(provider: string): string { ◀ trace formatNoApiKeyFoundMessage(provider)formatNoApiKeyFoundMessage(provider) 23 const providerDisplay = provider === UNKNOWN_PROVIDER ? "the selected model" : provider; 24 return "src-str">`No API key found for ${providerDisplay}.\n\n${getProviderLoginHelp()}`; 25 }
findInitialModel 优先级findInitialModel priority
#location主线时刻 / through-line moment
1CLI --provider/--model最高优先级
2scopedModels[0]非 resume 时
3settings defaulthasConfiguredAuth
4defaultModelPerProvideranthropic/claude-sonnet-…
5availableModels[0]任意有 key 的模型
failformatNoModelsAvailableMessage/login 指引
SOURCE  ·  packages/coding-agent/src/core/model-resolver.ts find-model
621 export async function findInitialModel(options: { 622 cliProvider?: string; 623 cliModel?: string; 624 scopedModels: ScopedModel[]; 625 isContinuing: boolean; 626 defaultProvider?: string; 627 defaultModelId?: string; 628 defaultThinkingLevel?: ThinkingLevel; 629 modelThinkingLevels?: Record<string, ThinkingLevel>; 630 modelRuntime: ModelRuntime; 631 }): Promise<InitialModelResult> { 632 const { 633 cliProvider, 634 cliModel, 635 scopedModels, 636 isContinuing, 637 defaultProvider, 638 defaultModelId, 639 defaultThinkingLevel, 640 modelThinkingLevels, 641 modelRuntime, 642 } = options; 643 644 let model: Model<Api> | undefined; 645 let thinkingLevel: ThinkingLevel = DEFAULT_THINKING_LEVEL; 646 647 // 1. CLI args take priority CLI args 优先CLI args first 648 if (cliProvider && cliModel) { 649 const resolved = resolveCliModel({ 650 cliProvider, 651 cliModel, 652 modelRuntime, 653 }); 654 if (resolved.error) { 655 console.error(chalk.red(resolved.error)); 656 process.exit(1); 657 } 658 if (resolved.model) { 659 return { model: resolved.model, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined }; 660 } 661 } 662 663 // 2. Use first model from scoped models (skip if continuing/resuming) 664 if (scopedModels.length > 0 && !isContinuing) { scoped models 第二scoped models second 665 const scopedModel = scopedModels[0]; 666 const perModel = modelThinkingLevels?.["src-str">`${scopedModel.model.provider}/${scopedModel.model.id}`]; 667 return { 668 model: scopedModel.model, 669 thinkingLevel: scopedModel.thinkingLevel ?? perModel ?? defaultThinkingLevel ?? DEFAULT_THINKING_LEVEL, 670 fallbackMessage: undefined, 671 }; 672 } 673 674 // 3. Try saved default from settings if auth is configured. 675 if (defaultProvider && defaultModelId) { settings default + hasConfiguredAuthsettings default + hasConfiguredAuth 676 const found = modelRuntime.getModel(defaultProvider, defaultModelId); 677 if (found && modelRuntime.hasConfiguredAuth(found.provider)) { 678 model = found; 679 const perModel = modelThinkingLevels?.["src-str">`${defaultProvider}/${defaultModelId}`]; 680 if (perModel) { 681 thinkingLevel = perModel; 682 } else if (defaultThinkingLevel) { 683 thinkingLevel = defaultThinkingLevel; 684 } 685 return { model, thinkingLevel, fallbackMessage: undefined }; 686 } 687 } 688 689 // 4. Try first available model with valid API key 690 const availableModels = [...modelRuntime.getAvailableSnapshot()]; ◀ trace getAvailableSnapshot 过滤无 key providergetAvailableSnapshot filters unconfigured providers 691 692 if (availableModels.length > 0) { 693 // Try to find a default model from known providers 694 for (const provider of Object.keys(defaultModelPerProvider) as KnownProvider[]) { defaultModelPerProvider 偏好defaultModelPerProvider preference 695 const defaultId = defaultModelPerProvider[provider]; 696 const match = availableModels.find((m) => m.provider === provider && m.id === defaultId); 697 if (match) { 698 return { model: match, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined }; 699 } 700 } 701 702 // If no default found, use first available 703 return { model: availableModels[0], thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined }; ◀ trace 兜底:第一个 availablefallback: first available 704 } 705 706 // 5. No model found 707 return { model: undefined, thinkingLevel: DEFAULT_THINKING_LEVEL, fallbackMessage: undefined }; 无模型 → undefinedno model → undefined 708 }
SOURCE  ·  packages/coding-agent/src/core/sdk.ts sdk-boot
208 // If still no model, use findInitialModel (checks settings default, then provider defaults) 209 if (!model) { 210 const result = await findInitialModel({ findInitialModel 调用点findInitialModel call site 211 scopedModels: [], 212 isContinuing: hasExistingSession, 213 defaultProvider: settingsManager.getDefaultProvider(), 214 defaultModelId: settingsManager.getDefaultModel(), 215 defaultThinkingLevel: settingsManager.getDefaultThinkingLevel(), 216 modelThinkingLevels: settingsManager.getAllModelThinkingLevels(), 217 modelRuntime, 218 }); 219 model = result.model; 220 if (!model) { ◀ trace 无 model → formatNoModelsAvailableMessageno model → formatNoModelsAvailableMessage 221 modelFallbackMessage = formatNoModelsAvailableMessage(); 222 } else if (modelFallbackMessage) { 223 modelFallbackMessage += "src-str">`. Using ${model.provider}/${model.id}`; fallback 文案拼接 provider/idfallback message appends provider/id 224 } 225 }
SOURCE  ·  packages/coding-agent/src/core/model-runtime.ts runtime-auth
561 getProviderAuthStatus(providerId: string): AuthStatus { ◀ trace getProviderAuthStatus:runtime/stored/envgetProviderAuthStatus: runtime/stored/env 562 if (this.credentials.hasRuntimeApiKey(providerId)) return { configured: true, source: "runtime" }; 563 if (this.snapshot.storedProviders.has(providerId)) return { configured: true, source: "stored" }; 564 const configured = configuredRequestAuthStatus( 565 this.config.getProvider(providerId), 566 this.extensionProviders.get(providerId), 567 ); 568 if (configured) return configured; 569 const check = this.snapshot.auth.get(providerId); 570 return check ? { configured: true, source: "environment", label: check.source } : { configured: false }; 571 } 572 573 private async prepareRequest<TOptions extends ProviderRequestOptions & ModelsRequestTransforms>( 574 model: Model<Api>, 575 options: TOptions | undefined, 576 ): Promise<{ 577 provider: Provider; 578 model: Model<Api>; 579 options: Omit<TOptions, "transformHeaders"> & ProviderRequestOptions; 580 }> { 581 const provider = this.models.getProvider(model.provider); 582 if (!provider) throw new ModelsError("provider", "src-str">`Unknown provider: ${model.provider}`); 583 const resolution = await this.getAuth(model, { prepareRequest getAuthprepareRequest getAuth 584 apiKey: options?.apiKey, 585 env: options?.env, 586 signal: options?.signal, 587 }); 588 if (!resolution) throw new ModelsError("auth", "src-str">`Provider is not configured: ${model.provider}`); ◀ trace 无 resolution → Provider is not configuredno resolution → Provider is not configured

主线 prompt 能跑通的前提:findInitialModel 返回的 model 在 getAvailableSnapshot() 里,且 streamSimpleapplyAuth 能解析出 apiKey。OAuth provider(如 Anthropic Claude Pro)走 credential store + refresh;纯 API key 走 env 或 /login 写入的 auth.json。

Through-line prompt requires: findInitialModel model is in getAvailableSnapshot(), and applyAuth resolves apiKey at streamSimple. OAuth providers use credential store + refresh; API key uses env or auth.json from /login.

实验:删掉 ~/.pi/agent/auth.json 中 anthropic 条目再启动 pi,应看到 formatNoModelsAvailableMessage 或 formatNoApiKeyFoundMessage。 Experiment: remove anthropic from ~/.pi/agent/auth.json and start pi; expect formatNoModelsAvailableMessage or formatNoApiKeyFoundMessage.
DEPTH 深度细读 · C16.4+Deep dive · C16.4+

C16.4 模型目录生成管线Model catalog pipeline

packages/ai/src/models.generated.ts 头部注释:auto-generated by scripts/generate-models.ts,勿手改。

models.generated.ts header: auto-generated by generate-models.ts, do not edit manually.

导入 AMAZON_BEDROCK_MODELS、ANTHROPIC_MODELS、OPENAI_MODELS 等 40+ provider 常量,汇总为 MODELS 对象。

Imports 40+ provider constants into MODELS object.

每个 Model 含 id、provider、contextWindow、maxTokens、cost、reasoning 支持等字段。

Each Model has id, provider, contextWindow, maxTokens, cost, reasoning support.

C16.5 认证路径Auth paths

packages/ai/src/oauth.ts 与 bun-oauth.ts 处理交互式 OAuth(GitHub Copilot、OpenAI Codex 等)。

oauth.ts and bun-oauth.ts handle interactive OAuth for Copilot, Codex, etc.

env-api-keys.ts 从环境变量读取 ANTHROPIC_API_KEY 等;getApiKey 注入 agentLoop config。

env-api-keys.ts reads env vars; getApiKey injected into agentLoop config.

agent-session.ts formatNoApiKeyFoundMessage 在冷启动无 key 时给出 auth-guidance。

formatNoApiKeyFoundMessage gives auth-guidance on cold start without key.

C16.6 主线 prompt 的模型选择Model selection for through-line

CLI --model provider/id 或交互式 /model 选择;model_change entry 写入 JSONL。

CLI --model provider/id or /model picker; model_change entry in JSONL.

第一次 stream 用选定 Model 调 streamSimple;thinkingLevel 映射到 reasoning 参数。

First stream uses selected Model via streamSimple; thinkingLevel maps to reasoning.

换模型不丢会话:SessionManager 保留历史,新 model_change entry 标记切换点。

Model switch preserves session: model_change entry marks switch point.

C16.7 ModelRegistry 运行时ModelRegistry runtime

coding-agent ModelRegistry 读取 generated catalog,合并用户 settings 与 extension 贡献的 provider config。

ModelRegistry reads generated catalog, merges user settings and extension provider configs.

modelsAreEqual 用于避免重复 emit model_select 事件。

modelsAreEqual avoids duplicate model_select events.

C16.8 与 textbook checkpoint 05 对照vs textbook checkpoint 05

textbook Provider Adapter 教 SSE→统一事件;生产 pi-ai api/*.ts 实现各厂商 adapter。

Textbook checkpoint 05 teaches SSE→unified events; production api/*.ts implements per-vendor adapters.

C16.9 读完后应能回答的 5 个问题 · 认证与模型目录Five questions you should answer after reading · 认证与模型目录

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 16

站 16:streamSimple 用 Model + Auth 调 provider adapter。

Station 16: streamSimple calls provider adapter with Model + Auth.

SOURCE  ·  packages/ai/src/models.generated.ts models
// auto-generated by scripts/generate-models.ts export const MODELS = { ... 40+ providers ... };
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 05Provider Adapterprovider-adapter.tspackages/ai/src/api/*.ts · packages/ai/src/models.ts
附录Appendix 常见问题FAQ
Q如何添加新 provider?Add provider?

在 pi-ai providers/ 加 models 文件并跑 generate-models。

Add models file under providers/ and run generate-models.

QOAuth 失败怎么办?OAuth fails?

检查 bun-oauth 回调与 ~/.pi 凭据存储。

Check bun-oauth callback and ~/.pi credential storage.

Q默认模型在哪设?Default model?

settings.json 与 CLI --model。

settings.json and CLI --model.

C16.10 打开源码:packages/ai/src/models.generated.tsOpen source: packages/ai/src/models.generated.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/ai/src/models.generated.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/ai/src/models.generated.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 17 · TERMINAL

差分渲染 TUI

Differential-render TUI

CSI 2026 · firstChanged → lastChanged

CSI 2026 · firstChanged → lastChanged

模块Module
模块Module
Package
tuitui
线程Thread
站 17St. 17
输出Output
tui-main-screen.tstui-main-screen.ts

Pi TUI 用 CSI 2026 差分更新:firstChangedlastChanged 行重绘,而非全屏刷新。流式 token 因此不闪烁。

Pi TUI uses CSI 2026 differential updates: redraw firstChanged to lastChanged lines, not full screen. Streaming tokens don't flicker.

SOURCE  ·  packages/tui/src/tui.ts tui
// SGR 2026 — synchronized output / damage tracking render(firstChanged, lastChanged): void
策略全屏刷新差分渲染
带宽O(screen)O(changed lines)
闪烁明显minimal
实现复杂度需 damage tracking

C17.1 message_update → 像素message_update → pixels

AgentSession 把 message_update 交给 interactive mode → TUI 组件树 → Markdown 增量解析 → 终端 write。

AgentSession hands message_update to interactive mode → TUI component tree → incremental Markdown → terminal write.

FIG TUI 差分渲染:只重绘变更行区间,流式 token 不触发全屏闪烁。 TUI differential render: redraw only changed line range; streaming tokens avoid full-screen flicker.
DEPTH 深度细读 · C17.4+Deep dive · C17.4+

C17.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/tui/src/tui-main-screen.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/tui/src/tui-main-screen.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C17.5 关键函数与数据流key functions and data flow

tui-main-screen.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in tui-main-screen.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C17.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/tui/src/tui-main-screen.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/tui/src/tui-main-screen.ts.

C17.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C17.8 读完后应能回答的 5 个问题 · 差分渲染 TUIFive questions you should answer after reading · 差分渲染 TUI

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/tui/src/terminal.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/tui/src/terminal.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/tui/src/terminal.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/tui/src/terminal.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 17

站 17:packages/tui/src/tui-main-screen.ts 处理主线。

Station 17: packages/tui/src/tui-main-screen.ts on through-line.

SOURCE  ·  packages/tui/src/tui-main-screen.ts src
let firstChanged = -1; let lastChanged = -1;
附录Appendix 常见问题FAQ
Q差分 TUI最关键文件?Key file?

packages/tui/src/tui-main-screen.ts

packages/tui/src/tui-main-screen.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C17.9 打开源码:packages/tui/src/terminal.tsOpen source: packages/tui/src/terminal.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/tui/src/terminal.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/tui/src/terminal.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 18 · TERMINAL

Interactive Mode

Interactive mode

Editor · Markdown · tool renderers

Editor · Markdown · tool renderers

模块Module
模块Module
Package
coding-agentcoding-agent
线程Thread
站 18St. 18
输出Output
interactive-mode.tsinteractive-mode.ts

Interactive mode 组合 Editor(输入)、Markdown(输出)、tool renderers(read 结果预览)。主线 prompt 在 Editor 提交,最终结果在 Markdown 区滚动。

Interactive mode composes Editor (input), Markdown (output), tool renderers (read preview). Through-line prompt submits from Editor; final answer scrolls in Markdown area.

SOURCE  ·  packages/tui/src/components/editor.ts editor
export class Editor extends Component { ... }
Alt-screenAlt-screen
全屏 TUI 不污染 scrollbackfullscreen TUI preserves scrollback
keybindingskeybindings
可配置 · 与 readline 不同configurable · unlike readline
tool 卡片tool cards
read 显示路径+行数read shows path + line count
DEPTH 深度细读 · C18.4+Deep dive · C18.4+

C18.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/coding-agent/src/modes/interactive/interactive-mode.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/coding-agent/src/modes/interactive/interactive-mode.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C18.5 关键函数与数据流key functions and data flow

interactive-mode.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in interactive-mode.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C18.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/coding-agent/src/modes/interactive/interactive-mode.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/coding-agent/src/modes/interactive/interactive-mode.ts.

C18.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C18.8 深度走读清单deep walk checklist

① 打开 packages/coding-agent/src/modes/interactive/interactive-mode.ts;② 从 export 符号跟踪 README 路径调用链;③ 标注每个 await 的 IO 类型。

① Open packages/coding-agent/src/modes/interactive/interactive-mode.ts; ② trace README call chain from exports; ③ label each await's IO type.

④ 在 agent-loop.test.ts 或 coding-agent 测试中找覆盖此站的用例;⑤ 用 pi --verbose 验证事件顺序。

④ Find tests covering this station; ⑤ verify event order with pi --verbose.

记录三个不变量:若修改会破坏主线 prompt 的行为假设。

Record three invariants: assumptions whose change would break the through-line.

将观察结果与 pi-textbook workshop 的 checkpoint 测试输出 diff。

Diff observations against pi-textbook workshop checkpoint test output.

C18.9 读完后应能回答的 5 个问题 · Interactive ModeFive questions you should answer after reading · Interactive Mode

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 18

站 18:packages/coding-agent/src/modes/interactive/interactive-mode.ts 处理主线。

Station 18: packages/coding-agent/src/modes/interactive/interactive-mode.ts on through-line.

SOURCE  ·  packages/coding-agent/src/modes/interactive/interactive-mode.ts src
// Interactive Mode · packages/coding-agent/src/modes/interactive/interactive-mode.ts // through-line: README.md
附录Appendix 常见问题FAQ
QInteractive Mode最关键文件?Key file?

packages/coding-agent/src/modes/interactive/interactive-mode.ts

packages/coding-agent/src/modes/interactive/interactive-mode.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C18.10 打开源码:packages/tui/src/components/editor.tsOpen source: packages/tui/src/components/editor.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/tui/src/components/editor.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/tui/src/components/editor.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 19 · EXTENSIONS

Extension API 面

Extension API surface

registerTool · registerCommand · events

registerTool · registerCommand · events

模块Module
模块Module
Package
coding-agentcoding-agent
线程Thread
站 19St. 19
输出Output
types.tstypes.ts

扩展通过 registerTool · registerCommand · 事件 hook 注入能力——无需 MCP,同进程 TypeScript。

Extensions inject via registerTool · registerCommand · event hooks — no MCP, same-process TypeScript.

SOURCE  ·  packages/coding-agent/src/core/extensions/types.ts ext
export interface ExtensionAPI { registerTool(def: ToolDefinition): void; on(event, handler): void; }
Hook时机用例
session_start会话加载恢复扩展状态
before_compact压缩前保留 artifact 索引
tool_execution_end工具后自动 lint
FIG Extension 钩子:ExtensionRunner 在 AgentSession 生命周期注入,无需 MCP 同进程协议。 Extension hooks: ExtensionRunner injects at AgentSession lifecycle — no MCP same-process protocol.
📘 pi-textbook checkpoint 12:Resources + Extensions(resources-extensions)· 生产映射:packages/coding-agent/src/core/extensions/ · resource-loader.ts 📘 pi-textbook checkpoint 12: Resources + Extensions (resources-extensions) · production: packages/coding-agent/src/core/extensions/ · resource-loader.ts
DEPTH 深度细读 · C19.4+Deep dive · C19.4+

C19.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/coding-agent/src/core/extensions/types.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/coding-agent/src/core/extensions/types.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C19.5 关键函数与数据流key functions and data flow

types.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in types.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C19.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/coding-agent/src/core/extensions/types.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/coding-agent/src/core/extensions/types.ts.

C19.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C19.8 读完后应能回答的 5 个问题 · Extension API 面Five questions you should answer after reading · Extension API 面

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 19

站 19:packages/coding-agent/src/core/extensions/types.ts 处理主线。

Station 19: packages/coding-agent/src/core/extensions/types.ts on through-line.

SOURCE  ·  packages/coding-agent/src/core/extensions/types.ts src
// Extension API · packages/coding-agent/src/core/extensions/types.ts // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 12Resources + Extensionsresources-extensionspackages/coding-agent/src/core/extensions/ · resource-loader.ts
附录Appendix 常见问题FAQ
QExtension API最关键文件?Key file?

packages/coding-agent/src/core/extensions/types.ts

packages/coding-agent/src/core/extensions/types.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C19.9 打开源码:packages/coding-agent/src/core/extensions/types.tsOpen source: packages/coding-agent/src/core/extensions/types.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/coding-agent/src/core/extensions/types.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/coding-agent/src/core/extensions/types.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 20 · EXTENSIONS

ExtensionRunner

ExtensionRunner

jiti 加载 · 事件合并 · hook 注入

jiti load · event merge · hook injection

模块Module
模块Module
Package
coding-agentcoding-agent
线程Thread
站 20St. 20
输出Output
runner.tsrunner.ts

ExtensionRunnerjiti 动态加载扩展 TS,合并事件处理器,在 agent 生命周期关键点注入 hook。

ExtensionRunner uses jiti to load extension TS, merges event handlers, injects hooks at agent lifecycle points.

SOURCE  ·  packages/coding-agent/src/core/extensions/runner.ts runner
export class ExtensionRunner { async loadExtensions(paths: string[]): Promise<void> }

C20.1 事件合并语义Event merge semantics

多个扩展可订阅同一事件;Runner 按加载顺序调用;before_compact 可返回修改后的 summary。

Multiple extensions can subscribe; Runner calls in load order; before_compact can return modified summary.

📘 pi-textbook checkpoint 13:Composition Root(composition-root)· 生产映射:packages/coding-agent/src/core/agent-session.ts · sdk.ts 📘 pi-textbook checkpoint 13: Composition Root (composition-root) · production: packages/coding-agent/src/core/agent-session.ts · sdk.ts
DEPTH 深度细读 · C20.4+Deep dive · C20.4+

C20.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/coding-agent/src/core/extensions/runner.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/coding-agent/src/core/extensions/runner.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C20.5 关键函数与数据流key functions and data flow

runner.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in runner.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C20.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/coding-agent/src/core/extensions/runner.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/coding-agent/src/core/extensions/runner.ts.

C20.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C20.8 深度走读清单deep walk checklist

① 打开 packages/coding-agent/src/core/extensions/runner.ts;② 从 export 符号跟踪 README 路径调用链;③ 标注每个 await 的 IO 类型。

① Open packages/coding-agent/src/core/extensions/runner.ts; ② trace README call chain from exports; ③ label each await's IO type.

④ 在 agent-loop.test.ts 或 coding-agent 测试中找覆盖此站的用例;⑤ 用 pi --verbose 验证事件顺序。

④ Find tests covering this station; ⑤ verify event order with pi --verbose.

记录三个不变量:若修改会破坏主线 prompt 的行为假设。

Record three invariants: assumptions whose change would break the through-line.

将观察结果与 pi-textbook workshop 的 checkpoint 测试输出 diff。

Diff observations against pi-textbook workshop checkpoint test output.

C20.9 读完后应能回答的 5 个问题 · ExtensionRunnerFive questions you should answer after reading · ExtensionRunner

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 20

站 20:packages/coding-agent/src/core/extensions/runner.ts 处理主线。

Station 20: packages/coding-agent/src/core/extensions/runner.ts on through-line.

SOURCE  ·  packages/coding-agent/src/core/extensions/runner.ts src
export class ExtensionRunner /** executes extensions and manages lifecycle */
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 12Resources + Extensionsresources-extensionspackages/coding-agent/src/core/extensions/ · resource-loader.ts
cp 13Composition Rootcomposition-rootpackages/coding-agent/src/core/agent-session.ts · sdk.ts
附录Appendix 常见问题FAQ
QExtensionRunner最关键文件?Key file?

packages/coding-agent/src/core/extensions/runner.ts

packages/coding-agent/src/core/extensions/runner.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C20.10 打开源码:packages/coding-agent/src/core/extensions/runner.tsOpen source: packages/coding-agent/src/core/extensions/runner.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/coding-agent/src/core/extensions/runner.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/coding-agent/src/core/extensions/runner.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 21 · EXTENSIONS

Pi Packages

Pi packages

npm · git · 可分享的 harness 能力

npm · git · shareable harness capabilities

模块Module
模块Module
Package
coding-agentcoding-agent
线程Thread
站 21St. 21
输出Output
package-manager.tspackage-manager.ts

pi package 命令管理可分享的 harness 能力包——npm 或 git 依赖,把工具+扩展+资源打包分发。

pi package manages shareable harness capability bundles — npm or git deps packaging tools+extensions+resources.

分发格式消费者
npmpackage.json + pi manifestpi install
gitrepo URL + refpi package add
localpath开发调试
扩展生态替代 MCP 市场——类型安全,但门槛是写 TypeScript。 Extension ecosystem replaces MCP marketplace — type-safe, but requires writing TypeScript.
DEPTH 深度细读 · C21.4+Deep dive · C21.4+

C21.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/coding-agent/src/core/package-manager.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/coding-agent/src/core/package-manager.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C21.5 关键函数与数据流key functions and data flow

package-manager.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in package-manager.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C21.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/coding-agent/src/core/package-manager.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/coding-agent/src/core/package-manager.ts.

C21.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C21.8 深度走读清单deep walk checklist

① 打开 packages/coding-agent/src/core/package-manager.ts;② 从 export 符号跟踪 README 路径调用链;③ 标注每个 await 的 IO 类型。

① Open packages/coding-agent/src/core/package-manager.ts; ② trace README call chain from exports; ③ label each await's IO type.

④ 在 agent-loop.test.ts 或 coding-agent 测试中找覆盖此站的用例;⑤ 用 pi --verbose 验证事件顺序。

④ Find tests covering this station; ⑤ verify event order with pi --verbose.

记录三个不变量:若修改会破坏主线 prompt 的行为假设。

Record three invariants: assumptions whose change would break the through-line.

将观察结果与 pi-textbook workshop 的 checkpoint 测试输出 diff。

Diff observations against pi-textbook workshop checkpoint test output.

C21.9 读完后应能回答的 5 个问题 · Pi PackagesFive questions you should answer after reading · Pi Packages

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 21

站 21:packages/coding-agent/src/core/package-manager.ts 处理主线。

Station 21: packages/coding-agent/src/core/package-manager.ts on through-line.

SOURCE  ·  packages/coding-agent/src/core/package-manager.ts src
// Pi Packages · packages/coding-agent/src/core/package-manager.ts // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 12Resources + Extensionsresources-extensionspackages/coding-agent/src/core/extensions/ · resource-loader.ts
附录Appendix 常见问题FAQ
QPi Packages最关键文件?Key file?

packages/coding-agent/src/core/package-manager.ts

packages/coding-agent/src/core/package-manager.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C21.10 打开源码:packages/coding-agent/src/core/extensions/loader.tsOpen source: packages/coding-agent/src/core/extensions/loader.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/coding-agent/src/core/extensions/loader.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/coding-agent/src/core/extensions/loader.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 22 · PERSISTENCE

JSONL 会话树

JSONL session tree

parentId · leafId · fork

parentId · leafId · fork

模块Module
模块Module
Package
coding-agentcoding-agent
线程Thread
站 22St. 22
输出Output
session-manager.tssession-manager.ts

每个会话是 JSONL 文件:parentId 链构成树,leafId 指向当前叶节点,fork 创建 parentSession 子会话。

Each session is a JSONL file: parentId chain forms tree, leafId points to current leaf, fork creates child with parentSession.

FIG JSONL 会话树:parentId 构成有向树;sibling 分支不覆盖旧路径;leafId 选择 active path。 JSONL session tree: parentId forms directed tree; sibling branches don't overwrite; leafId selects active path.
SOURCE  ·  packages/coding-agent/src/core/session-manager.ts sm
export const CURRENT_SESSION_VERSION = 3; export interface SessionMessageEntry { type: "message"; parentId: string | null; }

C22.1 主线 prompt 落盘后的 JSONL 行JSONL lines after through-line prompt

SOURCE  ·  ~/.pi/sessions/*.jsonl jsonl
{"type":"session","id":"...","cwd":"/path/to/project"} {"type":"message","parentId":null,"message":{"role":"user","content":"读取 README.md..."}} {"type":"message","parentId":"...","message":{"role":"assistant","stopReason":"toolUse",...}}
Entry type用途LLM 可见?
messageuser/assistant/tool
compaction压缩摘要✓(替换早期)
branch_summaryfork 摘要
model_change审计
📘 pi-textbook checkpoint 10:Session Tree(session.ts)· 生产映射:packages/coding-agent/src/core/session-manager.ts 📘 pi-textbook checkpoint 10: Session Tree (session.ts) · production: packages/coding-agent/src/core/session-manager.ts
SOURCE  ·  packages/coding-agent/src/core/session-manager.ts walkthrough
1 appendFileSync(sessionPath, JSON.stringify(entry)) // 追加 JSONL 行 / append JSONL line 2 parentId: string | null // 树边 / tree edge 3 fork(fromEntryId) // 创建分支会话 / branch session 4 getLeafId() // 当前叶指针 / current leaf
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 10Session Treesession.tspackages/coding-agent/src/core/session-manager.ts
DEPTH 深度细读 · C22.4+Deep dive · C22.4+

C22.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/coding-agent/src/core/session-manager.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/coding-agent/src/core/session-manager.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C22.5 关键函数与数据流key functions and data flow

session-manager.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in session-manager.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C22.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/coding-agent/src/core/session-manager.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/coding-agent/src/core/session-manager.ts.

C22.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C22.8 读完后应能回答的 5 个问题 · JSONL 会话树Five questions you should answer after reading · JSONL 会话树

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/coding-agent/src/core/session-manager.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/coding-agent/src/core/session-manager.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/coding-agent/src/core/session-manager.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/coding-agent/src/core/session-manager.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 22

站 22:packages/coding-agent/src/core/session-manager.ts 处理主线。

Station 22: packages/coding-agent/src/core/session-manager.ts on through-line.

SOURCE  ·  packages/coding-agent/src/core/session-manager.ts src
export const CURRENT_SESSION_VERSION = 3; parentId: string | null;
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 10Session Treesession.tspackages/coding-agent/src/core/session-manager.ts
附录Appendix 常见问题FAQ
QJSONL 会话树最关键文件?Key file?

packages/coding-agent/src/core/session-manager.ts

packages/coding-agent/src/core/session-manager.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C22.9 JSONL 一行究竟长什么样What one JSONL line actually looks like

每条 entry 含 idparentIdtypemessageevent 载荷。主线 prompt 的第一行 user entry 的 parentId 指向 session 根或上一 leaf;assistant toolUse 行的 parentId 指向 user entry——形成树而非链表。

Each entry has id, parentId, type, and message or event payload. First user entry's parentId points to session root or prior leaf; assistant toolUse points to user entry — a tree, not a linked list.

fork(fromEntryId) 创建新 session 文件,复制祖先链到切点,后续 append 挂在新分支上。这是 Pi 替代 sub-agent 的方式:不是进程内 spawn,而是会话树分支

fork(fromEntryId) creates a new session file, copies ancestor chain to cut point; subsequent appends hang on the new branch. Pi's sub-agent alternative: session tree branch, not in-process spawn.

CHAPTER 23 · PERSISTENCE

Compaction 与 Harness

Compaction and harness

上下文压缩 · SQLite lanes

context compression · SQLite lanes

模块Module
模块Module
Package
coding-agentcoding-agent
线程Thread
站 23St. 23
输出Output
index.tsindex.ts

Compaction:上下文超阈值时,历史消息不动(JSONL 完整保留),但重建发给模型的 context——用 LLM 生成摘要替换早期消息。

Compaction: when context exceeds threshold, history stays in JSONL but rebuilt context for model — LLM summary replaces early messages.

SOURCE  ·  packages/coding-agent/src/core/compaction/index.ts compact
export async function compact(...): Promise<CompactionResult> export function shouldCompact(tokens: number): boolean
历史History
JSONL 永不删JSONL never deleted
上下文Context
压缩后变短shorter after compact
SQLite lanesSQLite lanes
扩展可存 artifact 索引extensions store artifact index

C23.1 .harness 与 compaction 分工Harness vs compaction roles

agent-loop 不管 token 数;AgentSession 在 turn_end 检查 shouldCompact;扩展可在 before_compact 注入结构化摘要。

agent-loop ignores token count; AgentSession checks shouldCompact at turn_end; extensions inject structured summary in before_compact.

FIG Compaction:历史完整保留在 JSONL;发给模型的 context 按 token 预算重建,早期事实以摘要补回。 Compaction: history stays complete in JSONL; model context rebuilt under token budget with summary for early facts.
📘 pi-textbook checkpoint 11:Context Compaction(context.ts)· 生产映射:packages/coding-agent/src/core/compaction/ 📘 pi-textbook checkpoint 11: Context Compaction (context.ts) · production: packages/coding-agent/src/core/compaction/
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 11Context Compactioncontext.tspackages/coding-agent/src/core/compaction/
DEPTH 深度细读 · C23.4+Deep dive · C23.4+

C23.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/coding-agent/src/core/compaction/index.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/coding-agent/src/core/compaction/index.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C23.5 关键函数与数据流key functions and data flow

index.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in index.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C23.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/coding-agent/src/core/compaction/index.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/coding-agent/src/core/compaction/index.ts.

C23.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C23.8 读完后应能回答的 5 个问题 · Compaction 与 HarnessFive questions you should answer after reading · Compaction 与 Harness

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 23

站 23:packages/coding-agent/src/core/compaction/index.ts 处理主线。

Station 23: packages/coding-agent/src/core/compaction/index.ts on through-line.

SOURCE  ·  packages/coding-agent/src/core/compaction/index.ts src
// Compaction · packages/coding-agent/src/core/compaction/index.ts // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 11Context Compactioncontext.tspackages/coding-agent/src/core/compaction/
附录Appendix 常见问题FAQ
QCompaction最关键文件?Key file?

packages/coding-agent/src/core/compaction/index.ts

packages/coding-agent/src/core/compaction/index.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C23.9 打开源码:packages/coding-agent/src/core/compaction/Open source: packages/coding-agent/src/core/compaction/

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/coding-agent/src/core/compaction/,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/coding-agent/src/core/compaction/ in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 24 · LANDSCAPE

对比 Claude Code / Cursor

vs Claude Code / Cursor

minimal harness 的设计取舍

design trade-offs of a minimal harness

模块Module
模块Module
Package
pi-monopi-mono
线程Thread
背景Background
输出Output

对比三条路线:Claude Code(密封 CLI + MCP)、Cursor(密封 IDE + 全生态)、Pi(开源 harness + 终端 + JSONL)。

Three routes: Claude Code (sealed CLI + MCP), Cursor (sealed IDE + ecosystem), Pi (open harness + terminal + JSONL).

维度Claude CodeCursorPi
目标用户开箱即用IDE 用户想读源码的人
会话专有云专有本地 JSONL 树
扩展MCPVS CodeTS Extension API
可观测高(每事件可 log)
主线 prompt 可 trace✓ --verbose

C24.1 设计取舍表Design trade-off table

Pi 牺牲:一键 MCP 市场、GUI 权限、IDE 集成。Pi 获得:fork 会话、RPC 嵌入、完整事件日志、教学友好的代码体积。

Pi sacrifices: one-click MCP, GUI permissions, IDE integration. Pi gains: fork sessions, RPC embed, full event log, teachable code size.

不是「哪个更好」——是「你要闭包体验还是开卷考试」。 Not «which is better» — «do you want closed-loop UX or open-book exam».
特性Claude CodeCursorPi
MCP 工具市场✗ (Extension API)
子 agent✗ (session fork)
Plan 模式
JSONL 会话树
源码可读
--verbose 事件
IDE 集成
OAuth 40+ 模型
DEPTH 深度细读 · C24.4+Deep dive · C24.4+

C24.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后, 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C24.5 关键函数与数据流key functions and data flow

内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C24.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 的具体行。

Under --verbose, stderr event types trace to lines in .

C24.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C24.8 读完后应能回答的 5 个问题 · 对比 Claude Code / CursorFive questions you should answer after reading · 对比 Claude Code / Cursor

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

SOURCE  ·  — src
// 生态对比 · — // through-line: README.md
附录Appendix 常见问题FAQ
Q生态对比最关键文件?Key file?

.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C24.9 打开源码:packages/coding-agent/src/core/sdk.tsOpen source: packages/coding-agent/src/core/sdk.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/coding-agent/src/core/sdk.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/coding-agent/src/core/sdk.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 25 · LANDSCAPE

RPC 与 pi-server

RPC and pi-server

JSONL 进程协议 · CBOR 远程会话

JSONL process protocol · CBOR remote sessions

模块Module
protocol + serverprotocol + server
Package
schemas.ts + rpc-entry.tsschemas.ts + rpc-entry.ts
线程Thread
站 21St. 21
输出Output
JSONL / CBORJSONL / CBOR

--mode rpc 把 AgentSession 变成 JSONL 帧协议:stdin 收命令,stdout 吐事件。远程场景用 pi-server + CBOR。

--mode rpc turns AgentSession into JSONL frame protocol: stdin commands, stdout events. Remote uses pi-server + CBOR.

SOURCE  ·  packages/coding-agent/src/modes/rpc/ rpc
// {"type":"prompt","text":"读取 README.md..."} // → AgentEvent stream on stdout
模式传输用例
rpcJSONL stdin/outCI · 脚本
pi-serverHTTP + CBOR远程 harness
interactiveTUI日常开发
DEPTH 深度细读 · C25.4+Deep dive · C25.4+

C25.4 TypeBox schemaTypeBox schema

packages/protocol/src/schemas.ts:PROTOCOL_VERSION = 1;SessionPhaseSchema 对齐 AgentHarnessPhase(idle/turn/compaction/branch_summary/retry)。

schemas.ts: PROTOCOL_VERSION = 1; SessionPhaseSchema aligns with AgentHarnessPhase.

ModelRefSchema、ThinkingLevelSchema、JsonValueSchema 用 Type.Strict 禁止 additionalProperties。

ModelRefSchema, ThinkingLevelSchema, JsonValueSchema use strict objects.

帧格式在 framing.ts + codec.ts:长度前缀 JSONL 或 CBOR 二进制。

Frame format in framing.ts + codec.ts: length-prefixed JSONL or CBOR binary.

C25.5 RPC 模式启动链RPC boot chain

main.ts 解析 --mode rpc → rpc-entry.ts 读 stdin JSONL 命令 → 调用 AgentSession 方法 → 写 stdout 响应。

main.ts --mode rpc → rpc-entry.ts reads stdin JSONL commands → AgentSession methods → stdout responses.

主线 prompt 可作为 rpc prompt 命令注入,无需 TUI。

Through-line injectable as rpc prompt command without TUI.

适合 CI、外部 IDE 插件、多 pi 实例编排。

Suits CI, external IDE plugins, multi-pi orchestration.

C25.6 pi-server 远程会话pi-server remote sessions

packages/server CBOR over HTTP 暴露远程 AgentSession;client 包 acquireSession 获取 lease。

server package exposes remote AgentSession via CBOR HTTP; client acquireSession gets lease.

exclusive vs shared lease 防止并发写(client README 30 行)。

exclusive vs shared lease prevents concurrent writes (client README line 30).

C25.7 与 session fork 组合With session fork

RPC + fork:父进程 rpc fork 命令创建 branch session,子 pi 实例处理子任务——替代 sub-agent 的官方路径之一。

RPC + fork: parent rpc fork creates branch session, child pi handles subtask — official sub-agent alternative.

C25.8 主线 prompt 的 RPC traceRPC trace of through-line

管道 stdin 发送 prompt 命令 + pi --mode rpc --verbose 可无 TUI 观察完整事件 JSONL。

Pipe stdin prompt command + pi --mode rpc --verbose observes full event JSONL without TUI.

对比 interactive:同一 AgentSession.prompt() 路径,仅 I/O 层从 TUI 换为 stdin/stdout 帧。

vs interactive: same AgentSession.prompt() path, only I/O layer swaps TUI for stdin/stdout frames.

C25.9 rpc-entry.ts 命令面rpc-entry.ts command surface

packages/coding-agent/src/rpc-entry.ts 解析每行 JSON 命令:prompt、abort、set_model、fork 等,映射到 AgentSession 公开方法。

rpc-entry.ts parses each JSON line command: prompt, abort, set_model, fork, etc., mapping to AgentSession public methods.

响应帧同样 JSONL 序列化 AgentEvent,外部编排器可重建与 --verbose 等价的日志。

Response frames serialize AgentEvent as JSONL; external orchestrator can rebuild --verbose-equivalent logs.

C25.10 读完后应能回答的 5 个问题 · RPC 与 pi-serverFive questions you should answer after reading · RPC 与 pi-server

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 21

站 21:RPC JSONL 帧或 CBOR 远程驱动 AgentSession。

Station 21: RPC JSONL frames or CBOR remote drives AgentSession.

SOURCE  ·  packages/protocol/src/schemas.ts proto
export const PROTOCOL_VERSION = 1; export const SessionPhaseSchema = Type.Union([...]);
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 13Composition Rootcomposition-rootpackages/coding-agent/src/core/agent-session.ts · sdk.ts
附录Appendix 常见问题FAQ
QRPC 与 interactive 共享 AgentSession 吗?Share AgentSession?

是,同一类不同 I/O 层。

Yes, same class, different I/O layer.

QCBOR 何时用?When CBOR?

pi-server 远程;本地 RPC 用 JSONL。

pi-server remote; local RPC uses JSONL.

Q如何测 RPC?Test RPC?

管道 stdin/stdout + 断言响应帧序列。

Pipe stdin/stdout + assert response frame sequence.

C25.11 打开源码:packages/protocol/src/Open source: packages/protocol/src/

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/protocol/src/,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/protocol/src/ in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

CHAPTER 26 · CODA

怎么自己 trace 一轮

How to trace a turn yourself

--verbose · event log · session file

--verbose · event log · session file

模块Module
模块Module
Package
coding-agentcoding-agent
线程Thread
站 26St. 26
输出Output
main.tsmain.ts

自己 trace 主线 prompt 的三件套:pi --verbose 看 AgentEvent、打开 JSONL 会话文件、对照 pi-textbook workshop 测试。

Trace the through-line yourself: pi --verbose for AgentEvents, open JSONL session file, compare pi-textbook workshop tests.

01
pi --verbosestderr 打印完整事件流
02
~/.pi/sessions/找到最新 .jsonl
03
agent-loop.test.ts对照单元测试
04
workshop/test/agent-loop.test.tspi-textbook 离线复现
SOURCE  ·  terminal verbose
$ pi --verbose $ 读取 README.md,用一句话告诉我这个项目做什么 // agent_start → turn_start → message_update → ...

C26.1 推荐阅读顺序Suggested reading order

1. pi-textbook checkpoint 00 七里程碑 → 2. pi-mono agent-loop.ts runLoop → 3. agent-session.ts prompt() → 4. 本文 C02 22 站地图。

1. pi-textbook cp 00 seven milestones → 2. pi-mono agent-loop.ts runLoop → 3. agent-session.ts prompt() → 4. this article C02 22-station map.

读完之后,用一句话回答:
「读取 README.md,用一句话告诉我这个项目做什么」——
在 Pi 里,这句话究竟触发了什么?
After reading, answer in one sentence:
«read README.md and tell me what this project does in one sentence» —
what exactly does that line trigger in Pi?
Field Note · 10
答案应该能指出:AgentSession.prompt → agentLoop → 两次 streamSimple → read tool → JSONL append → TUI message_update。 Answer should cite: AgentSession.prompt → agentLoop → two streamSimple → read tool → JSONL append → TUI message_update.
DEPTH 深度细读 · C26.4+Deep dive · C26.4+

C26.4 主线 prompt 如何穿过此模块through-line crossing

用户输入「读取 README.md,用一句话告诉我这个项目做什么」后,packages/coding-agent/src/main.ts 定义了此阶段的类型契约与副作用边界。

After «read README.md and tell me what this project does in one sentence», packages/coding-agent/src/main.ts defines type contracts and side-effect boundaries here.

在 pi-mono 仓库打开该文件,搜索 exportAgentEvent——这是比读 README 更快的定位法。

In pi-mono, search export and AgentEvent — faster than README alone.

设计原则:核心循环(agent-loop)不 import TUI;本模块通过事件与回调与上游通信。

Principle: agent-loop never imports TUI; this module talks upstream via events/callbacks.

pi-textbook 对应 checkpoint 提供剥离 OAuth/TUI 后的最小可运行切片,建议先跑测试再读生产文件。

Matching textbook checkpoint offers a minimal slice without OAuth/TUI — run its test before production file.

C26.5 关键函数与数据流key functions and data flow

main.ts 内的 async 函数通常是 IO 边界:磁盘(read/write JSONL)、网络(streamSimple)、或子进程(bash)。

async functions in main.ts are usually IO boundaries: disk, network, or subprocess.

成功路径与错误路径都必须 emit 事件,否则 TUI 会卡在 streaming 状态、JSONL 会缺 entry。

Success and error paths must emit events or TUI stalls streaming and JSONL misses entries.

length 截断时 agent-loop.tsfailToolCallsFromTruncatedMessage 批量失败 tool calls,防止执行损坏参数。

On length truncation, failToolCallsFromTruncatedMessage fails all tool calls to avoid corrupted args.

对照 packages/agent/test/agent-loop.test.ts 中的同名场景测试用例。

Compare with matching scenarios in packages/agent/test/agent-loop.test.ts.

C26.6 与 agent-loop 的接口interface with agent-loop

AgentLoopConfigpackages/agent/src/types.ts)列出所有注入点:convertToLlmgetSteeringMessagesprepareNextTurn 等。

AgentLoopConfig in types.ts lists injection points: convertToLlm, getSteeringMessages, prepareNextTurn, etc.

AgentSession 在构造 config 时绑定本模块实现;换模式(print/rpc/interactive)不换 agent-loop。

AgentSession binds this module when building config; modes change, agent-loop does not.

fork 项目时优先替换本模块而非复制 agent-loop——这是 Pi 社区的实际 fork 模式。

When forking, replace this module first rather than copying agent-loop — the common community pattern.

--verbose 模式下 stderr 事件类型可回溯到 packages/coding-agent/src/main.ts 的具体行。

Under --verbose, stderr event types trace to lines in packages/coding-agent/src/main.ts.

C26.7 设计权衡与常见坑trade-offs and pitfalls

显式事件总线 vs 隐式回调:Pi 选择前者,事件类型在 AgentEvent union 中版本化。

Explicit event bus vs implicit callbacks: Pi chooses the former; events versioned in AgentEvent union.

扩展应 subscribe 事件而非 monkey-patch 私有方法——ExtensionRunner 提供正规 hook 面。

Extensions should subscribe to events, not monkey-patch privates — ExtensionRunner provides hooks.

JSONL CURRENT_SESSION_VERSION = 3:改 entry schema 必须 bump version 并提供迁移。

JSONL CURRENT_SESSION_VERSION = 3: schema changes need version bump and migration.

对比 Claude Code:闭源栈无法确认 compaction 触发点;Pi 的 shouldCompact 在源码中可读。

vs Claude Code: closed stack hides compaction triggers; Pi's shouldCompact is readable.

C26.8 读完后应能回答的 5 个问题 · 怎么自己 trace 一轮Five questions you should answer after reading · 怎么自己 trace 一轮

① 主线 prompt 进入本章模块时,第一个被调用的 export 函数是什么?② 它 emit 的第一个 AgentEvent 类型是什么?③ 若在此层抛错,TUI 会卡在什么状态?④ 对应的 pi-textbook checkpoint 测试命令是什么?⑤ packages/agent/src/agent-loop.ts 中哪一行最值得打断点?

① First export called when through-line enters this module? ② First AgentEvent type emitted? ③ If this layer throws, what TUI state stalls? ④ Matching pi-textbook checkpoint test command? ⑤ Best breakpoint line in packages/agent/src/agent-loop.ts?

不要一次读完整个文件。用「从测试入手」法:先 npm test -- <matching-test>,看失败断言指向哪行,再读那一行上下游 30 行。生产 pi-mono 的测试覆盖率在 agent-loop 与 session-manager 最高——优先读测试。

Don't read whole files at once. «Test-first» method: run npm test -- <matching-test>, see which assertion fails, read ±30 lines around that line. Highest test coverage in agent-loop and session-manager — read tests first.

对照本文 TRACE 盒与 --verbose stderr:每个 event type 应在源码中有且仅有一处「决策点」——若找不到,说明事件在更上层(AgentSession)或更下层(pi-ai adapter)合并发出。

Cross-check TRACE box with --verbose stderr: each event type should have exactly one «decision point» in source — if missing, event is merged upstream (AgentSession) or downstream (pi-ai adapter).

进阶:用 git blame packages/agent/src/agent-loop.ts 看最近改动——Pi 的 harness 接口仍在活跃演进,注释可能落后于 AgentLoopConfig 类型定义。类型即文档。

Advanced: git blame packages/agent/src/agent-loop.ts for recent changes — Pi harness APIs still evolve; comments may lag AgentLoopConfig types. Types are the doc.

TRACE · station 26

站 26:packages/coding-agent/src/main.ts 处理主线。

Station 26: packages/coding-agent/src/main.ts on through-line.

SOURCE  ·  packages/coding-agent/src/main.ts src
// 自己 trace · packages/coding-agent/src/main.ts // through-line: README.md
交叉引用Cross-ref pi-textbook checkpointpi-textbook checkpoint
CP主题教学 artifact生产映射
cp 00一次 README 读取的七个里程碑prologue.tspackages/coding-agent/src/main.ts · packages/agent/src/agent-loop.ts
cp 14Eval Capstoneeval-capstonepackages/evals/
附录Appendix 常见问题FAQ
Q自己 trace最关键文件?Key file?

packages/coding-agent/src/main.ts

packages/coding-agent/src/main.ts.

Q如何单测?Unit test?

mock StreamFn 或 ScriptedModel。

mock StreamFn or ScriptedModel.

Q与 textbook 差异?vs textbook?

生产含 OAuth/TUI/40+ providers。

Production adds OAuth/TUI/40+ providers.

C26.9 打开源码:packages/coding-agent/src/cli.tsOpen source: packages/coding-agent/src/cli.ts

本章主线 prompt 经过此模块时,请在 pi-mono 打开 packages/coding-agent/src/cli.ts,搜索 export 与测试文件中的同名场景。对照 pi-textbook 对应 checkpoint 的 workshop 测试——先让测试绿,再读生产实现。

When the through-line prompt crosses this module, open packages/coding-agent/src/cli.ts in pi-mono, search export and matching test scenarios. Cross-read the pi-textbook checkpoint workshop test — green test first, then production.

建议命令:cd packages/agent && npm test -- agent-loop(路径因章而异)。测试文件是行为契约的executable spec,比注释更可靠。

Suggested: cd packages/agent && npm test -- agent-loop (path varies). Tests are executable specs of behavior contracts — more reliable than comments.

--verbose 跑一轮主线 prompt,把 stderr event 类型与源码中的 emit 调用逐行对齐——这是本文 C26 推荐的 trace 方法。

Run the through-line with --verbose, align stderr event types with emit calls in source — the trace method recommended in C26.

✦ ✦ ✦
阅读Reads

留下评论Leave a comment

评论Comments

加载中…Loading…