优先级与时间窗
高。 这些是已经在 develop 上的行为缺陷,不是未来功能:v1 已协商的连接现在就会读出、并在某些路径写出 v2 草案 wire。与 #149 的关系是:#149 记录「v2 有哪些东西没建模」,本 issue 记录「已建模的这些东西泄漏进了 v1 通道」。
修法与 #165(草案面公开边界)有交集(读路径的正解顺带会被那边的重构做掉),建议两个一起排,并先定公开面方向再动手 —— 否则这里做的写路径闸门会被返工。
结论
PR #160 建模的 v2 wire 与 v1 通道之间没有完整隔离,三处具体缺陷:
- 读路径完全没有版本闸门 —— v1 连接收到 v2 判别值仍会物化成草案类型交给上层。
- 四族在 V1 默认写上下文照常吐 v2 报文 —— whole-message、terminal、tool_call_content_chunk、plan_update。
StructuredDiff 在 v1 读得进、写不出 —— 破坏 round-trip,违反 AGENTS.md §11「协议宽松度不得反向收紧」明文的原样 round-trip 要求。
这三条合起来意味着 AGENTS.md §11 要求的「显式草案上下文的隔离 wire 测试」目前只覆盖 2/6 族。
证据链
1. 版本检查全都只在 Write 一侧
src/SalmonEgg.Acp/Protocol/SessionWorkStateUpdateTypes.cs:199 Write: if (AcpProtocolWriteContext.Current != V2) throw
src/SalmonEgg.Acp/Protocol/SessionWorkStateUpdateTypes.cs:150-178 Read: 无任何版本检查
src/SalmonEgg.Acp/Tool/DiffTypes.cs:248 同样只在 Write
src/SalmonEgg.Acp/Protocol/OtherSessionTypes.cs:275 同样只在 Write
src/SalmonEgg.Acp/Protocol/SessionUpdateTypes.cs:70-76 7 个 v2 判别子静态注册在 public SessionUpdate 上
AcpProtocolWriteContext 顾名思义只是写上下文(src/SalmonEgg.Acp/Protocol/AcpProtocolWriteContext.cs),读路径没有对应概念。于是在已协商为 v1 的连接上,agent 发来 "sessionUpdate":"agent_message" / "terminal_update" / "state_update",SDK 会照样绑定成草案类型交给上层。
AcpProtocolVersion.RuntimeServed = V1 这个「唯一权威」(AcpProtocolVersion.cs:63)在读方向上其实是半开的。任何只比对这个常量的门禁都发现不了这一点。
2. 四族零闸门(逐文件计数)
Protocol/SessionUpdateTypes.cs AcpProtocolWriteContext 出现 0 次
Protocol/TerminalUpdateTypes.cs AcpProtocolWriteContext 出现 0 次
Protocol/V2SupplementalTypes.cs AcpProtocolWriteContext 出现 0 次
Protocol/SessionWorkStateUpdateTypes.cs 出现 1 次 ← state_update 有闸门
Tool/DiffTypes.cs 出现 1 次 ← structured diff 有闸门
tests/SalmonEgg.Acp.Tests/Protocol/TerminalUpdateTypesTests.cs:105-129 就是在默认(V1)上下文里断言 terminal_update 的 v2 报文形态成立 —— 现状被测试固化了。
3. StructuredDiff 的 round-trip 破坏
StructuredDiff 与 v1 的 flat diff 共用 "diff" 判别值,只靠有没有 changes 数组区分(src/SalmonEgg.Acp/Tool/DiffTypes.cs:185-187)。于是 v1 连接上收到一个带 changes 的 diff → 绑成 StructuredDiff → 在 V1 上下文不可再序列化(:248 抛 JsonException)。
在 PR #160 之前,这种载荷会落到 CustomToolCallContent 原样 round-trip。所以这是一次行为回归,而不只是「新功能没做完」。
正解与反模式
反模式:别照抄现有的「写时抛」
现有 state_update / structured diff 用的是「先绑定成草案类型,写的时候如果不是 V2 上下文就抛」。这个形状本身就是次优解 —— 它制造了「读得进、写不出」,也就是第 3 条缺陷的形状。如果给剩下四族照抄这个写法,等于把 round-trip 破坏面从 1 族扩大到 5 族。
正解:读路径分流
v1 上下文下根本不该绑定成草案类型,而应走 passthrough:这样既不吐 v2 报文,又保持原样 round-trip,两条 AGENTS 规则同时满足。
好消息是基础设施已经就位。 SessionUpdate 的多态配置(SessionUpdateTypes.cs:55-58)已经是:
[JsonPolymorphic(
TypeDiscriminatorPropertyName = "sessionUpdate",
UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType,
IgnoreUnrecognizedTypeDiscriminators = true)]
未知判别值回落到基类 + ExtensionData 承载完整 payload(已有测试 SessionUpdatePolymorphismTests.cs:39 Deserialize_UnknownSessionUpdate_FallsBack…)。也就是说 —— 把 v2 判别子从 [JsonDerivedType] 列表里摘掉,v1 上下文下的 round-trip 就自动是对的。
但 [JsonDerivedType] 是静态特性,无法按运行时上下文动态注册。要做上下文相关分流,必须走手工分派 —— 而 SessionUpdateParams 已经有手工转换器 SessionUpdateParamsJsonConverter(SessionUpdateTypes.cs:113),同文件 :224 的 StateSessionUpdateWireFormat 就是现成先例(注释里已写明 STJ 不支持第二判别子,所以扁平化只能落在容器转换器上)。
这正是它与 #165 的交集:那边若选择把判别子搬进转换器 + 草案面 internal 化,这里的读路径闸门顺带就做完了。所以先定那边的方向。
"diff" 判别值的处置
v1 flat diff 与 v2 structured diff 共用判别值这件事无法靠改判别值解决(协议就是这么定的)。建议:在 v1 上下文下,带 changes 的 diff 走 CustomToolCallContent passthrough(恢复 #160 之前的行为);只在 v2 上下文下绑成 StructuredDiff。
验证要求
照 #149 已定的标准,逐项都要:
- v1 与 v2 两条线的报文级断言,互不串味 —— v1 报文不得出现 v2 字段,反之亦然。六族都要有(现在只有 state_update 和 structured diff 有)。
- 读方向也要有断言,不能只测写方向:v1 上下文下喂 v2 判别值 → 断言落到 passthrough 且原样 round-trip 回去(字节级或字段级不丢);v2 上下文下喂同一份 → 断言绑成对应草案类型。
- 反向验证:拆掉版本判别后,对应报文断言必须转红。
TerminalUpdateTypesTests.cs:105-129 这类在默认上下文断言 v2 形态的测试要一并改成显式 v2 上下文,否则它会锁死错误行为。
scripts/gates/run-acp-sdk-gates.sh 全绿;scripts/gates/wasm-capability-boundary-smoke.mjs:96-126 已覆盖「真实 WASM initialize 只发 v1 形态」,本次改动不应触及它,但要确认仍绿。
参考资料
优先级与时间窗
高。 这些是已经在 develop 上的行为缺陷,不是未来功能:v1 已协商的连接现在就会读出、并在某些路径写出 v2 草案 wire。与 #149 的关系是:#149 记录「v2 有哪些东西没建模」,本 issue 记录「已建模的这些东西泄漏进了 v1 通道」。
修法与 #165(草案面公开边界)有交集(读路径的正解顺带会被那边的重构做掉),建议两个一起排,并先定公开面方向再动手 —— 否则这里做的写路径闸门会被返工。
结论
PR #160 建模的 v2 wire 与 v1 通道之间没有完整隔离,三处具体缺陷:
StructuredDiff在 v1 读得进、写不出 —— 破坏 round-trip,违反 AGENTS.md §11「协议宽松度不得反向收紧」明文的原样 round-trip 要求。这三条合起来意味着 AGENTS.md §11 要求的「显式草案上下文的隔离 wire 测试」目前只覆盖 2/6 族。
证据链
1. 版本检查全都只在 Write 一侧
AcpProtocolWriteContext顾名思义只是写上下文(src/SalmonEgg.Acp/Protocol/AcpProtocolWriteContext.cs),读路径没有对应概念。于是在已协商为 v1 的连接上,agent 发来"sessionUpdate":"agent_message"/"terminal_update"/"state_update",SDK 会照样绑定成草案类型交给上层。AcpProtocolVersion.RuntimeServed = V1这个「唯一权威」(AcpProtocolVersion.cs:63)在读方向上其实是半开的。任何只比对这个常量的门禁都发现不了这一点。2. 四族零闸门(逐文件计数)
tests/SalmonEgg.Acp.Tests/Protocol/TerminalUpdateTypesTests.cs:105-129就是在默认(V1)上下文里断言terminal_update的 v2 报文形态成立 —— 现状被测试固化了。3.
StructuredDiff的 round-trip 破坏StructuredDiff与 v1 的 flat diff 共用"diff"判别值,只靠有没有changes数组区分(src/SalmonEgg.Acp/Tool/DiffTypes.cs:185-187)。于是 v1 连接上收到一个带changes的 diff → 绑成StructuredDiff→ 在 V1 上下文不可再序列化(:248抛JsonException)。在 PR #160 之前,这种载荷会落到
CustomToolCallContent原样 round-trip。所以这是一次行为回归,而不只是「新功能没做完」。正解与反模式
反模式:别照抄现有的「写时抛」
现有 state_update / structured diff 用的是「先绑定成草案类型,写的时候如果不是 V2 上下文就抛」。这个形状本身就是次优解 —— 它制造了「读得进、写不出」,也就是第 3 条缺陷的形状。如果给剩下四族照抄这个写法,等于把 round-trip 破坏面从 1 族扩大到 5 族。
正解:读路径分流
v1 上下文下根本不该绑定成草案类型,而应走 passthrough:这样既不吐 v2 报文,又保持原样 round-trip,两条 AGENTS 规则同时满足。
好消息是基础设施已经就位。
SessionUpdate的多态配置(SessionUpdateTypes.cs:55-58)已经是:未知判别值回落到基类 +
ExtensionData承载完整 payload(已有测试SessionUpdatePolymorphismTests.cs:39 Deserialize_UnknownSessionUpdate_FallsBack…)。也就是说 —— 把 v2 判别子从[JsonDerivedType]列表里摘掉,v1 上下文下的 round-trip 就自动是对的。但
[JsonDerivedType]是静态特性,无法按运行时上下文动态注册。要做上下文相关分流,必须走手工分派 —— 而SessionUpdateParams已经有手工转换器SessionUpdateParamsJsonConverter(SessionUpdateTypes.cs:113),同文件:224的StateSessionUpdateWireFormat就是现成先例(注释里已写明 STJ 不支持第二判别子,所以扁平化只能落在容器转换器上)。这正是它与 #165 的交集:那边若选择把判别子搬进转换器 + 草案面 internal 化,这里的读路径闸门顺带就做完了。所以先定那边的方向。
"diff"判别值的处置v1 flat diff 与 v2 structured diff 共用判别值这件事无法靠改判别值解决(协议就是这么定的)。建议:在 v1 上下文下,带
changes的 diff 走CustomToolCallContentpassthrough(恢复 #160 之前的行为);只在 v2 上下文下绑成StructuredDiff。验证要求
照 #149 已定的标准,逐项都要:
TerminalUpdateTypesTests.cs:105-129这类在默认上下文断言 v2 形态的测试要一并改成显式 v2 上下文,否则它会锁死错误行为。scripts/gates/run-acp-sdk-gates.sh全绿;scripts/gates/wasm-capability-boundary-smoke.mjs:96-126已覆盖「真实 WASM initialize 只发 v1 形态」,本次改动不应触及它,但要确认仍绿。参考资料
https://raw.githubusercontent.com/zed-industries/agent-client-protocol/main/schema/v2/schema.jsonhttps://agentclientprotocol.com/llms.txt→protocol/v1/schema.md