Pi Harness 学习路径从一次调用到稳健 Agent
0 / 7 完成
PI AGENT HARNESS · BEGINNER PATH

从一次模型调用,
搭出一个可靠的 Harness

一条可以边读、边运行、边检查的路线:先认识 Pi 的包边界,再做 最小模型调用 → 最小 Agent 循环 → 带工具的 harness → 可恢复、可审计的实现。所有 API 示例以 Pi 当前官方文档和源码为准。

◇ 零基础友好:先命令行,再 TypeScript◇ 7 个学习节点◇ 示例不需要把密钥写进代码
LESSON 00 · 准备工作

先让 Pi 在你的电脑上启动

学习过程会从运行现成的 CLI 开始。完成这一步后,你可以直接检查项目行为,再往下拆解底层包。

  1. 安装 Node.js 22.19 或更新版本;先用 node -v 确认当前版本。
  2. 按官方 README 的 npm 安装方式安装 CLI(忽略依赖生命周期脚本)。
  3. 进入一个专门的练习目录启动 pi;在 Pi 中用 /login 配置一个受支持的模型提供方。
  4. 完成登录后,先让 Pi 回答一个简单问题;再退出并确认 pi --version 正常。
安装与启动
node -v
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
pi --version
mkdir pi-lab && cd pi-lab
pi
!

先划清安全边界:Pi 的内置工具和扩展以 Pi 进程本身的操作系统权限运行。项目 trust 决定是否加载部分项目资源,不会把工具调用变成沙箱。练习请用专用目录和非敏感文件;不可信代码或自动化任务应在容器等隔离边界中运行。

练习 · 建立“可撤销”的实验区

创建一个临时目录,放入一份可丢弃的 notes.txt。让 Pi 只总结这份文件;查看会话里实际发生的读文件动作。检查点:你能说明 Pi 进程在这个目录之外是否仍有权限吗?

工作目录帮助 Pi 定位默认资源、会话和内置工具的默认路径,但不会限制进程访问其他有权限的路径。若要限制影响范围,应通过 OS 用户权限、容器或其他真正的隔离方式实现;trust 和“看见工具轨迹”都不是安全边界。
下一课:认包 →
LESSON 01 · 项目全景

先看清:你究竟在组合哪几层?

把 harness 想成“让模型完成任务的运行系统”:它不仅发请求,还要管理消息、工具、运行状态和与使用者的交互。Pi 把这些职责拆成可复用的包。

Pi 核心包关系图Coding agent 与应用层使用 agent core;Agent core 调用 pi-ai 访问模型并执行工具;TUI 展示界面,durable、telemetry 与 chord 是可选的周边能力。CLI / SDKpi-coding-agentRUNTIMEpi-agent-coreMODEL I/Opi-ai终端界面pi-tui可持久化运行时pi-durable显式遥测契约pi-telemetry应用可选择组合Chord:独立的插件 / 服务组合运行时(非核心依赖)
实线表示常见核心调用关系;下方包是按需要选用的周边能力,不是做最小 harness 的必装依赖。
01 · 模型边界

@earendil-works/pi-ai

统一多提供方模型 API:模型目录、认证解析、流式与非流式调用、工具声明以及 token / 成本信息。它负责“怎样和模型说话”。

02 · Agent 循环

@earendil-works/pi-agent-core

基于 pi-ai 的有状态 runtime:维护消息与运行状态、发起请求、处理工具调用、继续循环并发出事件。它负责“怎样把请求变成一轮任务”。

03 · 可直接使用的产品层

@earendil-works/pi-coding-agent

交互式编码代理 CLI 与 TypeScript SDK:在核心 runtime 上提供会话、资源发现、设置、工具和多种运行入口。它负责“给人或应用一个完整的 Pi”。

包 / 能力职责与适用时机起步判断
pi-tui

差分渲染、终端组件、输入焦点和布局;用于构建自己的终端界面。

先不用
pi-durable

耐久化 conversation / task / document runtime;提供 memory、JSONL、SQLite 存储入口,是独立的持久化能力。

需要可恢复数据时研究
pi-telemetry

厂商中立的显式 telemetry contract 与 schema;带参考内存 adapter,不自带 exporter 或某个后端。

先定义观测点,再接 adapter
chord

面向插件组合的独立 runtime:facets、typed services、replicated state、远程服务边界。它不依赖 monorepo 里的其他 Pi workspace package。

多环境插件系统才需要
↗

依赖方向口诀:从底向上是“模型 API → Agent runtime → 面向人的 CLI / 面向应用的 SDK”。不要为了跑第一个模型请求把 TUI、Chord、telemetry 或持久化一起塞进来。

练习 · 选对抽象

给下面三个目标各选一个主要入口:① 只想问一次模型;② 在自己的 Node 服务中加一个有状态工具循环;③ 直接把 Pi 的会话与配置能力嵌进 TypeScript 应用。

① pi-ai;② pi-agent-core 的 Agent runtime;③ pi-coding-agent 的 SDK。若只是让操作系统启动另一个 Pi 进程并拿最终文本,CLI print mode 常常更简单;若要双向控制长驻进程,才考虑 RPC。
LESSON 02 · 直接调用模型

让 pi-ai 做一件最简单的事

这一课先不做 Agent,也不执行工具。我们只注册一个 provider、找到模型、发一条 user message、读取完整回复。用这一层可以分辨:问题来自模型接入,还是之后的工具循环。

安装与最小例子

minimal-ai.ts
import { createModels } from "@earendil-works/pi-ai";
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";

const models = createModels();
models.setProvider(openaiProvider());

const model = models.getModel("openai", "gpt-4o-mini");
if (!model) throw new Error("Model not found in this provider catalog");

const response = await models.complete(model, {
  messages: [{
    role: "user",
    content: "用一句话解释什么是 agent harness。",
    timestamp: Date.now(),
  }],
});

for (const block of response.content) {
  if (block.type === "text") console.log(block.text);
}
console.log("tokens:", response.usage.input, response.usage.output);

在项目目录执行:npm init -y,再 npm install @earendil-works/pi-ai;把 package.json 设为 ESM("type": "module"),然后用支持 TypeScript 的运行器或编译器运行示例。需要有效的提供方认证,依照官方 provider authentication 文档配置;不要把 API key 写进源码或网页。

读懂数据形状

Models 集合负责根据 provider + model 标识路由。这里使用完整 catalog 以便快速开始;产品只需一个 provider 时,可像示例一样导入 provider 子路径,减少不必要的模型目录和 SDK 包体。complete() 返回最终 AssistantMessage;若需要逐字增量和工具调用事件,再选择 stream() 并遍历事件。

练习 · 对照流式和非流式

先让这段 complete() 代码成功运行。然后把它改成官方文档中的 models.stream(model, context):监听 text_delta 输出增量,最后用 await stream.result() 拿到完整消息。观察二者最终文本是否一致。

流式输出是多个事件;最终完整消息来自 stream 的 result。不要把 JSONL 事件当作一个整段 JSON,也别只从 delta 拼出长期保存的消息而忽略最终 message。输出类型可能包含 text 与 toolCall;只有注册工具后才进入工具执行阶段。
!

模型 ID 和 provider catalog 会更新;若查不到模型,应从该 provider 当前目录查询,不要把示例 ID 当永久契约。Pi 的官方 README 强调可选择 provider 子路径以控制 bundle 大小。

LESSON 03 · 最小 Harness

用 Agent 接过消息和循环

核心差别不是“模型变聪明了”,而是应用开始持有可变状态,并定义模型调用、事件观察和工具执行怎样接起来。Pi 的 Agent 是有状态 wrapper:拥有当前 transcript、发出 lifecycle 事件并执行工具。

minimal-agent.ts
import { Agent } from "@earendil-works/pi-agent-core";
import { createModels } from "@earendil-works/pi-ai";
import { openaiProvider } from "@earendil-works/pi-ai/providers/openai";

const models = createModels();
models.setProvider(openaiProvider());
const model = models.getModel("openai", "gpt-4o-mini");
if (!model) throw new Error("Model not found");

const agent = new Agent({
  initialState: {
    systemPrompt: "你是一个简洁、准确的学习助手。",
    model,
  },
  streamFn: models.streamSimple.bind(models),
});

agent.subscribe((event) => {
  if (event.type === "message_update" &&
      event.assistantMessageEvent.type === "text_delta") {
    process.stdout.write(event.assistantMessageEvent.delta);
  }
});

await agent.prompt("用三点解释:模型 API 和 harness 有什么不同?");
console.log("\\n消息条数:", agent.state.messages.length);

这段与官方 agent-core Quick Start 使用同一结构:创建 provider / model,构造 Agent,把 models.streamSimple.bind(models) 传作 streamFn,订阅文本增量,然后 prompt()。绑定方法很重要:让调用时的 this 仍然是 Models 集合。

Harness 的最小组成

组成最小做法本阶段要问的问题
推理后端

model + streamFn

从哪个 provider / 模型来?凭证如何注入?
状态

agent.state.messages

消息如何保留、裁剪或恢复?
事件通道

agent.subscribe()

UI 怎样展示文字、工具和结束状态?
执行策略

本课暂不启用工具

模型请求之外,哪些操作有副作用?
练习 · 验证事件而不是猜事件

打印事件类型;核对 agent_start、message_update、message_end、turn_end 与 agent_end 是否出现。随后把订阅函数写成 async 并延迟一小段时间,观察 Agent 等待订阅者完成这一点。

官方 README 说明 Agent.subscribe 的监听器按注册顺序执行并会被等待;run 完结时包含 awaited agent_end listeners 的结算。不要把 raw agentLoop 的异步事件处理也假设为同样的屏障:README 特别指出其观测性 event stream 不会等待你的 async handler 结束。
✓

到这里你有了最小 harness:它已管理一个运行态会话并公开事件。它还不是生产系统:目前无用户自定义工具、无应用持久层、无策略边界、无超时与评测。

LESSON 04 · 工具、状态与权限

工具调用:模型提议,宿主程序执行

Agentic 循环会把工具声明发给模型。模型返回结构化的 tool call 后,Pi runtime 验证参数、运行本地 execute、把 tool result 写回 transcript,再决定是否继续模型请求。记住,真正拥有能力的是执行工具的宿主进程。

一个无外部副作用的工具形状
import type { AgentTool } from "@earendil-works/pi-agent-core";
import { Type } from "@earendil-works/pi-ai";

const addTool: AgentTool = {
  name: "add_numbers",
  label: "Add numbers",
  description: "Add two finite numbers and return the sum.",
  parameters: Type.Object({
    a: Type.Number({ description: "First number" }),
    b: Type.Number({ description: "Second number" }),
  }),
  execute: async (_toolCallId, params) => {
    const sum = params.a + params.b;
    return {
      content: [{ type: "text", text: String(sum) }],
      details: { sum },
    };
  },
};

agent.state.tools = [addTool];

AgentTool 的 schema 描述要传给模型的参数;execute 是本地业务逻辑,返回 text content(也可有 details 给 host 使用)。Pi 官方示例还支持 onUpdate 发进度、AbortSignal 中断以及每工具的执行顺序配置。失败应 throw error,让 runtime 把工具错误交给模型,不要伪装成成功文本。

先做可控的前置拦截

用 beforeToolCall 阻止未开放的工具
const agent = new Agent({
  initialState: { systemPrompt, model, tools: [addTool] },
  streamFn: models.streamSimple.bind(models),
  beforeToolCall: async ({ toolCall, args, context }) => {
    if (toolCall.name === "delete_file") {
      return {
        block: true,
        reason: "delete_file is disabled in this lesson",
        terminate: true,
      };
    }
  },
});

这只是运行时策略的一个挂点。实际应用还应由 host 检查经 schema 验证后的参数、用户身份与授权、路径归属、额度和副作用级别;对写文件、发邮件、交易等高影响动作,增加合适的确认或隔离流程。不能仅凭模型自述、提示词或者项目 trust 决定是否允许。

!

项目 trust ≠ 权限沙箱。Pi 官方安全文档明确说:工具和扩展按启动 Pi 的 OS 用户权限工作;trust 决定一部分项目资源是否加载,但不限制工具能访问什么。

练习 · 逐渐扩大工具能力

先接上 add_numbers 并用明确提示要求它计算 17 + 25。检查工具参数、工具结果、随后模型答复。再写一个“读取白名单内文件”的工具:先做路径规范化,再确保目标仍位于允许目录内;不通过检查就 throw error。初次练习不要实现任意 shell 工具。

① event 中出现有 schema 对应参数的 tool call;② execute 确实跑在你的 Node 进程;③ 结果有清楚的 content 并进入对话;④ 越权路径被 host 拦截,而不只是提示模型不要访问;⑤ 重复/失败/中断时状态没有伪造成功。
LESSON 05 · 使用完整的 Pi

何时用 CLI,何时嵌 SDK?

如果你想使用 Pi 的现成会话、工具、设置和资源发现,不一定要自己重新拼装 agent-core。pi-coding-agent 提供交互 CLI,以及 Node.js / Bun 中的 TypeScript SDK。先根据进程边界和控制需求选入口。

入口数据形态 / 生命周期适合的任务
Interactive

终端 UI;持续到用户退出

人直接与 Pi 协作
Print

最终文本到 stdout;一次运行

脚本只要最终回答
JSON

JSONL 会话 / agent 事件;一次运行

程序要消费结构化进度
RPC

JSONL 命令与响应;进程长驻,可双向控制

应用跨进程控制 Pi
TypeScript SDK

Pi 直接嵌入 Node.js / Bun 进程

需要进程内访问 AgentSession 与 Pi 能力
CLI 入口
# 人交互
pi

# 一次调用,只取最终文本
pi --print "总结这个仓库的目录结构"

# 一次调用,订阅 JSONL 事件
pi --mode json "检查测试失败的原因" > events.jsonl

# 长驻 RPC 进程;命令通过 stdin 每行发送 JSON
pi --mode rpc --no-session

注意:JSON mode 输出的是一串换行分隔的事件,不是单个 JSON 响应;RPC 的 prompt 命令被接受,不代表 run 已经结束,完成时应持续读事件直至 agent_settled。执行行为和权限仍沿用运行 Pi 的进程,模式不等于沙箱。

最小 TypeScript SDK

sdk-minimal.ts
import { createAgentSession } from "@earendil-works/pi-coding-agent";

const { session } = await createAgentSession();
try {
  await session.prompt("当前目录里有哪些文件?");
  console.log(session.getLastAssistantText());
} finally {
  session.dispose();
}

此工厂默认使用当前工作目录、已发现资源、存储设置和配置凭证;会话默认持久化。若宿主明确不想写 session 文件,官方 SDK 提供 SessionManager.inMemory()。不再用时调用 dispose(),它会中止活动工作并清理 extension context 和事件监听器。

练习 · 用最低成本满足需求

分别为三个需求选模式并写出理由:A. CI 里只要最终摘要;B. 桌面界面要实时显示 token / tool 活动,但运行完退出;C. 自己写的 Node 应用要在同一进程内订阅和控制 Pi。

A 用 --print;B 用 --mode json,消费 JSONL 事件;C 用 TypeScript SDK。如果 B 还要求后续任意时刻继续发送命令,则改为长期 RPC 进程。不同模式复用核心 agent / sessions / resources / tools,区别在 I/O 和进程生命周期。
LESSON 06 · 从演示走向应用

把“能跑”变成“能恢复、能检查”

健壮性不是一层万能包装,而是明确管理失败、状态、安全与可观察性。下面按实施顺序增量加固,不必一次上齐所有 package。

阶段 A边界清楚输入、工具和身份由 host 管
阶段 B取消可控超时、abort 与 idle 收敛
阶段 C状态可恢复定好 transcript 与存储所有者
阶段 D效果可验证事件、回归集与成本可观测

建议的加固顺序

  1. 输入与输出契约:校验用户输入和工具参数;对外返回可预期的状态,而不是只看 Assistant 的最终文字。
  2. 取消与超时:从请求路径传播 AbortSignal;需要中断时调用 agent.abort(),需要等待清理完成时再 waitForIdle()。
  3. 工具幂等:写入型工具考虑幂等键、去重、明确超时与副作用回滚边界;返回错误要保持错误态。
  4. 会话与上下文:先说清谁是 transcript 的权威来源。pi-agent-core 管当前 Agent state;coding-agent SDK 的 SessionManager 管持久 session 与 active branch;pi-durable 为独立的持久化 runtime / storage 能力,不要未经设计把它们混为一个存储契约。
  5. 事件与日志:分别记录状态转换、工具输入输出摘要、provider 请求和错误;敏感文本 / token 不应原样写到遥测。pi-telemetry 是显式 contract,要由应用接到选用的后端。
  6. 模型与成本:设置预算、最大步骤、重试上限及可解释的失败策略。不要把无限重试当恢复机制;对可重试的 provider 错误和不可重试的工具 / 权限错误分别处理。
  7. 安全隔离:将 Pi 或自有工具部署在最小权限用户 / 容器等隔离边界;生产凭证只注入运行环境,并尽量限制权限范围。
  8. 回归测试:为工具 schema、正常路径、空结果、拒绝权限、异常、取消、重复调用和上下文变长建立测试;固定期望行为,而不是只比较生成文案。
i

Pi 里可直接查证的控制点:Agent API 当前有 state、subscribe()、prompt()、continue()、abort()、waitForIdle(),并有 beforeToolCall / afterToolCall、prepareRequest、transformContext 等 hooks。先从官方类型声明和对应 README 验证版本,再为具体业务设计外围策略。

练习 · 写一张故障演练卡

对你的示例逐条模拟:① provider 超时;② JSON/tool 参数无效;③ 工具抛异常;④ 用户取消;⑤ 存储失败;⑥ 重试后结果重复。每种情况写“对调用者的结果、transcript / 持久状态、是否重试、如何告警”。至少把 ③、④、⑥ 做成自动测试。

问自己:失败是不是被伪装成正常文本?abort 后是否等待清理?同一工具调用重放是否会制造双重副作用?错误和重试有没有安全上限?持久化提交边界能否说明?遥测是否意外带出 API key 或用户内容?回答不出来的地方就是下一轮实现任务。
CAPSTONE · 结课项目

做一个只读“仓库讲解助手”

目标不是复制整个 coding-agent,而是做出一个权限有边界、能观测执行过程、出现故障时能说明白的最小产品原型。

  1. 基线:先完成一条 pi-ai 调用,保证 provider 和环境凭证可用。
  2. 升级:用 pi-agent-core 构造 Agent,只加入两个只读工具:列出预设目录下的文件、读取目录内的一个文本文件。
  3. 限制:工具实现中校验解析后的路径始终落在练习仓库目录;不开放任意命令或文件写入。
  4. 界面:先用 Node CLI 输出 Agent 文本和工具事件;需要直接使用 Pi 的完整会话能力时,再对照 SDK 重新实现。
  5. 验证:增加成功、文件不存在、目录穿越尝试、工具异常、取消等测试案例,写出你期望的退出码 / 错误呈现。
  6. 复盘:画出模型调用、工具执行、事件、会话状态、策略检查之间的边界;标注此实现还缺哪些生产保障。
★

达成标准:不是“模型答得很好”,而是你能说明任务在何处读取状态、工具如何被允许执行、失败怎样结束、下次怎样恢复,以及什么证据能证明行为符合预期。

官方资料 · API 变动时以此为准

Pi monorepo 与完整包目录项目定位、所有 package 职责与权限说明
打开仓库 ↗
Coding Agent 文档索引CLI、自动化入口、安全与工作方式
官方索引 ↗
Coding Agent README / QuickstartNode 版本、npm 安装与首次运行
README ↗
pi-ai READMEprovider、model、stream / complete、工具声明
API 文档 ↗
pi-agent-core READMEAgent state、循环、事件、hooks 和 tool shape
Runtime 文档 ↗
Coding Agent CLI Integration / SDKPrint、JSON、RPC 与 Node.js / Bun SDK
入口比较 ↗
Run Pi safely项目 trust、工具权限与隔离边界
安全指南 ↗

核验时间:2026-09-29,基于上述仓库 main 分支资料。Pi 会持续发布,示例适用于该次核验时的 API 形状,不是固定版本承诺;升级依赖后先对照 Quickstart、README 与 TypeScript declarations 更新用例。

回到课程顶部 ↑