0%

Multica 学习 04:从 Coding Agent 到智能体

Multica 的一次运行,始于 Go 后端服务创建的任务队列,终于一条可查看的运行记录。daemon 领取任务后,在安装了 Claude Code、Codex、Cursor、OpenCode 等 Coding Agent 的机器上启动相应程序,再把过程和结果传回后端服务。不同 Coding Agent 的启动方式和输出格式各不相同。本文沿着一次 Run 的过程,介绍 Multica 如何统一这些 Coding Agent,如何在它们之上配置自己的智能体,以及工具调用和 Coding Agent 输出如何被记录下来。

运行时边界

先把“谁负责什么”分开,后面的代码就容易读了。Multica 的运行时由三类组件组成:

本文会刻意区分两个容易混淆的概念:Coding Agent 指 Claude Code、Codex、Cursor 这类被 daemon 启动的底层程序;Multica 智能体指用户在工作区里配置的角色。后者包含名称、指令、skill、模型、工具和权限,前者负责根据这些上下文实际发起推理和工具调用。源码中的 AgentData、agent_id 等字段指的是 Multica 智能体,不是某个具体的 Coding Agent 程序。

  • Go 后端服务(server/)通过 HTTP API 和 WebSocket 保存智能体配置、Runtime 注册信息及 agent_task_queue 队列,负责权限校验、任务领取和状态转换;数据库保存这些持久化记录。
  • daemon运行在安装了这些 Coding Agent 的机器上,探测可用程序,准备任务工作目录,启动 Coding Agent 进程及其工具子进程,并上传运行事件。
  • Coding Agent是由 daemon 启动的底层程序,例如 Claude Code、Codex 或 Cursor。它接收 Multica 提供的提示和上下文,通过自己的协议调用工具、处理任务并返回结果。

Go 后端服务和 daemon 之间既有 HTTP API,也有 WebSocket RPC。队列任务由后端服务产生,daemon 通过 claim 接口领取;实时连接用于心跳、任务可用通知和事件广播。Coding Agent 及其工具在 daemon 所在的机器上运行,代码仓库也主要在这台机器的任务工作目录中被读取和修改,因此 Multica 不需要把整个仓库上传到后端。

从目录上看,边界也很明确:

1
2
3
4
5
6
server/internal/service       创建、领取和完成一次 Run
server/internal/handler daemon API 与客户端 API
server/internal/daemon 本机环境、队列轮询、进程生命周期
server/pkg/agent Coding Agent 适配器和统一事件模型
server/internal/daemon/execenv 工作目录、上下文文件和 skill 注入
packages/views 执行记录和时间线 UI

这里的 Runtime 不是模型本身,而是 daemon 可以启动的一套 Coding Agent 运行入口,通常对应 PATH 上发现的一个命令,例如 claude 或 codex。自定义 Runtime 则由 runtime_profile 提供 protocol_family、command_name 和固定参数。智能体是工作区里的长期配置,Run(数据库里的 agent_task_queue 记录)才是一次具体执行。

统一接口

不同 Coding Agent 的共同需求其实很少,Multica 把这部分压缩成一个小接口。server/pkg/agent/agent.go 中的定义如下:

1
2
3
type Backend interface {
Execute(ctx context.Context, prompt string, opts ExecOptions) (*Session, error)
}

Execute 不直接返回一段字符串,而是返回一个 Session。这是设计的关键:Coding Agent 通常会在最终答案前产生很多中间事件,调用工具时还可能长时间没有文本输出,因此调用方需要同时消费事件流和最终结果。

1
2
3
4
5
6
7
8
9
type Session struct {
Supplement func(context.Context, string) error
SupplementReady func() bool
ToolActivity func() (int32, time.Time)
InterruptBackgroundTools func() bool
TerminalObserved func() bool
Messages <-chan Message
Result <-chan Result
}

其中 Messages 是过程流,Result 只发送一次最终结果。几个可选回调让 daemon 不必了解某个 Coding Agent 的内部实现:ToolActivity 用于工具看门狗,TerminalObserved 用来区分“已经得到最终结果但还在清理”与“进程真的卡住”,Supplement 则为支持中的运行时追加一条用户消息。

统一事件模型也很小:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const (
MessageText MessageType = "text"
MessageThinking MessageType = "thinking"
MessageToolUse MessageType = "tool-use"
MessageToolResult MessageType = "tool-result"
MessageStatus MessageType = "status"
MessageError MessageType = "error"
MessageLog MessageType = "log"
)

type Message struct {
Type MessageType
Content string
Tool string
CallID string
Input map[string]any
Output string
Status string
Level string
SessionID string
}

适配器的工作就是把外部协议翻译成这些事件。例如,Claude 的 stream-json、Codex app-server 的通知、ACP 的 session/update,最终都会变成 MessageToolUse、MessageToolResult、MessageThinking 或 MessageText。daemon 只处理统一模型,不需要为每个供应商写一套任务循环。

ExecOptions 承载一次执行的差异化配置:工作目录、模型、超时、恢复用的会话 ID、MCP 配置、thinking level、服务等级、额外参数和 provider 需要的 system prompt。它还区分 ExtraArgs、智能体自己的 CustomArgs 和 Runtime profile 的 LaunchPrefix。后者必须放在可执行文件之后、所有 backend 参数之前,才能正确支持类似 wrapper start profile --agent-flag 的命令结构。

最终结果同样有统一结构:

1
2
3
4
5
6
7
8
9
type Result struct {
Status string // completed, failed, aborted, timeout, cancelled
Output string
Error string
DurationMs int64
SessionID string
Usage map[string]TokenUsage
ResumeRejected bool
}

ResumeRejected 很有用:它表示“旧会话本身无法继续”,daemon 才会尝试创建新会话;网络错误、限流、认证失败不会被误判成会话损坏。

协议适配

统一接口解决的是调用方的复杂度,适配器解决的是协议复杂度。这里的“协议”指 daemon 与 Coding Agent 子进程之间的机器通信契约,不是模型 API,也不是 MCP。它至少包含四层:

  1. 启动和传输:启动什么命令、参数如何排列,prompt 通过 argv、stdin 还是请求消息发送,事件从 stdout 还是其他通道读取。
  2. 消息格式和分帧:输出是一行一个 JSON(JSONL/NDJSON),还是带 id、method、params 的 JSON-RPC 2.0 请求、响应和通知。
  3. 运行生命周期:如何初始化,如何创建或恢复 session/thread,如何开始一轮 turn,怎样表示文本、思考、工具调用、工具结果和最终完成。
  4. 控制语义:取消、权限确认、追加用户消息、错误分类、token 用量和会话 ID 如何表达。

定义这套协议是为了让 daemon 能把一次运行当成可观测、可控制的会话,而不是执行一个命令后等待一段最终字符串。没有协议,daemon 无法可靠地区分 正在调用工具、已经生成最终答案但进程还在退出、会话恢复被拒绝 和 进程彻底卡住;也无法把不同 Coding Agent 的中间事件、取消和恢复接入同一条任务时间线。适配器因此负责把某个 provider 的启动参数、握手、事件解析和控制请求翻译成统一的 Session、Message 和 Result。

源码中的 backend 大致按下面的协议族分组。这里的“协议族”是 wire contract 的归类;同一族里的 Coding Agent 仍可能有版本差异和少量私有扩展:

协议族 Coding Agent 协议大意和适配器职责
Claude stream-json Claude Code、CodeBuddy、Qwen Code、Cursor CLI 以非交互模式启动,通过 stdin 接收 prompt,并在 stdout 输出逐行 JSON 事件。事件通常覆盖 assistant 文本增量、thinking、工具调用和工具结果;适配器要处理各家事件字段的差异,以及 stdin 是否需要继续打开。Claude 还会在运行中发送 control_request,所以 claude.go 不能写完 prompt 就关闭 stdin。
Codex app-server Codex 启动 codex app-server --listen stdio://,在 stdin/stdout 上使用双向 JSON-RPC 2.0 消息。daemon 先做 initialize,再用 thread/start 或 thread/resume 建立会话,用 turn/start 发起一轮;过程通知描述 turn、文本、工具和 token 用量,取消走 turn/interrupt。适配器必须维护 request ID、thread ID、turn 边界,并区分恢复失败和传输失败。
ACP(Agent Client Protocol) Hermes、Kimi、Reasonix、Kiro、Qoder(含 qoderclicn)、Trae、Grok、QwenPaw、MCode、Dim、ZeroClaw 一套标准化的 Agent Client Protocol,使用 JSON-RPC 2.0 over stdio。典型流程是 initialize、认证/能力协商、session/new 或 session/resume,再发送 prompt;agent 通过 session/update 推送文本、工具和状态,遇到权限请求时发 session/request_permission。源码复用共享 ACP client,但每个 agent 仍要处理自己的启动子命令、能力声明、session 恢复错误和少量扩展。
OpenCode 风格 NDJSON OpenCode、CodeArts、DevEco 以 run --format json 启动,stdout 是逐行 JSON 事件流,prompt 通常写入 stdin。三者的事件形状相近,但命令参数、配置环境变量和版本行为并不完全相同;例如 OpenCode 2.x 移除了 --dir,因此 opencode.go 必须按版本选择工作目录传递方式,CodeArts 和 DevEco 也保留独立适配器。
Pi JSON event stream Pi、Oh-My-Pi(OMP) pi -p --mode json(OMP 使用相同事件协议)通过 stdout 输出 turn、消息和工具执行事件,并用 session 文件保存会话。适配器要把 turn_start、tool_execution_*、turn_end 等事件转换成统一时间线,并处理自动重试和 session 路径。OMP 是 Runtime 身份,不是新的协议族,所以复用 Pi backend。
Copilot JSONL GitHub Copilot CLI --output-format json 输出自有的 JSONL envelope,每行有点号分隔的事件名(如 session.start、assistant.message_delta)和 data。prompt 通过 -p 参数传入;适配器要从多种事件拼出最终 assistant turn、会话 ID 和用量,并保留工具事件。
OpenClaw JSONL OpenClaw openclaw agent --json 在 stdout 输出 NDJSON,session ID 由 daemon 管理,stderr 仍是诊断日志。适配器要处理版本差异(新版本把 JSON 结果写到 stdout)、事件流和最终结果提取。
DSH versioned JSONL DSH Runtime DSH 有自己的版本化 JSONL stdio 协议,不兼容 ACP。请求和事件带有 v、type、request_id、session_id 等字段;DSH 自己拥有 agent loop、session store、模型目录、工具和 MCP client,Multica 只负责把这些 frame 翻译成 Backend 事件。
Antigravity print/stream JSON Antigravity CLI agy -p 是一次性 prompt 通道,旧版本主要返回普通文本,新版本可返回 stream-json 事件和 token 用量;会话 ID 还可能只出现在日志或本地 transcript。适配器因此同时兼容纯文本、事件流和 transcript 回读。

JSONL、NDJSON 和 JSON-RPC 2.0 不是同义词。JSONL/NDJSON 只规定“每行一个 JSON 值”的分帧方式,事件字段由 provider 自己定义;JSON-RPC 2.0 还规定请求、响应、通知、错误和 ID 的关联方式。ACP 和 Codex 都可以在 stdio 上使用 JSON-RPC,但它们的 method、session 生命周期和事件语义不同,不能因为都叫 JSON-RPC 就共用同一个 provider 适配器。反过来,多个 agent 即使都输出 JSONL,也不代表它们的事件 schema、prompt 输入方式或恢复语义相同。

这些实现并不是一个“万能 JSON 解析器”。每个文件都保留了供应商特有的边界处理。例如 server/pkg/agent/opencode.go 会检查版本,因为 OpenCode 2.x 移除了 --dir;server/pkg/agent/claude.go 要保持 stdin 打开,以便处理运行中的 control request;ACP 适配器要识别 session/resume 的拒绝原因;DSH 则必须校验自己的协议版本和 request_id。这正是适配器存在的原因:上层只消费统一事件和最终结果,provider 的协议细节留在边界内。

Backend 工厂与 Runtime 身份

协议适配负责解释 wire protocol;这一层负责根据 provider、Runtime profile 和启动配置选择正确的 backend。创建 backend 的入口仍然只有一个。协议族通过 New 构造,像 Oh-My-Pi 这样的内置 Runtime 身份则由 ResolveBackend 转到 NewRuntime:

1
2
3
4
5
6
func ResolveBackend(provider string, cfg Config) (Backend, error) {
if IsBuiltinRuntime(provider) {
return NewRuntime(provider, cfg)
}
return New(provider, cfg)
}

SupportedTypes 是自定义 Runtime profile 的白名单,目前包含 claude、codebuddy、codex、copilot、opencode、codearts、deveco、openclaw、hermes、pi、cursor、kimi、reasonix、dsh、kiro、antigravity、qoder、qoderclicn、traecli、grok、qwen、qwenpaw、mcode、dim 和 zeroclaw。这张表和数据库里的 protocol_family 约束保持同步,避免用户保存一个没有对应 backend 的 profile。

协议族和 Runtime 身份是两个概念。builtin_runtimes.go 里的 BuiltinRuntime 描述探测命令、显示名称、skill 目录、默认可执行文件和模型发现方式。当前的 omp(Oh-My-Pi)就是例子:它的 ProtocolFamily 是 pi,但有独立的命令、模型发现和展示名称。这样增加一个兼容同一协议的新 Coding Agent 时,不需要复制 Pi 的事件解析器。

Go 后端服务在启动任务时把真实可执行文件、版本和 profile 固定参数传给 agent.Config:

1
2
3
4
5
6
7
8
9
backend, err := agent.ResolveBackend(provider, agent.Config{
ExecutablePath: entry.Path,
LaunchPrefix: profileFixedArgs,
CLIVersion: resolvedVersion,
Env: agentEnv,
TaskID: task.ID,
RuntimeID: task.RuntimeID,
BuiltinRuntime: !usesCustomProfileCommand,
})

BuiltinRuntime 用来限制只对经过验证的官方 Coding Agent 启用兼容性分支。一个自定义命令即使声明自己使用 Hermes 或 Pi 协议,也不能自动获得某个官方版本专属的参数假设。

智能体配置

Coding Agent 提供理解提示和调用工具的运行能力,Multica 的智能体才是面向用户配置的工作角色。AgentData 会随任务 claim 一起下发,里面包括名称、instructions、skills、模型、thinking level、环境变量、命令行参数和 MCP 配置。

这解决了一个实际问题:用户需要的是 代码审查员、发布助手 或 项目协调者,而不是一条裸的 claude 命令。不同智能体可以使用同一个 Runtime,也可以让同一个智能体在不同机器的 Runtime 上运行;配置属于智能体,进程属于 Runtime。

上下文文件

每次 Run 的准备阶段,daemon 会调用 execenv.InjectRuntimeConfig,在隔离工作目录写入运行时能识别的上下文文件。Claude 读取 CLAUDE.md,Codex、OpenCode、Hermes 等读取 AGENTS.md,其他 Coding Agent 使用各自的规则目录。文件里包含 Multica 命令的使用约定、工作区信息、智能体指令和已安装 skill 的入口。

这条路径比把所有内容都塞进每次 prompt 更稳定:上下文文件可以被 provider 原生加载,也更适合 prompt cache。只有确实不会读取该文件的 provider 才通过 ExecOptions.SystemPrompt 内联运行时 brief,当前判断集中在 providerNeedsInlineSystemPrompt:

1
2
3
4
5
6
7
8
func providerNeedsInlineSystemPrompt(provider string) bool {
switch provider {
case "openclaw", "kimi", "traecli", "qwenpaw":
return true
default:
return false
}
}

每轮的具体问题仍然通过 BuildPrompt 生成。Issue 任务会携带 Issue、触发评论、状态和项目上下文;Chat 任务会携带用户消息和附件;Autopilot run-only 任务则携带自动化说明和触发 payload。这样 稳定的工作规则 和 这一轮的即时输入 分层,恢复会话时也不会把旧的任务细节误当成新的系统规则。

skill 与工具

Skill 在 Go 后端服务中是可复用的配置和文件包。claim 时,Go 后端服务把智能体允许使用的 skill 数据发给 daemon;daemon 在准备环境时把它们写入对应的原生目录,或通过 .agent_context 让 Coding Agent 发现。运行时还会合并智能体自己的 MCP 配置、Runtime 级 MCP、Remote MCP broker 和插件 hook MCP。

一个 Multica 智能体因此拥有三层能力来源:

  • 规则:instructions、工作区 context 和运行时 brief,决定它应该怎样工作。
  • 知识:skill 文件、项目资源和仓库说明,决定它能读取哪些领域知识。
  • 工具:Coding Agent 原生工具、Multica 命令、MCP server 和插件 hook,决定它能执行什么。

daemon 还会注入任务范围的 MULTICA_TOKEN、MULTICA_WORKSPACE_ID、MULTICA_TASK_ID 等环境变量。Token 在 Go 后端服务 claim 时按智能体和 Task 绑定,daemon 不会把自己的高权限凭据传给子进程。Coding Agent 代表这个智能体调用 multica issue、multica comment 或 multica repo checkout 时,权限仍然落在这一次 Run 上。

这就是 Multica 在 Coding Agent 之上构建“自己的智能体”的方式:不重新实现模型推理,也不要求每个 provider 支持 Multica 的全部概念,而是把身份、规则、skill、工具、权限和持久会话装配到一次受控的执行环境中。

一次运行

下面按实际调用顺序看一次 Run。任务可能来自 Issue、Chat、Autopilot 或 quick create,但进入 daemon 后都复用同一条执行链。

队列调度

用户操作最终会创建 agent_task_queue 记录,初始状态通常是 queued。daemon 的 batch poller 先从并发槽位信号量中取出可用 slot,再调用 claim 接口。Go 后端服务在事务中挑选可执行任务,将其变成 dispatched,同时返回 Runtime、智能体、项目、评论、会话和 skill 等完整上下文。

daemon 收到任务后启动 handleTask。它先确认 Runtime 仍然在线、智能体身份没有被篡改,再为任务创建带完整 Task ID 的 logger,并开始准备阶段。

环境准备

runTask 会依次完成这些工作:

  • 解析 Runtime profile 和可执行文件,必要时重新定位被升级替换的 Coding Agent 程序。
  • 准备或复用隔离工作目录、仓库缓存和本地 worktree。
  • 创建任务临时目录、MCP broker 和插件 hook server。
  • 根据智能体、Runtime 和任务上下文写入 CLAUDE.md、AGENTS.md、skill 文件及其他配置。
  • 检查旧会话是否能在当前工作目录、Codex rollout 或 Hermes session store 中找到。
  • 调用 Go 后端服务的 StartTask,把状态从 dispatched 变成 running。

StartTask 放在环境准备之后,是为了避免 Go 后端服务已经显示“运行中”,但工作目录还没有创建完成。准备阶段还有独立的 lease 和超时,失败会走统一的任务失败分类,不会留下一个看起来正在运行、实际没有进程的任务。

进程启动

环境就绪后,daemon 构造 prompt 和 ExecOptions,选择智能体的模型(智能体自己的设置优先,其次是 daemon 的 provider 默认值),然后调用 agent.ResolveBackend(...).Execute(...)。对于 Claude,backend 可能启动类似下面的命令;这只是示意,具体参数由 buildClaudeArgs 生成:

1
claude --output-format stream-json ...

对于 Codex,backend 连接 app-server;对于 ACP 运行时,backend 启动 stdio 进程并执行 session/new 或 session/resume。prompt 通常通过 stdin 或协议消息发送,不直接拼进命令行,避免长上下文触发操作系统的 argv 限制,也避免出现在进程列表中。

事件循环

executeAndDrain 同时做四件事:读取 Session.Messages、把事件转成 task_message、等待 Session.Result,以及监控取消和无输出超时。

工具事件会维护 CallID → Tool 映射,让乱序或缺少 tool 名称的结果仍能配对;文本和 thinking 片段会在相邻且类型相同的时候合并。每条事件都有 daemon 观察到的时间和递增 seq,同一次任务重试也继续使用原来的序号,避免两次尝试在时间线中交错。

运行中的取消由 watchTaskCancellation 定期读取 Go 后端服务的任务状态触发。用户取消、任务被重新分配、后端服务把任务置为终态或任务行被删除,都会取消同一个 context.Context,最终由 backend 负责终止进程树。另有 idle watchdog:没有消息且消息队列为空时才判定无响应;工具调用尚未返回时使用更长的工具预算,避免把长时间的编译或测试误判成卡死。

终态回写

backend 发出 Result 后,daemon 先等待最后一批 transcript 刷新,再按 completed、failed、cancelled 或 timeout 调用 Go 后端服务接口。会话 ID、工作目录、分支名、错误原因和 token 用量分别回写到任务记录和用量表。后端服务的完成/失败接口使用状态条件更新,因此旧 daemon 或重复回调不会覆盖新一次运行的终态。

运行时间线

能否看到 Coding Agent 日志 需要先区分两种日志。Multica 能稳定保存的是结构化的执行时间线,而不是每个 Coding Agent 的全部原始 stdout/stderr。

事件持久化

daemon 以约 500 毫秒为节奏批量调用 /api/daemon/tasks/{taskId}/messages。Go 后端服务的 ReportTaskMessages 会先做权限校验、敏感信息脱敏和 PostgreSQL 文本清理,再批量写入 task_message:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE task_message (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
task_id UUID NOT NULL REFERENCES agent_task_queue(id) ON DELETE CASCADE,
seq INTEGER NOT NULL,
type TEXT NOT NULL,
tool TEXT,
content TEXT,
input JSONB,
output TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
output_truncated BOOLEAN
);

写入的类型包括:

  • text:Coding Agent 面向用户的输出片段。
  • thinking:provider 能提供的思考或 reasoning 片段。
  • tool_use:工具名、调用 ID 和结构化输入。
  • tool_result:工具输出。daemon 会先生成预览,过大的输出会标记 output_truncated。
  • error:运行过程中明确发出的错误事件。

MessageStatus 主要用于尽早保存会话 ID,不作为用户时间线的正文;MessageLog 当前主要由 Copilot 等适配器用于承载 provider 的提示或告警,而 daemon 的持久化分支不会把它当作普通 transcript 条目保存。因此,数据库里的时间线是“适合用户查看的运行记录”,不是 provider 原始协议的逐字转储。

实时与回放

Go 后端服务每批写入后,会为 Issue 或 Chat 任务发布 task:message 事件。Web、Desktop 和 Chat 页面通过 WebSocket 将新消息合并到 Query 缓存;断线或打开历史记录时,客户端调用 GET /api/tasks/{taskId}/messages 回放完整序列。前端的 buildTimeline 按 seq 排序,并把相邻的文本和 thinking 片段合并,工具调用和工具结果则保留为独立条目。

因此,用户可以在运行过程中看到:

  • Coding Agent 已经输出的文字和 reasoning(取决于 provider 是否提供)。
  • 调用了哪个工具、调用参数是什么。
  • 工具返回了什么结果,结果是否被截断。
  • 运行是完成、失败、取消还是超时,以及最终回答。

这部分就是 Issue 详情页的 Execution log 和 Chat 的 live timeline 所展示的内容。

原始进程日志

Coding Agent 进程自己写到 stderr 的诊断信息走另一条路径。以 Claude 为例,backend 把 stderr 同时写入 daemon 的 slog 日志和一个有上限的 tail buffer;进程异常退出时,tail 会附加到 Result.Error,帮助解释 V8 崩溃、进程 panic 或 provider 错误。Codex、OpenCode 等 backend 也有各自的启动日志、stderr tail 和错误解析。

这意味着当前实现的边界是:

  • 能获取:结构化工具调用、工具结果、Coding Agent 文本、部分 thinking、错误事件、会话状态,以及 daemon 日志中的启动/退出/stderr 诊断。
  • 不能保证:把每个 provider 的原始 stdout/stderr、所有内部调试帧、完整 ACP JSON 或 app-server 通信逐字保存到 task_message。
  • 查看位置:用户可见内容在任务的 Execution log 或 Chat 时间线;原始进程诊断需要查看 daemon 日志,通常只有错误尾部会进入任务错误字段。

这个取舍是有意的。原始工具输出可能包含密钥、整个文件内容或二进制字节,直接保存会放大泄露和存储风险;结构化事件经过 daemon 和 Go 后端服务两侧脱敏,工具输出也有预览上限。需要排查 provider 协议问题时,应结合 Task ID 查看 daemon 日志,而不是把用户界面当作原始调试转储。

会话与恢复

一次 Run 结束后,Go 后端服务会保存 session_id 和 work_dir。下一次同一 Issue 或 Chat 会话产生任务时,daemon 会先判断工作目录和 provider 的会话存储是否仍然可达,再决定是否把 ResumeSessionID 传给 backend。

不同 provider 的会话位置不同:Claude 会按工作目录查找项目会话,Codex 需要对应 rollout 文件,Hermes 使用任务 overlay 中的 session store,而 Pi/OMP 的会话 ID 本身可能是独立的 JSONL 路径。gateResumeToReachableSession 和各 backend 的 resume 检测把这些差异隔离起来。

如果 provider 明确拒绝旧会话,daemon 只对这类“从会话本身无法恢复”的错误重试一次 fresh session;网络中断、配额、认证和模型不可用会保留会话指针,交给后续重试继续上下文。这样既能自愈损坏的会话,又不会把暂时性的 provider 故障变成无上下文的新对话。

小结

Multica 的运行时实现可以归纳成一条连续的分层:Go 后端服务用 Task 记录一次工作,daemon 把 Task 变成隔离环境,Backend 把各家 Coding Agent 变成统一的事件流,智能体配置再通过 instructions、skills、MCP 和任务 Token 把一个通用 Coding Agent 变成具体的 Multica 智能体。运行过程中的工具调用和 Coding Agent 输出会进入结构化时间线,daemon 自身则保留更适合排障的进程日志。

理解这条链路后,新增一个 Coding Agent 的工作范围也很清楚:实现一个能把原生协议翻译成 Session 的 backend,补上 Runtime 探测和启动参数,再验证上下文文件、会话恢复、取消和事件解析。队列、权限、执行记录和 UI 不需要为这个 provider 重新发明一套系统。