pi-soft-compact0.1.6
Pi extension: compaction as an authorized model tool call (cache-preserving /compact and auto-compact).
Untested on omp
This is a pi package. No one has reported whether it works on omp yet. If you try it, add your result below.
Reports 0
No reports yet.
Readme
pi-soft-compact
把 Pi 的 /compact 与 auto-compact 改造成一次 "已授权的模型工具调用",
用纯追加的消息流替换原版的"有损序列化 + 整段冷 prefill",
在弱缓存模型(如本地 vLLM)上把压缩从数 MB 冷输入 + 数百次调用重试的恶性循环
降为一次性完整输入 + 1 次成功调用。
设计文档(PLAN.md)、逐行变更表(CHANGES-0.1.6.md)、开发/测试手册
(DEVELOPMENT.md)、问题档案(FIXES.md,正文以 FIXES #n 引用)都只在
源码仓库,不随 npm 包分发——安装成本包时找不到这些文件是正常的。
它做什么
- 常驻
compact工具:一直在工具列表里(工具集永不改变 → 前缀 100% 可缓存)。 说明中内嵌 Pi 当前安装版本的压缩提示词标准(自动跟随 Pi 更新, 内置 1.4.0 副本兜底)。未授权调用 → 返回"未授权"提示;授权调用 → 模型把整段会话写成结构化摘要。 - 授权标记:
/compact或 auto-compact 触发时,扩展取消 Pi 的原版压缩并 立即送达标记(pi.sendMessage(marker, {triggerTurn})):- manual → 会话已空闲,
triggerTurn直接新起一个 run,首条消息就是标记; - threshold → run 进行中,标记作为 steer 注入当前 run,模型在紧接着的
下一次响应里就调
compact(不用等下一条用户消息)。 两种情况标记都是纯追加:系统提示一个字不改,前缀缓存完整保留。 另有两个走before_agent_start注入的入口:会话恢复时的僵尸标记重武装、 用户把标记整条作为消息发送(锚定匹配:首行必为完整标记行;仅引用标记 文本的讨论消息不算授权,FIXES #14)。标记文本自带一次性条款:模型若看到 标记之后已有压缩条目(或完成确认),即忽略该标记。
- manual → 会话已空闲,
- 工具闸门:授权期间,
tool_call钩子拦截一切非compact工具, 引导弱模型直接干活而不是乱读文件。闸门是全工具闸门,所以带三道必出口 (每授权周期):块数预算(PI_COMPACT_GATE_BLOCK_MAX,默认 8 次后自动撤回 授权)、周期 TTL(PI_COMPACT_AUTH_TTL_MS,默认 15 分钟)、 用户主动/compact-cancel。 - 按 Pi 原流程落地:
turn_end/agent_before_settle边界把工具产出的摘要 提交为CompactionEntry(fromHook: true,与 Pi 官方的扩展压缩同构),并紧随 追加一条[COMPACTION COMPLETE / 压缩已完成]确认消息:标记留在保留上下文中 会持续显示,确认消息告诉模型该标记已被消费,避免后续 run 里的模型看到 过期的"只能调 compact"指令后反射性调用工具。manual→ run 到此结束;auto→ run 继续,模型用压缩后的上下文接着干活。 - 兜底:模型不开窍没调
compact时,用 Pi 同款提示词发起一次性摘要 (等价于原生行为,只是晚一轮)。 - 僵尸恢复:
--session恢复时发现"有标记但没有后续压缩条目" → 自动重新武装,下一个 run 收尾。僵尸周期的年龄取标记消息自己的时间戳, 所以恢复一个旧会话时,过期 TTL 会在第一次工具调用时把闸门自行打开。 - overflow 不拦截:上下文已超窗时没有"成功请求"这个先决条件,
放行 Pi 原版一次性压缩保证有界恢复(机械死区,源码仓库
PLAN.md§1.3)。 - pi-goal-x 兼容(只读,不改对方代码):goal-x 的
[POST-COMPACTION RESYNC]标记只在 Pi 原生日志发出session_compact时武装, 而 pi-soft-compact 的边界自提交不走那条路 → 有活动 goal 的会话会被丢失重同步。 解决:pi-soft-compact 自提交时若检测到活动 goal(读PI_GOAL_ROOT或<cwd>/.pi/goals下的 goal 文件 +goal_events.jsonl),就把等价的重同步块 附到compact-complete确认消息后——优先动态 import pi-goal-x 自己导出的buildPostCompactionGoalDelta生成(粘贴的就是官方原件),import 失败时退回 内置同构文本。走原生管线提交的压缩(含 goal-x 自己的武装)不重复注入。 - 失败调用瘦身(任意工具):参数被 token 上限截断或不合 schema 时,Pi 的错误回执会把
整段被拒参数回显(
…Received arguments: {…},轻松几 KB~几十 KB),而且它会在后续每一轮 请求里重复出现。这类失败走 Pi 的"立即结果"路径,tool_call/tool_result钩子都看不到它, 唯一挂载点是 toolResult 消息的message_end。扩展把回执换成短诊断(工具名 + 失败原因 + 截断位置 + 参数前 120 字符样本 + 重试指令),文件/模型上下文/TUI 一致短版; 小于PI_COMPACT_SLIM_MIN_CHARS(默认 600)的回显保留原文(小回显是有用证据)。 对弱模型这不只是省上下文:实测它会去续写那半截被截断的摘要,不回显就直接卡住不重试(FIXES #6/#18)。
安装(所有方式互斥,同一扩展只能走一条加载路径,否则加载两个实例、标记消息重复)
跨机器使用(推荐),每台机器装/卸:
pi install npm:pi-soft-compact # 安装:所有会话生效
pi remove npm:pi-soft-compact # 卸载
pi update --extensions # 更新到新版
本机开发(下文 $REPO = 本仓库克隆目录):
# 方式 A:项目级符号链接(只在该项目下的会话生效;
# 指向活源码,改完 /reload 即生效)
ln -s $REPO/extensions .pi/extensions/pi-soft-compact
# 方式 B:用户级 extensions 目录符号链接 → 本机所有会话都加载(想常驻时用)
mkdir -p ~/.pi/agent/extensions
ln -s $REPO/extensions ~/.pi/agent/extensions/pi-soft-compact
# 方式 C:pi 包管理器(写 settings.json 条目;本地路径不复制,同样指向活源码;
# 若已有符号链接先删掉)
pi install $REPO
或临时(不持久化):
pi --extension $REPO/extensions/index.ts
npm 包只含
extensions/+ README.md(npm pack 实测:tarball 41.9 kB,解压 120.5 kB,共 7 个文件;package-lock.json 不进包);@sinclair/typebox是 pi 的 host 提供包(声明为 peerDependency,不重复安装);pi-goal-x 可选:目标机装了 就用它的真实 builder,没装用内置备份。
配置(环境变量,可选)
| 变量 | 默认 | 说明 |
|---|---|---|
PI_COMPACT_KEEP_RECENT_TOKENS |
20000 | 压缩切点保留的最近 token 数(对齐 Pi 的 compaction.keepRecentTokens) |
PI_COMPACT_RESERVE_TOKENS |
16384 | 预留 token(对齐 compaction.reserveTokens) |
PI_COMPACT_PI_DIST |
自动 | 强制指定 Pi 的 dist 目录(排障用) |
PI_COMPACT_ONE_SHOT_TIMEOUT_MS |
300000 | 兜底一次性摘要 LLM 调用的硬上限(Pi 等待扩展钩子,无界调用会卡死压缩操作;见 FIXES #15) |
PI_COMPACT_GATE_BLOCK_MAX |
8 | 单个授权周期内工具门最多挡几次,用尽即撤回授权(FIXES #16) |
PI_COMPACT_AUTH_TTL_MS |
900000 | 授权周期寿命;超期不再关门、不再烧兜底摘要(FIXES #16) |
PI_COMPACT_SLIM_MIN_CHARS |
600 | 参数回显超过这个长度才瘦身(越小越激进;设很大即关闭)(FIXES #18) |
PI_COMPACT_POST_PUBLISH_GATE |
未设(关) | =1 时启用“压缩后第一个回合拦 npm 发布类命令”的工具门,并在完成确认里附上该约束。默认关:通用包不应把作者个人的发布纪律强加给所有用户(“摘要里的命令是状态记录、不是待办”这句通用约束始终在) |
PI_GOAL_ROOT |
<cwd>/.pi/goals |
goal-x 存储根目录(重同步检测用) |
PI_GOAL_X_DIR |
自动查找 | pi-goal-x 包目录(<cwd>/node_modules、~/.pi/agent/npm/node_modules) |
PI_GOAL_X_DISABLE |
未设 | =1 时跳过对 pi-goal-x 的动态 import,只用内置备份构造器 |
Pi 1.4.x 的扩展接口没有读取 settings 的入口,所以切点参数用环境变量 对齐;不设置时与 Pi 默认值一致。
使用
- 手动:直接
/compact [补充要求],无需再发任何消息。原生压缩被取消后, 标记立即作为新 run 的触发消息送达,模型接着调compact出摘要;工具结果带terminate,run 立即结束、没有多余的收尾消息,直接回到输入态等待你下一条消息。 (TUI 会先闪一行Error: Compaction cancelled——那是扩展取消 原生压缩的固定回执,随后的标记 run 才是实际压缩,属正常现象。) 已有授权但模型尚未出摘要时再按一次/compact:重新投递标记(不再是哑弹, FIXES #14),不会静默取消。 - 手动取消压缩授权:按 Esc 取消正在跑的压缩(授权随即作废,FIXES #11);
任何时候想彻底解除工具门 / 作废残留授权,输入
/compact-cancel。 - 手动授权的文本入口:把标记整条作为消息发送(首行必须是
[COMPACTION AUTHORIZED / 压缩已授权])效果等同/compact。只是引用/粘贴 含标记文本的讨论消息不算授权(锚定匹配,FIXES #14;以前会误 arm 并把你的 消息从上下文里删掉)。 - 自动:接近窗口上限时标记 steer 进当前 run,模型在同一个 run 内调
compact完成压缩,随后用压缩后的上下文继续当前工作(不等下一条用户消息; 避免带着超大上下文跑多个 turn 撞上溢出)。 - 溢出:维持 Pi 原版行为(保证有界恢复)。
本轮(0.1.6)改了什么、改在哪一行、如何一键回滚:见源码仓库
CHANGES-0.1.6.md。
文件
| 文件 | 作用 |
|---|---|
extensions/index.ts |
扩展入口(全部状态机逻辑) |
extensions/goal-x-compat.ts |
goal-x 重同步兼容层(只读 goal 文件/ledger,动态 import 官方 builder) |
extensions/pi-compact-api.ts |
从已安装 Pi 的 dist 读取现行压缩提示词/工具函数,失败退回内置副本 |
extensions/prompts-fallback.ts |
内置提示词副本(pi 1.4.0,逐字节对齐) |
extensions/marker.ts |
[COMPACTION AUTHORIZED] 标记文本 |
测试
测试只存在于源码仓库(npm 包只含 extensions/ + README):单测 43 个场景
(≈0.7s);E2E 是真实 pi 进程(RPC)+ 真实会话文件跑扩展本身,模型端点
由脚本化 mock 提供,所以 9 个套件合计 ≈1.4 秒;两者并发跑(npm run test:all)≈1.5 秒
(同一批套件可选跑真实模型)。
覆盖手动/阈值/溢出三条路径、引用文本误 arm、Esc 取消、工具闸门与重开、
一次性兜底、goal-x 重同步分工,并附带“变异测试配方”(验证断言真能变红)。
详见源码仓库的 DEVELOPMENT.md。
已知代价 / 边界
- 失败调用不回显参数:任意工具的参数被 token 上限截断或不合 schema 时,回执里的整段被拒参数被替换为短诊断(保留失败原因与前 120 字符样本),省掉后续每轮的重复上下文;短回显(<600 字符)原样保留(FIXES #6/#18)。
- 旧压缩条目不重发:提交时若保留区包含已被新摘要概括的旧压缩条目,切点直接跳过去(条目仍留在会话文件中,只是不再发送),避免摘要里的摘要反复占底座。
- 格式与原生完全对齐:摘要尾部的
<read-files>/<modified-files>清单与details字段由 Pi 自己的utils.js函数从会话文件工具调用中机械计算 (与原生压缩同一套代码),落盘结构与原生压缩条目格式同构;唯一区别是 6 节 摘要正文由会话模型写(原生由专职一次性调用写)。 - 摘要里区分“模型推断”与“用户原话”:摘要格式末尾追加一节
## User's Own Words / 用户原话(逐字),要求逐字拄录用户的诉求、纠正、禁止项与决定 (不许转述、不许推想、不得掺入模型文本);其余各节显式标注为“模型在压缩时刻的推断”。 完成确认消息里也复述这个区分,并要求下一个实例在依据推断出的状态(文件、git、测试、版本) 行动前先核对实况。软约束:工具不校验这一节是否存在,弱模型不会因此压不动(不给压缩 添新的失败模式)。实现上是附加在 Pi 原生提示词之后(Pi 原文逐字内嵌,不被改写)。 /compact后的Error: Compaction cancelled是固定回执(扩展取消原生 压缩的必然结果),紧随其后的标记 run 才是实际压缩;想完全无痕可以在session_before_compact里返回{compaction}直接给摘要(牺牲"会话模型 自己写摘要"的设计)。- 弱模型偶尔会在无授权时试探性调用
compact→ 收到"未授权"后继续正常干活 (多一次 0-output 的小 turn);压缩已落地后,残留标记可能造成同样的反射调用, 由标记内的一次性条款 + 完成确认消息中和。 - re-trigger 行为:授权窗口内
session_before_compact再次触发时—— 手上已有摘要 → 交给 Pi 原生管线提交;是 manual(你又按了/compact) → 重新投递标记(永不哑弹,FIXES #14);是 threshold 且 run 活跃 → 取消并让 模型在同一个 run 里再试 compact;会话真空闲且无摘要 → 一次性兜底摘要 (带超时上限,见下条)。 - 兜底摘要永不无界:一次性摘要跑在 Pi 等待的钩子里,因此每次调用都
绑定
AbortSignal.any([超时, 压缩操作 signal, run signal])。旧版用ctx.signal(run 信号,Esc 不生效),端点挂起时压缩操作永不收尾 →isCompacting恒真 → TUI 把之后每条消息塞进"压缩队列"永不投递 (会话变砖,FIXES #15)。 - 被中断的 run 不烧一次性摘要:run 因 Esc / re-trigger 等被 abort 且授权 未消费时,不跑兜底摘要,只重新武装——下一条消息的 run 会重新注入标记。
- 摘要由对话模型自己写:弱模型摘要质量有限(这是设计目标:用便宜的一次
写换掉数百次失败重试;质量兜底 = 一次性摘要回退 + 后续可再
/compact)。 - 摘要超长被截断时按失败处理,走兜底。
- 僵尸恢复:会话在“标记已发、压缩未完成”时被中断(如关机),重新打开时自动 重新武装,下一条消息的 run 会先完成压缩再继续(该 run 内其他工具暂时被拦截; 周期的年龄取标记自己的时间戳,旧会话恢复时第一次工具调用就会因超 TTL 而放行)。