Skip to content

为 ACP block 数据提供机器可读的对外契约(schemaVersion + ./contract 导出) #368

Description

@tjp72

下游 pi-billion-memory 目前只能 glob ~/.pi/agent/sessions/**/*.jsonl.acp.json 直接解析 sidecar 来读取 ACP 压缩块。这不是 bug,但绑死私有文件格式的代价是静默失效:字段改名或目录调整时下游会扫到 0 个文件而不报错,最后以「bcp 是不是改了格式」的 issue 形式回到你们这里。

请求很小,按成本从低到高,做到前三条就够;不要求任何行为变更,全部是加法:

  1. R1(几分钟):sidecar 顶层加 schemaVersion(+ producer),并约定「字段缺失 = v1」「未知版本 = 跳过 + 只记一次日志、不得改写」。
  2. R2(成本≈0)package.json 增加 "./contract" subpath 导出:BcpBlockV1 类型 + schema/bcp-block-v1.json(JSON Schema,可选 reader)。跨进程、跨重启都有效,且不需要事件通道。
  3. R3(一句文档):把「sidecar 用 tmp+rename 原子替换」写成承诺(代码已经是这个模式)。
  4. R4(可选):压缩成功落盘后 pi.events.emit("bcp:blocks", …);明说它只覆盖同进程/同会话,文件仍是 source of truth
  5. R5(可选):compressdetails 若要做,请只放有界信息(如 {schemaVersion, acceptedIds, revision})——details 会写进会话 .jsonl,整份 blocks 会在 sidecar 之外再抄一份。
  6. P3(pi.appendEntry 写 custom entry)不是必须,可以以后再说。

背景

  • bcp 是 pi 侧压缩的 owner:session_before_compact 返回 cancel: true,压缩由 ACP 完全接管,结果只写 <sessionFile>.acp.json
  • 下游需要这些块做跨会话检索(ACP 块 → SQLite FTS5 → memory_search / memory_expand)。块本身很有价值,但目前没有任何公开接口能拿到。
  • 下游坚持零运行时依赖(dependencies: {}),并已把 bcp 声明为可选 peer("billion-context-pi": ">=0.1.65")——所以「类型 + schema」这种契约对双方都是最低成本。

现状(0.1.65 dist,可直接核验)

事实 核验方式
压缩由 bcp 接管 grep -o 'session_before_compact", () =>' dist/index.jsgrep -o 'cancel: true' dist/index.js
只写 sidecar grep -o 'STATE_SUFFIX = "[^"]*"' dist/index.js.acp.json
canonical blocks 不对外 compress 工具结果全部是 details: void 0dist/compress-tool.d.ts
入口没有块类型 dist/index.d.ts 只导出 createAcpExtensionexports 只有 "."
没有事件、没有 custom entry grep -c appendEntry dist/index.js → 0;.emit( → 0
读写无版本校验 dist/state.d.ts 只有 SessionStateStore / LiveRefOrigin 等内部类型
侧挂文件已原子写 writeFile(tmp, JSON.stringify({ ...state, liveRefOrigins }… + rename 模式,但没有对外承诺

旁证:isCompressSuccessText / isCompressNoopTextdist/compress-tool.d.ts)说明连 bcp 自己也要从工具结果的文本里判断成败——文本通道有损,而结构化通道目前不存在。README:216-236 已经把 sidecar 描述为用户可见产物并说明了 #299 的 replay 语义,但没有任何机器可读的版本/契约。

问题

  1. 格式耦合:字段改名、路径调整、null 化 → 下游静默失效(扫到 0 文件或解析成 null),没有任何报错。
  2. 没有版本标识:无法区分格式代际,只能「解析失败、下轮重试」,也就无法提示用户「升级下游」还是「升级 bcp」。
  3. 没有权威数据compress 的入参是模型产出的草案,最终落盘值可能被你们修正/拒绝;下游拿不到「最终被接受的那份」。
  4. 变更没有通知渠道:上游发版后,下游只能靠用户反馈发现问题——而这批反馈往往会打回你们(对我们来说是多一个误报来源,对你们来说是噪音)。

请求(任选,不必全做)

R1 sidecar 顶层加版本标识(几分钟)

{
  "schemaVersion": 1,
  "producer": { "name": "billion-context-pi", "version": "<version>" },
  "blocks": [ /* ... */ ]
}

语义建议一并写进 README / CHANGELOG:

  • 字段缺失 = schemaVersion: 1(否则现存 sidecar 全部变成「未知版本」);
  • 未知版本:下游应跳过并只记一次日志,不得改写文件;
  • 只在破坏性变更时升版本,其余字段只增不改。

R2 ./contract subpath 导出(成本≈0,最耐久)

"exports": {
  ".":          { "types": "./dist/index.d.ts",     "import": "./dist/index.js" },
  "./contract": { "types": "./dist/contract.d.ts",  "import": "./dist/contract.js" }
}

contract 里只放三样东西:BcpBlockV1 类型、SCHEMA_VERSION 常量、schema/bcp-block-v1.json(JSON Schema)。零运行时依赖、跨进程、跨重启有效,比「承诺事件 payload 只增不改」维护成本低得多;下游可以直接对着 schema 跑校验和集成测试。

R3 把原子写写成承诺

现在的写入模式已经是 tmp+rename;请把它写进 README(或加一条测试)。下游的水位线(mtime/size)+ 整文件替换是绑定的,非原子写会让下游读到撕裂的 JSON。

R4 落盘成功后 emit 事件(可选)

pi.events.emit("bcp:blocks", {
  schemaVersion: 1,
  sessionId,
  sessionFile,
  cwd,
  revision,               // 单调递增(或落盘时间戳),供下游幂等/排序
  reason: "compress",     // compress | prune | ...
  added:   BcpBlock[],    // 本次新增
  updated: BcpBlock[],    // 本次变更(若有)
  removed: string[],      // 被替换/吞并的 blockId(若有)
});
  • 建议在「保存成功之后」发出;增量语义维护成本高,就直接发全量 blocks 快照——下游只需要一个明确的「当前真相」;
  • 但请写明边界:事件只能覆盖同进程、同会话,而 pbm 的价值在跨会话、可指向任意 sources root 的扫描。事件只是低延迟加成,文件仍是 source of truth

R5 compressdetails 若要做,请有界

pi 的 session format 里 toolResult message 带 details?docs/session-format.md:94-98:246 / :260 说明语义),也就是说 details落盘进会话文件。整份 blocks 放进去 = 在 sidecar 之外再抄一份大内容。建议只放 { schemaVersion, acceptedIds, revision } 这类有界引用,或干脆不做(R1/R2 已覆盖主要收益)。

P3(可选,以后再说)写 custom entry

pi.appendEntry("bcp.blocks", payload) + 下游 ctx.sessionManager.getEntries().filter(e => e.type === "custom" && e.customType === "bcp.blocks")。好处:由 pi 统一持久化、版本迁移、树管理,custom entry 不进 LLM 上下文(pi 文档 docs/extensions.md:1620)。代价:会话文件变大(每次压缩写一份,写放大),且跨会话回填仍要开别人的文件。

块的最小稳定字段(字段名可按你们习惯调整)

interface BcpBlockV1 {
  // --- stable core(建议只增不改)---
  blockId: string;              // 幂等键
  startId: string;
  endId: string;
  summary: string;
  topic?: string;
  // --- optional bag(允许缺失或改语义)---
  compressedTokens?: number;
  msgIds?: string[];
  refs?: string[];
  sourceSessionFile?: string;
  cwd?: string;
  createdAt?: string;           // ISO 8601
}

不需要暴露内部修剪/索引细节,只要「最终被接受的块」。

兼容性

  • 以上全部是加法:不改 sidecar 位置、现有读写路径和 pi 集成行为;
  • 下游按 blockId 幂等 upsert,文件扫描保留为历史回填;
  • 若采纳 R2,建议在 CHANGELOG 承诺:BcpBlockV1 字段只增不改不删,破坏性变更升 schemaVersion

相关 issue

它们和本 issue 指向同一个缺口:ACP 状态目前只有「私有文件格式」这一个出口。本 issue 只请求机器可读的版本/类型边界,不请求行为变更。

参考

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions