PiAgent 01|从环境配置到第一个 Agent#
先跑通最小调用,再沿着
ModelRuntime → AgentSession → Agent Loop建立源码阅读地图
Pi 最适合用来学习的地方,不是它能在终端里回答问题,而是它把一个可工作的 Coding Agent 拆成了几条可以追踪的链路:模型如何被选中,会话如何装配,消息如何进入循环,工具如何产生副作用,事件又如何被外部消费。
这篇文章不把官方 API 重新抄成字典,而是先完成一次最小启动,然后解释启动过程里每个对象的职责。后面的文章都会回到这张地图:当我们修改模型、系统提示词、工具或 Web 接口时,究竟是在修改哪一层。
1. 先建立三个观察角度#
Pi 同时具有三个身份:
- 终端产品:默认提供读文件、执行命令、编辑文件等编码能力,并通过 TUI 呈现运行过程。
- Agent 教材:核心循环、工具协议、事件和会话不是一个不可拆的黑盒,能够从类型和生命周期继续往下追。
- 可嵌入 SDK:可以只使用
pi-ai的模型接口,也可以使用pi-agent-core的循环,或者直接使用pi-coding-agent装配完整的AgentSession。
这三个身份对应三种不同的依赖关系。TUI 需要会话和事件;SDK 需要稳定的运行时边界;源码阅读则需要区分“谁拥有状态”和“谁只负责转换”。如果把所有对象都看成一个叫 Agent 的类,后面遇到自定义工具、会话持久化或 Web 流式输出时就很容易改错层。
2. 一次任务的完整路径#
把“分析当前项目”拆开,运行时大致沿着下面的路径前进:
用户 prompt
→ AgentSession 组合当前上下文
→ Agent Loop 请求模型
→ 模型返回文本增量或 tool call
→ 工具校验参数并执行副作用
→ ToolResultMessage 回到 transcript
→ Loop 根据 stop reason 决定继续还是结束
→ session 事件被 UI、扩展、日志和持久化层消费这里有一个重要的区分:模型只提出下一步,宿主程序决定这一步能否执行。模型返回一个 bash 调用,并不意味着命令已经运行;工具层还要做 schema 校验、权限检查、执行、错误包装和结果回写。
3. 最小运行环境:先确认硬约束#
当前官方 Pi Coding Agent 文档要求 Node.js 22.19.0 或更高版本。这个约束来自包的 engines.node,不是示例作者随意选择的版本。版本太低时,问题会在依赖安装、ESM 或运行时 API 处暴露,应该先处理环境而不是继续排查 Agent 逻辑。
node --version
npm install @earendil-works/pi-coding-agent
npm install -D tsxPi 的配置默认位于 ~/.pi/agent/。受限服务器或容器可以通过 PI_CODING_AGENT_DIR 指向项目内目录,也可以在 SDK 调用中显式传入 agentDir。两种方式改变的是配置来源,不改变 Agent Loop 的语义。
一个最小的 models.json 可以使用 OpenAI 兼容协议:
{
"providers": {
"internal": {
"baseUrl": "https://api.example.com/v1",
"api": "openai-completions",
"apiKey": "$INTERNAL_API_KEY",
"models": [
{ "id": "model-id", "name": "Internal Model" }
]
}
}
}生产环境不要把真实 Key 写进版本库。配置值支持从环境变量或命令解析,认证信息也可以独立放在 auth.json。这里的重点不是记住 JSON 字段,而是知道:模型能不能被选中,首先取决于 Provider 定义与鉴权是否被运行时加载。
4. 12 行代码背后的装配过程#
下面是最小的进程内 SDK 示例。它只做四件事:加载可用模型、创建会话、订阅文本事件、发送 prompt。
import {
createAgentSession,
ModelRuntime,
} from "@earendil-works/pi-coding-agent";
const modelRuntime = await ModelRuntime.create();
const available = await modelRuntime.getAvailable();
const model = available[0];
if (!model) {
throw new Error("没有可用模型,请检查 models.json 与 auth.json");
}
const { session } = await createAgentSession({ model, modelRuntime });
try {
const off = session.subscribe((event) => {
if (
event.type === "message_update" &&
event.assistantMessageEvent.type === "text_delta"
) {
process.stdout.write(event.assistantMessageEvent.delta);
}
});
await session.prompt("用一句话介绍你自己。");
off();
} finally {
session.dispose();
}4.1 ModelRuntime.create() 做了什么#
ModelRuntime 可以理解为模型与认证的配置仓库。创建它时,运行时会读取认证与模型配置,并把用户配置和内置 Provider 定义合并到内存中。它本身不是一次模型请求;默认创建过程主要是本地配置加载。
getAvailable() 再根据 Provider 是否有有效鉴权,过滤出当前真正能用的模型。因此,models.json 中“声明了一个模型”和 getAvailable() 返回这个模型是两件事。排查“模型列表为空”时,应按顺序检查配置目录、JSON 结构、Key 来源和 Provider 名称。
4.2 createAgentSession() 是装配边界#
创建会话时,Pi 会把多项基础设施接起来:
ModelRuntime → 模型、Provider、认证
SettingsManager → 默认模型、重试、压缩等设置
SessionManager → JSONL 会话树或内存会话
ResourceLoader → 系统提示词、skills、extensions、上下文文件
Agent → 循环、消息、工具和生命周期如果没有显式传入这些对象,Coding Agent 会使用默认实现。默认实现适合本地单用户 CLI;当应用变成多用户 Web 服务时,通常需要替换 SessionManager,并重新设计 ResourceLoader 的资源边界。后面文章会分别解释这些替换点。
4.3 subscribe() 不负责启动运行#
session.subscribe() 只是登记一个观察者,返回取消订阅函数。真正触发 Agent 运行的是 session.prompt()。这两个调用的时序非常重要:先订阅,再 prompt,才能观察到完整的 agent_start、文本增量、工具事件和结束事件。
message_update 还会嵌套一层 assistantMessageEvent。文本输出取 text_delta 的 delta;思考输出(模型支持且开启时)取 thinking_delta 的 delta。事件协议把“正在生成什么”和“最终生成了什么”分开,UI 可以据此做增量渲染,而不是等待最终消息。
4.4 prompt() 与 dispose() 的生命周期#
prompt() 返回一个 Promise,Promise resolve 表示这次调用的 Agent 运行已经收束,而不是表示只发生了一次模型请求。若任务需要工具,内部可能经过多轮模型响应和工具结果。
dispose() 应放在 finally 中。它负责中止后台任务、清理订阅和释放会话级资源。只在正常路径调用清理函数,会让异常路径留下监听器、重试任务或未结束的工具执行。
5. 从源码阅读的正确顺序#
不要从 TUI 开始读。建议按照数据和控制流阅读:
packages/coding-agent/src/sdk.ts:看createAgentSession如何装配运行时。packages/coding-agent/src/agent-session.ts:看 Session 如何包住 Agent、会话和扩展。packages/agent/src/agent-loop.ts:看一次 prompt 如何进入多轮循环。packages/ai/src/types.ts:看模型消息、流式事件和 Provider 类型。packages/coding-agent/src/core/tools/:看内置工具如何进入执行管道。packages/tui/:最后再看终端如何消费事件和渲染状态。
这种顺序背后的原则是:先确定状态和控制流,再看表现层。如果先看 UI,很容易把“一个状态被显示出来”误认为“这个状态由 UI 创建”。
6. 几个必须记住的边界#
| 现象 | 真正负责的层 | 排查方向 |
|---|---|---|
| 没有可用模型 | ModelRuntime | 配置目录、Provider、认证与模型 ID |
| prompt 不产生工具调用 | 系统提示词、工具集合、模型能力 | 工具是否激活、schema 是否可理解 |
| 文本不能逐字显示 | 事件投影层 | 是否订阅 message_update/text_delta |
| 任务结束后进程不退出 | Session/工具资源 | 是否调用 dispose(),是否还有后台任务 |
| Web 用户之间串上下文 | SessionManager/服务装配 | 是否为用户隔离 Session 与持久化路径 |
7. 小结#
Pi 的学习入口不是某个神奇的 run() 函数,而是一个清晰的装配链:配置先进入 ModelRuntime,基础设施被 createAgentSession() 组合,Agent 负责循环,session 提供 prompt、事件和资源生命周期。
后续每篇文章只回答一个更窄的问题:包如何分层、循环何时继续、模型差异在哪里被消化、工具如何安全执行、消息如何转换、事件如何投影,以及会话如何恢复。掌握这条主线,比记住几十个 API 名称更能帮助你驾驭 AI 生成的代码。
源码定位(以官方仓库当前结构为准)
- SDK 装配:
packages/coding-agent/src/sdk.ts - 会话生命周期:
packages/coding-agent/src/agent-session.ts - Agent 类型与循环:
packages/agent/src/agent.ts、packages/agent/src/agent-loop.ts - 模型和流式事件:
packages/ai/src/types.ts - 配置与 Provider:
packages/coding-agent/src/core/config.ts、packages/coding-agent/src/core/model-runtime.ts