Goal:让独立裁判决定何时停
/goal 给会话设一个停止条件,主循环每次想收尾都先请一次单独的裁判调用读完对话再判定,未达成就把缺口写回去接着干,一个目标最多被踢回 12 次。
长任务里最常见的失败不是做错,而是做到一半就宣布完成:测试没跑、跑了没看结果,或者把“应该能行”当成“已经行了”。README 把这叫作自主工作中的“乐观停止”(README.zh.md:184)。上游 OpenCode 的主循环只要模型给出不再调用工具的结束回复就退出(见主循环 SessionPrompt),没有第二道把关。
MiMo 新增的 Goal 机制给会话挂一个自然语言写的停止条件,主循环准备退出时,先让另一次模型调用读完对话记录来判定。文件头注释(packages/opencode/src/session/goal.ts:19)说明了“独立”指什么:
The judge is a separate model call that only reads the transcript — it does not do the work, so its verdict stays cold relative to the working agent's optimism.
怎么用
/goal <条件>:设定目标。命令登记在packages/opencode/src/command/index.ts:172,模板只有$ARGUMENTS。SessionPrompt.command遇到它先调用goal.set,再照常展开模板(packages/opencode/src/session/prompt.ts:4957),所以条件文本本身就是这一轮发给模型的用户消息,模型立刻开始朝它干活。/goal、/goal clear、/goal reset:清除目标,只写入一条 synthetic 的“Goal cleared.”并带noReply,不触发模型回复。- 非交互:
mimo run --command goal "所有测试通过并已提交",--command会被转成同一个session.command调用(packages/opencode/src/cli/cmd/run.ts:681)。 - 没有配置项。重入上限
MAX_GOAL_REACT写死为 12,注释里留着“TODO: lift to mimocode.json config”(prompt.ts:184)。 - 目标只存在内存里:每个项目实例一张
Map,键是 sessionID,值是condition与计数react(goal.ts:111),实例销毁或进程重启就没了。
TUI 侧栏会出现一个 Goal 区块,显示条件原文和裁判的最新结论;每条被判过的 assistant 消息下面还有一行可折叠的标记,细节见文末。
两个出口上的闸门
上游 v1.14.19 的 runLoop 在“上一条回复已经收尾”和“这一步的结果是 break”两处直接 break 退出。MiMo 在这两处前面都插了 goalGate:
- 循环顶部,上一条 assistant 已经结束、被
classifyAssistantStep判为收尾(prompt.ts:3756)。模型正常说完“做好了”走的是这里。 - 一步处理完、结果是 break(
prompt.ts:4846)。处理器返回stop(工具调用被用户拒绝、本步出错)、结构化输出已产出等情况都会走到这里。
其他出口,比如压缩流程返回 stop、复读检测多次恢复无效后终止会话,不经过裁判。
goalGate(prompt.ts:3247)依次做这些事:
- 只管主智能体:
agentID不是main就直接放行。子智能体另有actor.preStop重入,上限MAX_PRE_REACT为 3(packages/opencode/src/actor/spawn.ts:39),见 Actor。 - 取对话:用
MessageV2.filterCompactedEffect按会话的contextFrom、contextWatermark截出主智能体此刻能看到的那段历史,与循环顶部取历史的方式一致,所以重建或压缩边界之前的内容裁判也看不到。 - 调裁判:外面套了一层
Effect.catch,出错时当作ok: true并标上judgeFailed。注释的意图是裁判出问题就放行(fail-open),不把用户困在循环里,实际效果见下文。 - 达成或不可能:发布带裁决的
session.goal事件,清除目标,放行。 - 未达成:
bumpReact加一;超过 12 同样发布裁决、清除目标、放行。否则新建一条 synthetic 用户消息,返回true让调用处continue。
写回去的提醒(prompt.ts:3338)把裁判的理由原样带上:
text: [
"<system-reminder>",
`Your goal is not yet satisfied: "${active.condition}".`,
"A judge reviewed the transcript and reported what is still missing:",
verdict.reason,
"Keep working toward the goal. Do not stop until it is genuinely met or impossible.",
"</system-reminder>",
].join("\n"),几个容易忽略的细节:
- 计数挂在目标上:
react只在goal.set时归零(goal.ts:119)。常量注释写的是“per turn”,但用户中途再发消息并不重置它;一个目标最多被踢回 12 次,也就是裁判最多判 13 次。 - 闸门不问为什么停:用户在授权框里拒绝了工具调用,处理器把本步标成 blocked 并返回
stop;Try-Best 暂停一轮也是同样的路径。只要目标还在,这些情况都会先请裁判,判为未达成时循环照样继续。 - 每次都是全量:每次判定都把主智能体当前的整段历史重新发一遍;
evaluate只取返回的对象,代码里没有把这次调用的用量记进会话。 - 放行分支未必走得到:
evaluate的签名是Effect.Effect<Verdict>(goal.ts:97),错误通道为 never,模型请求又包在Effect.promise里,请求报错只会成为缺陷(defect)。packages/opencode/src/workflow/runtime.ts:1548的注释写明,这个 Effect 版本里 catch 类算子不处理缺陷。照此推断,裁判请求失败时走不到“当作达成、清除目标”的分支,缺陷会直接穿出goalGate;仓库里没有覆盖这条路径的测试。
裁判怎么判
evaluate(goal.ts:144)是一次普通的结构化输出调用:
- 模型:
goalGate传入的是lastUser.model(prompt.ts:3264),即会话当前在用的模型。所以“独立”指的是独立的一次调用、不参与干活的角色,而不是换一个模型;packages/opencode/src/provider/transform.ts:1139的注释也写明goal.tsjudges with the SESSION's model。 - 输入:历史经
MessageV2.toModelMessagesEffect转成原生模型消息,工具调用、工具结果与图片都保留(goal.ts:159),裁判能直接核对工具输出,而不是只读助手的自述。 - 请求:system 放
JUDGE_SYSTEM,中间是整段对话,末尾追加一条用户消息问条件是否已满足(goal.ts:76);temperature为 0,用generateObject按 zod 定义的Verdict(ok、可选的impossible、reason)取结果。
JUDGE_SYSTEM 规定了三种回答形状(goal.ts:66):
Your response must be a JSON object with one of these shapes:
- {"ok": true, "reason": "<quote evidence from the transcript that satisfies the condition>"}
- {"ok": false, "reason": "<quote what is missing or what blocks the condition>"}
- {"ok": false, "impossible": true, "reason": "<explain why the condition can never be satisfied>"}后面几句是防乐观的关键:找不到明确证据就回“insufficient evidence in transcript”(goal.ts:71);impossible 只用于条件自相矛盾、依赖的资源或能力不可用,或者确实穷尽了合理办法的情况,拿不准时不带它。助手自己说做不到也不能照单全收(goal.ts:73):
the assistant claiming the goal is impossible is evidence, not proof
两处和提供方有关的适配:
impossible是可选字段,而 OpenAI 系 SDK 默认对json_schema开严格模式,要求每个字段都出现在required里,请求会被拒。ProviderTransform.structuredOutputOptions为@ai-sdk/openai、@ai-sdk/azure、@ai-sdk/openai-compatible显式传strictJsonSchema: false(transform.ts:1165)。- OAuth 登录的
openai提供方不发 system 消息,而是把JUDGE_SYSTEM放进instructions提供方选项并设store: false,改用streamObject取结果(goal.ts:217)。
界面上的裁决
每次判定、设定、清除都会发布 session.goal 事件。TUI 把它合并进同步 store(packages/opencode/src/cli/cmd/tui/context/sync.tsx:545):裁决按被判定的 assistant 消息 ID 存进 verdicts,清除事件只清掉 condition,已有裁决保留。
- 侧栏:
sidebar/goal.tsx以order: 350排在 LSP 区块下面,显示条件原文和一行“Judge: …”,取值是met、impossible、error (stopped)或round N · not met(packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/goal.tsx:21)。达到上限没有单独的状态,条件行消失,只剩最后一次的round 13 · not met。 - 逐轮标记:会话视图给每条被判过的 assistant 消息加一行“Judge: …”标记,点击展开裁判给出的理由(
packages/opencode/src/cli/cmd/tui/routes/session/index.tsx:1868),方便回头查是哪一轮没过。
这些状态同样只在内存里;重新打开 TUI 后逐轮标记不会恢复,但写进历史的 synthetic 提醒消息还在。
下一篇:树状任务——task 工具在 MiMo 里不再派子智能体,而是一套能持久化、能按编号引用的工作项。