主题
OpenClaw 源码解读(十四)ACP 控制平面
一、导读
ACP(Agent Client Protocol)是 OpenClaw 为 IDE 集成 设计的标准协议层。它让 Cursor、VS Code、Claude Desktop 等外部 IDE 客户端,通过统一的 JSON-RPC 流式接口与 OpenClaw Gateway 通信——就像一个"翻译官"站在 IDE 和网关之间,把 ACP 标准协议翻译成 OpenClaw 的内部 Gateway RPC。
本文将沿着一条从外到内的主线来拆解这套系统:
IDE 客户端 ←(ACP协议/ndJSON)→ ACP Server ←(WebSocket)→ Gateway
↕
Control Plane(管理器/运行时缓存/Actor队列)涉及的核心源码全部位于 src/acp/ 目录,约 30 个文件,~4000 行代码。
ACP 控制平面/01-infographic-acp-system-overview-1775150800967.png)
二、ACP 协议两端:Client 与 Server
2.1 Server 端 —— server.ts
serveAcpGateway() 是 ACP 服务器的入口函数。它做了三件事:
- 建立 Gateway WebSocket 连接:加载配置、解析凭证、启动
GatewayClient,并等待onHelloOk确认握手完成。 - 桥接 stdio 到 ndJSON 流:将
process.stdin/process.stdout转为 Web Streams,通过ndJsonStream()创建双向 JSON 传输通道。 - 创建 ACP Agent:实例化
AgentSideConnection(来自@agentclientprotocol/sdk),把翻译器AcpGatewayAgent注入其中。
关键设计:ACP Server 是一个 stdio 进程(不是 HTTP 服务器),IDE 通过 spawn 子进程与它通信。这与 MCP 的 stdio 模式完全一致,意味着 IDE 只需 spawn openclaw acp 就能获得一个 ACP Agent。
IDE → stdin → ndJsonStream → AcpGatewayAgent → GatewayClient → WebSocket → Gateway
IDE ← stdout ← ndJsonStream ← AcpGatewayAgent ← GatewayClient ← WebSocket ← Gateway2.2 Client 端 —— client.ts
Client 端是 OpenClaw 自己的 ACP 消费者实现(交互式客户端),核心在 createAcpClient():
- Spawn ACP Server 子进程:解析命令行参数、确定
openclaw acp的路径,调用node:child_process.spawn()启动。 - 建立
ClientSideConnection:SDK 提供的 ACP 客户端连接,桥接子进程的 stdin/stdout。 - 权限控制系统:定义了工具调用的权限审批逻辑
resolvePermissionRequest()。
权限审批机制值得特别关注:
typescript
// 安全自动批准的工具 ID
const SAFE_AUTO_APPROVE_TOOL_IDS = new Set(["read", "search", "web_search", "memory_search"]);
// 对 read 工具做路径范围检查——只有在 cwd 内的读取才自动批准
function isReadToolCallScopedToCwd(...) {
const absolutePath = resolveAbsoluteScopedPath(rawPath, cwd);
return isPathWithinRoot(absolutePath, path.resolve(cwd));
}这套机制确保了:
read、search等只读工具自动批准(前提是 read 路径在工作目录内)DANGEROUS_ACP_TOOLS中的工具强制交互式确认- 非 TTY 环境下所有工具一律拒绝(30 秒超时)
ACP 控制平面/02-infographic-client-server-bridge-1775150801910.png)
三、翻译器:AcpGatewayAgent
translator.ts 中的 AcpGatewayAgent 是整个 ACP 系统的核心。它实现了 @agentclientprotocol/sdk 的 Agent 接口,充当 ACP 协议和 Gateway 之间的双向翻译器。
3.1 Session 生命周期
IDE: newSession() → AcpGatewayAgent → 生成 UUID sessionId
→ 解析 meta(sessionKey/label/reset)
→ 通过 Gateway 解析或创建 session
→ 存入 SessionStore
→ 发送 availableCommands 到 IDE
→ 返回 { sessionId }三种进入方式:
newSession:创建全新 ACP 会话,生成 UUID 作为 sessionIdloadSession:加载已有会话(支持 IDE 重启后恢复)unstable_listSessions:列出所有 Gateway 会话(通过sessions.listRPC)
3.2 Prompt 处理流程
prompt() 方法是最复杂的翻译环节:
IDE prompt → 提取文本 → 附加 cwd 前缀 → chat.send RPC → 等待事件流关键安全措施:
- 2MB 大小限制:逐块计算字节数,在内存分配前拒绝超大 prompt(CWE-400 防护)
- Abort 控制:每个 prompt 创建 AbortController,支持 IDE 端的取消操作
- 幂等键:使用
runId作为idempotencyKey,确保重复请求不会产生多次执行 - 速率限制:session 创建受固定窗口限流保护(默认 120 次/10 秒)
3.3 事件流翻译
Gateway 返回的事件被翻译成 ACP 标准的 sessionUpdate 通知:
| Gateway 事件 | ACP sessionUpdate | 说明 |
|---|---|---|
chat.delta | agent_message_chunk | 增量文本(只发送新增部分) |
chat.final | stopReason: end_turn | 正常完成 |
chat.aborted | stopReason: cancelled | 被取消 |
chat.error | stopReason: refusal | 出错 |
agent.tool.start | tool_call | 工具调用开始 |
agent.tool.result | tool_call_update | 工具调用结束 |
增量文本的处理特别精巧——Gateway 返回的是 累积全文,但 ACP 需要 增量 chunk:
typescript
const sentSoFar = pending.sentTextLength ?? 0;
if (fullText.length <= sentSoFar) return;
const newText = fullText.slice(sentSoFar);
pending.sentTextLength = fullText.length;ACP 控制平面/03-infographic-translator-core-1775150802717.png)
四、事件映射与工具推断 —— event-mapper.ts
事件映射器处理三类转换:
4.1 Prompt 内容提取
extractTextFromPrompt() 支持三种 ACP ContentBlock 类型:
text:直接提取文本resource:提取内嵌资源文本resource_link:转换为[Resource link (title)] uri格式
对控制字符做了完整的转义处理(\0、\r、\n 等),防止注入。
4.2 附件提取
extractAttachmentsFromPrompt() 从 prompt 中提取 image 类型的 ContentBlock,转换为 Gateway 的附件格式(base64 data + mimeType)。
4.3 工具类型推断
inferToolKind() 通过工具名称的关键字匹配推断工具类型:
typescript
if (normalized.includes("read")) return "read";
if (normalized.includes("write") || normalized.includes("edit")) return "edit";
if (normalized.includes("delete") || normalized.includes("remove")) return "delete";
if (normalized.includes("search") || normalized.includes("find")) return "search";
if (normalized.includes("exec") || normalized.includes("run")) return "execute";
if (normalized.includes("fetch") || normalized.includes("http")) return "fetch";这个推断结果会作为 ToolKind 发送给 IDE,帮助 IDE 做差异化的 UI 展示。
ACP 控制平面/04-infographic-event-mapper-1775150803507.png)
五、Session 映射 —— session-mapper.ts
Session 映射器解决的核心问题是:ACP sessionId 如何映射到 Gateway sessionKey?
5.1 Meta 解析
parseSessionMeta() 从 ACP 请求的 _meta 字段中提取会话参数,支持多种别名:
typescript
sessionKey: readString(record, ["sessionKey", "session", "key"]),
sessionLabel: readString(record, ["sessionLabel", "label"]),
resetSession: readBool(record, ["resetSession", "reset"]),
requireExisting: readBool(record, ["requireExistingSession", "requireExisting"]),这种多别名设计让不同 IDE 客户端可以用自己习惯的字段名。
5.2 Session Key 解析策略
resolveSessionKey() 按优先级解析:
1. meta.sessionLabel → Gateway sessions.resolve(label)
2. meta.sessionKey → 直接返回 / Gateway sessions.resolve(key)
3. opts.defaultLabel → Gateway sessions.resolve(label)
4. opts.defaultKey → 直接返回 / Gateway sessions.resolve(key)
5. fallbackKey → 最终兜底当 requireExisting 为 true 时,所有 key 都必须通过 Gateway 验证确实存在。
ACP 控制平面/05-infographic-session-mapper-1775150804211.png)
六、内存 Session Store —— session.ts
ACP 服务端用纯内存 Map 管理活跃会话,支持以下特性:
| 特性 | 实现 |
|---|---|
| 最大会话数 | 默认 5000,超限时驱逐最旧的空闲会话 |
| 空闲过期 | 默认 24 小时 TTL,定期 reap |
| Run 追踪 | 双向映射 sessionId ↔ runId |
| 取消控制 | cancelActiveRun() 触发 AbortController |
驱逐策略的设计很有意思——它遵循"活跃运行不可驱逐"原则:
typescript
const evictOldestIdleSession = () => {
for (const [sessionId, session] of sessions.entries()) {
if (session.activeRunId || session.abortController) continue; // 跳过正在运行的
if (session.lastTouchedAt >= oldestLastTouchedAt) continue;
oldestLastTouchedAt = session.lastTouchedAt;
oldestSessionId = sessionId;
}
return removeSession(oldestSessionId);
};ACP 控制平面/06-infographic-session-store-1775150804956.png)
七、策略控制 —— policy.ts
策略层提供细粒度的 ACP 功能开关:
7.1 三级开关
acp.enabled = false → 整个 ACP 关闭
acp.dispatch.enabled = false → 仅关闭 dispatch(分发)
acp.allowedAgents = [...] → 白名单过滤7.2 Agent 白名单
typescript
export function isAcpAgentAllowedByPolicy(cfg, agentId): boolean {
const allowed = (cfg.acp?.allowedAgents ?? [])
.map(entry => normalizeAgentId(entry))
.filter(Boolean);
if (allowed.length === 0) return true; // 空白名单 = 允许全部
return allowed.includes(normalizeAgentId(agentId));
}白名单为空时默认放行所有 agent——这是一种"默认开放"的设计,降低了配置门槛。
八、ACP 控制平面核心 —— AcpSessionManager
control-plane/manager.core.ts 中的 AcpSessionManager 是整个 ACP 系统最复杂的组件。它管理 ACP 会话的完整生命周期:初始化、运行、取消、关闭、驱逐。
8.1 核心数据结构
typescript
class AcpSessionManager {
private readonly actorQueue = new SessionActorQueue(); // Actor 并发控制
private readonly runtimeCache = new RuntimeCache(); // 运行时句柄缓存
private readonly activeTurnBySession = new Map<string, ActiveTurnState>(); // 活跃 Turn
private readonly turnLatencyStats: TurnLatencyStats; // 延迟统计
private readonly errorCountsByCode = new Map<string, number>(); // 错误计数
}四大支柱:Actor 队列、运行时缓存、Turn 追踪、可观测性统计。
8.2 Session 解析三态
resolveSession() 返回三种状态:
typescript
type AcpSessionResolution =
| { kind: "none"; sessionKey: string } // 非 ACP 会话
| { kind: "stale"; sessionKey: string; error: AcpRuntimeError } // 有 ACP key 但缺 meta
| { kind: "ready"; sessionKey: string; meta: SessionAcpMeta } // 就绪stale 状态发生在会话的 ACP 元数据丢失(可能是手动删除或迁移导致),此时会提示用户重新创建。
8.3 初始化流程
initializeSession() 是创建 ACP 会话的完整流程:
1. 规范化 sessionKey 和 agentId
2. 驱逐空闲运行时句柄
3. 进入 Actor 队列(串行化)
4. 获取运行时后端(requireRuntimeBackend)
5. 验证并发会话限制
6. 调用 runtime.ensureSession()
7. 构造并持久化 SessionAcpMeta
8. 缓存运行时句柄失败回滚特别严谨——如果元数据写入失败,会自动 close 已创建的运行时:
typescript
try {
await this.writeSessionMeta({ ... });
} catch (error) {
await runtime.close({ handle, reason: "init-meta-failed" }).catch(...);
throw error;
}8.4 Turn 执行引擎
runTurn() 是核心的"一轮对话"执行逻辑:
1. 解析 session → 确保运行时句柄
2. 应用运行时控制(mode/model/配置)
3. 设置状态为 "running"
4. 创建 AbortController + 注册 Turn
5. 遍历 runtime.runTurn() 的 AsyncIterable 事件流
6. 成功 → 记录延迟统计 → 状态改为 "idle"
7. 失败 → 记录错误 → 状态改为 "error"
8. finally:
- 非 oneshot → 协调运行时身份标识
- oneshot → 关闭运行时并清除缓存AbortSignal 的处理使用了双层设计:
typescript
const internalAbortController = new AbortController();
const onCallerAbort = () => internalAbortController.abort();
input.signal?.addEventListener("abort", onCallerAbort, { once: true });
// 组合信号
const combinedSignal = AbortSignal.any([input.signal, internalAbortController.signal]);外层 input.signal 来自调用方(比如用户取消),内层 internalAbortController 来自管理器自身(比如 cancelSession)。使用 AbortSignal.any() 合并两个信号源。
ACP 控制平面/07-infographic-manager-turn-engine-1775150805968.png)
九、Session Actor Queue —— 并发控制
session-actor-queue.ts 实现了 per-session 串行化 的 Actor 模型:
typescript
export class SessionActorQueue {
private readonly queue = new KeyedAsyncQueue();
async run<T>(actorKey: string, op: () => Promise<T>): Promise<T> {
return this.queue.enqueue(actorKey, op, {
onEnqueue: () => { /* 增加 pending 计数 */ },
onSettle: () => { /* 减少 pending 计数 */ },
});
}
}基于 KeyedAsyncQueue(来自 plugin-sdk),每个 sessionKey 被归一化为 actorKey,同一个 actor 的操作严格串行,不同 actor 完全并行。
这个设计解决了 ACP 场景下的经典问题:同一个 IDE 会话的多个操作(比如用户快速连发两条消息)必须按序执行,但不同会话之间不应互相阻塞。
Manager 暴露了 getTotalPendingCount() 供可观测性使用——在 /acp status 中可以看到全局队列深度。
ACP 控制平面/08-infographic-actor-queue-1775150806720.png)
十、Runtime Cache —— 运行时句柄缓存
runtime-cache.ts 管理 ACP 运行时句柄的内存缓存:
typescript
type CachedRuntimeState = {
runtime: AcpRuntime; // 运行时实例
handle: AcpRuntimeHandle; // 会话句柄
backend: string; // 后端标识
agent: string; // Agent ID
mode: AcpRuntimeSessionMode; // persistent | oneshot
cwd?: string; // 工作目录
appliedControlSignature?: string; // 已应用的控制配置签名
};10.1 缓存命中判断
ensureRuntimeHandle() 的缓存命中需要四项全部匹配:
typescript
const backendMatches = !configuredBackend || cached.backend === configuredBackend;
const agentMatches = cached.agent === agent;
const modeMatches = cached.mode === mode;
const cwdMatches = (cached.cwd ?? "") === (cwd ?? "");任一不匹配就清除缓存并重新创建。
10.2 空闲驱逐
Manager 的 evictIdleRuntimeHandles() 使用 collectIdleCandidates() 找出超过 TTL 的缓存条目:
typescript
const candidates = this.runtimeCache.collectIdleCandidates({
maxIdleMs: idleTtlMs,
now,
});
for (const candidate of candidates) {
await this.actorQueue.run(candidate.actorKey, async () => {
if (this.activeTurnBySession.has(candidate.actorKey)) return; // 正在运行的不驱逐
// ... close runtime
this.evictedRuntimeCount += 1;
});
}驱逐操作也通过 Actor Queue 串行化,避免驱逐和 Turn 执行的竞态。
10.3 控制配置签名
appliedControlSignature 是一个 JSON 字符串指纹,记录上次成功推送到运行时的配置:
typescript
function buildRuntimeControlSignature(options): string {
return JSON.stringify({
runtimeMode: normalized.runtimeMode ?? null,
model: normalized.model ?? null,
permissionProfile: normalized.permissionProfile ?? null,
timeoutSeconds: normalized.timeoutSeconds ?? null,
backendExtras: entries,
});
}每次 runTurn() 前,如果签名未变则跳过控制推送,避免重复 RPC 调用。
ACP 控制平面/09-infographic-runtime-cache-1775150807487.png)
十一、Runtime 抽象层
11.1 AcpRuntime 接口
runtime/types.ts 定义了运行时后端必须实现的接口:
typescript
interface AcpRuntime {
ensureSession(input: AcpRuntimeEnsureInput): Promise<AcpRuntimeHandle>;
runTurn(input: AcpRuntimeTurnInput): AsyncIterable<AcpRuntimeEvent>;
cancel(input: { handle; reason? }): Promise<void>;
close(input: { handle; reason }): Promise<void>;
// 可选能力
getCapabilities?(input): Promise<AcpRuntimeCapabilities> | AcpRuntimeCapabilities;
getStatus?(input): Promise<AcpRuntimeStatus>;
setMode?(input): Promise<void>;
setConfigOption?(input): Promise<void>;
doctor?(): Promise<AcpRuntimeDoctorReport>;
}必选方法 4 个(ensure/runTurn/cancel/close),可选方法 5 个(getCapabilities/getStatus/setMode/setConfigOption/doctor)。
11.2 事件流类型
AcpRuntimeEvent 是一个判别联合类型,覆盖 5 种事件:
| type | 说明 | 附加字段 |
|---|---|---|
text_delta | 文本增量 | text, stream(output/thought) |
status | 状态更新 | text, used/size |
tool_call | 工具调用 | toolCallId, status, title |
done | 完成 | stopReason |
error | 错误 | message, code, retryable |
11.3 Session 模式
typescript
type AcpRuntimeSessionMode = "persistent" | "oneshot";- persistent:会话保持,Turn 结束后运行时不关闭(标准模式)
- oneshot:一次性,Turn 结束后自动 close 运行时并清除缓存
11.4 后端注册中心
runtime/registry.ts 使用 Symbol 全局注册表管理后端:
typescript
const ACP_RUNTIME_REGISTRY_STATE_KEY = Symbol.for("openclaw.acpRuntimeRegistryState");后端选择逻辑:
- 指定 ID → 精确匹配
- 未指定 → 遍历所有后端,优先选健康的
- 找不到 → 抛出
ACP_BACKEND_MISSING错误
健康检查通过可选的 healthy() 方法实现:
typescript
function isBackendHealthy(backend: AcpRuntimeBackend): boolean {
if (!backend.healthy) return true; // 没有健康检查 = 默认健康
try { return backend.healthy(); }
catch { return false; }
}ACP 控制平面/10-infographic-runtime-abstraction-1775150808583.png)
十二、错误体系
runtime/errors.ts 定义了 7 种 ACP 错误码:
| 错误码 | 含义 |
|---|---|
ACP_BACKEND_MISSING | 后端未安装/未注册 |
ACP_BACKEND_UNAVAILABLE | 后端暂时不可用 |
ACP_BACKEND_UNSUPPORTED_CONTROL | 后端不支持请求的控制操作 |
ACP_DISPATCH_DISABLED | 策略禁止了 ACP dispatch |
ACP_INVALID_RUNTIME_OPTION | 运行时选项验证失败 |
ACP_SESSION_INIT_FAILED | 会话初始化失败 |
ACP_TURN_FAILED | Turn 执行失败 |
withAcpRuntimeErrorBoundary() 是统一的错误边界包装器——任何异常都会被规范化为 AcpRuntimeError:
typescript
async function withAcpRuntimeErrorBoundary<T>(params: {
run: () => Promise<T>;
fallbackCode: AcpRuntimeErrorCode;
fallbackMessage: string;
}): Promise<T> {
try { return await params.run(); }
catch (error) { throw toAcpRuntimeError({ error, fallbackCode, fallbackMessage }); }
}十三、Session Identity 系统
runtime/session-identity.ts 解决了一个复杂的问题:ACP 会话在不同后端之间的身份标识协调。
13.1 身份状态机
typescript
type SessionAcpIdentity = {
state: "pending" | "resolved";
source: "ensure" | "status" | "event";
acpxRecordId?: string; // 后端记录 ID
acpxSessionId?: string; // 后端会话 ID
agentSessionId?: string; // 上游 Agent 会话 ID
lastUpdatedAt: number;
};身份从 pending 开始,当获取到 acpxSessionId 或 agentSessionId 后转为 resolved。
13.2 身份合并策略
mergeSessionIdentity() 实现了"只升级不降级"的合并逻辑:
current=pending + incoming=resolved → 采用 incoming(升级)
current=resolved + incoming=pending → 保留 current(不降级)
current=resolved + incoming=resolved → 采用 incoming(覆盖)13.3 三种身份来源
- ensure:调用
runtime.ensureSession()时获得的初始身份 - status:调用
runtime.getStatus()时获得的实时身份 - event:从运行时事件流中获得的身份更新
Manager 在 Turn 结束后会调用 reconcileRuntimeSessionIdentifiers() 来协调身份,确保持久化的 meta 和缓存中的 handle 都保持最新。
十四、运行时选项系统
control-plane/runtime-options.ts 管理 6 个可配置的运行时选项:
| 选项 | 类型 | 限制 |
|---|---|---|
runtimeMode | string | ≤ 64 字符 |
model | string | ≤ 200 字符 |
cwd | string | ≤ 4096 字符,必须绝对路径 |
permissionProfile | string | ≤ 80 字符 |
timeoutSeconds | number | 1 ~ 86400 |
backendExtras | Record<string, string> | ≤ 32 个键值对 |
每个选项都有严格的验证:长度限制、控制字符检查、键名正则校验(/^[a-z0-9][a-z0-9._:-]*$/i)。
inferRuntimeOptionPatchFromConfigOption() 支持从 key-value 配置推断运行时选项:
"model" → { model: value }
"approval_policy" / "permissions" → { permissionProfile: value }
"timeout" → { timeoutSeconds: parseInt(value) }
"cwd" → { cwd: value }
其他 → { backendExtras: { key: value } }十五、运行时控制推送
manager.runtime-controls.ts 负责在 Turn 执行前将配置推送到运行时后端:
1. 计算当前选项的签名
2. 比较缓存中的 appliedControlSignature
3. 如果一致 → 跳过
4. 如果不一致 → 解析运行时能力 → 推送 setMode/setConfigOption推送的配置项通过 buildRuntimeConfigOptionPairs() 转换为键值对:
model → "model"
permissionProfile → "approval_policy"
timeoutSeconds → "timeout" (转字符串)
backendExtras 中的其他键 → 直接传递如果后端不支持请求的控制操作,会抛出 ACP_BACKEND_UNSUPPORTED_CONTROL。
十六、Spawn 与清理
control-plane/spawn.ts 处理 ACP 会话创建失败后的清理:
typescript
async function cleanupFailedAcpSpawn(params) {
// 1. 关闭运行时
await runtimeCloseHandle.runtime.close({ handle, reason: "spawn-failed" });
// 2. 通过 Manager 关闭会话
await acpManager.closeSession({ sessionKey, reason: "spawn-failed" });
// 3. 解绑 session binding
await getSessionBindingService().unbind({ targetSessionKey, reason: "spawn-failed" });
// 4. 删除 Gateway 会话
await callGateway({ method: "sessions.delete", ... });
}四层清理,每层都用 .catch() 吞掉错误——best-effort 原则,确保清理不会因某一步失败而中断。
十七、Commands 系统
commands.ts 定义了 ACP 会话中可用的斜杠命令列表:
typescript
export function getAvailableCommands(): AvailableCommand[] {
return [
{ name: "help", description: "Show help and common commands." },
{ name: "status", description: "Show current status." },
{ name: "think", description: "Set thinking level (off|minimal|low|medium|high|xhigh)." },
{ name: "model", description: "Select a model (list|status|<name>)." },
{ name: "reset", description: "Reset the session (/new)." },
// ... 共 26 个命令
];
}这些命令在 session 创建/加载时通过 available_commands_update 推送给 IDE,IDE 可以据此构建命令面板。
十八、可观测性
Manager 暴露 getObservabilitySnapshot() 提供全局状态快照:
typescript
type AcpManagerObservabilitySnapshot = {
runtimeCache: {
activeSessions: number;
idleTtlMs: number;
evictedTotal: number;
lastEvictedAt?: number;
};
turns: {
active: number;
queueDepth: number;
completed: number;
failed: number;
averageLatencyMs: number;
maxLatencyMs: number;
};
errorsByCode: Record<string, number>;
};这为运维提供了关键指标:活跃会话数、队列深度、Turn 延迟统计、错误码分布。
十九、安全设计总结
ACP 系统的安全设计贯穿各层:
| 层级 | 安全措施 |
|---|---|
| 协议层 | 2MB prompt 大小限制(CWE-400 防护) |
| 认证层 | Gateway token/password,支持文件读取避免命令行泄漏 |
| 策略层 | 三级开关 + Agent 白名单 |
| 权限层 | 工具调用分级审批(自动/交互/拒绝) |
| 会话层 | 速率限制(120次/10秒)+ 并发限制 |
| 运行时层 | 选项验证(长度/字符/路径)、统一错误边界 |
| 清理层 | 四层 best-effort 清理 |
ACP 控制平面/11-infographic-security-layers-1775150809593.png)
二十、设计模式总结
| 模式 | 应用位置 | 效果 |
|---|---|---|
| Actor Model | SessionActorQueue | per-session 串行化,跨 session 并行 |
| Translator | AcpGatewayAgent | ACP 协议 ↔ Gateway RPC 双向翻译 |
| Cache-Aside | RuntimeCache | 运行时句柄缓存 + 空闲驱逐 |
| Factory + Registry | AcpRuntimeBackend | 插件化运行时后端 |
| Error Boundary | withAcpRuntimeErrorBoundary | 统一错误规范化 |
| Rate Limiter | FixedWindowRateLimiter | 会话创建限流 |
| Idempotency Key | runId | 防止重复 prompt 执行 |
| Signature-based Skip | appliedControlSignature | 避免重复配置推送 |
ACP 控制平面/12-infographic-design-patterns-1775150810748.png)
二十一、推荐阅读顺序
src/acp/types.ts— 基础类型定义src/acp/policy.ts— 策略控制src/acp/session.ts— 内存 Session Storesrc/acp/session-mapper.ts— Session Key 解析src/acp/event-mapper.ts— 事件翻译src/acp/translator.ts— 核心翻译器(AcpGatewayAgent)src/acp/server.ts— 服务端入口src/acp/client.ts— 客户端 + 权限系统src/acp/runtime/types.ts— 运行时抽象接口src/acp/runtime/errors.ts— 错误体系src/acp/runtime/registry.ts— 后端注册中心src/acp/runtime/session-identity.ts— 身份协调src/acp/control-plane/session-actor-queue.ts— Actor 队列src/acp/control-plane/runtime-cache.ts— 运行时缓存src/acp/control-plane/runtime-options.ts— 选项系统src/acp/control-plane/manager.core.ts— Manager 核心
二十二、思考题
为什么 ACP Server 用 stdio 而不是 HTTP? 这和 MCP 的 stdio 传输有什么共同的设计考量?
Actor Queue 的 per-session 串行化会成为性能瓶颈吗? 什么场景下可能需要放松这个约束?
Session Identity 的"只升级不降级"策略在什么边界情况下可能出问题? 比如后端更换了会话 ID 但保持了 pending 状态。
运行时控制的签名比较(
appliedControlSignature)使用 JSON.stringify 做序列化——这种方式的局限性是什么? 有没有更可靠的签名方案?如果你要为 ACP 添加一种新的运行时后端(比如集成 GitHub Copilot),需要实现哪些接口?最小可行实现需要几个方法?