附录 F · pi Harness 代码地图

为什么需要一张地图

前面各章,你几乎在每个模式里都“用 pi 实现”了一遍:ReAct 循环(第 3 章)、上下文压缩(第 9 章)、会话持久化(第 12 章)、Hooks 与事件(第 8 章)、运行时分层(第 23 章)……你大概已经默认 pi 就是“那个帮你跑 Agent 的东西”。但你大概率从没真的打开过 packages/agent/src/harness/ 这个目录,看它内部到底长什么样。

这一附录就是给你一张地图。它不引入任何新概念,只做一件事:把前面散落在各章的 pi 片段,按真实的源码目录拼成一张完整的引擎剖面图。读完你再回头看“Agent = Model + Scaffolding + Harness”(第 1 章),会清楚地知道——Harness 这一层,在 pi 里具体是哪些文件、各自管什么。

一句话定位:pi 把“狭义 Harness(执行引擎)”显式抽成了一个 harness/ 模块,和 Scaffolding(system-prompt.ts)、运行时入口(agent.ts)严格分离。这张地图,就是 harness/ 模块的导览。

第 2 章的 10 条判别清单,落到 pi 的代码上

第 2 章给了你一张“判别 Agent 有没有 Harness”的 10 条清单。要先说明:它不是某个标准组织的条文,而是一张从 Hugging Face Agent 术语表与真实开源 harness(pi、OpenAI Agents SDK、Claude Agent SDK、LangGraph)的共性能力中提炼的“判别透镜”。下面把它逐条映射到 pi 的真实代码——你就知道 pi 在 Harness 这一层到底做了什么:

#判别维度(第 2 章)pi 的代码落点
1执行循环agent-loop.tsrunAgentLoop(双层循环)
2工具执行agent-loop.tsexecuteToolCallsParallel/Sequential + before/after 钩子
3状态持久化session/ 的会话树(id/parentId,可分支、JSONL 落盘)
4循环控制 / 停止agent-harness.ts 的阶段机 + shouldStopAfterTurn + abort()
5异常兜底agent-loop.ts:工具异常→createErrorToolResult 回填,循环继续
6上下文工程compaction/ 的 token 估算 + LLM 结构化摘要
7可观察性agent-harness.tssubscribe/on + 全链路事件流
8权限 / 安全边界无内置(诚实边界),靠 env/nodejs.ts + 容器化(第 28 章)
9扩展机制skills.ts + 生命周期 Hooks(第 8 章)+ 四个扩展面(第 35 章)
10动态切换agent-harness.tssetModel / setThinkingLevel / setTools

后面每节,就是这张表里每一行的代码展开。

总览:harness/ 目录在哪儿、装了什么

pi 的运行时在 packages/agent。剥开入口文件,真正的执行引擎全在 packages/agent/src/harness/ 下:

packages/agent/src/
├── agent.ts / agent-loop.ts / node.ts / proxy.ts / index.ts   # 基础运行入口
└── harness/                       ← 狭义 Harness 实现层
    ├── agent-harness.ts          # 核心类 AgentHarness(编排器)
    ├── agent-loop.ts             # 执行循环 runAgentLoop(模型↔工具的真正驱动)
    ├── system-prompt.ts          # Scaffolding(系统提示,模型可见层)
    ├── prompt-templates.ts       # 提示模板
    ├── skills.ts                 # 技能加载与调度
    ├── messages.ts / types.ts    # 消息结构与类型契约
    ├── compaction/               # 上下文压缩(Context Engineering 的执行侧)
    │   ├── compaction.ts         #   token 估算 + 切点 + LLM 结构化摘要
    │   ├── branch-summarization.ts
    │   └── utils.ts
    ├── env/nodejs.ts             # 执行环境(文件系统 / Shell)
    └── session/                  # 状态管理与持久化
        ├── session.ts            #   Session 类(分支树、buildContext)
        ├── jsonl-storage.ts      #   JSONL 落盘存储
        └── ...

记住这条主线,后面每节对应一个文件:编排器管全局 → 循环管一轮 → 会话管状态 → 压缩管上下文 → 类型管契约

编排器:AgentHarness(agent-harness.ts)

AgentHarness 是 harness 的总指挥,本质是一个带阶段机消息队列的状态机。它不自己跑循环,而是把循环委托给 runAgentLoop,自己负责“什么时候能跑、跑的时候谁能插话、跑完怎么落盘”。它直接对应判别清单的第 4、7、10 项。

几个关键设计:

  • 阶段机 phase"idle" | "turn" | "compaction" | "branch_summary" | "retry"。除 idle 外不接受新的 prompt(),以此防止并发冲突——你不能同时发起两轮(第 4 项:循环控制)。
  • 运行中插话队列steerQueue(运行中指导)、followUpQueue(追加后续)、nextTurnQueue(下一轮预排)。模型跑着的时候你打字进去,会被安全排队,而不是打乱循环。
  • 延迟写 pendingSessionWrites:运行中产生的所有状态变更(消息、模型切换、工具增减)先入队,在 turn_end / agent_end / finally 里统一 flush 到存储——保证“要么全写、要么不写”。
  • 动态切换setModel() / setThinkingLevel() / setTools() / setActiveTools() 在空闲时直写、运行时进延迟写队列(第 10 项)。

它暴露的方法也印证了 Harness 该管的事:prompt()/skill()/promptFromTemplate()(发起)、abort()(中止)、compact()(压缩)、navigateTree()(分支导航)、subscribe()/on()(订阅与钩子,第 7 项:可观察性)。

这一层对应第 1 章说的:Harness 决定“行动如何被真正执行”,而 Scaffolding 决定“该按什么规则行动”。AgentHarness 管执行,system-prompt.ts 管规则,二者在同一目录并列但职责分明。

执行循环:runAgentLoop(agent-loop.ts)

第 3 章你看过一个简化的 while 骨架。真实代码在 agent-loop.tsrunAgentLoop,结构是双层循环——它对应判别清单的第 1、2、5 项:

  • 外层 while:处理 followUp 消息——如果模型本该停了,但队列里还有后续指令,就带着新消息再转一轮。
  • 内层 while:处理工具调用与 steer 消息——每轮先流式拿模型回复,有 toolCall 就执行并回填 Observation,没有就准备结束。

单轮内部的关键步骤:

// 伪代码骨架(省略流式/事件细节)
while (hasToolCalls || pendingMessages.length) {
  const message = await streamAssistantResponse(ctx, config);  // Thought + Action
  if (message.stopReason === "error" || "aborted") { end(); return; }
  const toolCalls = message.content.filter(c => c.type === "toolCall");
  if (toolCalls.length) {
    const results = await executeToolCalls(ctx, message, config);  // Observation
    hasMoreToolCalls = !results.terminate;
  }
  if (await config.shouldStopAfterTurn?.({ message, results, ctx })) { end(); return; }
  pendingMessages = await config.getSteeringMessages?.() ?? [];   // 运行中插话
}

比第 3 章骨架多出来的工程细节,正是“能上生产的 Harness”和“玩具循环”的分水岭(对应第 2、5 项):

  • 并行 / 串行:工具按 executionMode 或全局 toolExecution 决定 executeToolCallsParallel 还是 Sequential(第 2 项:工具执行)。
  • 钩子拦截beforeToolCall 可阻断(block)或改写参数;afterToolCall 可修补结果、标记 terminate。这就是第 8 章 Hooks 的真正落点。
  • 永不崩溃:工具找不到 / 参数非法 / 执行抛错 → 生成 createErrorToolResult 回填,循环继续,而不是整个 Agent 挂掉(第 5 项:异常兜底)。

状态与会话树:session/(session.ts)

第 12 章讲过会话格式与持久化,但没展开 pi 怎么存。答案是一棵树Session 把每条 entry 存成 { id, parentId, ... },顺着 parentId 连回根(对应第 3 项:状态持久化)。

这意味着会话不是一条线性时间线,而是一个可分支的树

  • getBranch(fromId?) 沿 getPathToRoot(leafId) 取回从根到某个叶子的路径。
  • buildContext() 把这条路径重建成当前上下文——所以你能从任意历史节点“分叉”出新对话,旧分支不丢。
  • moveTo(entryId, summary) 把叶子指针移到别处,并写入一条 branch_summary 记录为什么分叉。

落盘在 jsonl-storage.ts,用 JSONL 追加写;运行时变更先进 pendingSessionWrites,由编排器在合适时机统一 flush(呼应上面的“延迟写”)。

会话树是 pi 能做“RPC 远程驱动 + 后台长时 Agent”(第 24、33 章)的底层支撑:远程端操作的不是一段文本,而是一棵可寻址、可恢复、可回放的会话树。

上下文压缩:compaction/(compaction.ts)

第 9 章讲过 Compaction 的概念。这里看 compaction.ts 怎么落地(对应第 6 项:上下文工程):

  • token 估算estimateTokens 用“字符数 ÷ 4”的启发式(图片按 4800 字符计),estimateContextTokens 再结合模型返回的 usage 修正。
  • 触发条件shouldCompact(contextTokens, contextWindow, settings)contextTokens > contextWindow - reserveTokens 时触发,默认 reserveTokens = 16384keepRecentTokens = 20000
  • 切点选择findCutPoint 从末尾向前累加 token 到“保留最近 20k”预算内,选一个不拆散中间状态的切点。
  • LLM 结构化摘要generateSummary 调模型按固定模板产出——## Goal / ## Constraints / ## Progress(Done/In Progress/Blocked)/ ## Key Decisions / ## Next Steps / ## Critical Context,并要求保留确切文件路径、函数名、错误信息。已有旧摘要时走 UPDATE_SUMMARIZATION_PROMPT 做增量更新,而不是从头写。

压缩后只保留“摘要 + 最近历史”,写一条 compaction entry;buildContext 重建时自动把摘要注入上下文顶部。长任务因此不会撑爆窗口,且关键信息(含读改写过的文件清单)被保留。

类型契约:types.ts

types.ts 是 harness 的“接口说明书”,值得单独记一笔,因为它体现了 pi 对“可靠”的执念(也支撑第 5、7 项的健壮性):

  • Result<T, E> 模式:文件系统 / Shell / 执行环境的方法一律返回 { ok, value } | { ok: false, error }不靠抛异常传递可控错误——调用方必须处理 err 分支。
  • 六个稳定错误类FileError / ExecutionError / CompactionError / BranchSummaryError / SessionError / AgentHarnessError,各自带明确 ErrorCode 枚举。
  • 执行环境接口FileSystemShellExecutionEnv(= FileSystem + Shell)把“怎么碰文件、怎么跑命令”抽象成接口——换平台只换实现,harness 内核不动。

这正是第 23 章“依赖倒置、换一层不动其他层”在类型层面的体现:harness 只依赖接口,不依赖某家文件系统或 shell 的实现。

用这张地图回看全书

拼起来看,前面学的每个模式,都能在 harness/ 里指到具体位置:

你学过的模式在 harness/ 的落点
ReAct 循环(第 3 章)agent-loop.tsrunAgentLoop
工具调用与并行/串行(第 4 章)agent-loop.tsexecuteToolCallsParallel/Sequential
Hooks 与事件(第 8 章)agent-harness.tssubscribe/on + 循环里的 before/after 钩子
上下文工程(第 9 章)compaction/compaction.ts
会话树与持久化(第 12 章)session/session.ts + jsonl-storage.ts
运行时分层(第 23 章)packages/agent 整体,harness/ 是运行时层内部
在其上构建(第 35 章)AgentHarness 暴露的四个扩展面之外的“引擎本身”

回看第 1 章那句话——“你学的是模式,不是 pi 的 API”——现在你既懂了模式,也看清了模式在一个真实 harness 里长什么样。换到 Claude Agent SDK、LangGraph 或任何还没诞生的 harness,你都能照着这张地图,快速找到“它的执行循环、会话存储、上下文压缩分别在哪”。

更实际的做法:拿第 2 章 5 家对比表里任意一家 harness,照这 10 条逐项去找对应物——你会发现都能找到,只是内置与否、暴露程度不同。这恰好印证了“模式相同、实现各异”。

动手看看

打开 packages/agent/src/harness/,按这张地图走一遍:

  1. agent-harness.tsphase 字段和三个队列,确认“运行中插话”是怎么不打乱循环的(对应判别第 4 项)。
  2. agent-loop.tsrunAgentLoop,对照第 3 章骨架,标出 beforeToolCall / afterToolCall 两个钩子点(第 2、7 项)。
  3. session/session.tsbuildContext,画一条从 leaf 回溯到 root 的路径,体会“会话树”为何能分支(第 3 项)。
  4. compaction/compaction.tsSUMMARIZATION_PROMPT,看它要求保留哪些“硬信息”(第 6 项)。

走完这四步,你就不再只是“用过 pi”,而是“读过 pi 的引擎”。

小结

  1. pi 把狭义 Harness 显式抽成 packages/agent/src/harness/,与 Scaffolding(system-prompt.ts)严格分离——这是“Agent = Model + Scaffolding + Harness”在代码层的直接印证。
  2. 第 2 章的 10 条判别清单,逐条都能落到 pi 代码:执行循环(runAgentLoop)、工具执行(并行/串行+钩子)、会话树(session/)、阶段机与停止(AgentHarness)、异常兜底(createErrorToolResult)、上下文压缩(compaction/)、可观察性(subscribe/on)、扩展(skills+hooks)、动态切换(setModel 等)。
  3. 编排器 AgentHarness 用阶段机(idle/turn/compaction/branch_summary/retry)+ 运行中插话队列 + 延迟写,保证“何时能跑、谁可插话、怎么落盘”都受控。
  4. 执行循环 runAgentLoop 是双层循环:外层处理 follow-up、内层处理工具调用与 steer;并行/串行、钩子拦截、异常兜底都在这一层,比第 3 章骨架多了“能上生产”的工程细节。
  5. 会话树 session/ 把对话存成可分支的 {id, parentId} 树,buildContext 沿根重建——这是 RPC 远程驱动与后台长时 Agent 的底层支撑。
  6. 上下文压缩 compaction/ 用 token 估算 + 切点 + LLM 结构化摘要(保留文件路径/函数名/错误),让长任务不爆窗口且关键信息不丢。
  7. 类型契约 types.tsResult 模式、六个稳定错误类、执行环境接口,把“可靠”写进类型系统——也是分层与依赖倒置的体现。
  8. 这张地图把全书散落的 pi 片段拼成完整引擎剖面:你既懂了模式,也看清了模式在真实 harness 里的位置,换任何框架都能照图索骥。