ACP 自动化协议
ACP 桥接器将 harness 的持久化 Agent 会话通过基于 stdio 的 JSON-RPC 暴露给可信的程序化客户端,处理标准会话控制、提示、配置、语义更新和一次性工具授权。
目的与范围
本文聚焦 @deepseek-ai/dsh-acp 的服务端入口、会话创建与恢复、协议路由及生命周期。MCP 挂载、模型选项生成、提示内容转换和更新编解码分别由 mcp.ts、model-control.ts、content.ts、updates.ts 等模块承担;这里仅说明入口与会话对象如何调用它们,不推断其内部算法。对于子 Agent 作为 ACP 客户端的行为,请参阅相应的 subagent 专题;本文只讨论此服务端桥接。index.ts · session.ts
概述
插件在 Cordis apply 阶段捕获持久化服务,建立连接级的 sessions 映射和恢复中的 activating 集合;每个 AcpSession 拥有一个确切的顶层 Agent 引用、模型控制对象、提示准入状态、顺序输出队列和可复用的关闭 Promise。这样会话事件不会因为 ID 相同而错误地投递给另一 Agent。默认通过进程 stdin/stdout 上的 ndJsonStream 连接 SDK,config.stream 可替代该传输。index.ts · session.ts · index.ts
架构
Source: index.ts, session.ts
最后一条回边表示 AcpSession 经传入的 notify 回调把更新送回入口持有的 SDK 客户端,并非重新发起 ACP 请求。ctx.agents.create / resume 的 setup 中先安装模型控制并挂载请求中的 MCP server,成功返回后才构造会话记录。index.ts · session.ts
协议入口与会话生命周期
初始化、路由和依赖
inject 声明 agents、llm、sessionPersistence、sessions;初始化探测当前提供商/模型是否支持 ACP 图片提示,返回 SDK 的 PROTOCOL_VERSION、HTTP MCP 能力、图片能力及 close/list/resume 会话能力。音频与 embedded context 明确为 false,authMethods 为空,authenticate 直接成功。图片能力是在连接初始化时计算,之后传给 record.prompt,而不是每个请求重新计算。index.ts · index.ts · index.ts
SDK 路由覆盖 initialize、authenticate、session/new、list、resume、close、setConfigOption、prompt,以及 session/cancel 通知。底层 handler 的请求 signal 传给有取消意义的会话/持久化操作;cancel 是通知,仅对当前映射中的会话调用 cancel()。index.ts
创建:组成后发布,发布后落盘
newSession 先检查桥接器未关闭与工作目录参数,生成 UUID 类型的会话 ID,再创建带工作目录、MCP 列表、初始模型配置及通知函数的 AcpSession。创建时 ctx.agents.create 在 setup 中安装 AcpModelControl 和 MCP;返回完整记录才加入 sessions。随后查询配置选项并调用 ctx.sessions.flush,使空会话也能持久化。若选项读取或 flush 出错,映射被移除且记录被关闭;若连接在创建中关闭,也先关闭新记录再报错。MCP 配置错误转换为协议 invalidParams。index.ts · session.ts
恢复:排他激活与工作区校验
resumeSession 拒绝当前桥接器中、activating 集合中或全局 ctx.sessions 中的活动 ID。随后从 sessionPersistence.stat 读取头部,排除不存在的、子 Agent 来源及带 parentSession 的会话,并用 sameDirectory 核验工作目录。activating 在异步恢复的 finally 中移除,防止并发恢复同一 ID。恢复过程中 ctx.agents.resume 的 setup 使用最近日志头中的 provider/model 选择,缺失时回退到部署配置,并重新挂载本次请求的 MCP 连接。恢复成功后才加入映射,选项发现失败则反向清理。index.ts · session.ts · session.ts
Sources:
列表、配置与关闭
listSessions 要求可选 cwd 为绝对路径,解析游标后从持久化服务列出记录;过滤活动、激活中、子 Agent、缺失/非绝对路径和不匹配目录的会话。按创建时间降序、ID 作平局排序,再按游标与页大小截取;只有尚有后续结果时返回 nextCursor。这是可恢复会话的候选列表,不是活动会话清单。index.ts
setSessionConfigOption 仅面向已映射会话,委托 record.setConfig,将 AcpModelConfigError 转换为 invalidParams。LLM adapter 更新事件使所有记录执行 topologyChanged();它在选项发现后把 config_option_update 串入 outputTail,避免与既有输出乱序。closeSession 即使关闭出错也在 finally 中删除自身映射;错误通过 errorChain 包入内部错误。index.ts · index.ts · session.ts
使用示例
以下均为仓库内的实际代码摘录,展示插件配置、协议装配及按会话隔离的 Agent 组成;并非额外定义的客户端调用 API。
1export const name = 'acp'
2export const inject = ['agents', 'llm', 'sessionPersistence', 'sessions']
3
4export interface AcpConfig {
5 provider?: string
6 model?: string
7 sessionListPageSize?: number
8 stream?: Stream
9}Source: index.ts
inject 是运行依赖,stream 是运行时传输覆盖,Schema 并未将其作为常规部署配置字段。
1const handle = await ctx.agents.create({
2 sessionId: options.sessionId,
3 meta: { cwd: options.cwd },
4 agentOptions: options.agentOptions,
5 signal: options.signal,
6 setup: async (agentCtx) => {
7 modelControl.install(agentCtx)
8 await mountAcpMcpServers(agentCtx, options.mcpServers, options.cwd)
9 },
10})
11return new AcpSession(ctx, handle, modelControl, options.notify)Source: session.ts
组成发生在 create 内,随后才向桥接器公布记录;失败的组成不会产生已发布映射。
1.onRequest(methods.agent.session.new, ({ params, signal }) => implementation.newSession(params, signal))
2.onRequest(methods.agent.session.list, ({ params, signal }) => implementation.listSessions(params, signal))
3.onRequest(methods.agent.session.resume, ({ params, signal }) => implementation.resumeSession(params, signal))
4.onRequest(methods.agent.session.close, ({ params }) => implementation.closeSession(params))
5.onRequest(methods.agent.session.setConfigOption, ({ params, signal }) => implementation.setSessionConfigOption(params, signal))
6.onRequest(methods.agent.session.prompt, ({ params, signal }) => implementation.prompt(params, signal))
7.onNotification(methods.agent.session.cancel, ({ params }) => implementation.cancel(params))Source: index.ts
配置选项
| 选项 | 类型 | 默认值 | 作用 |
|---|---|---|---|
provider | string? | 未指定 | 新 Agent 的提供商路由;缺失时不向 agentOptions 写该字段。 |
model | string? | 未指定 | 新 Agent 的模型;只有与 provider 同时存在才形成初始 ModelSelection。 |
sessionListPageSize | number? | Schema 中为 100 | 会话列表每页最多返回的摘要数;Schema 限制为至少 1 的自然数;apply 还会通过 resolveSessionListPageSize 取得运行值。 |
stream | Stream? | stdin/stdout 的 ndJsonStream | 仅运行时传输覆盖;生产默认使用 stdio,测试可注入。 |
依据:index.ts · index.ts · index.ts。不要将可选的 provider 或 model 单独配置解释成强制绑定的完整模型路由。
API 参考
下列是读取到的入口处理器与会话方法,ACP wire 方法由 SDK 的 methods.agent.* 常量绑定;不是 HTTP 路由。index.ts · index.ts
| 方法签名 | 参数与返回 | 已核实的失败或边界 |
|---|---|---|
apply(ctx: Context, config: AcpConfig): void | 注册协议连接与事件订阅;不返回连接句柄。 | 关闭后请求会由 assertOpen 拒绝。 |
newSession(params: NewSessionRequest, signal: AbortSignal): Promise<NewSessionResponse> | 用 cwd、mcpServers 创建记录,返回 sessionId、configOptions。 | MCP 配置错误映射为 invalidParams;激活失败清理记录。 |
resumeSession(params: ResumeSessionRequest, signal: AbortSignal): Promise<ResumeSessionResponse> | 恢复 ID 和请求提供的工作区及 MCP,返回配置选项。 | 活动、非合格持久化记录或目录不匹配时 invalidParams。 |
listSessions(params: ListSessionsRequest, signal: AbortSignal): Promise<ListSessionsResponse> | 可选 cwd 和游标;返回 sessions 与可选 nextCursor。 | 非绝对路径或错误游标映射为 invalidParams。 |
setSessionConfigOption(params: SetSessionConfigOptionRequest, signal: AbortSignal): Promise<SetSessionConfigOptionResponse> | 传递 configId、value,返回完整配置选项。 | 未知会话或 AcpModelConfigError 为 invalidParams。 |
closeSession(params: CloseSessionRequest): Promise<CloseSessionResponse> | 关闭记录并返回空对象。 | 关闭失败为带错误链的内部错误。 |
prompt(params: PromptRequest, requestSignal: AbortSignal): Promise<PromptResponse> | 委托会话 prompt,传入初始化时确定的图片能力。 | 未知会话为 invalidParams;提示内部处理细节此处未验证。 |
cancel(params: CancelNotification): Promise<void> | 通知已有会话取消;总是返回 resolved promise。 | 找不到映射时不执行操作。 |
处理器证据:index.ts · index.ts。会话层可见的 configOptions(signal?: AbortSignal): Promise<SessionConfigOption[]> 与 setConfig(configId: string, value: unknown, signal?: AbortSignal): Promise<SessionConfigOption[]> 都先调用 assertActive() 再委托 AcpModelControl;具体选项 ID 和有效值应以返回的选项为准,不能从入口代码推断。session.ts
失败、并发与运行注意事项
- 会话归属:
ownedRecord同时查询 ID 并校验record.owns(agent);持久化事件校验ownsSession(session)。这样事件和审批请求只作用于桥接器真正持有的对象。审批要求callId;否则交给next();有效请求先drainUpdates(),然后仅提供allow-once/reject-once,除cancelled外未知选择按拒绝处理。index.ts - 并发恢复:
activating覆盖从校验到异步stat/resume完成的窗口;列表也排除这段期间的 ID。已有全局活动会话同样不能恢复。index.ts · index.ts - 传输与输出:
notify捕获通知发送异常、写日志而不向事件源传播;每会话outputTail串行化模型拓扑更新通知。提示准入状态包含inflight,但未读取完整提示实现,故不在这里推断其所有竞态结论。index.ts · session.ts · session.ts - 收尾:连接关闭或 Cordis effect 触发
quiesce;它先标记closed、复用单一quiescingPromise、并发Promise.allSettled关闭快照中的记录,清除映射,再把失败合并为AggregateError。传输关闭与收尾错误都记录警告。关闭会话的实现先同步请求取消再进行异步清理(入口代码中的注释明确指出这一顺序)。index.ts - 分页成本:列表对
persistence.list()的结果进行异步过滤、全量排序后分页,而非先由持久化层分页;规模增大时应关注全量扫描和目录比较开销。此处是代码路径推论,不代表已经进行基准测试。index.ts
扩展边界与相关链接
扩展协议行为时,优先区分连接级逻辑(apply 的 SDK 路由、映射与 teardown)和会话级逻辑(AcpSession 的 Agent/MCP 组成与输出队列);改动 wire 能力声明时同步检查初始化返回和真实处理器。插件不在自己的 apply 回调之外延迟访问注入的持久化服务,而是先捕获到局部变量。index.ts · index.ts