数据截至 (上游 commit 59a71b235dad)
Harness:会话树、系统提示、Skills 与上下文压缩
30 秒导读: 第 2 章讲的 agent 循环只解决"这一回合怎么跑完"。但真实的编码 agent 要跨几十上百回合工作,还要在上下文窗口撑爆前不崩。本章讲的
AgentHarness就是循环之上那一层——它把"循环、会话记忆、模型、上下文压缩"编排到 一起,负责记住、装配、瘦身。
本章聚焦 packages/agent/src/harness/ 这个目录。它是 pi-agent-core 里"有状态"的那半边;工具集、CLI 那些具体接线属于 第 4 章。
1. 这是什么(零基础也能懂)
一句话定义: AgentHarness 是一台长会话编排器——它反复调用 agent 循环,把每一步都记进一棵可分叉的会话树,每回合重新拼系统提示,并在历史太长时自动把老对话压成摘要。
为什么需要它(问题场景):
假设你在终端里让 AI 改一个大项目,聊了两小时。这中间会发生一堆循环本身不管的事:
- 你关掉终端明天再来——对话得存下来、能恢复。
- 你想回到半小时前那个岔路口试另一种改法——历史得能分叉、能回溯。
- 聊到第 80 回合,token 堆到把 20 万的上下文窗口塞满——老历史得自动压缩,否则模型直接拒绝请求。
- 你中途想切个更强的模型、或临时启用某个技能(Skill)——这些变更也得被记住、下回合生效。
循环不管这些。Harness 就是专门管这些的那一层。
它由哪几块组成:
| 组成 | 职责 | 文件 |
|---|---|---|
编排器 AgentHarness | 驱动一个个回合、管状态机、发事件/钩子 | harness/agent-harness.ts |
会话 Session + 存储 | 把每一步落成一棵 append-only 的树,可存可读可分叉 | harness/session/ |
| 系统提示 + 消息装配 | 每回合把提示、历史、Skills 拼成模型能吃的格式 | harness/system-prompt.ts、messages.ts |
| Skills | 从目录扫技能文件,注入提示 | harness/skills.ts |
| 压缩 compaction | 历史太长时压成结构化摘要 | harness/compaction/ |
一句话直觉: 把 agent 循环当成 CPU 的一个指令周期,那么 Harness 就是操作系统——它管进程调度(回合)、管磁盘(会话持久化)、管内存回收(压缩)。
2. 顶层全景(它大概怎么转)
2.1 分层:循环之上的"状态与记忆"层
怎么读这张图: 从上到下是"控制流谁调用谁";Harness 夹在应用和循环之间,右侧三根柱子是它依赖的资源。
应用 (pi-coding-agent / 你的代码)
│ harness.prompt("改这个 bug")
▼
┌─────────────────────────────────┐ ┌──────────────┐
│ AgentHarness 编排器 │────▶│ 会话树/存储 │ 记住每一步
│ · phase 状态机(idle/turn/…) │ ├──────────────┤
│ · 每回合装配提示+历史+工具 │────▶│ Models 层 │ 发请求(见 01)
│ · 事件广播 + 钩子拦截 │ ├──────────────┤
│ · 撑爆前触发 compaction │────▶│ ExecutionEnv │ 读文件/跑命令
└─────────────────────────────────┘ └──────────────┘
│ 每回合调用一次
▼
runAgentLoop(...) ← 第 2 章的循环
Harness 自己不跑模型、不执行工具——那是 Models 层(见 01)和循环(见 02)的活。它只做"编排 + 记忆 + 瘦身"。
2.2 各部件一句话职责
| 部件 | 干什么 | 关键符号 (file:line) |
|---|---|---|
AgentHarness | 顶层类,持有全部可变状态,对外暴露 prompt/skill/compact/navigateTree(后两者在基类是 unavailable 桩,实现在 coding-agent 的 AgentSession) | agent-harness.ts:305 class AgentHarness |
| 每回合重建上下文 | 每回合开始时重算"提示+历史+模型+工具"(早期 createTurnState 已移除,现在每回合直接 buildContext) | session/context.ts:90 buildSessionContext |
Session | 会话的领域对象:往树上 append 各类条目、读分支 | session/session.ts:102 |
buildSessionContext | 把树的一条分支重放成模型要的 messages[] | session/context.ts:90(自 session.ts 拆出) |
JsonlSessionStorage | 落盘:一行一条 JSON(JSONL)的 append-only 日志 | session/jsonl/storage.ts:48 |
convertToLlm | 把内部 AgentMessage 转成 provider 的 Message | messages.ts:120 |
loadSkills / formatSkillInvocation | 扫技能目录、生成注入块 | skills.ts:49 / skills.ts:38 |
prepareCompaction / compact | 选切割点、生成结构化摘要 | compaction/compaction.ts:545 / :630 |
2.3 主线走一遍(一次 prompt 的高层旅程)
harness.prompt(text)
1. 检查 phase===idle,否则抛 "busy" [agent-harness.ts:609]
2. createTurnState():读会话分支 → buildContext()
重算 systemPrompt(可含 Skills 清单) [session/context.ts:90]
3. executeTurn():把 text 包成 UserMessage,
调 runAgentLoop(第 2 章) [agent-harness.ts:531]
4. 循环每发一个事件 → handleAgentEvent()
· message_end → 立刻 append 进会话树 [agent-harness.ts:489]
· turn_end → flush 待写、发 save_point
· agent_end → phase 回 idle
5. 返回最后一条 assistant 消息
压缩(compact)和回溯(navigateTree)是另外两个入口,不在 prompt 主线里——它们由应用在合适时机主动调用(比如检测到 token 快满了)。