00 Pi 是一套“终端操作系统”,不是更聪明的对话框
定义:Agent Harness(持久化与并发) + pi-ai(多 Provider 抽象) + 自扩展(30+ 事件与工具) + 自研 TUI(差分渲染)。判断标准:能否在终端内完成“长程、有状态、可回溯、可扩展”的工程任务。
问题 vs 方案 为什么需要 Harness
| 维度 | Pi 的选择 | 常见 Agent 的局限 | 后果 |
|---|---|---|---|
| 循环 | 三级:Loop(纯函数)/ Agent(状态+队列)/ Harness(持久化 Lane) | 单体 loop,一处失败全丢 | 长任务不可恢复 |
| 会话 | DAG:Entry{id,parentId} 可 fork/navigate | 线性历史,仅追加 | 分支探索成本高 |
| 扩展 | TS 模块,30+ 声明式事件 | 仅 MCP/函数调用 | 团队能力难沉淀 |
| TUI | 差分渲染 + 原生集成 | Ink/Blessed 封装 | 大输出与交互受限 |
分层目标(同场三档) 按需取用
- 会用:跑通
pi --print最小闭环,理解消息与工具流 - 会选型:为自建网关选 Api/compat,判断何时用 Harness
- 会扩展:写一个
tool_call审计拦截 + 一个自定义 Tool
兜底 混合兜底:有 ANTHROPIC_API_KEY 等走真实模型,无 Key 自动走 faux + 本地 mock,全程可动手。
讲义:11 个 Package 的精确职责与源码位置
agent/src/agent.ts|agent-loop.ts— 通用运行时;harness/— 持久化 Lane/Drive/Storageai/src/types.ts— 统一 Model/Context/Message;providers/*, api/*— 30+ Provider, 10 Apicoding-agent/src/core/agent-session.ts— 全模式共享编排;session-manager.ts— JSONL DAGtui/src/tui.ts, layout.ts— 差分渲染;chord/src— Facet/RPC;protocol/src/cbor, framing.ts— CBORtelemetry/src— Span Schema;session-backends/sqlite-node— SQLite Storage
01 五层单向依赖 · 核心数据结构
不变量:上层可依赖下层,下层不依赖上层(唯一例外:agent 通过接口依赖 telemetry 产出 Span)。违反则循环依赖,测试与发布成本陡增。
Message 模型 packages/ai/src/types.ts:422
type Message = UserMessage | AssistantMessage | ToolResultMessage;
// AgentMessage 支持通过声明合并扩展自定义类型:
type AgentMessage = Message | CustomAgentMessages[keyof CustomAgentMessages];
// coding-agent 的 CustomMessage 即由此注入
type Context = { systemPrompt?: string; messages: Message[]; tools?: Tool[] };
设计意图:低层循环无需理解 CustomMessage,convertToLlm 在 LLM 边界过滤。
Session Entry DAG 可 fork 的关键
Entry { id: uuidv7, parentId, type: message|custom|compaction|branchSummary }
branchTip(name) -> tipId // 分支叶指针(values.ts)
scanBranch({ start: tipId, order: "newestFirst"|"oldestFirst" })
每条 Entry 追加写入,branchTip 移动即分支;fork(entryId) 复制历史前缀。
现成品 #2 — DAG 来自版本控制思想的拼贴
展开:为什么是 DAG 而非线性?(工程例)
问题:长程任务中“试了一种实现,想回退并保留摘要”。
线性:只能截断历史,丢失分支上下文。
DAG:navigateTree(targetId, {summarize:true}) 保留 BranchSummaryEntry,新分支含旧尝试的压缩摘要,可回溯决策。
02 Agent Loop:内层由 tool 驱动,外层由 followUp 驱动
循环不变量 agent-loop.ts:156
- 内层:只要有
toolCalls或steering,继续下一 turn。 - 外层:内层结束后再问
getFollowUpMessages(),有则继续。 prepareNextTurn前条件性再 poll steering,避免one-at-a-time在同一 turn 吞两条。stopReason=="length"时当批次工具调用视为“可能截断”,全部失败重试(保守但安全)。
并发契约 agent-loop.ts:409
| 模式 | prepare | execute | 结果顺序 |
|---|---|---|---|
| sequential | 串行 | 串行 | 源码序 |
| parallel | 串行(保证 beforeToolCall 确定性) | 并发 | tool_execution_end 按完成序,ToolResult 按源码序 |
单工具可覆为 executionMode:"sequential"(如写文件);terminate 需批次全体一致才终止。
Hands-on 1 · 最小闭环(faux,无需 Key)
import { Agent } from "@earendil-works/pi-agent-core";
import { faux } from "@earendil-works/pi-ai/providers/faux";
const model = { id:"faux", api:"openai-completions", provider:"faux", baseUrl:"",
reasoning:false, input:["text"], cost:{input:0,output:0,cacheRead:0,cacheWrite:0},
contextWindow:8192, maxTokens:1024 };
const agent = new Agent({ streamFn: faux.streamSimple, initialState:{ model, messages:[] }});
agent.subscribe(e => console.log(e.type)); // agent_start → turn_start → message_* → agent_end
await agent.prompt("hi");
await agent.waitForIdle();
Notebook §3 有可运行版(/tmp/h1.mjs + npx tsx);有 Key 者将 fauxModel 替换为真实 Model 对比延迟与 usage。
agent_end 且无异常;agent.state.messages 新增一条 assistant。讲义:Harness Drive 状态机(9 步)与文件位置
harness/runtime/drive/:boundary.ts → checkpoint.ts → generation.ts → tool-placement.ts → tools.ts → response.ts → reconcile.ts ↔ retry.ts/deferred.ts/recovery.ts。Notebook §3 用 Python 渲染状态流,可对照源码逐文件精读。
03 pi-ai:以 Api 而非 Provider 组织能力
问题:30+ 厂商 × 10 种协议 × 自建网关的“类 OpenAI/Anthropic 但有差异”。方案:按 KnownApi 组织实现,compat 20+ 开关做差异收敛;Model 由生成脚本产出,不手改。
分层与加载 tree-shake 友好
providers/anthropic.ts → api/anthropic-messages.ts
providers/openai.ts → api/openai-completions.ts
↘ api/openai-responses.ts
api/lazy.ts 按需加载,避免全量打包
index.ts 仅导出 core,无副作用
Model 关键字段 types.ts:845
Model<TApi> {
id, api: TApi, provider, baseUrl,
reasoning: boolean,
thinkingLevelMap?: Record<ThinkingLevel, string|null>,
input: ("text"|"image")[],
cost: { input, output, cacheRead, cacheWrite, tiers? },
contextWindow, maxTokens,
compat?: OpenAICompat | AnthropicCompat | BedrockCompat
}
兼容开关(接入网关的抓手) 节选
| 开关 | 含义 | 何时用 |
|---|---|---|
| supportsReasoningEffort | 是否支持 reasoning_effort | 自建 OpenAI 网关 |
| requiresThinkingAsText | 思考块转 <thinking> 文本 | 不原生支持 thinking 的网关 |
| thinkingFormat | 6 种:openai/openrouter/… | 路由层差异 |
| sessionAffinityFormat | 亲和头 openai/openrouter | 缓存路由 |
Hands-on 2 · 注册自建 Provider
export default function(pi){
pi.registerProvider("my-proxy", {
baseUrl:"https://proxy.example.com", // 或 http://localhost:8787
api:"anthropic-messages",
models:[{ id:"my-model", name:"My", reasoning:false, input:["text"],
cost:{input:0,output:0,cacheRead:0,cacheWrite:0}, contextWindow:200_000, maxTokens:8192 }]
});
}
// 落盘:~/.pi/agent/extensions/my-proxy/index.ts → 重启后 /model 可见
StreamFn 永不 throw;失败必须编码为 error 事件 + stopReason:"error"。04 会话即 DAG:JSONL 追加 + 分支 + 压缩
AgentSession 的精确职责 core/agent-session.ts:306
prompt():命令拦截 →inputhandlers → Skill/Template 展开 → 校验 model/auth →before_agent_start注入 CustomMessage →runAgentLoop_handlePostAgentRun:可重试错误 → 溢出压缩 → 队列续跑(有其一则continue())_handleAgentEvent:message_end双写AgentState.messages+SessionManagerJSONL;turn_end才 flush CustomMessage(避免夹在 tool_call/result 间)
压缩三类 reason
| reason | 触发 | 行为 |
|---|---|---|
| threshold | prepareNextTurn 前 shouldCompact() | 后台摘要,保留 cutPoint 后消息 |
| overflow | LLM 报 context_overflow | 强制压缩后重试被中断 turn |
| manual | 用户 /compact | 同 threshold,可被扩展 cancel |
Hands-on 3 · 观察与操作
pi --print "在 /tmp/pi-ws 创建 hello.txt"
ls -lh ~/.pi/agent/sessions/*.jsonl
head -1 ~/.pi/agent/sessions/*.jsonl | python3 -m json.tool
# fork / 导航(扩展命令或 SDK)
# pi --help | grep -E "fork|navigate"
Notebook §5:若本地有会话,Python 直接解析 JSONL 并还原为树;否则用模拟数据演示。
/tmp/pi-ws 看到文件;会话文件新增 3+ 行(含 header + user + assistant/toolResult)。05 扩展:30+ 事件 · 8 工具 · UI 覆盖
事件拼贴(精确列表) types.ts:1252
project_trust resources_discover session_start session_before_* context before_provider_request before_agent_start agent_start/end/settled turn_* message_* tool_execution_* tool_call tool_result model_select input user_bash
要点:tool_call 的 input 就地可变,多扩展链式改同一对象;block 任一即阻断;单扩展异常仅产 handler_error。
export default function(pi){
pi.on("tool_call", e=>{
if(e.toolName==="bash" && e.input.command.includes("rm -rf /"))
return { block:true, reason:"Refusing destructive command" };
});
pi.registerTool(defineTool({
name:"jira", description:"Fetch Jira",
parameters: Type.Object({ key: Type.String() }),
execute: async (id,{key})=>({ content:[{type:"text", text:key}], details:{} })
}));
}
8 内置工具(均可被覆写)
| 工具 | 输入 | 输出 details | 可覆 |
|---|---|---|---|
| read | path | content+truncated | 是 |
| bash / powershell | command | stdout/stderr/exitCode | 是(Gondolin 覆写) |
| edit / write | old/new/path | diff | 是 |
| grep / find / ls | pattern/path | matches/entries | 是 |
UI 覆盖能力
select/confirm/input/notify · setWidget/setFooter/setHeader · custom(Component) · setEditorComponent · theme。扩展自带 TUI,无需改主程序。
~/.pi/agent/extensions/audit/index.ts 后,pi --help 热重载验证;故意跑 rm -rf / 应被拦截。对比:扩展 vs MCP 的精确差异
扩展与 Pi 同进程、可注册 UI 与生命周期、可改 input 就地;MCP 跨进程、仅工具。Pi 选择扩展优先,MCP 可通过扩展再桥接。
06 硬化:从源码到发布的全链路
check 门禁 package.json:21, AGENTS.md
biome check --write --error-on-warnings . + check:pinned-deps / runtime-deps / ts-imports / entry-graphs + check:shrinkwrap / install-lock + tsgo --noEmit + check:browser-smoke # 每次改代码后必跑,未全绿不提交
供应链(精确)
save-exact=true+min-release-age=2(避同日投毒)npm-shrinkwrap.json+install-lock/锁定传递依赖--ignore-scripts安装(CI 与pi update --self)npm audit定时 + 生命周期脚本 allowlistrelease:local隔离 npm/Bun 冒烟
演进路线 wulutech/08
| 优先级 | 事项 | 价值 |
|---|---|---|
| P0 | watchSession 实现 · 扩展 worker 隔离 · 模型目录过期提示 | 可观测与安全 |
| P1 | SamplingParams 分型 · 工具并发 Span · 存储 Span | 可维护与可调试 |
| P2 | 声明式权限 · 压缩可回滚 · 协议版本协商 | 团队规模化 |
07 三选一 · 5 分钟路演
选题(抽签盒决定)
Track A · 工程化:为团队 pi 增加声明式权限(allow/deny)+ npm run check 门禁。
Track B · 扩展:Jira 关联扩展:tool_result 注入链接或 jira Tool。
Track C · 会话产品化:基于 SessionManager 的“会话回放 HTML 导出”后处理脚本(参考 core/export-html/)。
DADA 抽签:随机选题,精确交付
评审(精确权重)
| 维度 | 权重 | 看什么 |
|---|---|---|
| 可用性 | 40% | 现场可跑,有无 faux 兜底 |
| 可扩展性 | 30% | 是否通过扩展/配置实现 |
| 可观测性 | 30% | 是否有 Span/日志/清单 |
产出
每组 1 个可运行扩展或脚本 + 1 张架构拼贴(可用本页 Mermaid 导出)+ 1 次 5 分钟路演。投票选“最佳现成品”。
路演 checklist — 表格 B-7
- 5 分钟内讲清问题与方案
- 现场 demo 可跑(faux 兜底)
- 精确说明可扩展点与可观测点
✓ Hands-on 通关清单 — 官僚表格(互动)
勾选状态自动保存到本地(localStorage)。完成一项即有微动效;全部完成触发纸屑(仅动效,不遮挡)。
- H1 · faux 最小闭环跑通 — 看到 agent_end
- H2 · Custom Provider 注册并在 /model 可见
- H3 · fork + navigateTree + JSONL 可视化
- H4 · 审计扩展拦截 rm -rf / 自定义 Tool 可调用
- 环境:node ≥22.19 + npm run check 全绿
- 分组课题路演完成(每组 5 分钟)
附 会前环境准备 — 48h 前必做
node -v # >=22.19.0
npm install --ignore-scripts
npm run build:offline # 或 npm run build(需联网拉模型)
npm run check # 需全绿,未绿不提交(AGENTS.md)
./pi-test.sh --help # 源码直跑冒烟
# 有 Key 者(任选其一):
export ANTHROPIC_API_KEY=... # 或 OPENAI_API_KEY / GOOGLE_API_KEY
pi --model anthropic/claude-sonnet-4 --print "hi" # 真实冒烟
会场需:16:9 投影 · Wi-Fi · 每组一白板 · 讲师机 HDMI。备用:node:24-bookworm-slim Docker 镜像。