Pi Agent Harness MANIFESTO WORKSHOP APPROVED 全日 6.5h · 强动手 v0.85.1
09:00 — 09:30 · 开场 目标对齐

00 Pi 是一套“终端操作系统”,不是更聪明的对话框

定义:Agent Harness(持久化与并发) + pi-ai(多 Provider 抽象) + 自扩展(30+ 事件与工具) + 自研 TUI(差分渲染)。判断标准:能否在终端内完成“长程、有状态、可回溯、可扩展”的工程任务。

DADA 注 达达的“现成品”(readymade)在此指:Pi 的每个包都是从真实工程痛点上剪下的现成品——保留其精确功能,仅以拼贴重组呈现。本页视觉为达达,表述为工程精确。

问题 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/Storage
  • ai/src/types.ts — 统一 Model/Context/Message;providers/*, api/* — 30+ Provider, 10 Api
  • coding-agent/src/core/agent-session.ts — 全模式共享编排;session-manager.ts — JSONL DAG
  • tui/src/tui.ts, layout.ts — 差分渲染;chord/src — Facet/RPC;protocol/src/cbor, framing.ts — CBOR
  • telemetry/src — Span Schema;session-backends/sqlite-node — SQLite Storage
09:30 — 10:30 · 架构 五层与不变量

01 五层单向依赖 · 核心数据结构

不变量:上层可依赖下层,下层不依赖上层(唯一例外:agent 通过接口依赖 telemetry 产出 Span)。违反则循环依赖,测试与发布成本陡增。

graph TB subgraph UI ["UI"] TUI["TUI interactive"] PRINT["print"] RPC["rpc"] end subgraph ORCH ["Orchestration"] AS["AgentSession"] MR["ModelRuntime"] SM["SessionManager"] end subgraph RT ["Runtime"] AG["Agent"] AH["AgentHarness Lane"] LOOP["agent-loop"] end subgraph AI ["Model"] PAI["pi-ai Model/Context"] PROV["30+ Providers"] end subgraph INFRA ["Infra"] CHORD["Chord"] PROTO["Protocol CBOR"] TEL["Telemetry"] end UI --> ORCH --> RT --> AI --> INFRA

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 边界过滤。

要点 自定义能力不污染 Provider 契约;扩展的自由度来自“在边界转换”。

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,新分支含旧尝试的压缩摘要,可回溯决策。

10:45 — 12:00 · 内核 双层循环与并发

02 Agent Loop:内层由 tool 驱动,外层由 followUp 驱动

sequenceDiagram participant U as User participant S as AgentSession participant A as Agent participant R as runLoop participant M as pi-ai Stream U->>S: prompt(text) S->>A: prompt([UserMessage]) A->>R: runAgentLoop R->>M: stream(model, llmContext) M-->>R: EventStream text/thinking/toolcall R->>R: executeToolCalls (parallel/sequential) R-->>A: turn_end / agent_end A-->>S: _handleAgentEvent persist

循环不变量 agent-loop.ts:156

  • 内层:只要有 toolCallssteering,继续下一 turn。
  • 外层:内层结束后再问 getFollowUpMessages(),有则继续。
  • prepareNextTurn 前条件性再 poll steering,避免 one-at-a-time 在同一 turn 吞两条。
  • stopReason=="length" 时当批次工具调用视为“可能截断”,全部失败重试(保守但安全)。

并发契约 agent-loop.ts:409

模式prepareexecute结果顺序
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 渲染状态流,可对照源码逐文件精读。

13:00 — 14:00 · 模型 Api 维度与兼容

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 的网关
thinkingFormat6 种: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"
14:00 — 15:15 · 会话 DAG 与压缩

04 会话即 DAG:JSONL 追加 + 分支 + 压缩

graph LR H["SessionHeader v=8"] --> M1["User: create file"] M1 --> A1["Assistant + tool_call write"] A1 --> R1["toolResult"] R1 --> M2["User: add test"] M2 -.-> F["Forked Session"] M2 --> C["CompactionEntry summary"] C --> M3["User: continue"]

AgentSession 的精确职责 core/agent-session.ts:306

  • prompt():命令拦截 → input handlers → Skill/Template 展开 → 校验 model/auth → before_agent_start 注入 CustomMessage → runAgentLoop
  • _handlePostAgentRun:可重试错误 → 溢出压缩 → 队列续跑(有其一则 continue()
  • _handleAgentEventmessage_end 双写 AgentState.messages + SessionManager JSONL;turn_end 才 flush CustomMessage(避免夹在 tool_call/result 间)

压缩三类 reason

reason触发行为
thresholdprepareNextTurn 前 shouldCompact()后台摘要,保留 cutPoint 后消息
overflowLLM 报 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)。
15:30 — 16:30 · 扩展 一等公民

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_callinput 就地可变,多扩展链式改同一对象;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可覆
readpathcontent+truncated
bash / powershellcommandstdout/stderr/exitCode是(Gondolin 覆写)
edit / writeold/new/pathdiff
grep / find / lspattern/pathmatches/entries

UI 覆盖能力

select/confirm/input/notify · setWidget/setFooter/setHeader · custom(Component) · setEditorComponent · theme。扩展自带 TUI,无需改主程序。

Hands-on 4 落盘 ~/.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 定时 + 生命周期脚本 allowlist
  • release:local 隔离 npm/Bun 冒烟

演进路线 wulutech/08

优先级事项价值
P0watchSession 实现 · 扩展 worker 隔离 · 模型目录过期提示可观测与安全
P1SamplingParams 分型 · 工具并发 Span · 存储 Span可维护与可调试
P2声明式权限 · 压缩可回滚 · 协议版本协商团队规模化
一句话 Pi 把“编码 Agent”做成了可持久化/可分支/可观测/可扩展的终端操作系统会话 DAG + 扩展一等公民是最值得复用的组合。
16:30 — 17:30 · 课题 随机即命运,交付需精确

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)。完成一项即有微动效;全部完成触发纸屑(仅动效,不遮挡)。

0 / 6
  • 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 镜像。