本章只追踪一件事:
用户在桌面输入框发送“读取日志并修复配置”,这句话怎样穿过 Pi,最终变成回复、工具结果 和 Session 记录?
我们沿真实源码边界走 14 步。第一次阅读先看主干,第二次再打开每个文件对照。
1. 学习目标
完成本章后,你应能够:
- 从
AgentSession.prompt()追踪到 Provider Stream; - 解释 Skill、Extension 与 Tool 分别在哪一步介入;
- 根据事件判断当前处于 Message、Turn、Tool 还是 Agent Run;
- 找到 ToolResult 回到模型并触发下一 Turn 的位置;
- 解释 Session Entry、Compaction 与桌面 State 的不同职责。
2. 先看一张总图
flowchart LR
U["1 UI Intent"] --> P["2 AgentSession.prompt"]
P --> R["3 Resource / Skill Context"]
R --> BA["4 before_agent_start"]
BA --> AL["5 Agent Loop"]
AL --> PR["6 pi-ai Provider"]
PR --> MS["7 Message Stream"]
MS --> TC["8 ToolCall"]
TC --> EX["9 Extension Gate"]
EX --> AP["10 Approval / Final Policy"]
AP --> TE["11 Tool Execute"]
TE --> TR["12 ToolResult / Continue"]
TR --> SE["13 Session Persist / Compact"]
SE --> DS["14 Desktop State / Settled"]
这不是 14 个彼此独立的模块,而是一次 Run 中重复发生的流水线。步骤 5–12 可能循环多次。
3. 四层生命周期先分清
Agent Run
└── Turn 1
├── Assistant Message
└── Tool Calls / Tool Results
└── Turn 2
└── Assistant Message
- Message:一条 User、Assistant 或 ToolResult 消息;
- Turn:一次模型调用,以及随后属于该 Assistant Message 的工具执行;
- Agent Run:从 Prompt 开始,直到没有自动延续;
- Session:可以包含许多 Agent Run、分支和压缩记录。
把这些层混在一起,是阅读事件源码时最常见的困难。
4. 第 1 步:桌面 UI 产生 Intent
View 不直接构造 Pi Message。它发送宿主 Intent:
{
type: "send_prompt",
runtimeId,
generation,
text: "读取日志并修复配置",
}
Controller 校验 Runtime Owner、Generation、附件与当前 Run 状态,然后选择:
- Idle:普通 Prompt;
- Streaming:Steer 或 Follow-up;
- Session 正在替换:拒绝或暂存到宿主队列。
这里是桌面产品边界,不在 Pi 源码中。
5. 第 2 步:进入 AgentSession.prompt()
入口位于:
packages/coding-agent/src/core/agent-session.ts
prompt() 的 Preflight 会处理输入形态与运行状态。主线包括:
extension command
→ extension input event
→ expand prompt template / explicit skill invocation
→ streaming queue handling
→ model/auth validation
→ optional pre-run compaction
→ user message construction
→ before_agent_start
→ start Agent Run
Streaming Queue Handling 是旁路:调用 steer()/followUp() 后直接返回,不会继续执行模型
认证、预运行压缩、User Message Construction 或 before_agent_start。
Extension Command 或 input Handler 可以直接处理输入并返回;这种情况不会启动普通 Agent
Run。若 Agent 正在 Streaming,又没有给出合法 Queue 行为,Prompt 会被拒绝。
因此“Prompt 调用成功”只代表入口接受了请求,不等于一定向模型发送了消息。
6. 第 3 步:资源与 Skill 进入上下文
DefaultResourceLoader 在 Runtime 建立/Reload 时发现:
- Skills;
- Prompt Templates;
- Context Files;
- Extensions;
- Themes(本书不讲其 TUI 呈现)。
Skill 有两条使用路径。
模型自主选择时,摘要被格式化进 Base System Prompt,使模型知道“有哪些方法可以读取”;
模型通常还要通过 Read Tool 读取完整 SKILL.md:
flowchart LR
RL["ResourceLoader"] --> SM["Skill Metadata"]
SM --> SP["Base System Prompt"]
SP --> M["Model sees available skills"]
M --> RD["Read SKILL.md"]
RD --> NX["Next Turn follows instructions"]
Dynamic resources_discover 会先扩展 Resource Path,再重建相应资源与 Base Prompt。
用户显式输入 /skill:name 时,AgentSession 会在 Prompt Preflight 中直接读取并展开完整
SKILL.md,不需要模型再调用 Read Tool。两条路径最后都使用完整指令,但发生阶段不同。
7. 第 4 步:before_agent_start 修改整个 Run
在实际 Agent Run 启动前,AgentSession 发出 before_agent_start。Extension 可以追加:
- System Prompt;
- Message;
- 本次 Run 的上下文信息。
它的作用域是整个 Agent Run,不只是第一 Turn。此后每次 Provider 调用还可能经过 context
Event,对当前 LLM Context 做 Turn 级转换。
安全原则:
- 只追加经过边界标记的可信指令;
- 不把外部服务返回的自然语言直接提升为 System Authority;
- 把最终安全 Policy 放在 Tool 执行链,而不是只放 Prompt。
8. 第 5 步:Agent Loop 开始
核心文件:
packages/agent/src/agent-loop.ts
主干可以缩成:
prepare turn context
→ stream assistant response
→ collect tool calls
→ execute tools
→ append tool results
→ decide whether another turn is needed
继续下一 Turn 的常见原因:
- Assistant 产生未终止的 Tool Call;
- ToolResult 已加入上下文,需要模型继续判断;
- 存在 Steering/Follow-up Queue;
- AgentSession 触发 Retry/Compaction 后继续。
Agent Loop 不是 Workflow 图解释器;每次是否调用工具仍由模型生成决定。
9. 第 6 步:pi-ai 调用 Provider
Agent Core 把通用 Context 交给 pi-ai。pi-ai 根据 Model 的 api/Provider 找到对应 Stream
实现,再把通用 Message 和 Tool Schema 转成供应商请求。
Model
→ Provider Registry
→ Stream Function
→ Provider-specific Request
→ Normalized AssistantMessageEvent
这层解决“不同厂商协议不同、上层仍消费统一事件”的问题。Auth Resolution、Base URL、 Headers 与 Provider-specific Options 也在这一边界组合。
跨 Provider 切换时,历史消息可能需要转换或降级;不能假设每家都接受完全相同的 Thinking、 Image 或 ToolCall 格式。
10. 第 7 步:Streaming Message 形成
Provider Delta 被标准化为 Assistant Message Event:
start
→ text/thinking/toolcall updates
→ done or error
Agent Core 把它们进一步表现为:
message_start;message_update;message_end。
桌面 Adapter 可增量合并 Text/Thinking,但最终以 message_end 中的完整 Message Reconcile。
这样即使 UI Batch、漏掉某个 Delta,最终状态仍能校正。
Usage 和 Stop Reason 属于最终 Assistant Message/Provider 结果,Streaming 中的估算不应冒充 最终账单。
11. 第 8 步:Assistant Message 中出现 ToolCall
模型并不是“直接调用 JavaScript 函数”,而是生成结构化 ToolCall:
{
type: "toolCall",
id: "call_123",
name: "read",
arguments: { path: "logs/error.log" },
}
Pi 使用 Tool Name 找 Definition,并用 Schema 校验 Arguments。Streaming 期间的 Partial JSON 只适合展示预览,执行必须等待完整参数并通过验证。
多个 ToolCall 可以并行执行,所以事件用 toolCallId 关联,而不是依赖数组中“当前工具”。
12. 第 9 步:Extension 截获 Tool
Coding Agent 把 Agent Tool Hook 绑定到 Extension Runner:
tool_call handlers
→ optional argument transform / block
→ execute tool
→ tool_result handlers
→ optional result patch
关键错误语义:
tool_callHandler Throw 会使 Tool 失败且不执行;- 普通
tool_resultHandler Throw 会被报告并继续,因此不能靠 Throw 实现安全脱敏; - 多个 Extension Handler 按注册顺序经过链式处理。
此时 Tool Start 中看到的可能还是 Raw Args。审批和审计必须绑定最终 Canonical Args。
13. 第 10 步:审批与最终 Policy
高风险 Tool 的受控链建议是:
flowchart LR
RA["Raw Args"] --> CA["Canonicalize"]
CA --> CL["Classify"]
CL --> UI["Desktop Approval"]
UI --> FP["Final Host Policy"]
FP --> EX["Execute"]
Extension 可以通过 SDK UI Binding 或 RPC extension_ui_request 请求桌面确认。宿主用
Request ID、ToolCall ID、Session ID、Generation 做路由。
批准不是最终权限。执行后端还要重新检查:
- Path 是否仍在 Workspace;
- SQL/Command 是否仍等于批准的 Canonical 值;
- Capability 是否未过期且只能使用一次;
- 当前 Session/Window 是否仍然有效。
14. 第 11 步:Tool 执行
Tool Definition 的 execute() 接收最终参数、AbortSignal 与进度回调。Built-in Read/Write/
Edit/Bash 等 Tool 还可通过 Operations Interface 把真正执行路由到 Remote Workspace、
Container 或 Broker。
Pi 的 Tool Event 主干:
tool_execution_start
→ tool lookup / prepare / validate
→ extension tool_call / approval
→ execute
→ zero or more tool_execution_update
→ tool_execution_end
AbortSignal 必须继续传到底层 Fetch、Process 或 Driver。Abort 只能请求停止,不能撤销已经完成 的文件写入或外部 API 调用。
注意 tool_execution_start 在 Extension Gate、审批和 execute() 之前就发出,表示“这次
ToolCall 已进入执行流水线”,不是“后端已经真正开始产生副作用”。Pi 没有另一个统一事件
专门表示后端开始;若 UI/Audit 需要这一刻,应由受控 Backend 发宿主事件。
并行工具中,单个失败通常被转换为自己的 Error ToolResult;具体是否继续其他工具要看 Agent Loop 的并行执行与中止状态。
15. 第 12 步:ToolResult 回到模型
Tool 完成后产生 ToolResult Message:
{
role: "toolResult",
toolCallId: "call_123",
toolName: "read",
content: [{ type: "text", text: "..." }],
isError: false,
}
content 会进入模型上下文;details 可保存 UI/状态需要、但不一定应发给模型的结构化数据。
Extension 的 tool_result Handler 还可做结果裁剪、脱敏或附加标记。
Agent Loop 把 ToolResult 追加进当前消息序列,然后再次调用模型。模型看到日志后,才可能生成 第二个 Write ToolCall;写入完成后,才生成最终自然语言总结。
这一步解释了 Agent 的本质:
工具不是对话外的旁路,而是以 ToolResult Message 回到下一次模型判断。
16. 第 13 步:Session 持久化与 Compaction
AgentSession/SessionManager 把关键状态写成带 ID 与 Parent ID 的 JSONL Entry,包括:
- Messages;
- Model/Thinking 变化;
- Compaction;
- Branch Summary;
- Custom Entry;
- Session 元数据。
Session 是 Tree,不只是线性数组。当前模型 Context 来自所选 Branch,再经过转换与必要的 Compaction。
Context 接近模型窗口时:
older context
→ summarization
→ compaction entry
→ summary + retained tail become active context
原始 Session 历史仍存在;压缩不是删除文件中的旧对话。
17. 第 14 步:桌面 State 收敛
AgentSession Event 经过 Adapter 进入纯 Reducer:
Message Events → conversation slice
Tool Events → tools[toolCallId]
Approval Channel → approvals[requestId]
Session/Model Events → session slice
Retry/Compaction → recovery slice
agent_end 表示 Agent Core 这一轮 Run 结束,但 AgentSession 还可能自动 Retry、Compaction 或
处理 Queue;agent_settled 才表示实际 Agent Run 已稳定下来。例外是 Extension Command 或
Handled Input:它们可能不启动 Agent Run,也就不统一产生新的 Settled。
最终 UI 显示:
- 已提交的 User/Assistant/Tool 项;
- 当前运行状态回到 Idle;
- Tool 审批和结果已结束;
- Session 已更新;
- Usage/Diagnostic 可查看。
18. 一份调用链索引
| 步骤 | 入口/概念 | 首要源码 |
|---|---|---|
| 1 | Desktop Intent | 宿主代码 |
| 2 | Prompt Preflight | core/agent-session.ts |
| 3 | Resource/Skill | core/resource-loader.ts |
| 4 | Extension Run Context | core/extensions/runner.ts |
| 5 | Agent Loop | agent/src/agent-loop.ts |
| 6 | Provider Stream | ai/src/models.ts 与 Provider |
| 7 | Message Event | agent/src/types.ts、ai/src/types.ts |
| 8 | ToolCall/Validation | agent-loop.ts 与 Tool Schema |
| 9 | Extension Tool Hook | core/extensions/runner.ts |
| 10 | Approval | Extension UI Binding / RPC |
| 11 | Tool Execute | core/tools/* |
| 12 | ToolResult/Continue | agent-loop.ts |
| 13 | Persist/Compact | session-manager.ts、agent-session.ts |
| 14 | Desktop Projection | 宿主 Adapter/Reducer |
19. 三种阅读源码的方法
先按事件读
搜索 agent_start、message_update、tool_execution_end,适合理解 UI 时间线。
再按数据读
跟踪 AgentMessage、AssistantMessage、ToolResultMessage、SessionEntry,适合理解状态。
最后按控制流读
从 prompt() 进入 Agent Loop,再追 Provider 与 Tool,适合理解“为什么还会继续下一 Turn”。
不要一开始从整个 Monorepo 的 index.ts 逐文件顺序阅读。
20. 两条旁路
主链之外有两条容易误判的路径:
Extension Command / Handled Input
输入在 Preflight 被处理,不进入普通模型 Run。
User Bash
用户输入 !/!! 走 user_bash 与宿主执行路径,不是模型生成的 Bash ToolCall,也不自动
获得模型 Bash Tool 的 PI_* Session Environment。
读日志时必须先判断自己看到的是哪一条路径。
21. 本章重点总结
- Prompt 先经过 Extension Command/Input,再展开模板或显式 Skill,随后做队列、模型认证与 可能的预运行压缩;
- 自动 Skill 由摘要引导模型 Read;显式
/skill:name在 Preflight 直接展开完整内容; - Agent Loop 在模型、ToolCall、ToolResult 之间循环;
- pi-ai 把不同 Provider 统一成 Assistant Streaming Event;
- ToolCall 先校验,再经过 Extension Gate、Approval、Final Policy 与 Backend;
- ToolResult Content 回到模型,Details 可承载非模型结构化状态;
- Session JSONL 是带 Parent 的 Tree,Compaction 只改变活动 Context;
- 桌面 UI 消费的是 Event 投影,不能直接把 Delta 当完整持久状态;
agent_settled适用于真实 Agent Run,不是每种 Prompt Preflight 路径的统一回执。
22. 练习
- 从
AgentSession.prompt()开始,列出 ToolCall 执行前的四个关键边界。 - 为什么 Skill 摘要出现在 System Prompt 中,不代表 Skill 已经执行?
tool_resultHandler Throw 为什么不能实现安全脱敏的 Fail Closed?- ToolResult 的
content与details应分别保存什么? - 为什么 Session History 与当前模型 Context 不是同一个数组?
- 如果 UI 一直等待 Settled,哪两类输入可能让它永久等待?
- 设计一条日志字段,能把 Approval、Tool Execute 与 Audit 结果关联起来。