结论
AcpProtocolVersion.Latest = V2 但 Default = V1,且运行时硬性拒绝以 V2 初始化。已建模的只是方法名分叉与少数类型;v2 的核心变更——新 prompt 生命周期(state_update)、整消息 upsert、agent 自持终端、流式工具内容、权限主体、diff 结构化——一项都没有。本 issue 记录完整缺口清单与依赖顺序,不主张立刻实现(v2 仍是 draft)。
证据链
1. 代码自认未完成
src/SalmonEgg.Acp/Protocol/AcpProtocolVersion.cs:
/// Draft ACP v2 wire model. Live client support remains disabled until the complete
/// prompt, update, permission, configuration, and batch lifecycles are implemented.
public const int V2 = 2;
public const int Default = V1;
/// Highest protocol version whose wire contracts are modeled by this SDK. This does not
/// imply that the live client runtime is ready to negotiate that draft version.
public const int Latest = V2;
运行时闸门 src/SalmonEgg.Acp/Client/AcpClient.cs:189-196:
if (AcpProtocolVersion.IsSupported(@params.ProtocolVersion)
&& @params.ProtocolVersion != AcpProtocolVersion.V1)
{
throw new AcpException(
JsonRpcErrorCode.ProtocolVersionMismatch,
StableV1RuntimeOnlyMessage);
}
StableV1RuntimeOnlyMessage(:25):"ACP live client support is limited to stable protocolVersion 1 while newer modeled versions remain draft or incomplete."
2. 已经做对的部分(不要重做)
方法名按版本分叉已就位,AcpClient.cs:719 / :756:
var methodName = _protocolVersion == AcpProtocolVersion.V2 ? "auth/login" : "authenticate";
var methodName = _protocolVersion == AcpProtocolVersion.V2 ? "auth/logout" : "logout";
SessionResumeParams.ReplayFrom 已建模(OtherSessionTypes.cs:339,配 SessionReplayFrom 开放 type 判别,:185-295)。
3. 缺口清单(逐项 grep 核实,src/ 零命中;同名巧合已排除)
| v2 类型 / 机制 |
v2 schema 位置 |
我们 |
StateUpdate + RunningStateUpdate / IdleStateUpdate / RequiresActionStateUpdate |
:5579 :4619 :3341 :4531 |
无 |
AgentMessage / AgentThought / UserMessage(整消息 upsert 变体) |
:1214 :1250 :6494 |
无(只有 v1 的 *_chunk) |
TerminalUpdate / TerminalOutput / TerminalOutputChunk |
:5978 :5928 :5951 |
无 |
ToolCallContentChunk(流式工具内容) |
:6224 |
无 |
DiffChange / DiffPatch / DiffPatchFormat / DiffFileType / DiffPathChange / DiffPathPairChange |
:2181 :2340 :2356 :2310 :2377 :2389 |
无(v1 的三字段 path/oldText/newText 已合规,见 Tool/ToolCallContent.cs:63-77) |
RequestPermissionSubject / ToolCallPermissionSubject / CommandPermissionSubject |
:4454 :6290 :1790 |
无 |
PromptImageCapabilities / PromptAudioCapabilities / PromptEmbeddedContextCapabilities(bool → 对象) |
:4257 :4170 :4239 |
无(PromptCapabilities 三个字段仍是 bool,InitializeTypes.cs:537-556) |
McpHttpCapabilities / TerminalAuthCapabilities |
:3513 :5868 |
无 |
Icon / IconTheme |
:3289 :3319 |
无 |
SessionListCursor |
:5080 |
无(分页游标目前是裸 cursor) |
TextCommandInput |
:6019 |
无 |
PlanUpdateContent / plan 变体 |
:4109 |
无 |
4. 我们仍携带 v2 已删除的表面
按 /tmp/acp-v2-migration.md「Session modes become config options」(:586)与「session/load is gone. session/resume does both」(:554),下列在 v2 已移除,我们仍建模(v1 需要,因此不能直接删,只能做版本门控):
session/load:IAcpClient.cs、AcpClient.cs、OtherSessionTypes.cs、SessionUpdateTypes.cs
session/set_mode + SessionMode / SessionModesState / CurrentModeUpdate:IAcpClient.cs、AcpClient.cs、OtherSessionTypes.cs、SessionNewTypes.cs、SessionUpdateTypes.cs、PublicSurface.Types.txt
另有两处 v2 语义变更需在门控时一并处理:
- MCP
"type": "sse" 在 v2 移除,且 stdio 在 v2 必须带 "type": "stdio"(v1 无此字段)。我们的 Mcp/McpServerConfig.cs:407-443 三种都写、都读。
- v2 里
configOptions 的标识字段由 id 改名为 configId;我们 ConfigOptionTypes.cs:14 是 id。
- v2 里
messageId 在每个 chunk/message 上必填;我们 ContentChunkUpdate.MessageId 是 string?(SessionUpdateTypes.cs:165-169),v1 语义正确、v2 需收紧。
5. 最大的结构性差异:prompt 生命周期
/tmp/acp-v2-migration.md:248-295 原文要点:
In v2, the Agent MUST respond to session/prompt as soon as it has accepted the prompt, with an empty result
... the Agent MUST report where the user message was inserted into session history
... When foreground work starts or resumes, the Agent MUST send a state_update with "state": "running"
... When the Agent is ready to process a new prompt, it MUST report idle. When the transition ends foreground work, it MUST include the stop reason
即 v1 的「响应即整轮、stopReason 在响应里」变成「响应只是 ack、stopReason 由 idle 状态携带」。我们当前是纯 v1 模型:session/prompt 的返回值就是终态。这不是加几个类型能解决的,它改变了 AcpClient 与上层 ChatService / ChatViewModel 之间的完成语义,是本 issue 里唯一需要跨层重构的项。
取消语义也随之变化(:310):v2 不再用 "stopReason": "cancelled" 响应,改为发完 pending 更新后送一个带 cancelled stop reason 的 idle state_update。
影响
当前无实际用户影响——v2 是 draft,且我们 fail-closed 拒绝以 V2 初始化(这是正确的默认)。风险在于 Latest = V2 这个常量可能被误读为「已支持」:它的注释说清了,但常量名本身有歧义。
建议范围
不建议现在整体实现。建议:
- 先做无争议的一步:把
Latest 的语义在命名上说清(如改为 HighestModeled 或类似),消除「Latest 就是可用」的误读面。这一步独立、低风险。
- v2 转 stable 后再排后续,依赖顺序大致为:能力对象化 → 消息/messageId 收紧 →
state_update 生命周期(跨层,最重)→ 工具调用 upsert 与流式内容 → diff 结构化 → 权限主体 → 终端归属反转。
- 版本门控原则:v1/v2 差异用声明式分叉表达(现有
auth/login 那种按版本取名的写法可以延续),不要在共享类型上塞双语义字段。
验证要求
真正实现某一项时:
- 每项都要有 v1 与 v2 两条线的报文级断言,且互不串味(v1 报文不得出现 v2 字段,反之亦然)。
state_update 生命周期需真实 agent 端到端跑通,断言完成语义(何时算一轮结束)在两个版本下都正确。
- 反向验证:拆掉版本判别后,对应报文断言必须转红。
结论
AcpProtocolVersion.Latest = V2但Default = V1,且运行时硬性拒绝以 V2 初始化。已建模的只是方法名分叉与少数类型;v2 的核心变更——新 prompt 生命周期(state_update)、整消息 upsert、agent 自持终端、流式工具内容、权限主体、diff 结构化——一项都没有。本 issue 记录完整缺口清单与依赖顺序,不主张立刻实现(v2 仍是 draft)。证据链
1. 代码自认未完成
src/SalmonEgg.Acp/Protocol/AcpProtocolVersion.cs:运行时闸门
src/SalmonEgg.Acp/Client/AcpClient.cs:189-196:StableV1RuntimeOnlyMessage(:25):"ACP live client support is limited to stable protocolVersion 1 while newer modeled versions remain draft or incomplete."2. 已经做对的部分(不要重做)
方法名按版本分叉已就位,
AcpClient.cs:719/:756:SessionResumeParams.ReplayFrom已建模(OtherSessionTypes.cs:339,配SessionReplayFrom开放type判别,:185-295)。3. 缺口清单(逐项 grep 核实,
src/零命中;同名巧合已排除)StateUpdate+RunningStateUpdate/IdleStateUpdate/RequiresActionStateUpdate:5579:4619:3341:4531AgentMessage/AgentThought/UserMessage(整消息 upsert 变体):1214:1250:6494*_chunk)TerminalUpdate/TerminalOutput/TerminalOutputChunk:5978:5928:5951ToolCallContentChunk(流式工具内容):6224DiffChange/DiffPatch/DiffPatchFormat/DiffFileType/DiffPathChange/DiffPathPairChange:2181:2340:2356:2310:2377:2389path/oldText/newText已合规,见Tool/ToolCallContent.cs:63-77)RequestPermissionSubject/ToolCallPermissionSubject/CommandPermissionSubject:4454:6290:1790PromptImageCapabilities/PromptAudioCapabilities/PromptEmbeddedContextCapabilities(bool → 对象):4257:4170:4239PromptCapabilities三个字段仍是bool,InitializeTypes.cs:537-556)McpHttpCapabilities/TerminalAuthCapabilities:3513:5868Icon/IconTheme:3289:3319SessionListCursor:5080TextCommandInput:6019PlanUpdateContent/ plan 变体:41094. 我们仍携带 v2 已删除的表面
按
/tmp/acp-v2-migration.md「Session modes become config options」(:586)与「session/loadis gone.session/resumedoes both」(:554),下列在 v2 已移除,我们仍建模(v1 需要,因此不能直接删,只能做版本门控):session/load:IAcpClient.cs、AcpClient.cs、OtherSessionTypes.cs、SessionUpdateTypes.cssession/set_mode+SessionMode/SessionModesState/CurrentModeUpdate:IAcpClient.cs、AcpClient.cs、OtherSessionTypes.cs、SessionNewTypes.cs、SessionUpdateTypes.cs、PublicSurface.Types.txt另有两处 v2 语义变更需在门控时一并处理:
"type": "sse"在 v2 移除,且 stdio 在 v2 必须带"type": "stdio"(v1 无此字段)。我们的Mcp/McpServerConfig.cs:407-443三种都写、都读。configOptions的标识字段由id改名为configId;我们ConfigOptionTypes.cs:14是id。messageId在每个 chunk/message 上必填;我们ContentChunkUpdate.MessageId是string?(SessionUpdateTypes.cs:165-169),v1 语义正确、v2 需收紧。5. 最大的结构性差异:prompt 生命周期
/tmp/acp-v2-migration.md:248-295原文要点:即 v1 的「响应即整轮、stopReason 在响应里」变成「响应只是 ack、stopReason 由 idle 状态携带」。我们当前是纯 v1 模型:
session/prompt的返回值就是终态。这不是加几个类型能解决的,它改变了AcpClient与上层ChatService/ChatViewModel之间的完成语义,是本 issue 里唯一需要跨层重构的项。取消语义也随之变化(
:310):v2 不再用"stopReason": "cancelled"响应,改为发完 pending 更新后送一个带cancelledstop reason 的 idlestate_update。影响
当前无实际用户影响——v2 是 draft,且我们 fail-closed 拒绝以 V2 初始化(这是正确的默认)。风险在于
Latest = V2这个常量可能被误读为「已支持」:它的注释说清了,但常量名本身有歧义。建议范围
不建议现在整体实现。建议:
Latest的语义在命名上说清(如改为HighestModeled或类似),消除「Latest 就是可用」的误读面。这一步独立、低风险。state_update生命周期(跨层,最重)→ 工具调用 upsert 与流式内容 → diff 结构化 → 权限主体 → 终端归属反转。auth/login那种按版本取名的写法可以延续),不要在共享类型上塞双语义字段。验证要求
真正实现某一项时:
state_update生命周期需真实 agent 端到端跑通,断言完成语义(何时算一轮结束)在两个版本下都正确。