第 9 章:Agent 后端与协议适配
9.1 Multica 为什么不自己统一模型 API
Claude Code、Codex、Cursor、Pi 等工具已经各自实现了:
- 模型调用与 Provider 鉴权;
- Agent Loop;
- 文件、Shell、搜索等工具;
- Prompt/Context 发现;
- Session 与 Resume;
- 权限和 Sandbox;
- 流式事件与用量。
Multica 选择适配它们的 CLI 协议,而不是在 Server 重建所有能力。这样用户可继续使用本机登录态和工具生态,但 Multica 必须把 17 套启动参数、事件格式和失败语义压到一个接口。
9.2 统一接口
事实源在 server/pkg/agent/agent.go:
type Backend interface {
Execute(ctx context.Context, prompt string, opts ExecOptions) (*Session, error)
}
type Session struct {
Messages <-chan Message
Result <-chan Result
}
设计含义:
Execute的同步错误只表示“无法开始 Session”;- 运行期事件走
Messages; - 最终恰好一个
Result; - Message Channel 先关闭,Result 后送达;
- Daemon 不需要知道 Provider 如何读取 stdout、JSON-RPC 或文件 Transcript。
9.3 统一消息模型
七种消息类型:
| 类型 | 主要字段 | 用途 |
|---|---|---|
text | Content | 用户可见增量/文本 |
thinking | Content | 推理摘要或 Provider 暴露的思考 |
tool-use | Tool / CallID / Input | 工具开始 |
tool-result | Tool / CallID / Output | 工具结束 |
status | Status / SessionID | 生命周期、早期 Session Pin |
error | Content | 结构化运行错误 |
log | Level / Content | 诊断,不一定给普通用户 |
统一不等于“每个 Provider 都能填满全部字段”。Adapter 尽力映射,缺失能力保留为空,而不是伪造事件。
9.4 统一终态
Result 包含:
Status:completed/failed/aborted/timeout/cancelled;Output:Adapter 选择的最终用户可见答案;Error:失败说明;DurationMs;SessionID;- 按 Model 汇总的 Token Usage;
ResumeRejected:是否有正面证据表明续接被拒绝。
Token Usage 统一为 Input、Output、Cache Read、Cache Write;Grok 等 Provider 还可直接报告精确 Cost Ticks,避免用聚合 Token 重新估价时丢失请求级计费规则。
9.5 ExecOptions 是跨 Provider 的能力上限
主要选项:
Cwd / Model / SystemPrompt / ThreadName;MaxTurns / Timeout;SemanticInactivityTimeout / IdleWatchdogTimeout / HandshakeTimeout;ResumeSessionID / ResumeExpected;ExtraArgs / CustomArgs;McpConfig;ThinkingLevel / ServiceTier;OpenclawMode;ClaudeSettingsPath。
并非所有 Provider 都支持每项。接口采用“支持者消费,不支持者忽略”的渐进演进策略,避免新增一个 Codex Tier 让所有 Adapter 同时改签名。
9.6 当前 17 个 Provider
SupportedTypes 是当前快照的权威白名单:
claude, codebuddy, codex, copilot, opencode, deveco,
openclaw, hermes, pi, cursor, kimi, kiro, antigravity,
qoder, traecli, grok, qwen
它还必须与 runtime_profile.protocol_family 的数据库 CHECK 同步。仅在 Factory 增加 Case,而不扩迁移,会导致自定义 Runtime Profile 无法保存。
9.7 协议家族
可以按运输协议分组:
| 家族 | Provider | 启动/事件形状 |
|---|---|---|
| Stream JSON | Claude、CodeBuddy、Qwen、Cursor | CLI 输出 JSONL/stream-json,逐行映射消息 |
| App Server JSON-RPC | Codex | codex app-server --listen stdio://,初始化、Thread、Turn、通知 |
| ACP | Hermes、Kimi、Kiro、Qoder、Trae、Grok | stdio JSON-RPC,session/new、session/load、session/prompt |
| Run JSON | OpenCode、DevEco | run --format json 等 NDJSON Event |
| 独立 JSON | Copilot、OpenClaw、Pi | 各自 JSON/JSONL 模式 |
| Transcript/非交互 | Antigravity | 启动非交互命令并读取 Conversation Transcript |
Provider 同属一族也不代表参数完全相同。ACP Client 可复用,但认证、Model 设置、MCP Capability、Tool 名和 Resume 行为仍由各 Adapter 包装。
9.8 Stream JSON Adapter 的通用模式
以 Claude/CodeBuddy/Qwen 为例:
- 构造受控参数,指定非交互与机器可读输出;
- 启动子进程并获取 stdout/stderr;
- Scanner 逐行 JSON Decode;
- 按 Event Type 映射 Text/Thinking/Tool/Usage;
- 尽早发带 Session ID 的 Status;
- 解析最终 Result;
- Wait 子进程并区分 Exit Code、Context Cancel 与协议失败。
常见防线:
- 单行可能很大,需要扩大 Scanner Buffer;
- 坏 JSON 行记录受限诊断,不应泄漏整个 Prompt/凭据;
- stdout 是协议,不把普通日志混进去;
- stderr 进入 Log/Error,但不直接当最终用户输出;
- Channel 必须在所有返回分支关闭。
9.9 Codex App Server
Codex Adapter 不是简单 codex -p:
- 启动
codex app-server; - 在 stdio 上做 JSON-RPC Handshake;
- 创建或 Resume Thread;
- 启动 Turn;
- 读取 Item/Turn Notifications;
- 归并 Text、Reasoning、Command/Tool 与 Usage;
- 支持独立 Handshake Timeout、First-turn Semantic Progress Watchdog;
- 明确识别 Resume Rejection;
- 若预期续接却被迫 Fresh,向用户发 Continuity Notice。
MCP 通过任务级 Codex Config Block 管理,而不是盲目把任意 --mcp-config 参数拼到 App Server 后。
Codex 还要保证进程树被完整回收,尤其在 Windows 上,父进程退出不代表继承 stdout 的子进程关闭。
9.10 ACP 家族
ACP Adapter 大体遵循:
关键不是方法名,而是 Capability Negotiation:某 Provider 不支持的 Transport、Model 或 MCP 字段不能硬塞,否则 session/new 直接失败。
Resume 时若 session/load、set_model 或 prompt 明确返回 Session Not Found,Adapter 设置 ResumeRejected=true,Daemon 才有理由 Fresh Retry。
9.11 Resume Rejection 为什么必须是“正面证据”
网络断开、限流、鉴权失败、Provider 5xx 都不应该自动清除 Session;Fresh Session 不会治好这些问题,还会丢失对话连续性。
所以 ResumeRejected 的语义是:
Provider 明确证明指定 Session 不存在、不可加载或属于不兼容身份。
false 对部分 Provider 只表示“无法判断”。当前显式列为不可检测的有:
- Antigravity;
- Copilot;
- Cursor;
- DevEco;
- OpenCode。
新 Provider 默认按“应该可检测”处理:若不实现,就不会自动 Fresh Retry。这是一种 Fail Closed,避免猜测性重跑。
9.12 Custom Args 的信任边界
用户可给 Agent 配 custom_args,但不能覆盖 Multica 管理的协议参数:
- 输出格式;
- Prompt 输入;
- Resume Session;
- Permission/Approval Mode;
- App Server Transport;
- MCP Config;
- Cwd/Model 等关键字段。
每个 Adapter 定义 Blocked Standalone/Blocked With Value 参数并过滤冲突。否则用户一个 --output-format text 就会让 Daemon 的 JSON Parser 全部失效,或用另一个 --resume 绕过 Session 选择。
LaunchHeader 给 UI 展示“自定义参数追加在哪个命令骨架后”,但不会泄露完整内部参数或环境。
9.13 Process 生命周期
Adapter 的 Context 同时承载:
- 用户取消;
- Daemon Shutdown;
- 可选 Hard Timeout;
- Semantic/Idle Watchdog 触发。
runContext 的规则是:Timeout <= 0 不设墙钟 Deadline,让持续有事件的长任务由 Watchdog 管理。否则一个健康长任务会仅因耗时被杀。
进程停止要考虑:
- 关闭 stdin;
- 终止子进程组/Job Object;
- WaitDelay 防 Pipe 被后代进程永久持有;
- Drain 已到达的 stdout;
- 区分主动 Cancel 与进程异常退出。
9.14 Idle 与 Semantic Inactivity
“没有任何 stdout”与“有日志但没有语义进展”不同:
- Idle Watchdog:一段时间没有 Message;
- Tool Watchdog:工具仍在运行时使用更宽窗口;
- Semantic Inactivity:Codex 等可能持续发心跳/低价值事件,却没有 Text/Tool/Turn 进展;
- Handshake Timeout:协议还没建立;
- Hard Timeout:明确配置的总时限。
失败分类不同,重试和 Session Poison 策略也不同。
9.15 最终输出选择
Provider Stream 可能包含:
- 多个 Assistant Text Chunk;
- Tool Narration;
- 临时 Status;
- 最终 Result Field;
- stderr Warning;
- Fallback Message。
Adapter 要选择稳定的用户输出,而不是简单“最后一行 stdout”。例如:
- 优先结构化 Final Result;
- 若缺失,按 Provider 规则收集最后 Assistant Message;
- Tool Trace 不自动变成 Comment;
- Fallback 输出可能被标为会话毒化;
- 超大输出在 Server 端用固定安全提示替代,而不是截取可能包含执行细节的开头。
9.16 用量归一化
不同 Provider 报告:
- 单次或累计 Token;
- Model 名或 Alias;
- Cache Read/Write;
- Premium Request;
- Provider 直接成本。
Adapter 先归一为 map[model]TokenUsage,Daemon 再汇报 Server。Server 结合模型价格表和 Authoritative Cost 写 task_usage,并生成 Prometheus Counter。
不能假设所有 Provider 都有 Token;缺失应显示为未知,而不是 0 成本的确定事实。
9.17 怎样增加一个 Provider
至少要完成:
- 新建 Backend,实现 Channel 关闭和唯一 Result 约定;
- 在
SupportedTypes、New()、LaunchHeader注册; - 扩 Runtime Profile 数据库 CHECK Migration;
- Daemon 探测默认命令与版本;
- execenv 选择 Brief/Skill/MCP 注入方式;
- 前端 Provider 元数据、图标、Model/Thinking Capability;
- 明确 Resume、MCP、Usage、取消、Windows 行为;
- 加入 Stream Fixture 与协议测试;
- 验证 Custom Args 不能覆盖托管参数;
- 更新文档与发布资产。
只让 Factory 能创建对象,不代表 Provider 已完整接入产品。
9.18 本章结论
Multica 的统一 Agent 层不是抹平差异,而是建立最小公共协议:
- Prompt 和 Options 进入;
- 统一 Message Stream 与单一 Result 出来;
- Session、Usage、Resume Rejection 作为显式能力;
- 进程、参数和协议解析留在 Adapter;
- Daemon 只处理跨 Provider 一致的生命周期。
下一章继续回答:Prompt 与几百行任务上下文究竟怎样到达这些不同 Provider,并在后续 Task 中保持会话连续性。
上一章:执行环境、仓库、本地目录与 GC
下一章:Prompt、上下文与会话连续性。