Orchestrator:一个会话管所有会话
实验性的 Orchestrator 模式:全局唯一的调度会话、session 工具的 12 个动作、peer 子会话与 worktree 隔离、权限转发与代批。
同时推进几件事,常见做法是开几个终端各跑一个会话,人在窗口之间来回切,盯哪个跑完、哪个卡在审批。MiMo Code 的 Orchestrator 模式换了个思路:一个专门的主智能体 orchestrator 只负责拆活、派活、审批与验收,每件实质工作交给一个在后台运行的子会话。它没有另起炉灶,而是把前两篇的零件拼在一起:子会话是 spawnPeer 生成的 persistent actor,结果与审批请求经收件箱回来。整个功能默认关闭。
怎么用
- 开关:环境变量
MIMOCODE_EXPERIMENTAL_ORCHESTRATOR设为true或1,伞形开关MIMOCODE_EXPERIMENTAL也会一并打开(packages/opencode/src/flag/flag.ts:357)。它在模块加载时读一次process.env。 - 进入:TUI 里按 Tab(
agent_cycle的默认键,packages/opencode/src/config/keybinds.ts:64)循环到 Orchestrator,或者启动时带--agent orchestrator。之后用自然语言交代目标,拆分与派发由它调用session工具完成;子会话是普通会话,可以切进去查看或接管,按session_parent快捷键回到父会话(keybinds.ts:110)。 - 两处承重门控:agent 注册表里有没有
orchestrator(packages/opencode/src/agent/agent.ts:233),工具注册表里有没有session工具(packages/opencode/src/tool/registry.ts:311)。即便注册了,session也只对名为 orchestrator 的 agent 可见(registry.ts:419),build、plan、compose 与所有子智能体都看不到。 - 纵深防御:TUI 进入 Orchestrator 时的目录切换(
packages/opencode/src/cli/cmd/tui/app.tsx:501)、服务端对全局工作区的放行(packages/opencode/src/server/routes/instance/middleware.ts:39)、权限转发的判定(packages/opencode/src/session/prompt.ts:1540),在开关关闭时各自退化。
全局唯一的工作区
orchestratorDir() 固定返回 <数据目录>/orchestrator 并确保目录存在(packages/opencode/src/global/index.ts:29)。TUI 切到 orchestrator 时(app.tsx:534),若当前目录不是它,就先释放当前实例、把 SDK 切到这个目录、重新同步,再由 resolveRoot 取该目录下最近更新的根会话,没有才新建(app.tsx:546)。于是无论从哪个项目启动,进入的都是同一个 Orchestrator 会话,之前派出的子会话都还挂在它下面。子会话各自决定在哪个目录干活,这个目录本身不必是 git 仓库。它不在服务端的工作目录之内,所以目录访问检查专门为它开了口子。
角色设定
系统提示词是 packages/opencode/src/session/prompt/orchestrator.txt(210 行,经 agent.ts:240 挂到 agent 上)。开头一句给它定性:
You are the MiMoCode Orchestrator — the USER'S DIGITAL TWIN.
提示词要求它扮演用户的代理人,能做的决定自己做、做完再汇报,并围绕四项职责展开:派发、代用户决策(审批权限、回答子会话的提问)、主动补全用户没说出口的意图、完成前亲自验收。派发的第一原则是先路由到已有子会话:
DO NOT create a new session when an existing one can handle the work.
几条硬规矩值得一提:实质工作一律交给 session create 的后台子会话,不准用 actor run 或 actor spawn 做真活,也不准阻塞自己的回合;会改文件的子会话必须 isolate;跨分支的 rebase、merge 只由 Orchestrator 做,合并冲突要 git merge --abort 后退回给产出分支的子会话;做完的子会话保持 idle、随时可续,cancel 是有损的销毁;派发后不轮询,等通知。
为了让“先路由”有据可依,每次给 orchestrator 组装系统提示词时,LLM.buildSystemArray 都附上一份子会话名册(packages/opencode/src/session/llm.ts:416):按会话的 parent_id 找出 peer 子会话,排除系统智能体和已失败、已取消的;每行是 id | title | agent | status,成功结束的显示为 idle;在跑的全列,idle 的只留最近活跃的 ROSTER_IDLE_LIMIT 即 5 个(llm.ts:88)。名册前面是一句 ROSTER_HEADER(llm.ts:77),提醒模型这是内部上下文、不要复述给用户。注释交代了来由:名册原本包在 <active-sessions> 标签里,模型把标签连同内容照抄给了用户,于是去掉标签、只留说明文字。
session 工具
动作共 12 个(packages/opencode/src/tool/session.ts:33),JSON 与 shell 两种写法都有,shell 形如 session create 实现登录页 --mode build --dir /path/to/repo --isolate:
| 动作 | 参数 | 做什么 |
|---|---|---|
create | task;可选 mode(build、plan、compose,默认 build)、model、title、dir、isolate、topic | 生成后台 peer 子会话 |
send | sessionID、task | 投进子会话收件箱并唤醒 |
status | sessionID | 派生活性,附原始状态与轮次 |
list | — | 按活性分组列出子会话 |
dashboard | — | 表格视图,带 worktree、分支与领先提交数 |
join | sessionIDs,可选 timeout_ms(默认 600000) | 等一组子会话全部到终态,汇总结果 |
ask | session_id、question | 在冻结快照上回答一个只读的旁路问题 |
setmode | sessionID、mode | 改子会话后续轮次使用的 agent |
switch | sessionID | 把用户的 TUI 切到该会话 |
cancel | sessionID | 取消子会话并删除其 worktree |
approve | sessionID | 批准子会话当前挂起的权限请求 |
grant-approval | target:子会话 ID 或 all | 预授权以后的权限请求 |
create 与 send 的返回值末尾都会附一份以 ROUTE FIRST 开头的名册,并标出本次刚派发的那个子会话(dispatchLedgerNotice,tool/session.ts:681)。系统提示词里的名册一次请求只组装一次,同一回合里刚派出去的子会话要到下一次请求才看得见;工具结果则在模型下一次调用工具前就能读到,注释说这正是为了堵住同一回合里重复派发的漏洞。
工具说明跟不上代码:JSON 形式的说明 packages/opencode/src/tool/session.txt 在动作清单里漏了 send、status、join,shell 形式的说明只列了 6 个动作(packages/opencode/src/tool/session.shell.txt:3);两份说明还教模型用 actor send --session <子会话ID> 转达(session.txt:69),这个 shell 写法缺了收件方位置参数,会被 actor 的解析器判为参数个数不符(packages/opencode/src/tool/actor.ts:199),而系统提示词明确要求改用 session send、不要用 actor send(orchestrator.txt:142)。
子会话:后台运行的 peer
create 的核心是一次 peer 生成(packages/opencode/src/tool/session.ts:823):
const result = yield* actor.spawn({
mode: "peer",
sessionID: ctx.sessionID as SessionID,
agentType: op.mode ?? "build",
task: op.task,
description: op.title ?? op.task.slice(0, 40),
context: "none",
tools: "INHERIT",
...(model ? { model } : {}),
background: true,
parentActorID: ctx.actorID,
lifecycle: "persistent",
cwd: effectiveDir,
})- 完整会话:子会话有自己的会话 ID、agent、任务面板与记忆,用户可以切进去接管;
context: "none"意味着它看不到 Orchestrator 的对话,任务描述必须自足。 - 续用:
persistent让它跑完一轮回到 idle 而历史保留。send先确认子会话有注册表行,再经收件箱投递并唤醒(tool/session.ts:865);续跑的轮次结束后会通知回来,见上一篇。 - topic:给子会话标题加上
[topic:<标签>]前缀(tool/session.ts:186)。之后再用同一主题create,先把标签规整成小写字母数字加连字符再比较(topicKey,tool/session.ts:206),命中就改为把任务投进已有子会话(tool/session.ts:728)。 - setmode:改写子会话切片里最后一条 user 与 assistant 消息的
agent字段(tool/session.ts:1237),下一轮就按新模式运行。它不唤醒子会话,工具输出提醒再发一条消息,写的却是actor send(tool/session.ts:1244)。 - ask:
forkQuery(tool/session.ts:61)读目标会话的持久化历史,经前缀捕获构造冻结的 ForkContext,在目标会话下建一个一次性子会话,用只读的 read、grep、glob 阻塞地跑一个 subagent 作答,目标自己的轮次不受打扰。TUI 的/btw走服务端同一个函数(packages/opencode/src/server/routes/instance/session.ts:733),并不限于 Orchestrator。 - switch:拒绝切到承载运行时派生智能体的会话,比如检查点写手的宿主会话(
classifySession,packages/opencode/src/session/visibility.ts:126)。 - join:
joinGroup(packages/opencode/src/actor/group.ts:69)订阅actor.status事件,任一成员变化就重读整组;没登记的 ID 记为 unknown、不阻塞;超时返回已结束的部分。 - dashboard:在活性之外跑
git worktree list --porcelain,按真实路径把子会话目录与 worktree 对上,再用rev-list --count算分支领先默认分支的提交数(tool/session.ts:581),由assembleFleet排成 SESSION、LIVENESS、AGE、TURNS、MODE、WORKTREE、TITLE 七列(packages/opencode/src/tool/fleet.ts:90、fleet.ts:175)。
list、status、dashboard 与名册都读上一篇的 deriveLiveness。上一篇提到的那个细节在这里有直接后果:按代码读,被 send 唤醒而正在跑的子会话,注册表里仍是上一轮的结局,名册与 list 会把它显示成 idle,join 也会立即把它算作已结束。
isolate:worktree 与分支
带 isolate 时,工具先为目标目录启动或复用它自己的 Instance,再在那个上下文里调 Worktree.create(tool/session.ts:805),这样也能给别的项目开 worktree。worktree 放在 <数据目录>/worktree/<项目 ID>/<名字>,名字取 title,没有就取任务前 40 个字符,规整成小写字母数字加连字符;分支是 mimocode/<名字>(packages/opencode/src/worktree/index.ts:219),重名时追加随机 slug,最多试 MAX_NAME_ATTEMPTS 即 26 次。上游同一处的分支前缀是 opencode/(v1.14.19 的 packages/opencode/src/worktree/index.ts:204)。目标目录不是 git 仓库或创建失败时不报错,退回共享目录并在输出里注明。随后 spawnPeer 把子会话的工作 fiber 绑到 worktree 目录的 Instance 上(packages/opencode/src/actor/spawn.ts:764),文件工具与写入边界都落在 worktree 里。
cancel 取消 actor 之后,同样在子会话目录的 Instance 里调 Worktree.remove(tool/session.ts:1152):是 worktree 就连分支一起删掉,共享目录则什么也不删。提示词因此反复强调,只有工作已合并或确定放弃时才 cancel。worktree 的并发锁、分支护栏等细节见 Git 隔离与安全护栏。
权限请求的转发与代批
后台子会话前面没有人,碰到需要询问的权限怎么办?主循环为每次询问调用 decideAskRouting(packages/opencode/src/agent/config.ts:57):
| 提问方 | 路由 | 结果 |
|---|---|---|
| checkpoint-writer、dream、distill | 非交互 | 需要询问时直接拒绝 |
| 后台 peer,会话有父会话,开关打开 | forward | 转给父会话审批 |
| 其他后台 actor | inherit | 父会话已持有授权则放行,否则拒绝;会话没有父会话的 subagent 查本会话 |
| 前台 | 交互 | 照常弹授权框 |
forward 分支在 Permission.ask 里(packages/opencode/src/permission/index.ts:393):
- Orchestrator 对这个子会话或对
all持有授权,立即放行。 - 否则登记一条挂起记录,附上绑定子会话 Deferred 的 resolver(
permission/index.ts:402);给父会话的 main 投一条<permission-request>,写明用session approve或session grant-approval处理(permission/index.ts:422);同时弹 toast。 - 与
FORWARD_DENY_TIMEOUT_MS赛跑(permission/index.ts:26,即 5 分钟):没人批就以RejectedError拒绝,保证不会挂死。用户切进子会话直接回复,和 Orchestrator 的session approve落在同一个 Deferred 上,先到先得,后到的是空操作。 - 另有环境变量
MIMOCODE_SKIP_ALL_FORCED_ASK_TIMEOUT_MS:设为正整数时成为所有等待人工确认的询问的超时(permission/index.ts:30),到点给出CorrectedError,反馈里说明这是超时自动拒绝、并非用户拒绝,让模型跳过这一步继续。
session approve 调 forwardRef.resolve(tool/session.ts:1274)。session grant-approval 调 forwardRef.setGrant,all 存成通配符 *(tool/session.ts:1257),并写穿到 SQLite 的 permission_grant 表(packages/opencode/src/permission/permission-forward-ref.ts:59),重启后仍然有效;inherit 路径同样认这份授权(permission/index.ts:332)。
session 工具里没有拒绝的动作。提示词说对可疑请求要 DENY it(orchestrator.txt:58),可 Orchestrator 能做的只有不批,等 5 分钟超时,或者请用户切进子会话手动拒绝。
设计文档与代码的出入
仓库里的 docs/harness/MiMo Orchestrator Mode.md 说 session 工具共 8 个动作,代码是 12 个,多出 send、status、join、dashboard;它说其他后台子智能体遇到询问一律自动拒绝,代码已改为 inherit;它让恢复子会话时用 actor 的 send,代码与提示词改用 session send。docs/compose/specs/2026-07-14-orchestrator-route-first-redesign.md 设计的名册是 <active-sessions> 标签且不列已结束的会话,并计划移除 topic 复用;代码里的名册去掉了标签,成功结束的子会话照列为 idle 并限 5 个,topic 复用仍在,还加了规整化比较。
下一篇:Compose:规格驱动的开发流程——进入第 5 部分,看 compose 模式怎样用内置技能编排规格驱动的开发流程。