Skip to content

[Feature][Pi] Structured acp_delegate notifications for reliable turn folding and third-party UI integration #5750

Open

Description

@Pasitearko

Summary

Could the Pi integration expose acp_delegate notifications as persisted, typed custom messages with structured run identity and launch provenance, rather than only ordinary user messages containing notification text?

This would let extensions implement turn folding, task summaries, failure indicators, and other UI integrations without matching billion-context's human-readable wording with regular expressions. This is a request for a small producer-side interoperability contract, not for billion-context to implement a particular extension's UI.

Concrete consumer: pi-tidy

The pi-tidy maintainer explicitly deferred delegate-notification folding because identifying another extension's messages by their text is brittle:

If billion-context sent them as a dedicated custom message type carrying the run id, tidy could tie them to the turn that started them reliably.

The desired UI is to associate a completion/failure notice with the turn that launched the delegate, show counts such as 2 completed · ⚠ 1 failed, and expose the full notice when expanded. A late result must not become a new human turn or be attributed to whichever turn happens to be active when it arrives.

Current limitation

In the Pi delegate implementation inspected at daa082ea, notification delivery uses pi.sendUserMessage(text, { deliverAs: "steer" }). The producer already knows the run ID and terminal status, and launch tool-result details already include structured delegate information, but the notification transport does not expose an equivalent typed envelope to consumers.

Recognizing headers such as [acp_delegate completed] or [acp_delegate FAILED ⚠️] is fragile: wording changes, localized text, recovery/batched notices, and users quoting an actual notice all complicate identification. A metadata contract would avoid those ambiguities.

Suggested contract (field names are illustrative)

Pi already supports pi.sendMessage() with a dedicated customType, readable content, and persisted details. For example:

{
  customType: "billion-context:delegate-notification",
  content: originalNotificationText,
  display: true,
  details: {
    version: 1,
    runs: [
      {
        runId: "del_example",
        status: "completed",
        sourceSessionId: "...",
        originToolCallId: "...",
        // Optional alternative/additional persisted launch anchor:
        originEntryId: "..."
      }
    ]
  }
}

The important properties are:

  1. Dedicated type and versioned metadata. Consumers should not need to inspect content to decide whether a message is a delegate notification.
  2. Stable run identity and explicit status. Enough to deduplicate notices and distinguish completed/failed/cancelled outcomes. Auxiliary usage summaries should be distinguishable from terminal notifications so the same run is not counted twice.
  3. Resolvable launch ownership. Preserve a tool-call or persisted parent-message anchor captured at launch, together with session provenance. runId alone works only if the launch-to-turn mapping is also structurally available. Nested/codemode launches should have a root-call or equivalent persisted anchor, not require parsing a script or formatted acknowledgement.
  4. Persistence. Metadata should survive reload/resume and remain available alongside the original notification, rather than existing only on a transient event bus.
  5. Batches/recovery. If one notification covers several runs, expose every run explicitly. For batches spanning different owning turns, per-run display content or stable structured segments would let a consumer project each notice without parsing prose. Exact batching/schema design is up to the maintainer.

A simple initial contract would already be useful; consumers can leave unsupported or unresolvable cases visible rather than guessing ownership or silently hiding them.

Preserve existing agent behavior

Please keep the original model-visible notification text, persistence, steering/idle wake-up semantics, read/wait suppression, and failure visibility. This should not become a UI-only notification that the main agent no longer receives, nor request-local context injection.

Pi's custom-message conversion can pass content to the model while leaving details for extensions, so structured UI integration need not remove the existing notification from the model context. The sendMessage delivery options would still need to be chosen/tested to preserve current behavior; this is not a suggestion to mechanically swap the send call.

Useful acceptance checks

  • Run launched in turn A completes during turn B: provenance still identifies A.
  • Live delivery and reload/resume expose the same metadata, including batched/recovery notifications.
  • Completion/failure and usage summaries for one run can be distinguished and deduplicated.
  • Nested launches remain traceable to a persisted main-session origin.
  • Missing/compacted origins and unknown schema versions can safely remain visible.
  • A human pasting the same notification text is not classified as automation.
  • Main-agent notification content and wake-up behavior remain intact.

pi-tidy would own the consumer-side folding/rendering and conservative fallback behavior. A documented contract here would also benefit other transcript, task-panel, and UI extensions without each maintaining its own text parser.

Activity

  1. ranxianglei commented on Nov 5, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    🔶 需要决策
    (ai: vllm-qwen/qwen3.8-27b)

    查重:不重复。GitHub 全库搜「acp_delegate notification structured customType runId provenance」无命中;镜像在途 40+ 个 [PR] 无一针对本主题(最接近的 delegate 相关 PR 各管别的:占位符来源归属 / Pi 扩展列表识别 / user-scope 降级)。标签:pi + scope/wiring + p3(互操作缺口、非缺陷)+ needs-decision——因为这是功能请求且在定一个对外契约,按「功能类看到就实现禁止」先请你拍板方向再动手。

    八问

    1. 是什么问题:Pi 的 acp_delegate 子任务完成/失败通知,目前只能发成一条纯文字的 user 消息(措辞如 [acp_delegate completed] **reviewer** (runId \del_..`, exit 0)`)。第三方扩展(具体是 pi-tidy release v0.1.24 #59)想可靠地折叠/归属这些通知,只能正则去匹配措辞——措辞一改就失效。缺的是一个稳定的、结构化的「运行身份 + 启动来源锚点」信封。

    2. 什么原因导致的:通知的投递通道(sendUserMessage)本身只承载人类可读文本,没有 runId/status/来源锚点的类型化字段。根因是投递层没有结构化信封,不是措辞写得不好——所以修法在投递层加信封,不是继续调措辞。

    3. 影响范围与严重程度(按选项):

      • A 保持现状:只影响第三方 UI 扩展的接入体验(pi-tidy 无法折叠、每个扩展各自维护一套脆弱解析);bili 本体无 bug、无可用性损失 → 体验/互操作缺口,非缺陷,严重程度低。
      • B 加类型化信封(推荐):触及 pi-subagents 全部 4 条通知发送路径(单条直发 / 忙时合并 / 空闲批量 / 内联用量摘要)+ 启动期来源捕获;受益面是所有依赖 delegate 通知的外部扩展。做对=纯增益;做错可能波及模型可见文本与唤醒时序(见第 5 问)。
      • C = B + opt-in 开关:同 B,但多一个配置项,只有开启的实例走新路径。
    4. 怎么修复的(推荐方案思路):把 4 处 sendUserMessage(text,{deliverAs:"steer"}) 换成
      sendMessage({customType:"billion-context:delegate-notification", content:<同一份 text>, display:true, details:{version:1, runs:[{runId,status,sourceSessionId,originToolCallId}]}},{deliverAs:"steer"})。
      安全锚点:① content 逐字复用同一份构建好的 text + deliverAs 仍为 steer → 模型看到的字节完全一致、idle 唤醒语义不变;details 只在扩展侧(pi 的 convertToLlm 已实测把 role:"custom" 转成 role:"user" 且只带 content,details 不进模型线);② 在 execute(toolCallId,...) 启动时把 toolCallId + 宿主 session id 记进 DelegateRun,作为 originToolCallId/sourceSessionId(嵌套/codemode 场景锚到持久化的根调用锚点,不做脚本反解);③ host 无 sendMessage 能力时能力门控回退到 sendUserMessage。不改压缩引擎、不改代理 wire、不加新配置面。

    5. 是否有回归问题(按选项):

      • A:无。
      • B(均可防):① content 与旧 text 哪怕一字之差→模型可见文本漂移,必须复用同一份 text;② deliverAs 必须保持 steer,不能因换 API 变 followUp/nextTurn;③ 老会话没有 details→消费端须把「缺 details」当合法态优雅处理(继续可见);④ 不支持 sendMessage 的 host 版本须回退。用 e2e A/B 钉住「出站字节逐字一致」。
      • C:同 B,另加开关默认值风险(默认关=未开启者零变化但功能默认不可用;默认开≈等价 B)。
    6. 是否破坏缓存:不涉及。custom 消息在 pi 进程内就被转成 role:user 才出网,bili 代理看到的仍是 user 消息,前缀缓存字节不受影响。

    7. 是否改了设计方向:不涉及架构转向。纯增量:pi-subagents 边界内加一个对外互操作信封,压缩引擎/代理/协议都不动。

    8. 是否引入了新的配置:B 方案不引入任何新配置(零配置始终开启)。C 方案会新增 delegate.notifyStructured(布尔 opt-in)——这本身按 Config Surface Discipline 需你拍板才能加。

    待决问题

    要不要做这个对外互操作信封,以及做成哪种形态(A/B/C)。这是公开契约:一旦发布,pi-tidy 及后续扩展都会依赖字段名与 versioning,所以先定形再实现。

    选项对比表

    A 保持现状 B 类型化信封(推荐) C = B + opt-in 开关
    解决问题 ✗(消费端继续脆弱解析) ✓(可靠折叠/归属/去重) ✓(仅开启者)
    模型可见行为 不变 字节不变(content+steer 复用) 同 B
    回归风险 无 低且可防(见第5问) 同 B + 默认值选择
    新增配置面 无 无 有(需你拍板)
    工作量 0 中(4 路径 + 启动捕获 + e2e A/B) 中偏高(多一条代码路径)
    对老会话 — 缺 details=合法态,消费端兜底 同 B

    我的推荐

    B。理由:已对照 pi 0.83.0(devDep 钉死版本)的类型与 convertToLlm 实现验证过——sendMessage({customType,content,display,details},{deliverAs:"steer"}) 能让模型看到与现在逐字相同的内容、同时把 details 留在扩展侧,是真正低风险的纯增益;且不引入配置面,对所有实例一致生效。若你想更保守,选 C(但多一个配置项要一并拍板)。

    不决定的默认行为

    停在这里不动代码,issue 挂 needs-decision;pi-tidy 侧继续它的保守文本回退,不影响 bili 现有行为。等你选 A/B/C 后我按选定形态实现并出 PR。

  2. Pasitearko commented on Nov 6, 2026

    @Pasitearko
    Author

    如果c方案可以完整达成我的需求,那就使用c方案,开关默认为off,用户有需求可以自己开启

  3. ranxianglei commented on Nov 6, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    🔶 需要决策
    (ai: vllm-qwen/qwen3.8-27b)

    @Pasitearko 谢谢明确方向。先回答你的条件句:是的,C 方案(开关默认 off)能完整达成你列的全部验收项——前提是「开启者」受益,默认 off 的实例行为零变化。逐条对一下:

    • 启动于 turn A、在 turn B 期间完成 → details 里带启动时捕获的 originToolCallId+sourceSessionId,折叠归属到 A,与当前活跃 turn 无关 ✓
    • live 与 reload/resume 暴露相同元数据(含批量/恢复)→ custom 消息(role:"custom"+details)持久化进会话 JSONL,重载读回;批量/恢复路径走同一发送点,details.runs[] 覆盖该通知里的每个 run ✓
    • 完成/失败 vs 用量摘要可区分+去重 → details 用 kind(terminal/usage)+ runs[].status 区分 ✓
    • 嵌套/codemode 回溯到持久化的主会话来源 → 启动时锚到根调用 tool-call + 主会话 id,不做脚本反解 ✓
    • 缺失/被压缩的来源 + 未知 schema 版本安全可见 → version 字段 + 消费端把缺 details/未知版本当合法态,正文仍可见 ✓
    • 人类粘贴同样文本不被判为自动化 → 分类靠结构(role:"custom"+customType),不靠文本匹配;人手打的是 role:"user",天然不会被误判 ✓
    • 主代理内容与唤醒完整 → content 逐字复用同一份 text + deliverAs:"steer" 不变 ✓

    精确规格(供 owner 一行拍板)

    • 新配置键:delegate.notifyStructured(布尔),写在 ~/.pi/acp.json(global+project),默认 false。落在 pi-subagents 自己的 delegate.* 命名空间,完全套用现有 delegate.notifyIfRead/forceEnable 的模式;不加新的 BILI_* env、不动 billion-context 主配置。
    • off(默认):4 处通知点保持现状 sendUserMessage(text,{deliverAs:"steer"}),字节级零变化。
    • on:4 处通知点(单条直发 / 忙时合并 / 空闲批量 / 内联用量摘要)改发
      sendMessage({customType:"billion-context:delegate-notification", content:<同一份 text>, display:true, details:{version:1, kind:"terminal"|"usage", runs:[{runId,status,sourceSessionId,originToolCallId,agent?}]}},{deliverAs:"steer"})。
    • 来源捕获:启动时(execute(toolCallId,...))把 toolCallId + 宿主 session id 记进 DelegateRun(on/off 都捕获,成本可忽略);仅 on 时写进 details。
    • 能力门控回退:host 无 sendMessage 时即使 on 也回退 sendUserMessage。
    • 测试:e2e A/B 钉住「出站模型可见字节逐字一致」+ 开关两态 + 批量/恢复/用量三类 details 形状 + 老会话缺 details 兜底。

    八问(C-default-off 口径)

    1. 是什么问题:delegate 通知只有纯文字载体,第三方扩展无法可靠折叠/归属。
    2. 什么原因:投递层没有结构化信封(非措辞问题)。
    3. 影响范围/严重程度(off 默认):默认实例零变化;仅显式开启者获得可靠折叠——互操作增益,非缺陷修复。
    4. 怎么修:投递层加类型化信封(customType+content+details),来源启动期捕获,能力门控回退,开关 off 默认。
    5. 回归风险(off 默认):默认态=现状,无回归;on 态风险(content 须逐字一致 / deliverAs 须保持 steer / 老会话缺 details 兜底 / 无 sendMessage 回退)均可防,用 e2e A/B 钉住。
    6. 破坏缓存:不涉及(custom 消息进程内转 role:user 才出网,bili 代理仍见 user)。
    7. 改设计方向:不涉及,纯增量(pi-subagents 边界内加对外信封)。
    8. 新配置:新增 delegate.notifyStructured(布尔,默认 off,acp.json)——这是唯一的新配置面,按规则需 owner 签核后才能实现。

    待决 & 路由

    @Pasitearko 的 C-default-off 是明确的强推荐,我完全采纳为实施形态。但按本仓规则,最终 green-light + 这个新配置键的签核只能来自 owner @ranxianglei(功能类需 owner 拍板后才实现;新增配置面需 owner 事前批准)。@ranxianglei 若认可上面「精确规格」,回一句即可,我立即按此开分支出 PR(Closes #5750)。

    不决定的默认行为:停在决策门不动代码,issue 维持 needs-decision;pi-tidy 侧继续保守文本回退,不影响任何现有用户。

  4. ranxianglei commented on Nov 6, 2026

    @ranxianglei
    Owner

    c

  5. ranxianglei commented on Nov 6, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    收到,按 C 方案 实施:delegate.notifyStructured(默认 false),on 时 4 处通知点发类型化信封(customType + content 逐字不变 + deliverAs:"steer" + details{version:1, kind, runs[]}),启动期捕获 originToolCallId/sourceSessionId,host 无 sendMessage 时能力门控回退。开分支出 PR(Closes #5750),完成后回带八问结论。
    (ai: vllm-qwen/qwen3.8-27b)

  6. ranxianglei commented on Nov 6, 2026

    @ranxianglei
    Owner

    🤖 Powered by ework · qwen3.8-27b

    (ai: vllm-qwen/qwen3.8-27b)

    已按你拍板的 C 方案(开关默认 off,用户有需求可自行开启)实现并提交 PR:#5790 (Closes #5750)

    结构化八问

    1. 是什么问题:Pi 的 acp_delegate 完成/失败通知只以「纯文本 steering user message」形式到达宿主,第三方 UI(如 pi-tidy release v0.1.24 #59)只能靠正则解析措辞,才能把通知归到发起它的那一轮——脆弱,且迟到的结果会误挂到当前活跃轮次。
    2. 什么原因导致的:通知信道本身不携带任何机器可读的身份/来源信息,只有给人看的文字;没有可供消费方 key 的 typed surface。
    3. 影响范围与严重程度:仅影响 Pi 车道上想做可靠 turn-folding / 展开式通知的第三方集成;对主 agent、对模型可见行为均无影响。属能力缺口/集成摩擦,非可用性缺陷。默认 off ⇒ 现有安装零影响。
    4. 怎么修复的:新增 opt-in typed envelope。当 delegate.notifyStructured 开启且宿主支持 ExtensionAPI.sendMessage 时,4 个通知点改为投递 custom message,其版本化 details 携带 runId/status/agent + 启动溯源(originToolCallId 在启动时捕获、sourceSessionId best-effort);content 与旧消息逐字节一致、投递仍为 steer;宿主无 sendMessage 时自动回退纯文本路径(能力门)。
    5. 是否有回归问题:默认 off = 逐字节走旧路径(有测试钉住:走纯路径且 sendMessage 不被触碰)。既有通知套件全部不变通过(本地定向 129/129)。高危模式逐项过:模型可见字节不变(convertToLlm 丢弃 details → 线上不变)、失败即未送达的恢复语义保留、injectResult 的 usage 副作用次序保留。
    6. 是否破坏缓存:否。convertToLlm() 在请求离开 pi 之前就剥掉 details,代理/上游看到的仍是 role:user 且 content 一致,前缀缓存不受影响。
    7. 是否改了设计方向:无架构转向。只在 pi 扩展自身配置命名空间上加了 opt-in 面;4 个发送点收敛到一个共享 dispatcher。
    8. 是否引入了新的配置:是 —— delegate.notifyStructured(boolean,默认 false),位于 ~/.pi/acp.json(全局+项目)。迁移:无需迁移(默认 false = 旧行为);显式开启才启用。

    行为变更披露:默认 off 时与现状逐字节一致,无任何回归面;开启后投递信道由 sendUserMessage 切换为 sendMessage(typed custom message),但模型可见字节不变(同一 content + steer)——这是有意为之的 opt-in 变更。

    验证:tsc --noEmit 干净、tsup 构建成功;本地定向套件 129/129 绿(新增 tests/delegate-structured-notify.test.ts 覆盖:默认-off 一致性、结构化分发+内容逐字节一致、能力回退、batch 列全 run、溯源在场/缺省皆安全、idle 与 injectResult 两条路径;既有 3 个通知套件不变)。全量回归以 CI 为准。

    判定:可合入,等 CI 绿 + 你 review。合并权在你。

  7. added 5 commits that reference this issue on Nov 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions