Orchestrator:一个会话管所有会话

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

作者 David更新于 第 20 篇(共 33 篇)

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

动作参数做什么
createtask;可选 mode(build、plan、compose,默认 build)、model、title、dir、isolate、topic生成后台 peer 子会话
sendsessionID、task投进子会话收件箱并唤醒
statussessionID派生活性,附原始状态与轮次
list—按活性分组列出子会话
dashboard—表格视图,带 worktree、分支与领先提交数
joinsessionIDs,可选 timeout_ms(默认 600000)等一组子会话全部到终态,汇总结果
asksession_id、question在冻结快照上回答一个只读的旁路问题
setmodesessionID、mode改子会话后续轮次使用的 agent
switchsessionID把用户的 TUI 切到该会话
cancelsessionID取消子会话并删除其 worktree
approvesessionID批准子会话当前挂起的权限请求
grant-approvaltarget:子会话 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转给父会话审批
其他后台 actorinherit父会话已持有授权则放行,否则拒绝;会话没有父会话的 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 分钟超时,或者请用户切进子会话手动拒绝。

设计文档与代码的出入

仓库里的 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 模式怎样用内置技能编排规格驱动的开发流程。

本页目录