整体架构

pi 不是一个单体程序,而是四个可以独立使用的 npm 包,自底向上层层组装。理解这个分层,就理解了 pi 的一半——因为每一层都可以单独拿出来,拼进你自己的项目

四层结构

┌────────────────────────────────────────────────────────────┐
│  pi-coding-agent                                           │
│  交互式 CLI:四种运行模式、会话树、compaction、              │
│  扩展/技能/提示模板/主题、项目信任、包管理                  │
├────────────────────────────────────────────────────────────┤
│  pi-tui                                                    │
│  终端 UI:保留模式组件树 + 差分渲染 + 同步输出(防闪烁)     │
├────────────────────────────────────────────────────────────┤
│  pi-agent-core                                             │
│  agent 运行时:agent loop、工具执行、事件流、               │
│  steering/follow-up 队列、工具钩子                          │
├────────────────────────────────────────────────────────────┤
│  pi-ai                                                     │
│  统一 LLM API:4 种 wire 协议、30+ provider、流式事件、      │
│  工具调用(TypeBox 校验)、thinking、token/成本统计          │
└────────────────────────────────────────────────────────────┘
一句话职责你可以拿它来
LLM 层pi-ai把各家 provider 的 API 统一成一套流式接口做任何需要调 LLM 的程序
运行时层pi-agent-core驱动"LLM → 工具 → LLM"的循环,对外发事件做自定义 agent(不绑终端)
UI 层pi-tui在终端里高效渲染聊天式界面做自己的 TUI 应用
产品层pi-coding-agent组装以上三者,提供完整 coding agent 产品直接安装使用 / 用其 SDK 嵌入

分层的意义在于:每一层对上层只有一个小接口,上层可以随时被替换。不想用终端 UI?拿 pi-agent-core 做个 Web 界面。不想写 loop?直接用 pi-coding-agent 的 SDK,几行代码得到一个完整 agent。实战篇最后一章会完整走一遍这条"用积木拼 agent"的路线。

一次 prompt 的完整旅程

架构图是静态的,更有用的是跟着一次用户输入走完整个系统。下面这条链路基于 packages/agent/src/agent-loop.ts 的真实实现:

用户在编辑器输入 "帮我看看 src 下有哪些文件" 并回车


Agent.prompt(text)                                [pi-agent-core]
  │  构造 user 消息,启动 agent loop

emit agent_start → turn_start
emit message_start/message_end (user 消息落盘到会话 JSONL)


streamAssistantResponse()                         [pi-agent-core → pi-ai]
  │  1. transformContext(messages)   ← 钩子:裁剪/注入上下文(可选)
  │  2. convertToLlm(messages)       ← AgentMessage[] → LLM Message[]
  │  3. stream(model, context)       ← pi-ai 按 provider 发 HTTP/SSE 请求

流式返回:emit message_update(text_delta ...) ──→ UI 实时渲染打字机效果


emit message_end (assistant 消息完成)

  ├─ 消息里没有 toolCall ─────────────────┐
  │                                       │
  ▼                                       ▼
有 toolCall(比如 bash: ls src/)      emit turn_end
  │                                       │
  ▼                                       ▼
executeToolCalls():                    检查 follow-up 队列
  for 每个 toolCall:                     ├─ 有 → 注入,再跑一轮
    参数校验(TypeBox)                     └─ 无 → emit agent_end,结束
    beforeToolCall 钩子(可拦截)              │
    tool.execute()                     控制权交还用户,等待下一条输入
    afterToolCall 钩子(可改写结果)
  emit tool_execution_start/update/end
  toolResult 消息进入上下文


emit turn_end → 开始下一个 turn(再次调用 LLM)

几个要点:

  • 一个 turn = 一次 LLM 调用 + 由它触发的所有工具执行。loop 转到模型不再发起 toolCall 为止——pi 没有 max steps 之类的旋钮,作者的理由是"我从没遇到过需要它的场景"。
  • 一切都是事件。loop 不直接碰 UI,它只发事件(agent_startturn_startmessage_updatetool_execution_end……)。TUI、JSON 模式、RPC 模式都只是事件的不同消费者。这就是为什么 pi 能一个核心支持四种运行模式。
  • 上下文在进入 LLM 前才转换。loop 内部始终使用 AgentMessage[](可以包含 UI 专属消息类型),只有在调 LLM 的边界上才通过 convertToLlm 转成 provider 需要的格式。上下文工程的全部控制权因此都在你手里。

coding-agent 内部:产品层如何组装

packages/coding-agent/src 大致分三块:

src/
├── main.ts / cli.ts / cli/*        # 入口:解析参数、项目信任、启动对应模式
├── core/                           # 与 UI 无关的核心
│   ├── agent-session.ts            # 总装:把 model、tools、session、扩展组装成会话
│   ├── model-runtime.ts            # 模型与 provider 的运行时(认证、模型目录)
│   ├── tools/                      # 内置工具:read/bash/edit/write/grep/find/ls
│   ├── session-manager.ts          # 会话持久化:JSONL 文件 + id/parentId 树
│   ├── compaction/                 # 上下文压缩
│   ├── extensions/                 # 扩展加载与运行(自定义工具/命令/事件钩子/UI)
│   ├── skills.ts / prompt-templates.ts / themes ...
│   └── sdk.ts                      # 对外暴露的编程接口(createAgentSession)
└── modes/                          # 四种运行模式 = 事件流的四种消费者
    ├── interactive/                # 交互模式(pi-tui 渲染,components/* 一堆组件)
    ├── print-mode.ts               # 打印模式(-p,输出结果即退出)
    └── rpc/                        # RPC 模式(stdin/stdout JSONL,供其他进程集成)

组装顺序(简化):main.ts 解析 CLI → 创建 ModelRuntime(选定 provider/模型)→ createAgentSession(...) 把系统提示词、内置工具、会话管理器、扩展、技能全部装进一个 AgentSession → 交给四种模式之一去消费事件流。

为什么这样设计

作者在构建手记里给出的理由非常一致:控制上下文,观测一切

  • 事件驱动 + 会话 JSONL 落盘,意味着 agent 的每一次呼吸都可以被检查、被后处理;
  • 分层 + 小接口,意味着任何一层不合你意都可以换掉,而不需要 fork 整个项目;
  • 核心只保留"loop + 四个工具",其余全部交给扩展——这正是下一篇设计思想要展开的主题。

接下来:极简主义:pi 的核心哲学 →