DeepChat ACP v1 可靠性设计:让外部 Agent 会话在协议边界、认证与状态闭环上稳定运行
DeepChat ACP v1 可靠性设计:让外部 Agent 会话在协议边界、认证与状态闭环上稳定运行
DeepChat 通过 Agent Client Protocol(ACP)v1 接入外部 coding agent,本文基于仓库中 docs/features/acp-v1-reliability/spec.md 及其配套实现计划与终端认证规范,完整展开 ACP 集成的产品契约、能力协商、会话生命周期、认证流程、更新缓冲与持久化去重等可靠性设计。读完本文,你可以掌握 DeepChat 中 ACP 子系统的模块归属(AcpAgentRuntime / AcpAgentInstance / AcpProvider 的分工)、各 ACP RPC 的能力门控规则、通知缓冲与幂等导入的实现要点,以及如何用仓库内的测试套件验证这些行为。
1. 产品契约:ACP 会话不是本地事实源
规格文档首先定义了产品层面的三条硬约束:
- DeepChat 记录是本地会话的持久事实源(durable source of truth)。Agent 的远端会话列表只是一个工作区维度的"资源目录"(resource catalog)。
- 导入或恢复远端会话时,禁止三件事:不得重复本地消息、不得覆盖用户手动编辑的标题、不得静默写入其他远端会话。
- 用户必须能看懂就绪状态、认证要求、启动失败与会话恢复。诊断信息要能定位到具体出错的协议边界,同时不泄露凭证、不针对特定 agent 做产品分支。
另外文档明确了可选方法的启用原则:只由已初始化 agent 返回的 capabilities 决定,绝不根据其显示名称或猜测的包版本决定。这一原则贯穿后文所有能力门控(capability gating)设计。
模块归属(Ownership)
| 模块 | 职责 |
|---|---|
src/main/agent/acp/ |
拥有直接 ACP 进程、连接、会话、认证、终端与状态适配行为 |
src/main/provider/providers/acpProvider.ts |
仅承担 kind=deepchat + providerId=acp 的兼容请求,委托给共享 ACP 运行时 |
| 主进程 | 拥有能力检查、进程清理、文件系统授权、远端操作 |
| 渲染进程客户端 + typed routes/events | 暴露受支持的操作与归一化状态 |
| 会话持久化 | 拥有本地消息与远端会话关联;渲染进程状态不是独立的持久化权威 |
从源码结构看,当前实现与上述分工一致:直接执行路径位于 src/main/agent/acp/instance/(acpAgentRuntime.ts、acpAgentInstance.ts)与 src/main/agent/acp/client/(AcpConnectionManager、AcpSessionRuntime、AcpPromptController),而 AcpProvider 只作为兼容适配器。文档同时强调:终端认证的权威规范在 docs/features/acp-terminal-auth/spec.md,它已实现并负责认证方式选择、直接 PTY 执行、调用方所有权、重连与一次性会话准备重试;但它不代表整个 ACP v1 可靠性范围的验收完成。
2. 可靠性验收契约:23 项协议行为的完整要求
spec.md 的核心是一张覆盖全部 ACP v1 面的验收契约表。它不是"所有操作都已完成真机验证"的声明,而是完整的行为要求。以下按领域逐项展开,并给出仓库中的对应实现位置。
2.1 传输与启动
- 使用所选 agent 子进程传输上的 JSON-RPC。
- 优先使用 Registry 启动规格(launch spec);本地/全局包版本差异要记入诊断。文档指明 Registry 内容来自
resources/acp-registry/registry.json,该文件由package.jsonprebuild 阶段的scripts/fetch-acp-registry.mjs拉取生成。 - 初始化、认证、诊断探测都必须带超时,超时后执行拥有者的进程树清理。
- MCP 传输按 ACP agent 的
mcpCapabilities过滤:stdio默认可用,http/sse仅在 agent 声明后启用。对应实现见src/main/agent/acp/runtime/mcpTransportFilter.ts与mcpConfigConverter.ts。
2.2 初始化与能力快照
初始化要求发送受支持的协议版本、客户端信息与真实客户端能力,并保留 agent 信息、认证方法、能力快照;对不支持的协议连接要关闭并给出有意义的错误。
规格中的 AcpCapabilitySnapshot 在源码中直接落地为 acpCapabilities.ts:
// src/main/agent/acp/runtime/acpCapabilities.ts
export interface AcpCapabilitySupport {
loadSession: boolean
sessionList: boolean
sessionResume: boolean
sessionClose: boolean
sessionFork: boolean
}
export interface AcpCapabilitySnapshot {
protocolVersion?: schema.ProtocolVersion
agentInfo?: schema.Implementation | null
agentCapabilities?: schema.AgentCapabilities
sessionCapabilities?: schema.SessionCapabilities
promptCapabilities?: schema.PromptCapabilities
authMethods: schema.AuthMethod[]
mcpCapabilities?: schema.McpCapabilities
supports: AcpCapabilitySupport
}
export function buildCapabilitySnapshot(
initializeResult: schema.InitializeResponse
): AcpCapabilitySnapshot {
const agentCapabilities = initializeResult.agentCapabilities
const sessionCapabilities = agentCapabilities?.sessionCapabilities
return {
protocolVersion: initializeResult.protocolVersion,
// ...
authMethods: initializeResult.authMethods ?? [],
supports: {
loadSession: Boolean(agentCapabilities?.loadSession),
sessionList: Boolean(sessionCapabilities?.list),
sessionResume: Boolean(sessionCapabilities?.resume),
sessionClose: Boolean(sessionCapabilities?.close),
sessionFork: Boolean(sessionCapabilities?.fork)
}
}
}
客户端方向由 buildClientCapabilities 构造,实现上只声明 DeepChat 真实支持的能力:
// 同一文件
export function buildClientCapabilities(
options: AcpCapabilityOptions = {}
): schema.ClientCapabilities {
const caps: schema.ClientCapabilities = {}
if (options.enableFs !== false) {
caps.fs = { readTextFile: true, writeTextFile: true }
}
if (options.enableTerminal !== false) {
caps.terminal = true
}
// auth.terminal 只有在 terminal 能力可用且桌面 PTY 流程
// (enableTerminalAuth)就绪时才随 initialize 声明
if (options.enableTerminal !== false && options.enableTerminalAuth) {
caps.auth = { terminal: true }
}
return caps
}
初始化失败在诊断上分为三类:协议版本不匹配、进程退出、超时;初始化返回的 models、modes、configOptions 统一走配置状态归一化并发布 ready 事件(见 src/main/agent/acp/runtime/acpConfigState.ts)。
2.3 认证
认证规则可以浓缩为三条:
- 缺省方法类型视为
agent,此时直接调用authenticate({ methodId }); - 受支持的
terminal方法交互式运行 materialized agent 命令,退出码为0后重连并重新初始化——绝不把 terminal 方法 ID 传给authenticate; - 只有当桌面流程可用时才对外宣告 terminal auth;logout 需要 agent 宣告
auth.logout能力。
终端认证的权威细节在 docs/features/acp-terminal-auth/spec.md,其关键约束值得单独展开:
- 触发信号:数值错误码
-32000(auth required)被转换为类型化、可操作的认证状态,且不得被吞掉当作普通 resume/load 失败进而触发session/new回退。 - 启动物化共享:协议连接与终端认证进程共用同一份 materialized launch 快照(
command/args/env/cwd),terminal 方法在其上追加方法参数、按"方法 env 覆盖基础 env"合并:
// 终端认证运行时(等价逻辑)
pty.spawn(materialized.command, [...materialized.args, ...(method.args ?? [])], {
cwd: materialized.cwd,
env: { ...materialized.env, ...(method.env ?? {}) }
})
仓库中该 PTY 生命周期封装为 acpTerminalAuthRunner.ts(基于 node-pty 直接 spawn,不经过 shell),认证编排位于 acpAuthService.ts。
- 一次性重试契约:认证成功(terminal 退出码 0 后重连并重新 initialize 成功)只重试"被阻塞在 prompt 之前的会话准备",且至多一次;第二次再收到 auth required 即停止,防止认证死循环。
- 调用方所有权:每个认证运行绑定发起的
webContentsId,其他渲染进程不能读取输出、发送输入或取消;发起窗口被销毁即取消运行。PTY 运行上限十分钟,超时、取消、非零退出、信号终止都终止 PTY 进程树且不重试。 - 安全边界:不解析终端输出来推断成功(ACP 定义退出码为互操作信号)、不收集/持久化凭证、普通 info 日志不记录 env 值与 PTY 转录;challenge 与 live handle 的 launch signature 绑定,签名失配(改了命令/参数/env/安装/工作目录)即视为过期。
env_var 旧类型保留用于诊断但在产品控制流中归一化为 unsupported,引导用户使用既有的手动 env override。
2.4 会话生命周期:new / load / resume / close
规格对四个 RPC 各有一条能力门控要求:
session/new:仅当本地会话确实需要远端上下文时创建。传递已授权的cwd与兼容的 MCP servers;保留返回的 modes/models/config options,并把远端身份与本地会话关联。session/load:要求loadSession能力。在历史重放之前先注册或缓冲更新,staging 重放内容,再转换为 DeepChat message/block 记录,幂等地持久化。session/resume:要求 resume 能力。恢复已有关联,但不把远端状态当作"可以替换本地消息历史"的许可。session/close:要求 close 能力且必须有明确用户意图。本地关闭/删除默认只是 detach 关联;停止当前回合用session/cancel而非 close。
本地会话打开后的远端上下文恢复优先级(来自配套实现计划 plan.md)固定为:
existing AcpSessionLink + supports.sessionResume -> session/resume
existing AcpSessionLink + supports.loadSession -> session/load 导入/重放,然后 attach
no AcpSessionLink -> session/new
清理策略:用户"停止生成"只发 session/cancel;关闭本地会话默认 detach、不写远端;显式"关闭远端会话"才调用 session/close 并释放 terminal/listener;agent 进程异常退出时标记 handle 不健康、清理监听与终端,但不删除用户可恢复的 session id,从而保证"进程崩溃后本地/远端关联仍可恢复"。
2.5 会话目录与幂等导入
会话目录(session catalog)要求:list 能力门控、cwd 过滤、游标分页,远端记录以 agentId + canonicalWorkdir + remoteSessionId 作为稳定去重 key;重复导入复用同一本地关联。
持久化实现见 acpSessionPersistence.ts,其中 syncRemoteSessions 展示了完整的去重与并发防护:
// 简化自 src/main/agent/acp/runtime/acpSessionPersistence.ts
for (const remoteSession of input.sessions) {
const item = await this.withRemoteSessionSyncLock(
input.agentId, remoteSession.sessionId, () => this.syncRemoteSession(...))
result[item.status] += 1 // 'imported' | 'updated' | 'skipped'
}
session/list结果只更新目录/link 元数据,不产生消息;- 已存在关联时只更新
acpSync.lastSyncedAt等元数据,返回updated,绝不重复建 conversation; - 每个
agentId::sessionId走按 key 的互斥锁(withKeyLock),并发同步串行化; - 写入冲突(并发导入导致已存在)时静默删除多余 conversation 并回落到更新既有 link;
- 关联元数据包含
remoteSession(sessionId/cwd/title/updatedAt/_meta/syncedAt)与source: 'session/list',供诊断使用。
导入策略进一步规定:已导入过的远端会话再次同步时先比较 remoteUpdatedAt 与 lastImportedRemoteUpdatedAt,未变化则跳过;即使 updatedAt 变化也要用消息指纹去重。导入的远端消息先转成本地 message/block 格式再落库;稳定指纹防止重复导入时产生重复消息。
2.6 Prompt 回合与内容映射
规格对 prompt turn 的要求(对应 src/main/agent/acp/runtime/acpMessageFormatter.ts):
- 只发送当前用户回合及能力支持的内容,不再把完整本地历史拼成
USER:/ASSISTANT:文本; - 不再把 temperature/max-tokens 注入 prompt 文本;
- 本地会话 system prompt 只在首次绑定到新远端会话时作为 context text 发送一次;
- 取消目标始终是当前活跃回合。
输入/输出内容映射按 promptCapabilities 门控并带降级:
| DeepChat 内容 | ACP 内容 |
|---|---|
| 文本 | text |
| 本地/远程 URL 附件 | resource_link |
| base64 图片且 agent 支持图片 | image |
| 图片不支持时 | 降级为 resource_link 或文本 |
| 音频且支持 | audio |
| 音频不支持时 | 文本降级 |
| 内嵌文件/context 且支持 embedded context | resource 或文本 context |
输出侧:agent_message_chunk 映射为文本流 + content block;agent_thought_chunk 映射为 reasoning 流 + reasoning block;usage_update 进入回合元数据与调试日志(保留可用的 usage/cost/token 元数据,不编造缺失值);session_info_update 只更新关联元数据,永不覆盖用户手工编辑的本地标题。内容映射实现位于 src/main/agent/acp/runtime/acpContentMapper.ts。
2.7 工具调用、权限与文件系统
- 工具进度:保留 tool-call 身份、更新、参数、结果、locations、原始元数据、终端输出与 diff;普通
tool_call进度不得被当作权限请求展示。 - 权限请求:只有
session/request_permission进入权限流程,复用 DeepChat 现有权限界面;取消、超时、过期请求、缺失 resolver 都要以"不留下孤儿 overlay"的方式 settle。实现见src/main/agent/acp/runtime/acpPermissionBridge.ts。 - 文件系统:对外宣告的读/写能力必须对应真实 handler(handler 不可用就不宣告
fs能力);保留绝对路径、会话 workdir、二进制文件、大小与写授权检查;行号一律 1-based。实现见src/main/agent/acp/runtime/acpFsHandler.ts与acpPathGuard.ts,且会话 workdir 与既有安全策略是权威,不自动扩大文件系统访问。
2.8 终端
terminal/create直接执行command与args,除非请求的可执行文件本身就是 shell(不默认包 shell、不做字符串拼接);env与cwd通过会话边界解析;- 输出 buffer 保留最新内容于
outputByteLimit之内,且裁剪不得切断 UTF-8 字符。实现见 acpTerminalManager.ts:const maxOutputBytes = params.outputByteLimit ?? this.defaultMaxOutputBytes; kill/release幂等;release 后已渲染的输出允许继续可见。
2.9 计划、模式、配置项与斜杠命令
- Plans:每次 plan 更新替换当前 plan 条目,而不是向转录追加重复计划;
- Modes:保留初始 modes、模式变更与当前模式更新;UI 中优先使用 config-option 等价物,同时保持 legacy mode 兼容,
current_mode_update同步状态栏; - Config options:initialize/new/load/resume 及后续更新后都要归一化状态;变更失败时保留或恢复最后权威状态;
- Slash 命令:保留
available_commands_update,包括会话监听器注册前收到的通知(由 2.10 的缓冲机制保证);建议来源是会话状态,执行/cmd arg时仍走普通session/prompt文本,不新增 agent 专属 command RPC。
2.10 会话更新缓冲:解决"通知早于监听器"的时序问题
这是可靠性设计中非常具体的一条机制。问题场景:session/new 返回前后,agent 已经发出 session/update(例如 available_commands_update),但 DeepChat 的监听器尚未注册,早期状态会丢失。修复方式在源码中可以直接验证:
- acpProcessManager.ts 中
dispatchSessionUpdate找不到监听器时按sessionId写入短期 buffer,而不是直接丢弃; - buffer 带 TTL 与条数上限,防止异常 agent 无限占用内存;
registerSessionListener执行后立即按原始顺序 flush;TTL 过期仍无监听器则写 debug warning 并丢弃,保证"被丢弃的通知可诊断"。
源码中的常量与数据结构印证了规格约束:
// src/main/agent/acp/runtime/acpProcessManager.ts
const SESSION_UPDATE_BUFFER_TTL_MS = 30_000
const AUTH_CHALLENGE_TTL_MS = 10 * 60 * 1000
private readonly bufferedSessionUpdates = new Map<string, BufferedSessionUpdate[]>()
// registerSessionListener 后 flushBufferedSessionUpdates 按序投递并删除该 key
即 30 秒 TTL、按 session 维度的条目边界、过期清理都会留下 debug 日志(session/update.buffer.flush 等诊断动作)。
3. 会话与通知不变量(逐条可验证)
spec.md 列出的九条不变量,是整个可靠性的"红线":
- 导入的远端消息在持久化前必须转换为本地 message/block 格式;
- 稳定指纹防止导入或重放重复时产生重复消息;
- 早期通知在会话准备与监听器注册之间保持有序;
- 通知缓冲有 TTL 与条目边界,被丢弃的通知可诊断;
- 进程崩溃后保留可恢复的本地/远端关联;
- 远端目录同步只更新关联元数据,不创建重复本地会话、不覆盖本地编辑;
- 数值型 auth-required 错误停止会话回退逻辑并进入类型化认证流程;
- 认证成功只重试被阻塞的 prompt 前会话准备,且至多一次;
- 本地会话删除绝不隐式触发批量远端删除或同步。
4. 互操作矩阵:谁验证什么路径
规格定义了真机与确定性 fixture 的分工:
| Agent 样本 | 覆盖的必测路径 |
|---|---|
| DimCode | 工作区会话列表/导入、重复导入去重、new/resume/close(若宣告)、早期斜杠命令更新、modes、config options |
| Claude Code ACP | 初始化、auth-required 与 authenticate 流程、超时/进程清理、能力过滤后的 MCP 传输交接 |
| Codex ACP | Registry 启动选择、本地/全局版本漂移诊断、宣告的认证方法、会话列表不可用时的优雅行为 |
| 确定性本地 ACP fixture | 能力组合、早期通知、直接终端认证、重连失败、取消、过期 challenge、一次性重试 |
| 其他本地 agent | 仅在可执行文件/包明确可用时纳入;记录解析后的命令、版本、能力与结果;不可用的可选样本不代表协议支持,也不阻塞无关覆盖 |
每次互操作运行要记录包与可执行文件版本;无法确认可用性的样本不产生"支持"暗示。
5. 非目标与约束:明确不做什么
这些边界与做什么同等重要:
- 不实现 ACP v2,不引入未声明的协议能力;
session/fork这类实验性方法在普通聊天流程独立支持之前只属于诊断域; - 不为任何特定 agent 硬编码行为,文中点名 agent 只是兼容样本;
- 不改变非 ACP provider 的 prompt、MCP、权限或终端行为;
- DeepChat 宿主侧的 MCP v2 协商、Apps、Tasks 与授权扩展不包装也不重新解释 ACP agent 自己拥有的 MCP 连接,交接遵循 agent 声明的
mcpCapabilities; - 不自动扩大文件系统访问;不做主动的批量远端写入或双向会话同步;
- 渲染-主进程操作走 typed routes、typed events 与 renderer API 客户端,已退役的 legacy presenter 传输不是扩展点;
- 用户可见字符串走 i18n,诊断保持既有 Settings/ChatStatusBar 密度;代码、注释、类型、提交与技术文档使用英文。
6. 验证契约与回归覆盖
spec.md 要求保留针对以下维度的聚焦回归覆盖:能力真实性(capability truthfulness)、认证所有权与清理、有序通知投递、幂等导入、崩溃恢复、有界终端输出、文件系统授权、用户可见状态。既有认证测试套件已覆盖直接终端启动、重连、超时、渲染进程关闭、过期响应与调用方隔离。
仓库中的测试布局与上述要求对应:
- 主进程 ACP 运行时测试集中在 test/main/agent/acp/runtime/(
acpCapabilities.test.ts、acpProcessManager.test.ts、acpSessionPersistence.test.ts、acpTerminalManager.test.ts、acpPermissionBridge.test.ts、acpFsHandler.test.ts、acpMessageFormatter.test.ts、acpContentMapper.test.ts等 13 个文件); - 终端认证测试在 test/main/agent/acp/auth/(
acpAuthService.test.ts、acpTerminalAuthRunner.test.ts、acpTerminalAuthRunnerLifecycle.test.ts); - 兼容路径测试为 test/main/provider/acpProvider.test.ts。
pnpm run format
pnpm run i18n
pnpm run lint
pnpm run typecheck
pnpm test -- test/main/agent/acp
pnpm test -- test/main/provider/acpProvider.test.ts
需要强调的一点是 tasks.md 的自述:其中未勾选项是未对账的规划账本,不能因为没有代码/测试/真机证据就推断已完成;终端认证这一切片已实现,但"仅完成终端认证实现"并不关闭上述更宽的验证要求。
7. 小结:可靠性来自边界而非功能堆叠
DeepChat 的 ACP v1 可靠性设计的核心思路可以概括为三层:协议边界(所有可选行为都由能力快照门控,初始化/认证/探测一律带超时与进程清理)、数据边界(本地会话是事实源,远端会话只做目录、重放与运行时上下文三种角色,通过稳定 key + 指纹保证幂等)、状态边界(auth challenge 的调用方所有权、更新缓冲的 TTL 与顺序、崩溃后可恢复的关联)。规格、计划、任务与实现位于 docs/features/acp-v1-reliability/、docs/features/acp-terminal-auth/spec.md 与 src/main/agent/acp/,读者可以沿文中标注的文件路径与测试套件逐条核对上述每一条约束的落地情况。