Skip to content

chore(acp): ACP v2 draft 缺口清账(state_update 生命周期、整消息 upsert、终端归属、diff 结构化等) #149

Description

@YoungSx

结论

AcpProtocolVersion.Latest = V2Default = 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 三个字段仍是 boolInitializeTypes.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/loadIAcpClient.csAcpClient.csOtherSessionTypes.csSessionUpdateTypes.cs
  • session/set_mode + SessionMode / SessionModesState / CurrentModeUpdateIAcpClient.csAcpClient.csOtherSessionTypes.csSessionNewTypes.csSessionUpdateTypes.csPublicSurface.Types.txt

另有两处 v2 语义变更需在门控时一并处理:

  • MCP "type": "sse" 在 v2 移除,且 stdio 在 v2 必须"type": "stdio"(v1 无此字段)。我们的 Mcp/McpServerConfig.cs:407-443 三种都写、都读。
  • v2 里 configOptions 的标识字段由 id 改名为 configId;我们 ConfigOptionTypes.cs:14id
  • v2 里 messageId 在每个 chunk/message 上必填;我们 ContentChunkUpdate.MessageIdstring?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 这个常量可能被误读为「已支持」:它的注释说清了,但常量名本身有歧义。

建议范围

不建议现在整体实现。建议:

  1. 先做无争议的一步:把 Latest 的语义在命名上说清(如改为 HighestModeled 或类似),消除「Latest 就是可用」的误读面。这一步独立、低风险。
  2. v2 转 stable 后再排后续,依赖顺序大致为:能力对象化 → 消息/messageId 收紧 → state_update 生命周期(跨层,最重)→ 工具调用 upsert 与流式内容 → diff 结构化 → 权限主体 → 终端归属反转。
  3. 版本门控原则:v1/v2 差异用声明式分叉表达(现有 auth/login 那种按版本取名的写法可以延续),不要在共享类型上塞双语义字段。

验证要求

真正实现某一项时:

  • 每项都要有 v1 与 v2 两条线的报文级断言,且互不串味(v1 报文不得出现 v2 字段,反之亦然)。
  • state_update 生命周期需真实 agent 端到端跑通,断言完成语义(何时算一轮结束)在两个版本下都正确。
  • 反向验证:拆掉版本判别后,对应报文断言必须转红。

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

    documentationImprovements or additions to documentation

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions