跳到正文
Learn Everything
返回

Pi Coding Agent 源码导读(一):从输入到工具执行的整体架构

编辑文章

版本说明:本文研究对象是 @earendil-works/pi-coding-agent@0.85.1,对应 Git tag v0.85.1、commit d981de1。Pi 仍在快速迭代,后续版本的命名和调用链可能变化。

Pi Coding Agent 源码导读

当我们在终端里输入一句“帮我修复这个测试”时,表面上只是一次问答,实际经过了配置加载、上下文拼装、模型调用、工具执行、结果回灌、会话持久化和界面渲染等多个阶段。

Pi 的特别之处在于:它没有把所有能力塞进一个庞大的 Agent 类,而是把系统拆成多个边界清晰的软件包。理解这些边界,比记住某个函数名更重要。

本文先回答三个问题:

  1. Pi 的整体架构分成哪些层?
  2. 一条用户输入怎样穿过这些层,最终变成文件修改?
  3. Tool Use、Memory、Skills、Extensions 分别处在流程的什么位置?

Pi Coding Agent 的整体分层架构

一、结论先行:Pi 不是一个循环,而是一组分层协作的运行时

从当前稳定 CLI 的实际调用链看,可以把 Pi 分成四个核心软件层,以及两个横向支撑面。

层级核心包或模块主要职责不负责什么
交互与入口层pi-coding-agent/main.ts、Interactive/Print/JSON/RPC解析参数、确定运行模式、收集用户输入、展示事件不直接实现模型协议
应用编排层AgentSession、ResourceLoader、SessionManagerPrompt 预处理、资源装配、扩展 Hook、重试、压缩、持久化不直接实现底层 Agent 循环
Agent 执行层Agent、agentLoop维护运行状态,循环调用模型和工具,处理消息队列,发出生命周期事件不关心 TUI 长什么样
模型适配层pi-ai、ModelRuntime模型目录、认证、Provider 路由、协议转换、流式响应、用量统计不执行本地文件工具
横向能力面Tools、Skills、Prompt Templates、Extensions给模型增加动作、知识与运行时扩展能力不天然决定完整业务流程
横向状态面Agent State、JSONL Session、Context Files、Compaction保存当前上下文、历史分支、项目规则和压缩摘要不等同于向量数据库式长期记忆

最短的主调用链可以写成:

用户输入
  ↓
InteractiveMode / PrintMode / RpcMode
  ↓
AgentSession.prompt()
  ↓
Agent.prompt()
  ↓
runAgentLoop()
  ↓
ModelRuntime.streamSimple()
  ↓
具体 Provider API
  ↓
AssistantMessage / ToolCall
  ↓
AgentTool.execute()
  ↓
ToolResultMessage 回到下一轮模型上下文

这个链条揭示了 Pi 的核心设计:

AgentSession 管“产品级编排”,Agent 管“运行状态”,agentLoop 管“模型—工具循环”,pi-ai 管“如何与不同模型通信”。

二、先划清一个容易混淆的版本边界

在 v0.85.1 源码中可以同时看到两套相关架构:

1. 当前日常 CLI 的稳定主路径

日常运行 pi、pi -p、pi --mode json 和 pi --mode rpc 时,核心路径仍然使用:

AgentSession
  └─ Agent
      └─ runAgentLoop / runAgentLoopContinue
          └─ pi-ai Models / Provider

会话由 coding-agent/src/core/session-manager.ts 中的 SessionManager 保存为 JSONL v3 树结构。

2. experimental 路径中的 AgentHarness

pi-agent-core 同时包含一套更偏持久化服务端运行的 AgentHarness:它引入 Session、Branch、AgentLane、durable operation、事务状态和崩溃恢复机制。目前在 coding-agent/src/experimental/ 的 server/client 与 mini 示例中使用。

这套 Harness 很重要,但它不是当前交互式 CLI 每次请求所走的主路径。如果把两者混在一起,就会错误地认为日常 Pi CLI 已经在每一个工具调用前后持久化完整 operation state。

本系列前十一篇以当前稳定 CLI 为主,第十二篇再单独分析 AgentHarness。

三、启动阶段:Pi 如何组装出一个可运行的 Agent

运行 pi 后,入口函数并不是立即调用模型,而是先构造运行环境。

Pi 启动时从 CLI 参数到运行模式的装配流程

步骤 1:CLI 解析与模式判断

coding-agent/src/main.ts 中的 main() 首先处理:

这一步回答的是“Pi 要以什么方式运行”,而不是“Agent 要怎样推理”。

步骤 2:项目可信度判断

Pi 会检查项目目录中是否存在 .pi/settings.json、项目 Extensions、Skills、Prompt、Theme 或 .agents/skills。如果这些资源尚未被信任,交互模式会依据 defaultProjectTrust 请求确认。

需要注意:Project Trust 只是启动时的资源加载闸门,不是 Sandbox。一旦 Agent 开始工作,内置工具仍以当前操作系统用户权限读写文件和执行命令。

步骤 3:创建三类运行服务

createAgentSessionServices() 和 DefaultResourceLoader 会准备:

  1. SettingsManager:合并全局与项目配置;
  2. ModelRuntime:管理模型目录、认证和 Provider;
  3. ResourceLoader:发现 Extensions、Skills、Prompt Templates、Themes、Context Files 和 System Prompt。

这些资源都有全局、项目、Package 和 CLI 临时输入等不同来源。ResourceLoader 的作用,是把分散的文件和配置规整成后续可以消费的对象。

步骤 4:创建 SessionManager

SessionManager 负责:

它保存的是一棵 append-only 的会话树。分支不是复制整段历史,而是通过每条 entry 的 id 和 parentId 连接。

步骤 5:createAgentSession() 组装 Agent

这个工厂函数是稳定 CLI 与 SDK 共用的关键装配点。它会:

  1. 从 Session 或 Settings 选择初始模型;
  2. 恢复或限制 thinking level;
  3. 决定启用哪些内置、扩展和自定义工具;
  4. 创建 Agent,注入 convertToLlm、streamFn、Hook、消息队列和 Provider 参数;
  5. 创建 AgentSession,连接 Agent、SessionManager、Extensions 与 ResourceLoader;
  6. 重建 System Prompt 和工具注册表。

步骤 6:选择外层运行模式

最后,同一个 AgentSessionRuntime 被交给不同外壳:

这也是 Pi 能同时作为终端应用、脚本工具和嵌入式 SDK 的原因:交互模式不同,但底层 Session 和 Agent Loop 可以复用。

四、请求阶段:一句 Prompt 的标准执行流程

假设用户输入:

读取 package.json,找出构建命令并运行它。

这条指令并不会直接交给模型。它首先进入 AgentSession.prompt()。

阶段 1:输入预处理

AgentSession 按顺序处理:

  1. 判断是否为 Extension Command;
  2. 触发 Extension 的 input 事件,允许处理或改写输入;
  3. 展开 /skill:name;
  4. 展开文件型 Prompt Template;
  5. 如果 Agent 正在运行,则按 steer 或 followUp 入队;
  6. 刷新等待写入的 Bash 或 Custom Message;
  7. 检查模型与认证;
  8. 必要时在发送新消息前执行 compaction;
  9. 触发 before_agent_start,允许扩展追加消息或临时修改 System Prompt。

这里可以看出,Skills、Prompt Templates 和 Extension Commands 虽然都能通过 /... 触发,但实现完全不同:

阶段 2:进入 Agent 状态机

AgentSession._runAgentPrompt() 调用 Agent.prompt()。Agent 会:

Agent 是有状态包装器;真正的循环逻辑位于 agent-loop.ts。

阶段 3:构造模型上下文

每次调用模型前,streamAssistantResponse() 会执行:

AgentMessage[]
  → transformContext()
  → convertToLlm()
  → pi-ai Context
  → streamFunction()

其中:

阶段 4:Provider 流式生成

ModelRuntime.streamSimple() 根据 model.provider 和 model.api 找到对应 Provider 与协议实现,完成:

因此,上层 Agent Loop 不需要分别理解 Anthropic Messages、OpenAI Responses 或 Gemini API。

五、Tool Use:模型如何真正修改本地环境

一次带工具调用的 Agent Loop:模型、工具与会话持久化之间的消息循环

如果模型返回一个 Tool Call,例如:

{
  "name": "read",
  "arguments": { "path": "package.json" }
}

Agent Loop 会执行以下步骤。

1. 查找工具

从本轮 AgentContext.tools 中按名称查找。如果不存在,则生成 isError: true 的 Tool Result,让模型自行修正,而不是直接让整个进程崩溃。

2. 参数预处理与校验

工具可以先通过 prepareArguments() 修正参数,然后使用 TypeBox Schema 校验。参数不合法时同样生成错误 Tool Result。

3. 扩展拦截

AgentSession 把 Agent 的 beforeToolCall 与 afterToolCall 接到了 Extension Runner:

4. 执行工具

工具的 execute() 接收:

当前默认内置工具为 read、bash、edit 和 write;还提供 grep、find、ls 与 Windows 的 powershell。

5. 并行或串行

默认全局模式是 parallel。如果一次模型响应包含多个 Tool Call,Pi 会并发执行;但只要其中任一工具声明 executionMode: "sequential",整个批次就按顺序执行。

即使并行完成,最终生成的 Tool Result Message 仍按模型原始 Tool Call 顺序回灌,避免上下文顺序随机变化。

6. 结果回灌

每个结果被转换为:

{
  role: ("toolResult",
    toolCallId,
    toolName,
    content,
    details,
    usage,
    isError,
    timestamp);
}

随后追加到 currentContext.messages。只要工具批次没有要求终止,Agent Loop 就开始下一个 turn,把工具结果发回模型。

因此,Agent 的核心闭环是:

模型观察上下文
  → 选择工具
  → Pi 执行工具
  → 结果写回上下文
  → 模型观察新结果
  → 继续调用工具或给出最终答案

六、Memory:Pi 到底“记住”了什么

Pi 没有一个名为 MemoryModule 的统一组件。它的记忆能力分散在不同层次中。

记忆层次载体生命周期用途
当前工作记忆Agent.state.messages当前进程与当前 Session直接参与下一次模型请求
会话记忆JSONL Session Tree跨进程、可恢复保存消息、模型切换、分支与压缩记录
项目指令记忆AGENTS.md / CLAUDE.md启动或 reload 后持续注入项目规则和约定
程序性记忆Skills / Prompt Templates被发现后按需展开告诉 Agent 某类任务怎样完成
压缩记忆Compaction Summary上下文过长后用有损摘要替代较老消息
扩展状态Custom Entry / Tool Result details由扩展定义保存 Todo、检查点等自定义状态

这里最重要的结论是:

Pi 的内置 Memory 主要是“会话树 + 上下文投影 + 摘要”,不是自动检索个人知识库的向量记忆系统。

如果需要 embedding、向量数据库、跨项目用户画像或自动召回,应该通过 Extension、自定义 Tool 或外部 CLI 增加,而不是误以为 SessionManager 已经提供这些能力。

七、System Prompt、Context Files 与 Skills 如何汇合

buildSystemPrompt() 会把以下内容组合起来:

  1. Pi 的默认角色说明;
  2. 当前真正启用的工具和工具说明;
  3. 与工具相关的操作 guideline;
  4. .pi/SYSTEM.md 或自定义 System Prompt;
  5. APPEND_SYSTEM.md;
  6. 从全局、父目录到当前目录加载的 Context Files;
  7. Skills 的名称、描述和文件位置;
  8. 当前工作目录。

Skill 使用“渐进式披露”:启动时通常只把名称和 description 放进 System Prompt;当任务匹配时,模型再使用 read 读取完整 SKILL.md。如果用户明确执行 /skill:name,AgentSession 会直接读取文件并将正文包装进当前消息。

这解释了为什么 Skill 不是 Tool:

八、事件流:为什么 TUI、JSON 和持久化可以同时工作

Agent Core 不直接操作 UI,而是发出事件:

agent_start
turn_start
message_start
message_update*
message_end
tool_execution_start
tool_execution_update*
tool_execution_end
turn_end
agent_end

AgentSession 订阅这些事件,并按顺序完成三件事:

  1. 先交给 Extensions;
  2. 再通知 TUI、Print 或 JSON mode 等外部监听器;
  3. 在 message_end 时写入 SessionManager。

这是一种典型的事件驱动解耦:

九、一次完整执行的停止条件

Agent Loop 不会无限调用模型。它会在以下情况结束当前 run:

Agent Core 结束后,AgentSession 还会执行产品级收尾:

  1. 判断错误是否需要自动重试;
  2. 检查是否发生 context overflow;
  3. 检查是否达到主动 compaction 阈值;
  4. 处理由 agent_end 扩展新加入的队列消息;
  5. 必要时调用 agent.continue()。

所以 agent_end 不一定代表整个 AgentSession.prompt() 已经结束。真正对外稳定的收尾事件是 agent_settled。

十、为什么 Pi 选择这种架构

1. 模型协议与 Agent 逻辑解耦

Agent Loop 只消费统一 Message 和 Stream Event,新增 Provider 时不用改工具循环。

2. Agent Core 与 Coding 产品解耦

pi-agent-core 可以用于其他 Agent 应用;重试、会话树、项目规则和 TUI 则留在 pi-coding-agent。

3. 机制与工作流解耦

Pi 内核提供 Tool、Event、Session 和 Extension 机制,却刻意不内置唯一的 Plan Mode、Sub-Agent 或 Todo 流程。用户可以通过 Extensions 和 Packages 选择自己的实现。

4. 所有外壳共享同一事件模型

Interactive、Print、JSON、RPC 和 SDK 不必各写一套推理循环。

5. Memory 不被包装成神秘黑盒

Session、Context File、Skill 和 Compaction 的作用边界清晰,使用者可以知道哪些信息会进入模型、哪些只是界面或扩展状态。

十一、阅读源码时应该抓住的主线

如果你准备自己阅读 Pi 源码,推荐按照以下顺序:

packages/coding-agent/src/main.ts
  ↓
packages/coding-agent/src/core/sdk.ts
  ↓
packages/coding-agent/src/core/agent-session.ts
  ↓
packages/agent/src/agent.ts
  ↓
packages/agent/src/agent-loop.ts
  ↓
packages/ai/src/models.ts
  ↓
packages/ai/src/api/<具体协议>.ts

需要理解旁路能力时,再分别阅读:

resource-loader.ts    → Skills / Prompts / Extensions / Context Files
system-prompt.ts      → System Prompt 拼装
session-manager.ts    → JSONL 会话树与上下文恢复
core/tools/*          → 内置工具
modes/interactive/*  → TUI 交互

十二、本篇建立的架构坐标

现在可以把 Pi 的整体流程压缩成一句话:

Pi 先由 CLI 和 ResourceLoader 组装一个 AgentSession;AgentSession 将输入、资源、扩展、重试、压缩和持久化接到 Agent 上;Agent 再通过 agentLoop 驱动“模型生成—工具执行—结果回灌”的循环;pi-ai 负责把这个统一循环翻译成不同 Provider 的真实 API 请求。

后续文章会沿着这条主线逐层展开。下一篇先分析启动阶段:Settings、Project Trust、ResourceLoader、System Prompt 和工具注册表究竟以什么顺序组装。

参考资料与源码索引


编辑文章
分享这篇文章:

上一篇
Agent Workflow 与 Skills:一个管过程,一个管能力
下一篇
Pi Coding Agent 源码导读(二):启动、配置与资源发现