DeepChat ACP v1 可靠性设计:协议边界、数据所有权与外部 Agent 会话状态闭环
DeepChat ACP v1 可靠性设计:协议边界、数据所有权与外部 Agent 会话状态闭环
本文基于 DeepChat 仓库中 docs/features/acp-v1-reliability/plan.md 的实现计划,系统讲解 ACP(Agent Client Protocol)v1 接入的可靠性工程:如何在既有模块上补齐协议边界与状态闭环,覆盖能力快照、认证流、会话生命周期与导入、早到通知缓冲、内容映射、Terminal/文件系统边界,以及配套的测试矩阵与风险控制策略。读完本文,你能掌握 DeepChat 将外部 ACP agent(如 DimCode、Claude Code、Codex 等)稳定接入本地会话体系的核心设计,并能对照仓库源码验证每一项可靠性决策的落地实现。
一、总体策略:不重写,而是补齐协议边界与状态闭环
本次可靠性建设的第一原则是在现有模块上补齐协议边界和状态闭环,而不是重写 ACP 子系统。各模块职责被明确切分:
acpProcessManager.ts:负责 launch、initialize、client method dispatch、capability snapshot、debug log、session update buffer;acpSessionManager.ts:负责new/load/resume/close/list的选择、持久化、listener 注册和 terminal/session cleanup;acpProvider.ts:负责 chat turn、debug action、renderer 状态事件和 DeepChat stream event 输出;acpMessageFormatter.ts、acpContentMapper.ts、acpTerminalManager.ts、acpFsHandler.ts:分别收口 prompt、update、terminal、fs 四类规范;- shared contracts / presenter types 只增加必要字段,不新增并行的 ACP 框架。
落地方式推荐拆成 5 个可 review 的增量:capabilities/auth、session lifecycle、prompt/content/update、terminal/fs、UI diagnostics + E2E matrix。
需要说明当前仓库的实际组织:直接执行 kind=acp 的工作已归属 AcpAgentRuntime / AcpAgentInstance(见 docs/architecture/agent-system.md),而 acpProvider.ts 仅服务 DeepChat + ACP-provider 兼容路径。计划文档中提到的 runtime/ 目录模块(如 acpProcessManager.ts、acpSessionManager.ts)仍然承载了初始化、缓冲、terminal、fs 等核心实现,是理解 ACP 协议对接的最佳入口。
数据所有权原则是整套设计的基石:DeepChat 的 conversation/message 记录是事实源(source of truth);ACP agent 的 session 只是外部 session catalog 和运行时上下文。进入 DeepChat 后,必须先形成本地 link,再转换、去重、持久化为 DeepChat 自己的消息和 metadata。
二、Runtime Flow:从子进程启动到清理的完整链路
计划文档给出的端到端运行流程如下:
Registry/Local command
|
v
Launch subprocess + JSON-RPC stdio
|
v
initialize(protocolVersion, clientCapabilities, clientInfo)
|
+--> auth required? --> authenticate/logout/debug/UI
|
v
resolve DeepChat conversation:
existing AcpSessionLink -> resume/load remote context
imported remote session -> attach link + optional load import
new local conversation -> session/new remote context
|
v
bind listener + flush buffered session/update
|
v
session/prompt(current user content blocks)
|
v
session/update -> mapper -> stream events + state + debug log
|
v
cancel/detach/explicit remote close/release terminals/process cleanup
这条链路的关键点在于顺序:先 initialize 拿到能力快照,再决定 conversation 的解析路径(resume / load / new),然后先绑定 listener 并 flush 缓冲的 session/update,再发送 prompt——这个顺序正是后文「Session Update Buffer」要解决的竞态问题的直接体现。
三、数据所有权与同步模型:AcpSessionLink
DeepChat 不把远端 agent session 当作本地数据库的事实源。远端 session 只提供三类信息:
- catalog:
session/list返回的 sessionId、cwd、title、updatedAt、_meta; - replay:
session/load可能重放历史 update,用于导入远端历史; - runtime context:
session/resume或session/new后承接新的 prompt turn。
本地用一个 link 记录 DeepChat conversation 与远端 ACP session 的关系:
interface AcpSessionLink {
conversationId: string
agentId: string
canonicalWorkdir: string
remoteSessionId: string
remoteTitle?: string
remoteUpdatedAt?: string
lastImportedRemoteUpdatedAt?: string
lastImportFingerprint?: string
importedMessageFingerprints: string[]
syncState: 'cataloged' | 'imported' | 'attached' | 'stale' | 'error'
}
围绕这个 link 的约束是整个同步模型的「不变量」:
- 稳定去重 key 是
agentId + canonicalWorkdir + remoteSessionId; session/list只更新 catalog/link metadata,不创建重复 DeepChat conversation;- 用户选择导入时,如果 link 已存在,打开/更新已有 DeepChat conversation;如果不存在,创建本地 conversation 并写 link;
session/load重放内容先进入 staging buffer;转换为 DeepChat message/block 后,再按 message fingerprint 落库;- fingerprint 使用远端 session id、update type、role/channel、规范化 content、tool id、turn boundary 等字段生成;没有足够字段时仍要在同一次 import 内去重;
session_info_update只更新 link metadata;本地会话标题只有在「自动标题」状态下才可被建议更新;- 本地删除或关闭 conversation 默认只 detach link,不调用远端
session/close; - 默认只有两种情况写远端 session:用户在已绑定 conversation 中继续发送 prompt;用户显式选择
Close Remote Session; - 普通 app shutdown、conversation close、process cleanup 只释放本地 handle/listener/terminal,不自动调用远端
session/close。
这套约束直接回答了「为什么远端历史不会灌进本地造成重复消息」:catalog 同步、历史导入、运行时恢复三条路径彼此隔离,且导入路径强制 staging + fingerprint 去重。
四、协议对接设计
4.1 Transports 与 Registry Launch
- 保持 registry launch spec 为首选:binary > npx > uvx 的现有顺序不变;
- 在 diagnostics 中显示实际 command、args count、distribution type、registry version、local/global version hint;
- 每个初始化、认证、list/resume/close probe 都必须带 timeout;timeout 后清理子进程及其子进程树;
- MCP transport 继续按
mcpCapabilities过滤:stdio默认可用,http/sse仅 agent 声明后启用; - Host-owned MCP v2 probing 和 extension adapter 在 ACP 边界停止:不为 ACP agent 自己管理的 MCP 连接创建第二个 client、不协商 wire era、不持久化 Tasks,也不渲染 Apps;
- 对 Claude/Codex 这种可能拉起二级 CLI 的 wrapper,E2E probe 需要固定短超时和 cleanup 审计,避免残留进程。
4.2 初始化与能力快照(Capability Snapshot)
设计是新增一个轻量 snapshot 类型,挂在现有 process handle 上,不新增独立 manager:
interface AcpCapabilitySnapshot {
protocolVersion: number
agentInfo?: schema.AgentInfo
agentCapabilities?: schema.AgentCapabilities
sessionCapabilities?: schema.SessionCapabilities
promptCapabilities?: schema.PromptCapabilities
authMethods: schema.AuthMethod[]
mcpCapabilities?: schema.McpCapabilities
supports: {
loadSession: boolean
sessionList: boolean
sessionResume: boolean
sessionClose: boolean
sessionFork: boolean
authLogout: boolean
}
}
配套规则:
buildClientCapabilities只声明 DeepChat 已真实支持的能力;fs、terminal继续声明;auth.terminal只在交互 runner、typed route/event 和 renderer surface 已实现且可用时随initialize声明。Terminal auth 的规范以 docs/features/acp-terminal-auth/spec.md 为准;- 初始化失败分三类展示:protocol version mismatch、process exited、timeout;
- 初始化返回的
models、modes、configOptions统一走normalizeAcpConfigState,并发布 ready event。
仓库中的实现与计划高度一致。acpCapabilities.ts 定义了 AcpCapabilitySnapshot 接口,buildCapabilitySnapshot() 从 InitializeResponse 中提取 agentInfo、agentCapabilities、promptCapabilities、authMethods、mcpCapabilities,并把 supports 的五个布尔位分别派生自 agentCapabilities.loadSession、sessionCapabilities.list/resume/close/fork——即「能力一律来自 initialize 响应,绝不靠 display name 或包名猜测」。而 buildClientCapabilities() 体现了「只声明真实支持的能力」:fs 默认声明 readTextFile/writeTextFile(可被 enableFs: false 关闭),terminal 默认可用,auth.terminal 只有在 enableTerminalAuth 为 true 且 terminal 启用时才随 initialize 声明。
4.3 认证与登出
认证入口分三层:
- Presenter/debug:
authenticate(agentId, methodId, workdir?)、logout(agentId, workdir?); - Settings/diagnostics UI:展示 auth methods,并提供 Authenticate 按钮;
- Chat flow:遇到 ACP auth required 错误时,停止当前 turn,展示可操作的 auth state。
各 auth method 的处理方式:
| Auth type | 对接方式 |
|---|---|
agent 或默认类型 |
直接调用 connection.authenticate({ methodId }),成功后刷新 status;失败保留错误详情 |
legacy env_var |
首轮不新增凭证表单;显示为 unsupported,并引导使用现有 manual env override |
terminal |
直接运行当前连接的同一 materialized command/base args,加上 method args/env;exit 0 后重连并重新 initialize;不得把 terminal method ID 传给 authenticate |
logout 只在 agentCapabilities.auth.logout 存在时启用;logout 成功后关闭或失效当前 ACP session handle,避免继续使用旧认证上下文。这个三层入口与 acp-terminal-auth 规格中「terminal method 走 PTY 直跑、重连后重新 initialize、绝不调用 authenticate」的约束保持一致。
4.4 Session 生命周期与导入
acpSessionManager 增加 capability-gated lifecycle(对照 acpSessionManager.ts 及其测试 acpSessionManager.test.ts):
listSessions(agentId, cwd?, cursor?):循环读取分页,按 workspace 同步 external catalog;importSession(agentId, remoteSessionId, cwd):创建或复用 DeepChat conversation,写入AcpSessionLink;resumeSession(agentId, remoteSessionId, cwd):仅用于已绑定 conversation 的运行时上下文恢复;detachSessionLink(conversationId):解除本地 link,不写远端;closeRemoteSession(agentId, remoteSessionId):仅用户显式操作或活跃 runtime cleanup 时调用;loadSession(agentId, remoteSessionId, cwd):用于远端历史重放导入;newSession(agentId, cwd, mcpServers):只在新的 DeepChat conversation 需要远端上下文时调用。
本地 conversation 打开后的远端上下文恢复优先级固定为:
existing AcpSessionLink + supports.sessionResume -> session/resume
existing AcpSessionLink + supports.loadSession -> session/load for import/replay, then attach
no AcpSessionLink -> session/new
清理策略:
- 用户停止当前生成:只调用
session/cancel; - 用户关闭本地 conversation:默认 detach link,不调用远端 close;
- 用户显式关闭远端 session:若支持
session/close,调用 close;然后 release terminal/listener;最后更新 link state; - agent process 异常退出:标记 handle unhealthy,清理 listener/terminal,不删除用户可恢复的 session id;
sessionCapabilities.fork先做 debug-only,只有 capability 存在时开放,不进入主聊天流程。
导入策略:
session/list结果只写 external catalog,不直接生成 messages;session/load重放用于导入历史;导入过程先汇总成 DeepChat turn,再落库;- 已导入过的远端 session 再次同步时,先比较
remoteUpdatedAt和lastImportedRemoteUpdatedAt;未变化则跳过; - 即使
updatedAt变化,也必须用 message fingerprint 去重,避免重复导入相同 replay 内容; - 新 prompt turn 由 DeepChat 产生并持久化;agent response 通过 mapper 转换后追加到同一个本地 conversation。
4.5 Session Update Buffer:早到通知不能丢
问题背景:session/new 返回前后,agent 可能已经发送 session/update,但 DeepChat 的 listener 尚未注册,导致 commands/modes/config 早期状态丢失。
计划给出的修复方式与源码实现逐项对应:
dispatchSessionUpdate找不到 listener 时,不立即 drop,而是按sessionId写入短期 buffer;- buffer 带 TTL 和最大条数(30 秒、每 session 100 条),避免异常 agent 无限占内存;
registerSessionListener(sessionId, ...)后立即 flush buffer,并保留原始顺序;- 如果 TTL 过期仍无 listener,再写 debug warning 并丢弃。
acpProcessManager.ts 中可以看到 dispatchSessionUpdate() 正是「有 listener 就投递、没有就 bufferSessionUpdate」的二分逻辑;buffer 条目记录 receivedAt 时间戳,写入时用 slice(-MAX_BUFFERED_SESSION_UPDATES) 保留最近条目(L2130-L2146),并打出一条 <a href="https://link.gitcode.com/i/34e64d539d7cadc6ba6c0c1b6045becd" target="_blank">ACP] Buffered session update for unbound session ... 的 warning 便于诊断;flushBufferedSessionUpdates() 在 listener 绑定后按原顺序逐条投递,并写入 session/update.buffer.flush 的 debug 记录([L2148-L2164);TTL 修剪由 pruneBufferedSessionUpdates() 完成,常量 SESSION_UPDATE_BUFFER_TTL_MS = 30_000 即计划中的 30 秒上限(L186)。
4.6 Prompt Turn 与内容映射
acpMessageFormatter 的核心改造是改成 current-turn only:
- 从 DeepChat messages 中提取最后一个 user message;
- 不再把完整历史拼成
USER:/ASSISTANT:文本; - 不再把 temperature、maxTokens 注入 prompt 文本;
- 若 DeepChat session 有 system prompt,只在本地 conversation 首次绑定远端 runtime 时作为 context text 发送一次;
- 每个 content block 先判断 agent
promptCapabilities,不支持则降级。
输入映射策略:
| DeepChat content | ACP content |
|---|---|
| text | text |
| local/remote URL attachment | resource_link |
| base64 image + image supported | image |
| image unsupported | resource_link 或文本 fallback |
| audio + audio supported | audio |
| audio unsupported | 文本 fallback |
| embedded file/context + embeddedContext supported | resource 或 text context |
输出映射策略:
agent_message_chunk-> text stream + content block;agent_thought_chunk-> reasoning stream + reasoning block;- image/audio/resource/resource_link 尽量保留结构;UI 暂不支持的类型转可读文本,不丢 debug payload;
usage_update-> turn metadata + debug log;后续可在状态栏展示;session_info_update->AcpSessionLinkmetadata;自动标题可以更新,用户手工标题不覆盖。
对照 acpMessageFormatter.ts 的实现:format() 只在 options.includeSystemPrompt 为真时把 system prompt 作为首个 text block 发送,随后通过 findLastUserMessage() 定位最后一条 user message 并归一化其内容——不再遍历全量历史。能力降级逻辑在 toContentBlock() 中逐一落地:image 在 capabilities?.image 缺失时先降为 resource_link(有 uri 时)、再降为 [image <mimeType>] 文本;audio 不支持时降为 [audio <mimeType>] 文本;embedded resource 在 capabilities?.embeddedContext 缺失时降级为纯文本。这正是计划表格中每条降级路径的可运行实现。
4.7 工具调用与权限
工具调用保持现有 mapper,但修正关键语义:
tool_call表示工具生命周期,不默认当作权限请求;- 只有 ACP
session/request_permission才进入 DeepChat permission overlay; tool_call_update.content中的terminal、diff、content、locations、raw input/output 都保留到 block extra/debug;- permission resolver 增加 timeout 默认 outcome,用户取消或窗口关闭时返回 cancelled;
- remote control 侧沿用现有 permission/question 交互模型。
4.8 文件系统
acpFsHandler(acpFsHandler.ts)方向正确,计划以测试加固为主:
- read/write 继续要求 session workdir 已注册;
- 路径必须在允许 workspace 内;跨 workspace 写入拒绝;
- line number 按 1-based 处理;
- binary file、超大文件、无权限路径给结构化错误;
clientCapabilities.fs只有 handler 可用时声明;handler 初始化失败时不声明。
4.9 Terminals:直接 spawn 才是协议正确行为
acpTerminalManager 需要修正的协议细节:
terminal/create用params.command+params.args直接 spawn,不拼接 shell 字符串;- Windows 不默认包
powershell.exe -Command;只有 agent 明确要求 shell 时,command 本身就是 shell; params.cwd必须 resolve 到允许 workspace 或明确的 fallback;fallback 只能用于无 cwd 的 agent 兼容,并写 warning;outputByteLimit超限时从 buffer 开头裁掉,保留最新输出;裁剪必须在 UTF-8 字符边界;terminal/output返回当前 buffer、truncated、exitStatus;kill幂等;release释放 PTY 资源但不删除已进入 chat block/debug log 的输出。
acpTerminalManager.ts 的实现与上述条目一一对应:createTerminal() 中 spawn(params.command, params.args ?? <a href="https://link.gitcode.com/i/f0483d0f8affdf6c5c366441ca40a910" target="_blank">], ...) 直接按 argv 数组启动 PTY(xterm-256color,120x30),没有 shell 包装;输出回调里 retainTailAtCharBoundary(nextBuffer, state.maxOutputBytes) 执行「保尾裁剪 + UTF-8 边界」,一旦 Buffer.byteLength(nextBuffer, 'utf-8') 超过上限即置位 state.truncated([L109-L115);getTerminalSnapshot() 返回的正是 { output, truncated, exitStatus } 三元组(L144-L153)。
4.10 Plan、Modes、Config Options、Slash Commands
planupdate 每次替换当前 plan entries,避免重复追加;current_mode_update同步 ChatStatusBar 当前 mode;session/set_mode继续作为 legacy mode 能力;若 agent 用 config options 暴露 mode,UI 统一展示在 config options 区;config_option_update要覆盖 initialize/new/load/resume 后的所有路径;available_commands_update进入 active session state;输入框 slash suggestions 使用该 state;- 用户输入
/command arg仍走普通session/prompt,不新增 agent-specific command RPC。
4.11 扩展性
- 所有 official update type 必须有已知处理或显式 ignored reason;
_meta保留在 diagnostics/session metadata 中,不随意解析成业务字段;- 自定义 extension method/notification 继续走现有 ext debug action;名称必须保持下划线前缀约束;
- 未知 custom update 不打断 turn,只进入 debug log。
五、Shared Types 与 IPC Surface
优先扩展已有 shared contract/debug 类型,而不是另起炉灶:
AcpDebugActionType增加authenticate、logout、sessionList、sessionImport、sessionResume、sessionDetach、sessionCloseRemote、sessionFork;- 增加 renderer-safe status payload:
authMethods、authRequired、capabilities、externalSessions、sessionLinks、lastUsage、lastSessionInfo; - 新 typed route/client 用于 Settings/diagnostics 查询 ACP status 和执行 auth/session debug action;legacy presenter 只保留兼容;
- 所有用户可见 label/error 走
src/renderer/src/i18n。
六、UI/UX:紧凑的 Diagnostics 区
Settings 中 ACP agent 详情页增加一个紧凑 diagnostics 区,不做独立大页面:
ACP Agent Detail
+--------------------------------------------------+
| DimCode Ready v1 |
| Auth: Not required FS: on Terminal: on |
| Sessions: list/resume/close Prompt: image |
+--------------------------------------------------+
| [Authenticate] [Sync Sessions] [Run Diagnostics] |
+--------------------------------------------------+
| Workspace sessions |
| New Session 2026-06-02 10:10 [Import] |
| Refactor Thread linked [Open] |
+--------------------------------------------------+
| Last update |
| available_commands_update: /web, /init |
+--------------------------------------------------+
ChatStatusBar 保持紧凑:
+--------------------------------------------------+
| ACP: DimCode | Mode: Agent | Model: MiMo | / cmds |
+--------------------------------------------------+
错误态:
+--------------------------------------------------+
| ACP auth required: Claude Login |
| [Authenticate] [Open Diagnostics] |
+--------------------------------------------------+
设计意图是让用户能读懂就绪状态、认证要求、launch 失败和会话恢复:diagnostics 精确指出失败的协议边界,但不暴露凭证,也不为某个具体 agent 引入产品分支。
七、测试策略
单元测试覆盖(对应 test/main/agent/acp 目录,如 acpProcessManager.test.ts、acpProcessManagerCapabilities.test.ts、acpSessionManager.test.ts):
- capability snapshot parser:完整/缺失/未知字段;
- initialize client capabilities:auth terminal 声明受实现开关控制;
- session lifecycle gate:无 capability 不调用;有 capability 调正确 RPC;
- session import sync:同一
agentId + workdir + remoteSessionId不重复创建 conversation; - replay idempotency:重复
session/load不重复落 message/block; - update buffer:
session/update早到、listener 后到、TTL 过期; - prompt formatter:只发送最后 user message;system prompt only once;image/audio/resource fallback;
- content mapper:usage/session info/tool terminal/diff/plan/mode/config/slash commands;
- terminal manager:tail truncation、UTF-8 boundary、args 不拼接 shell;
- fs handler:workspace guard、1-based line、binary/large file error;
- permission resolver:approve/deny/cancel/timeout。
集成/手动矩阵按真实 agent 划分:
- DimCode:init -> list -> new -> commands -> close -> resume -> prompt;
- Claude Code ACP:init -> auth required -> authenticate flow -> cleanup;
- Codex ACP:registry launch spec -> version drift diagnostics -> auth methods;
- Regression:普通 non-ACP chat、MCP permission、DeepChat internal agent 不受影响。
Quality gates:
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
八、风险与缓解
| Risk | Mitigation |
|---|---|
| 不同 ACP wrapper 对 auth method 字段解释不一致 | diagnostics 显示 raw auth method;未知字段保留 _meta;只按官方 required 字段做控制流 |
| Claude/Codex wrapper 拉起子进程后 probe 卡住 | 所有 real-agent probe 必须 timeout + process tree cleanup |
| resume/load/new 语义混用导致历史重复 | DeepChat conversation 为事实源;远端 replay 先 staging 再 fingerprint 去重;prompt formatter current-turn-only |
| terminal command 兼容性变化 | 直接 spawn 是协议正确行为;若 agent 要 shell,agent 应把 shell 作为 command |
| session title 更新覆盖用户标题 | 只更新 ACP metadata;DeepChat 用户手工标题优先 |
九、小结:可靠性来自边界,而非重写
回顾整套设计,可以提炼出 DeepChat ACP v1 可靠性的四个支柱:
- 能力即事实:所有操作(load/resume/close/fork/logout、image/audio/embeddedContext、terminal auth)都由 initialize 返回的能力快照门控,快照直接来自 buildCapabilitySnapshot(),杜绝按 agent 名称猜测行为;
- 本地数据所有权:远端 session 只是 catalog/replay/runtime context 三类信息的提供者,link + fingerprint 保证导入幂等、标题不被覆盖、本地操作不隐式写远端;
- 有界与可诊断:早到通知有 TTL/条数上限的 buffer,probe 有 timeout 与进程树清理,terminal 输出有字节上限且按 UTF-8 边界裁剪,未知 update 只进 debug log 不打断 turn;
- 协议边界清晰:MCP 交接在 ACP 边界停止,host 不为 agent 自管的 MCP 连接建第二个 client;permission 只认
session/request_permission;slash 命令只走普通 prompt。
配套的完整验收契约见同目录的 spec.md(包含 Interoperability Matrix:DimCode、Claude Code ACP、Codex ACP 与确定性本地 ACP fixture),任务拆分见 tasks.md。三者共同构成「计划可 review、规格可验收、进度可追踪」的 ACP v1 可靠性工程闭环。