# Orchestrator：一个会话管所有会话

> 实验性的 Orchestrator 模式：全局唯一的调度会话、session 工具的 12 个动作、peer 子会话与 worktree 隔离、权限转发与代批。

- 作者：David（道雾轩）
- 专栏：MiMo Code 源码解读（https://daiw.net/manual/mimo-code.md）
- 最后更新：2026-09-17
- 原文：https://daiw.net/manual/mimo-code/orchestrator
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

同时推进几件事，常见做法是开几个终端各跑一个会话，人在窗口之间来回切，盯哪个跑完、哪个卡在审批。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`），在开关关闭时各自退化。

```mermaid
flowchart LR
  U["用户"] --> O["Orchestrator 会话，全局工作区里的根会话"]
  O -->|session create 或 send| A["peer 子会话 A：build，独立 worktree"]
  O -->|session create 或 send| B["peer 子会话 B：compose，共享目录"]
  A -->|完成、失败、停滞通知| I["Orchestrator 的收件箱"]
  B -->|转发的权限请求| I
  I -->|每次迭代开头排空| O
  O -->|session approve 或 grant-approval| B
```

## 全局唯一的工作区

`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`）：

```ts
        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 隔离与安全护栏](https://daiw.net/manual/mimo-code/git-guards)。

## 权限请求的转发与代批

后台子会话前面没有人，碰到需要询问的权限怎么办？主循环为每次询问调用 `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`）：

1. Orchestrator 对这个子会话或对 `all` 持有授权，立即放行。
2. 否则登记一条挂起记录，附上绑定子会话 Deferred 的 resolver（`permission/index.ts:402`）；给父会话的 main 投一条 `<permission-request>`，写明用 `session approve` 或 `session grant-approval` 处理（`permission/index.ts:422`）；同时弹 toast。
3. 与 `FORWARD_DENY_TIMEOUT_MS` 赛跑（`permission/index.ts:26`，即 5 分钟）：没人批就以 `RejectedError` 拒绝，保证不会挂死。用户切进子会话直接回复，和 Orchestrator 的 `session approve` 落在同一个 Deferred 上，先到先得，后到的是空操作。
4. 另有环境变量 `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 分钟超时，或者请用户切进子会话手动拒绝。

## 设计文档与代码的出入

<Callout type="info">
仓库里的 `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` 复用仍在，还加了规整化比较。
</Callout>

下一篇：[Compose：规格驱动的开发流程](https://daiw.net/manual/mimo-code/compose)——进入第 5 部分，看 compose 模式怎样用内置技能编排规格驱动的开发流程。
