SoL-Pi 源码原理与技术学习文档
从“让 Agent 少做事”转向“让 Harness 少重复工作”:解析 SoL-Pi 如何在不修改 Pi 上游源码的前提下,通过四个可组合机制降低模型轮次、上下文重放、超长 Observation 与日志阅读成本。
项目定位:SoL-Pi 到底是什么
仓库入口 src/sol-pi/index.ts 在 session_start 时读取配置,然后按开关注册四类扩展。所有机制默认关闭;项目级配置只有在 Pi 判断项目可信时才会读取。
| 设计原则 | 源码体现 | 工程价值 |
|---|---|---|
| No Pi patches | 只导入 Pi 公共 API,使用 ExtensionAPI / ExtensionContext / 公共 Tool Definition | 降低升级与维护成本 |
| Explicit opt-in | 默认四个 feature 全为 false | 安全、可回滚、便于 A/B |
| Preserve evidence | 原始 Observation/日志归档;Reducer 失败时保留原结果 | 压缩不等于丢证据 |
| Runtime ownership stays with Pi | 模型、认证、Provider URL、Shell 行为继续由 Pi 管理 | 扩展边界清晰 |
为什么需要 SoL-Pi:长时 Agent 的四类“结构性浪费”
1. 决策轮次浪费
修改文件之后,下一步往往高度可预测:测试、构建、启动或检查。如果中间仍让大模型再做一次“决定运行命令”的请求,这一轮推理价值很低。
2. 上下文重放浪费
一个大文件或长 Tool Result 首次读过之后,仍会跟随历史在后续 Provider Request 中一遍遍重放。
3. 日志阅读浪费
编译/测试可能输出数万行,但真正改变下一步决策的常常只有少数错误行与上下文。
4. 已完成子任务拖累
长任务里的已完成步骤继续留在活跃上下文中,直到原生自动 compaction 很晚才触发。
Auto-Research 方法:机制不是拍脑袋设计出来的
项目页公开的方法是一个规模化的 Harness 自动研究流程:从大量候选方向出发,每个方向形成独立 lineage,经过 rollout、证据分析、实现、审查以及两层验证。首轮提出 152 个方向,最终只有 4 个机制进入 SoL-Pi。
总体架构:四个机制卡在四个不同边界
入口注册逻辑
session_start
└─ loadSolPiConfig(ctx.cwd, getAgentDir(), ctx.isProjectTrusted())
├─ actionFusion → registerActionFusion()
├─ observationPack → registerObservationPack()
├─ evidencePreservingReducer → registerEvidencePreservingReducer()
└─ onlineContextCompact → registerOnlineContextCompact()
注意:入口通过 initialized 防止重复注册;Online Context Compact 在其它 context transformer 之后注册,以便它观察经过前序转换后的上下文。
Action Fusion:把“修改 → 验证”融合成一次 Tool 调用
目标
扩展复用 Pi 自带 createEditToolDefinition() / createWriteToolDefinition(),只在参数 Schema 上增加可选的 then_run。因此原工具的提示、参数 shim、渲染器与核心语义仍由 Pi 提供。
执行流程
- 按
ctx.cwd缓存 built-in tool definition。 - 解析真实文件路径,进入 SoL-Pi 的 per-file queue。
- 执行原始 edit/write。
- 若 mutation 失败:标记
[then_run:skipped],绝不执行命令。 - 若指定
then_run:计算 SHA-256,再让出一个 event-loop tick,再次计算。 - 目标内容发生变化则跳过 command,避免验证错误版本。
- 创建 Pi 原生 bash tool 运行命令,合并结果为一个 observation。
并发与失败语义
edit && test,而在“保留 Pi 原工具语义 + 明确 mutation/validation 的不同失败语义 + 文件竞争检测 + 用一个 Tool Result 回给模型”。ObservationPack:第一次完整看,后面只保留句柄,需要时精确召回
核心位置:Provider Context Projection
ObservationPack 不直接改 Session History,而是在 Pi 的 context 投影阶段替换后续要发给 Provider 的消息。这样会话日志仍保留原始结果,native compaction / resume 后也仍可通过 archive 召回。
为什么是“Projection”,不是“修改历史”
- 历史是事实记录;为了省 token 不应该破坏它。
- Provider Request 是昂贵的“视图”;这里可以根据生命周期做压缩。
- 句柄只负责寻址,原始 bytes 仍保存在 session-derived 目录。
公开研究中选出的 V2
项目页说明最终配置使用约 2048-byte head + 1536-byte tail + 前两次完整发送,之后进入 handle/recall 模式。这体现了一个重要折中:先保证模型建立对 Observation 的初始理解,再避免长期重放。
Evidence-Preserving Reducer:让便宜模型读长日志,但主 Agent 只接收可验证证据
问题边界
Reducer 面向长 diagnostic log,而不是通用摘要。它会把原始日志归档,然后让配置的 reducer provider/model 生成 compact receipt;只有 receipt 中保留的引用能在 source archive 中逐字验证时,才接受压缩结果。
| 校验失败点 | 处理 | 原因 |
|---|---|---|
| 不满足候选条件 | 保留原 Tool Result | 不为短输出增加复杂度 |
| Reducer model 不可用 / 调用失败 | Fail-open | 效率机制不能阻断任务 |
| Schema / source hash 不匹配 | 拒绝 receipt | 防止跨日志或结构错误 |
| 引用不是原文 exact quote | 拒绝 receipt | 避免“摘要幻觉”变成证据 |
| Likely-secret detector 命中 | 不远程 reduction | 降低敏感日志外发风险 |
| 压缩后并没有更小 | 保留原结果 | 无收益则不改变行为 |
Online Context Compact:从“窗口快满了再压缩”改为“子任务完成时判断是否值得压缩”
语义边界 + 经济边界
它通过 update_plan 维护结构化计划。当某个 step 从未完成变为 completed,就得到一个“语义上适合压缩”的候选点;但是否真的 compact,还要结合上下文压力、预计未来请求数、上下文增长和 cache write/read 成本比做经济性判断。
为什么要先 abort,再 compact
兼容文档指出,Pi 0.85.1 的 ExtensionContext.compact() 会终止活跃 Agent,而 agent_settled 只在整条 run drain 后触发。SoL-Pi 因此把 boundary 状态先持久化,然后主动 abort,让 compaction 在 settled 状态执行。
为什么 compact 后还能自动继续
成功后扩展通过 ExtensionAPI.sendMessage(..., triggerTurn: true) 注入一个隐藏的通用提醒,让 Pi 在 compacted context 上启动新 turn 并重建计划。取消或退出不会自动继续。
Plan 数据契约
[
{ "id": "step-1", "goal": "定位问题", "status": "completed" },
{ "id": "step-2", "goal": "实现修复", "status": "in_progress" },
{ "id": "step-3", "goal": "验证", "status": "pending" }
]
- step id 必须唯一;同一个 id 不应换 goal。
- 建议最多一个
in_progress。 - 有 pending 时,最好明确一个 in_progress。
- 状态写入 Pi session log 的 versioned custom entries,不另建 OCC sidecar 文件。
Pi 生命周期挂接:SoL-Pi 为什么能保持“独立扩展”
效率模型与实验:衡量的是“完成同样工作的成本”
Action Fusion 的机会估计
项目页公开的研究轨迹显示:Oracle Analysis 在 cross-turn transition 中发现约 12.3% 是“edit/write 后紧跟 command”的候选;如果所有 149 个候选都触发,轨迹反事实估计 model turns 可从 1,386 降至 1,237(约 -10.8%),总 tokens 从 32.38M 降至 28.64M(约 -11.5%)。这是轨迹反事实,不是实际重跑结果。
Online Context Compact 的成本直觉
提前 compaction 本身也有成本,因为它会产生摘要写入并破坏一部分 KV-cache 复用。因此 SoL-Pi 不把“子任务完成”直接等同于“立刻压缩”,而是使用配置项 cacheWriteReadRatio(默认 12.5)作为单一经济决策比率,并结合未来 horizon 与 window pressure 判断未来读取节省是否能偿还本次重写成本。
安全与失败回退:效率层必须“失败时回到原行为”
| 机制 | 新增权限/风险 | 保护与边界 |
|---|---|---|
| Action Fusion | 文件修改 + shell command | 只在可信项目读取 project config;mutation 失败不运行 command;文件变化检测 |
| ObservationPack | Session 目录保存大 Observation | 本地归档;不破坏原 session history |
| EPR | 可把 eligible log 发给 reducer model | 显式开关、Pi 管认证、likely-secret 检查、exact quote 校验、失败保留原结果 |
| OCC | Plan/进度信息进入 session log | 不创建独立 payload capture;隐藏 continuation 不含任务特定数据 |
安装、配置与复现
环境要求
npm install --global @earendil-works/pi-coding-agent@0.85.1
pi install git:github.com/NVlabs/SoL-Pi
# 项目本地安装
pi install git:github.com/NVlabs/SoL-Pi --local --approve
推荐从低风险机制开始
{
"version": 1,
"actionFusion": true,
"observationPack": true,
"evidencePreservingReducer": false,
"onlineContextCompact": false,
"cacheWriteReadRatio": 12.5
}
官方 README 也把这组配置作为保守起点:前两者不额外调用模型,也不会停止一个正在运行的 Agent run。
配置查找顺序
- 可信项目中的
.pi/sol-pi.json ~/.pi/agent/sol-pi.json- 都没有时使用内置默认值(四机制关闭)
项目配置覆盖全局配置,不做 merge。未知 key、错误 version、非 boolean feature、非法 ratio 等都会直接阻止扩展加载,而不是悄悄忽略。
源码验证命令
npm ci --ignore-scripts
npm run check
npm audit --audit-level=high
node scripts/check-pi-compat.mjs
npm run check = TypeScript typecheck + 完整 Vitest + npm pack --dry-run。
可迁移设计思想:如何应用到自己的 Agent Runtime
A. 把 Runtime 优化拆成四层
B. “存储”和“Prompt 投影”分离
Session log 保存事实,Provider projection 决定当前请求需要看多少。这比直接改聊天历史更稳定,也更适合 resume、branch、compaction。
C. 压缩系统必须有 Recall 通道
真正可靠的压缩不是“摘要后删掉”,而是“热上下文变小 + 冷证据仍能精确寻址”。
D. LLM 输出后加确定性校验器
EPR 最重要的不是“用小模型总结”,而是 exact-quote/source-hash/schema 检查。把生成模型当候选生成器,把程序当裁判。
E. 所有效率机制都要 Fail-open
优化失败应退化到原始路径,不应让主任务失败。效率插件的 SLO 首先是“不能降低可完成性”。
如果移植到 Java Agent Runtime,可抽象成这些接口
interface ContextProjector {
List<Message> project(Session session, List<Message> history);
}
interface ObservationArchive {
ObservationHandle archive(byte[] content);
byte[] recall(ObservationHandle id, int page);
}
interface ToolDecorator {
Tool decorate(Tool original);
}
interface CompactionPolicy {
CompactionDecision evaluate(PlanState plan, ContextUsage usage, Economics economics);
}
interface EvidenceVerifier<T> {
VerificationResult verify(T candidate, ArchivedSource source);
}
这样可以把 SoL-Pi 的思想迁移到任何 Think → Action → Tool → Observation 循环,而不绑定 TypeScript 或 Pi。
总结:SoL-Pi 真正值得学习的不是四个 Feature,而是一种 Harness 优化范式
保留任务能力与可验证证据 → 找到重复交互 → 在最窄边界做机制化替换 → 用公开 Runtime Hook 接入 → Fail-open → 用 held-out 任务验证节省是否真实迁移。
Action Fusion 消掉无信息增益的模型轮次;ObservationPack 把“可访问”与“每轮重放”分离;Evidence-Preserving Reducer 把便宜模型的阅读能力放在确定性证据验证之后;Online Context Compact 则把 compaction 从纯粹的 token-window 事件升级为“语义边界 + 经济判断”。四者共同说明:Agent 系统的成本优化空间,很大一部分存在于 Harness 的数据生命周期与控制流,而不一定要重新训练模型。
主要资料
- NVlabs/SoL-Pi GitHub:README、src/sol-pi、docs/configuration.md、docs/compatibility.md、SECURITY.md
- 论文:SoL-Pi: Recursively Scaling Auto-Research Loops for Efficient Agent Harness,arXiv:2609.20519
- NVIDIA Research 项目页:SoL-Pi 方法、四机制研究过程与实验图表
本文将“源码可直接确认的实现”与“论文/项目页公开的实验数据”分开表述;实验结果不等同于对任意模型、任意任务和任意 Pi 版本的性能保证。