返回课程首页

35

完整源码调用链走读

从用户输入一直追踪到模型回复、工具执行和会话保存。

源码基线:Pi v0.82.0 · commit 518855d

本章只追踪一件事:

用户在桌面输入框发送“读取日志并修复配置”,这句话怎样穿过 Pi,最终变成回复、工具结果 和 Session 记录?

我们沿真实源码边界走 14 步。第一次阅读先看主干,第二次再打开每个文件对照。

1. 学习目标

完成本章后,你应能够:

  1. AgentSession.prompt() 追踪到 Provider Stream;
  2. 解释 Skill、Extension 与 Tool 分别在哪一步介入;
  3. 根据事件判断当前处于 Message、Turn、Tool 还是 Agent Run;
  4. 找到 ToolResult 回到模型并触发下一 Turn 的位置;
  5. 解释 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-aipi-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_call Handler Throw 会使 Tool 失败且不执行;
  • 普通 tool_result Handler 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.tsai/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.tsagent-session.ts
14 Desktop Projection 宿主 Adapter/Reducer

19. 三种阅读源码的方法

先按事件读

搜索 agent_startmessage_updatetool_execution_end,适合理解 UI 时间线。

再按数据读

跟踪 AgentMessageAssistantMessageToolResultMessageSessionEntry,适合理解状态。

最后按控制流读

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. 练习

  1. AgentSession.prompt() 开始,列出 ToolCall 执行前的四个关键边界。
  2. 为什么 Skill 摘要出现在 System Prompt 中,不代表 Skill 已经执行?
  3. tool_result Handler Throw 为什么不能实现安全脱敏的 Fail Closed?
  4. ToolResult 的 contentdetails 应分别保存什么?
  5. 为什么 Session History 与当前模型 Context 不是同一个数组?
  6. 如果 UI 一直等待 Settled,哪两类输入可能让它永久等待?
  7. 设计一条日志字段,能把 Approval、Tool Execute 与 Audit 结果关联起来。