源码解析 第十一份 commit 076b790

别让 Agent 骗自己

MiMo Code 是 OpenCode 的深度 fork——核心包至今就叫 packages/opencode。 所以真正值得读的不是它继承了什么,而是小米在上游之上焊了什么: 一个只读 transcript 的独立裁判、四道死循环闸门、20 份一模型一份的提示词、 给 Codex 家族专用的四件工具 ABI。

5 222
仓库文件(83.8 MB)
20
份分模型家族提示词
12.5k
star(创建于 2026-06-10)
6
道「防自欺」装置
Part I

它是什么

先把身世说清楚:这是一个 fork。读 fork 的正确方式是读差分。

Chapter 01

一个 fork 的价值在于它焊了什么

打开仓库第一眼就该注意到的事:唯一的核心包叫 packages/opencode,而根 package.json 的 name 字段至今写着 "opencode"。

这不是命名随意。MiMo Code 是 OpenCode 的深度 fork——本系列第二份分析的对象。上游的 Effect 服务层、SolidJS TUI、SQLite 持久化、provider 抽象,MiMo 全盘继承。LICENSE 里两行版权并列(Copyright 2026 MiMo Code, Xiaomi 与 Copyright 2025 opencode),把这件事写得明明白白。

读法结果
❌ 当成全新项目从头读你会花 80% 的时间重读已经分析过的 OpenCode 骨架
✅ 读差分直接看小米焊上去的东西——那才是这份仓库的信息量

口号背后的产品主张

README 第一行:"MiMo Code: Where Models and Agents Co-Evolve"。这句话不是营销辞令,它对应两件具体的工程事实:

事实一
一模型一提示词
session/prompt/ 下有 20 份 system prompt——gpt.txt / gemini.txt / anthropic.txt / deepseek.txt / kimi.txt / glm.txt / minimax.txt / trinity.txt……按模型 ID 路由。
事实二
一模型一工具 ABI
GPT/Codex 家族看到的工具集和 Claude 家族不一样——前者只有 bash / apply_patch / view_image / exec 四件,后者是完整套装。
"共进化"的真实含义 Agent 的 harness 会为每个模型单独调形,而不是给所有模型一套通用外壳。

数字化的项目形状

字节文件是什么
207 962session/prompt.ts主循环(4 595 行)——Agent 心脏
128 317cli/cmd/tui/routes/session/index.tsxTUI 会话主视图
84 552workflow/runtime.ts确定性工作流运行时(1 607 行)
74 327session/checkpoint.tsCheckpoint 系统(1 648 行)
71 103provider/transform.tsprovider 请求变换
53 710tool/session.tsOrchestrator 的 session 工具
48 853actor/spawn.ts子 Agent 派发(1 010 行)
许可有两层 LICENSE 是 MIT,但仓库根目录还有一份 USE_RESTRICTIONS.md。里面有一条和本文主题直接相关:

「To use Xiaomi MiMoCode in a manner that autonomously executes high-risk actions without appropriate human oversight or authorization.」

一个把"防止 Agent 自欺"做成核心设计的项目,在许可条款里也写下了"不许无监督地自主执行高风险动作"——这两件事是一致的。
Chapter 02

全景架构:Effect 服务层 + 单进程 TUI

flowchart TB
    subgraph UI["① 终端 UI(SolidJS + OpenTUI)"]
        TUI["会话视图 · 输入框 · 任务面板 · 权限弹窗
10 种语言 i18n"] end subgraph CORE["② Session 引擎(同进程,Effect 服务层)"] PROMPT["SessionPrompt 主循环
prompt.ts 4595 行"] SYS["SystemPrompt 路由
按模型 ID 选 20 份之一"] CKPT["Checkpoint 系统"] GOAL["Goal 独立裁判"] PERM["Permission 规则集"] REG["ToolRegistry
按模型装配工具 ABI"] end subgraph EXT["③ 扩展面"] SKILL["26 个内置技能"] WF["Workflow(QuickJS 沙箱)"] MCP["MCP 客户端"] PLUG["插件 / hooks"] end subgraph OUT["④ 外部世界"] MODEL["任意 provider 模型
MiMo Auto / Codex OAuth / API key"] FS["文件系统 · Shell · Git"] DB[("SQLite
会话 / 记忆 FTS5 / 任务 / 权限")] end TUI --> PROMPT PROMPT --> SYS PROMPT --> GOAL PROMPT --> CKPT PROMPT --> REG --> PERM REG --> SKILL & WF & MCP & PLUG PROMPT -->|流式| MODEL PERM -->|裁决后| FS CKPT --> DB SKILL --> DB
图 1 进程拓扑。继承自上游:Effect 依赖注入、SQLite+Drizzle、provider 抽象、TUI 技术栈、ACP 协议。小米新增:Goal 裁判、Checkpoint、FTS5 记忆、20 份提示词、GPT 微内核、Workflow 运行时、Orchestrator、Dream/Distill、cron、inbox。

每个子系统都是一个 Context.Service + Layer,靠依赖注入拼起来。好处在测试和替换上很明显(换一个 Provider.layer 就能把整棵依赖树换掉),代价是阅读门槛陡——yield* 满屏。这是上游的选择,MiMo 全盘继承。

Chapter 03

与上游 OpenCode 的差分:一张增量清单

这一章是全文的地图。读 MiMo Code = 读这张表。

#小米新增一句话章节
1Goal 停止条件 + 独立裁判Agent 想停时,另一个模型读 transcript 判它是不是真做完了4
2四道死循环闸门空参数 / 重复动作 / 文本复读 / 步数上限,各有软硬两级5
3Try-Best 检测器 + 产物验证抓"我尽力了"式假性完成;checkpoint 写完要验7
420 份分家族提示词一个模型家族一份,按 ID 路由8
5Checkpoint 系统上下文快到顶时从快照+记忆+任务进度重建,而非简单压缩9
6FTS5 记忆 + BM25 相对地板SQLite 全文检索,按相对分数过滤常见词噪音10
7可调压缩点/context-limit 让模型比自己的窗口更早压缩11
8GPT 微内核给 Codex 家族一套只有 4 件的工具 ABI12
9exec + QuickJS在沙箱里组合宿主工具,但不放大权限13
10FORCED_ASK通配符 allow 压不住删除类操作14
11Workflow 运行时确定性 JS 脚本编排多 Agent,4 个内置流水线15
12Orchestrator 模式一个窗口管所有任务,子会话跑在独立 worktree16
13Dream / Distill / Evolve沉淀知识、蒸馏技能、改自己17
14Token Efficient 管线洗 bash 输出的 ANSI/进度条/密钥/超长行18
1526 内置技能 · cron · inbox · 语音 · 任务树生态面散见
共同的主题 这 15 条里有 6 条(1、2、3、5、10、13)都在解决同一类问题——Agent 会骗自己。它会说"做完了"其实没做完,会重复同一个动作以为在推进,会在参数为空时反复调同一个工具,会在删文件时把通配符授权当成许可。

如果说 opencode 的主题是"协议即产品"、hermes 是"闭环学习"、CodeWhale 是"安全即机制",那 MiMo Code 的主题就是「不信任模型的自我报告」。
Part II

别让 Agent 骗自己

四个层次、六道装置。这是整个项目最独特的地方。

Chapter 04

心脏地带:Goal 停止条件与独立裁判模型

长任务里最常见的失败不是崩溃,是乐观停止:模型干到一半觉得差不多了,输出一段"我已经完成了 XXX"然后收工。用户回来一看,测试没跑、边界没处理、文件根本没写。

/goal 命令给会话设一个停止条件。之后 session/goal.ts:17-21:

once a goal is set, the main runLoop refuses to stop until an independent judge model decides the condition is satisfied (or genuinely impossible). 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.

最后半句是设计精髓:裁判不干活,所以它的判断相对于干活者的乐观是"冷"的。

裁判提示词里最关键的一句

the assistant claiming the goal is impossible is evidence, not proof goal.ts:64-73 的 JUDGE_SYSTEM 里:

「Apply your own judgment when deciding this — the assistant claiming the goal is impossible is evidence, not proof; independently confirm the condition is genuinely unachievable rather than deferring to the assistant's self-assessment.」

这一句把裁判和被裁判者的关系钉死了。没有它,模型只要说一句"这做不到"就能让裁判放行,整个机制就废了。
细节一
temperature: 0
goal.ts:188 裁判必须可复现。
细节二
看原生 model messages
goal.ts:159-161「converted to native model messages (tool calls/results/images preserved) so the judge independently confirms the work rather than trusting the assistant's self-report」——它看得到真实的工具调用和返回值。
细节三
无证据即不通过
transcript 里没有清晰证据 → {"ok": false, "reason": "insufficient evidence"}。默认不放行。
LAB 01 Goal 裁判沙盘 拨动裁判结论和再入次数,走一遍 goalGate 的四道逃生口(prompt.ts:2505-2600)。注意每一条都是为了「让用户不被困住」而存在的。
再入次数 react1
MAX_GOAL_REACT = 12(子 agent 的 MAX_PRE_REACT 只有 3)
是 main agentgoalGate 只对主会话生效
裁判调用失败测试 fail-open
一句话 Goal 裁判是本系列里第一个把验收和施工用两次独立模型调用彻底分开的设计。Open Design 的五陪审员评审是同一会话的五个回合(共享上下文以保持一致),MiMo 反过来——故意不共享,因为要的就是"冷"。
Chapter 05

四道死循环闸门:空步 / 重复步 / 文本环 / 步数上限

Goal 管的是"该不该停",这一章管的是"别原地转圈"。四道闸门各抓一种病,而且每道都是软→硬两级。

LAB 02 四道闸门实验台 这是 empty-step-detection.ts 的 isEmptyStep 与 prompt.ts 的 stepSignature 的可运行复刻。选一个场景,看哪道闸门会拦下它、哪道会放行——放行的理由和拦下的理由一样重要。
输入的 step 序列
三道步级闸门的判定
重点看这两个场景 ① 「同动作换键序」——三次调用参数键序不同、叙述文字也不同,但仍被判为同一动作。因为 stableStringify 按排序后的键序列化,而签名刻意排除文本和 reasoning。
② 「安静的正常结束」——什么都没输出,但不被判为死循环。这是刻意划的边界,见下方。

闸门一最值得学的是它划出的边界

this guard does NOT try to catch "empty terminals" (steps that emit no tool call and no text). An empty terminal is a natural turn end, not a spin… Treating it as a loop caused frequent false positives on legitimate quiet steps (task done, sub-agent returned, reasoning-only steps, provider-executed tool calls). empty-step-detection.ts 文件头

"模型什么都没说"不是死循环,那是回合正常结束。早期版本把它当循环抓,误报到不可用。真正的"模型什么都不产出"由 wall-clock 和 provider 流超时兜底。

闸门二:两个细节是同一个洞察的两面

归一化
stableStringify 排序键
「models routinely re-emit the same arguments with keys in a different order({url,format} vs {format,url})— without this the signatures would differ and the repeated-step check would miss real loops.」prompt.ts:177-196
排除
签名不含文本与 reasoning
「in a ReAct loop the model narrates each step in slightly different words while taking the exact same action… counting either would mask the repeated action we want to catch.」prompt.ts:198-215
判断"是不是同一个动作"的通则 语义相同但表示不同的东西必须归一化(键序),语义不同但表示相似的东西必须排除(叙述文本)。搞反任何一个,检测器就废了。

闸门三:剥掉开场白才能看出复读

export function normalizeForLoopDetection(text: string): string {
  return text.trim().toLowerCase()
    .replace(/\s+/g, " ")
    .replace(/^(let me |i'll |i will |let's )/i, "")   // ← 剥掉开场白
    .slice(0, 200)
}

模型复读时开头常常在 "Let me…" / "I'll…" / "I will…" / "Let's…" 之间随机切换,归一化时把这些开场白剥掉,才能看出后面是同一句话。

闸门四:步数上限不只是"停",是"交代清楚"

CRITICAL - MAXIMUM STEPS REACHED
Tools are disabled until next user input. Respond with text only.

STRICT REQUIREMENTS:
1. Do NOT make any tool calls …
2. MUST provide a text response summarizing work done so far
3. This constraint overrides ALL other instructions, including any user requests

Response must include:
- Statement that maximum steps for this agent have been reached
- Summary of what has been accomplished so far
- List of any remaining tasks that were not completed
- Recommendations for what should be done next

第 3 条:用户在上下文里说过"必须改完这个文件"也压不住它。而且它要求的不只是停,是让被截断的回合仍然有交付物。

flowchart LR
    subgraph L1["步级(每一步都查)"]
        A["空参数工具调用
软→硬 2 级"] B["重复动作签名
连续 3 次"] C["文本复读 + n-gram
缓冲 5 / 触发 3"] end subgraph L2["回合级"] D["步数上限
禁工具 + 强制交代"] end subgraph L3["会话级"] E["Goal 独立裁判
MAX_GOAL_REACT=12"] end A & B & C -->|软提示不管用| D D --> E E -->|判定未达成| RETRY["继续干"] RETRY --> L1
图 2 三个层次各管各的,互不替代:步级闸门抓局部打转,回合级闸门抓预算耗尽,会话级裁判管"到底做完没有"。
Chapter 06

Agent 模式与子 Agent:13 个内置 agent 的三种身份

agent/agent.ts:35 定义三种身份:subagent / primary / all。内置 13 个 agent:

modeagent用途
primary
(用户可切换)
build默认,完整工具权限
plan只读分析模式
compose编排模式,spec 驱动开发
orchestrator一个窗口管所有任务(flag 门控)
max高预算模式
subagent
(系统按需创建)
general · explore通用 / 探索
title · summary · compaction起标题 / 摘要 / 压缩
checkpoint-writer自动维护 checkpoint.md
dream · distill沉淀知识 / 蒸馏技能

Sticky Tab:为什么 compose 进去就出不来

stateDiagram-v2
    [*] --> 未发首条消息
    未发首条消息 --> build: Tab
    未发首条消息 --> plan: Tab
    未发首条消息 --> compose: Tab
    build --> plan: 发消息后仍可互切
    plan --> build: 可互切
    build --> compose: 发首条消息后禁止
    plan --> compose: 禁止
    compose --> compose: 一旦进入即隔离
    
图 3 Sticky Tab。理由是工具调用可靠性:build ↔ plan 工具集是包含关系(plan 是 build 的只读子集)所以互切安全;compose 的技能集完全不同,切进切出会破坏可靠性。

After the first message the mode locks: Build and Plan can still switch between each other, but Compose is isolated once entered — keeping the skill/tool set fixed from session start significantly improves tool-call reliability. README

从 plan 切到 build 时会注入一段状态变更提示(session/prompt/build-switch.txt,仅 233 字节)——模式切换必须显式告知模型,否则它会继续按只读模式的习惯行事。

三个 ReAct 上限的分层

上限值作用域
MAX_GOAL_REACT12主会话的 goal 再入
MAX_PRE_REACT / MAX_POST_REACT3子 agent 的 preStop / postStop 再入 actor/spawn.ts:38,40
MAX_TASK_GATE_SUBAGENT_REACT—任务闸门 task/gate.ts

主会话给 12、子 agent 给 3——因为「main-session goals are usually larger」。

扩展点设计的一条好规则 MAX_PRE_REACT 的注释规划了将来的约束方向:「Plan: platform cap = hard ceiling, hook cap may only narrow, never widen.」

平台上限是硬天花板,钩子只能收紧不能放宽。(和 Open Design 的封闭 until 词汇表是同一个思路。)

停滞看门狗:三个周期互相错开

T40 stall watchdog scan cadence. Sits between the per-step turn heartbeat and the DEFAULT_LIVENESS_STALL_MS (90s) window, and just under the registry's own 60s stuck-scan, so a genuinely stalled child is caught within ~one window without hammering the DB. actor/spawn.ts:42-46

每步心跳 < 看门狗扫描 < 60s 注册表扫描 < 90s 停滞判定窗口。扫太频繁会打爆数据库,扫太慢会漏掉卡死的子进程。

系统 agent 的两处特殊待遇

// agent/config.ts:5
export const SYSTEM_SPAWNED_AGENT_TYPES: ReadonlySet<string> =
  new Set(["checkpoint-writer", "dream", "distill"])
待遇一
权限询问一律自动拒绝
它们没有通往人的路径——见 第 16 章的三分法。
待遇二
无效输出策略单独声明
config.ts:11-15「System agents must opt into an invalid-output contract instead of inheriting the user-facing primary retry」——用户看得见的重试会弹提示、占前台,后台 agent 需要另一套失败处理。
Chapter 07

Try-Best 检测器与产物级验证

session/try-best-detector.ts(9 283 字节)抓的是另一类自欺:模型说"我已经尽力了"、"这是目前能做到的最好结果"然后停下——但用户要的是做完,不是尽力。

Goal 裁判Try-Best 检测器
启用需用户显式 /goal,opt-in常开
手段独立模型读 transcript措辞层面的启发式
成本一次额外模型调用近乎零

配套还有 checkpoint-validator.ts 和 checkpoint-retry.ts——checkpoint 写完之后要验证,不合格要重试。同一条主线:让 checkpoint-writer 子 agent 写完快照,不代表快照是对的。

本系列三家"不信模型自我报告"的做法对照 openworker:让模型自己核对并发一张 verify-scorecard,宿主程序化检查卡片存在性。
Open Design:五个陪审员在同一会话里打分。
MiMo Code:独立裁判 + 措辞启发式 + 产物验证三管齐下。
Part III

上下文工程

一模型一提示词、快照重建、FTS5 检索、可调压缩点。

Chapter 08

一模型一提示词:20 份 prompt 与路由规则

LAB 03 提示词路由器 输入一个模型 ID,看 session/system.ts:26-39 那 17 行 if 怎么选中提示词,以及 tool/gpt.ts:11-13 怎么另外决定工具面。两套规则各判各的字符串——架构文档自己承认这是尚未统一的地方。
① 提示词路由(system.ts)
② 工具面(tool/gpt.ts)
判定轨迹(顺序有意义)
试试 gpt-4o 和 gpt-oss-120b gpt-4o 含 gpt-4 → 走 BEAST 而不是 GPT,且不进 GPT 工具 ABI。
gpt-oss-120b 含 gpt → 提示词走 GPT,但 usesGPTToolset() 因为含 oss 返回 false → 提示词和工具面不一致。这正是文档说的"两套字符串规则尚未统一"。

路由规则:一串 if

export function provider(model: Provider.Model) {
  const prompt = (id: string) => {
    if (id.includes("gpt-4") || id.includes("o1") || id.includes("o3")) return PROMPT_BEAST
    if (id.includes("gpt"))        return id.includes("codex") ? PROMPT_CODEX : PROMPT_GPT
    if (id.includes("gemini-"))    return PROMPT_GEMINI
    if (id.includes("claude"))     return PROMPT_ANTHROPIC
    // …trinity / kimi / deepseek / glm / minimax 小写化后匹配
  }
  return [prompt(model.id) ?? prompt(model.api.id) ?? PROMPT_DEFAULT]
}
要点一
顺序有意义
gpt-4/o1/o3 必须在 gpt 之前判,否则 gpt-4 会掉进 PROMPT_GPT。
要点二
双 ID 兜底
先用 model.id,不中再用 model.api.id——同一个模型经不同 provider 暴露时 ID 可能不同。
要点三
最后有兜底
没匹配上的模型走 PROMPT_DEFAULT(20.8 KB),不会裸奔。

环境块里的缓存优化

Anchored to the session's creation time (not request time) so this block stays byte-identical across every turn of a session — including ones that cross midnight — keeping it inside the Anthropic cached system prefix. system.ts:66-84

用请求时间会让跨午夜的会话在某一轮突然缓存失效。锚到会话创建时间就不会。

同一段还有一个诚实的风险标注:视觉模型列表是懒解析的,如果 provider 配置在会话中途变了,这一块会打破缓存前缀——「In practice provider config is stable within a session」。已知、可接受、写下来。

无视觉能力时的降级

You CANNOT see or interpret image content… Never attempt to analyze an image's visual content yourself. If a task needs image understanding, dispatch a vision-capable subagent via the actor tool, passing the image file path so the subagent can Read it. system.ts:105-118

并且列出当前配置里有视觉能力的模型(最多 3 个)供它 --model 调用,还给了示例命令。最后补一句区分:如果你要的是文件的二进制结构而不是视觉内容,用 hexdump -C。

"能力降级"的教科书写法 不只是说"你不行",而是说"你不行,但这条路可以走,这是具体走法"。
Chapter 09

Checkpoint:把上下文当成可重建的快照

传统做法是压缩:上下文快满了,让模型把前面的对话总结成一段替换原文。问题是总结有损且不可逆,而且总结本身也占上下文。

Context reconstruction — when context approaches the limit, rebuilds it from the latest checkpoint, project memory, task progress, and retained recent messages so the agent can continue the current task. README

flowchart TB
    P["上下文接近上限"] --> R["重建,而不是压缩"]
    R --> S1["checkpoint.md
结构化状态快照
(checkpoint-writer 子 agent 维护)"] R --> S2["MEMORY.md
项目持久知识
(/dream + 手工)"] R --> S3["notes.md
临时草稿区"] R --> S4["tasks/<id>/progress.md
每个任务的进度日志"] R --> S5["保留的近期消息"] S1 & S2 & S3 & S4 & S5 --> B["预算化注入
分节 token 配额 + 重要性排序"] B --> N["新的上下文"] N --> V["checkpoint-validator
不合格 → checkpoint-retry"]
图 4 重建流程。分节预算意味着 checkpoint 里不同小节有各自的配额,重要的节不会被次要的节挤掉。末端的验证/重试是"产物级"防自欺。

超长用户消息的截断:一个小而完整的工程样本

// 保留头 ~60% + 尾 ~30%,中间放省略标记并指回 messageID
const head = text.slice(0, Math.floor(capTokens * 0.6) * 4).replace(/[\uD800-\uDBFF]$/, "")
const tail = text.slice(-Math.floor(capTokens * 0.3) * 4).replace(/^[\uDC00-\uDFFF]/, "")
return [head,
  `[…elided ${elidedTokens} tokens; messageID=${messageID}; use the history tool with operation=around to fetch full content]`,
  tail].join("\n")
细节一
头 60% 尾 30%
头部有意图,尾部常有结论/最新要求,中间最可省。
细节二
代理对修复
slice() 按 UTF-16 码元切,可能把代理对劈成两半。剥掉尾随高代理和前导低代理——否则 emoji / 非 BMP 字符会渲染成乱码。
细节三
省略标记带回捞路径
messageID=… use the history tool with operation=around——模型知道怎么拿回全文。

高压提示的去抖:窗口要对齐到"状态真的变了"

Keying off the checkpoint boundary rather than a fixed message count is deliberate: a single sustained high-pressure turn can emit many tool-call steps — each its own message — so a fixed-size tail would let the already-nudged message slide out of the window and re-fire the nudge mid-turn. The boundary only advances when a checkpoint/rebuild actually discards context, which is exactly when a fresh nudge becomes useful again. prompt.ts:216-243

Chapter 10

记忆:SQLite FTS5 + BM25 相对地板

LAB 04 BM25 相对地板 查询 "context rebuild checkpoint"。因为查询词是 OR 连接的,每个 progress.md 都会因为匹配 "checkpoint" 这个常见词而进候选集。拖动 floorRatio 看噪音怎么被裁掉。
floorRatio(默认 0.15)0.15

四个检索工程细节

① 查询构造
token 级 FTS5 查询
「punctuation becomes separators, each alphanumeric run becomes a phrase-quoted literal, OR-joined」——避免用户查询里的标点被 FTS5 当成语法(- " * ( ) 在 MATCH 里都有含义)。memory/fts-query.ts
② 相对地板
为什么不用绝对阈值
「BM25 magnitudes are corpus-size-dependent — in a tiny corpus every score collapses toward 0 (low IDF), so any fixed absolute floor would wrongly wipe real hits. The #1 result is ALWAYS kept.」service.ts:71-84
③ 过量抓取
min(limit*3, 50)
「Over-fetch so the relative floor can trim common-word noise without starving the list」——先多抓 3 倍再过滤。service.ts:117-119
④ 方向翻转
bm25() 越小越好
FTS5 的 bm25() 越小越好,对外统一成越大越好(取负)。这类"外部约定与内部实现方向相反"的地方最容易出错,代码里显式注明了。

懒重建与跨产品索引

每次 search 前先 reconcile 一遍(可配置关闭),理由是「covers off-tool writes」——用户可能直接用编辑器改了 MEMORY.md,没走工具。fingerprint 字段让重建是增量的。

一个跨产品的彩蛋 memory.cc_index 配置打开后,会把 ~/.claude/projects 也纳入索引——读 Claude Code 的项目记忆。(MiMo 在多处主动兼容 Claude Code 的目录约定:技能、workflow、记忆索引都是。)
Chapter 11

压缩点可调:/context-limit 与成本档位

这是一个纯产品驱动的工程功能。README 给了三条动机,每条都很实在:

动机一
成本档位
「OpenAI prices GPT-5.6 prompts above 272K input at 2x input and 1.5x output for the whole request.」
动机二
标称窗口 ≠ 实得窗口
「The same model can have a different usable window depending on how you reach it — a ChatGPT/Codex subscription, a direct API key, or a reseller such as OpenRouter — so a catalog figure of 1M does not mean your route serves 1M.」
动机三
质量与延迟
「Very long contexts are slower and, past a point, not better.」
{
  "compaction": {
    "max_context": {
      "openai/gpt-5.6": "272K",   // token 数、"300K"、"1M"、或窗口的 "50%"
      "anthropic/*": "300K"        // 允许通配符,最长模式胜出
    }
  }
}
两条安全约束 + 一个 UI 细节 只能调低不能调高:「always clamped to what the provider actually accepts」;0 恢复模型自己的窗口。

提示符页脚显示 33.0K/260K↓ (13%)——那个 ↓ 表示当前有预算在生效,用户一眼知道自己不是在用完整窗口。
本系列里少见的一类功能 这是把商业计费规则直接编码进 Agent 配置。272K 这个数字不是技术阈值,是 OpenAI 的价格档位分界线。
Part IV

工具与权限

给不同模型不同的工具面,在沙箱里组合工具但不放大权限。

Chapter 12

GPT 微内核:给 Codex 一套更小的工具 ABI

docs/architecture/codex-microkernel-runtime.md 开门见山,而且先自我澄清术语:

"Codex 微内核运行时"是本文对当前架构的概括,不是源码中的正式模块名,也不表示操作系统级微内核。

做法是不为 GPT 新建 Agent 引擎,而是在统一 Session runtime 上做三件事:① GPT/Codex 专属 system prompt;② 通过 ToolRegistry 装配更小的工具 ABI;③ 提供 QuickJS exec 在不扩大权限的前提下组合宿主工具。

一句话原则 模型决定做什么,exec 负责如何组合,宿主决定是否允许以及如何产生副作用。
flowchart TB
    M{"usesGPTToolset(modelID)?
含 gpt- 且不含 oss / gpt-4"} M -->|是 · GPT/Codex 家族| G["4 件工具 ABI"] M -->|否 · 其他模型| N["完整工具套装"] G --> G1["bash —— 用 rg/sed 检查搜索 + 执行命令"] G --> G2["apply_patch —— 结构化 patch 改文本"] G --> G3["view_image —— 本地图片转附件"] G --> G4["exec —— QuickJS 里批量调用聚合宿主工具"] N --> N1["read / write / edit / multiedit"] N --> N2["grep / glob / notebook_edit"] N --> N3["bash / …"] G -.对 GPT 隐藏.-> N1 & N2
图 5 两套工具面。判定函数只有 3 行(tool/gpt.ts:11-13),排除 oss(开放权重模型工具行为不同)和 gpt-4(老模型走 beast 路线)。

为什么要给 GPT 更少的工具

表面看是"减法",实际是对齐模型的训练分布。Codex 系模型在 OpenAI 自家的 harness 里就是用 bash + apply_patch 这套 ABI 训练的,给它一套 Claude 风格的 read/write/edit/grep/glob,它反而更容易调错。

对照 Open Design OD 的做法是完全不定义工具,直接用被托管 CLI 的原生工具。MiMo 是同一个引擎,按模型换工具面。两者解决同一个问题——"不同模型习惯不同的工具"——但一个靠委托,一个靠适配。
文档里自己列出的未修复缺陷 apply_patch:「它会预验证全部 patch,但多文件写入不是事务性的,中途失败不会自动回滚已写文件。」
view_image:detail 只写 metadata 不改处理;没有独立的图片大小限制;exec 不能透传图片附件。

在架构文档里列出自己的未修复缺陷,是这份仓库文档质量的一个信号。
Chapter 13

exec 与 QuickJS:组合工具但不放大权限

模型提交一段 async function body,通过 tools.<name>() 调用宿主工具,一次 exec 批量调用聚合多个工具。危险显而易见:如果 exec 里能调到外层被禁的工具,权限体系就穿了。

三道防线

防线一
Late-bound registry
让 exec 取得和外层相同、已过滤的 Tool.Def:「外层不可见的 read、write、edit 不会在 exec 内重新出现」;MCP 子调用仍逐次执行 ctx.ask()。tool/tool-script-ref.ts:1
防线二
控制流工具被排除
task、actor、question、skill、workflow、cron、session 被排除,因为它们改变对话或调度状态,不适合隐藏在一次脚本调用中。
防线三
两层边界说清楚
QuickJS 隔离 guest code(没有 Node、process、fetch、timer、模块加载);真正副作用仍由宿主工具执行并过权限闸门。但 bash 仍是真实 Shell,不是容器 sandbox。
防线二的区分很精准 能产生副作用的工具(bash、edit)可以在 exec 里调,因为权限闸门照常生效;但能改变调度状态的工具不行,因为那会让一次工具调用在用户不知情的情况下改变整个会话的走向。

"活跃计算":一个避免误杀的计时器

const hostCallTracker = {
  start: () => { pending++; if (pending === 1) activeAccum += Date.now() - activeStart },
  end:   () => { pending--; if (pending === 0) activeStart = Date.now() },
}
// Active-time accounting: charge the guest only while no host hook promise is pending.

脚本等宿主工具返回的时间不算它的计算预算。否则一个正常脚本调 5 次 bash(每次 20 秒)就会被 60 秒预算误杀。Wall clock(30 分钟)单独兜底真正的卡死。

资源默认 / 上限
嵌套工具调用默认 50,最高 500
并发调用8
活跃计算默认 60 秒,最高 600 秒
Wall clock30 分钟
Guest 内存默认 64 MiB
代码 / 返回值 / 日志128 KiB / 256 KiB / 64 KiB
files.* 单文件10 MiB(只能读 worktree 或 OS tmp;只能写 OS tmp)
QuickJS 集成的三条硬约束(2026-06-01 技术验证结论) sandbox.ts:100-105
① sync-promise bridge(newPromise + executePendingJobs),NOT asyncify
② 需要一个并发 pump 配合 resolvePromise,让 host-promise 能结算
③ 每个 QuickJSHandle 必须在 context dispose 之前释放(否则进程 abort)——包括未结算的 deferred,否则 vm.dispose() 会因为活着的 GC 对象硬崩进程
一句话 exec 是这份仓库里工程密度最高的地方——它要同时满足"能组合"、"不放大权限"、"不误杀正常脚本"、"不崩进程"四个约束,每一个都留下了具体的实现痕迹。
Chapter 14

权限系统:FORCED_ASK 与通配符压不住的那条线

// permission/index.ts:188-195
// Permissions whose "allow" outcome must ALWAYS come from an explicit human ask.
// A wildcard rule like `permissions.allow: ["*"]` … MUST NOT be able to pre-authorize
// these — the whole point of a forced-ask permission is that the intent to
// perform an irreversible action must be recorded in-band, not inherited from
// a broad blanket rule.
const FORCED_ASK = new Set(["bash_delete"])
性质说明
通配符 allow 无效permissions.allow: ["*"] 不能预授权删除
历史批准也无效存下来的 {permission:"*", pattern:"*", action:"allow"} 同样不行
显式 deny 仍然生效强制询问 ≠ 强制执行
唯一旁路是工具侧环境变量MIMOCODE_AUTO_APPROVE_DELETE——要绕过必须显式、且在另一个层面绕
核心理由 「the intent to perform an irreversible action must be recorded in-band, not inherited from a broad blanket rule」——不可逆操作的意图必须当场记录,不能从一条宽泛的总授权里继承。

工具组别名与 findLast

permission/index.ts:591-614 用数组顺序表达优先级:EDIT_TOOLS(edit/write/apply_patch/multiedit)额外按 edit 这个组别名匹配,这样写 edit: "deny" 就能一次禁掉整个编辑家族。

findLast returns the last-merged matching rule, so a tool-specific rule placed after a group rule wins naturally. This preserves the convenience of edit: "deny" covering all edit-family tools while letting an explicit write: "allow" take precedence when present.

用数组顺序表达优先级,用 findLast 让后写的赢——不需要额外的优先级字段,且符合"配置文件里后写的覆盖先写的"这个直觉。

本系列的权限路线对照 CodeWhale:safe by construction(模型根本没有越权的工具)· openworker:五档模式 + 收件箱 · Claude Code:权限闸门 + hooks · Open Design:完全委托给被托管 CLI。

MiMo 走的是"通用规则 + 少数不可协商的例外"——规则可以宽,但有些线通配符跨不过去。
Part V

编排与生态

确定性工作流、一个窗口管所有任务、让 Agent 改自己。

Chapter 15

Workflow:确定性 JS 脚本编排多 Agent

Workflows are deterministic JavaScript scripts that orchestrate multiple agents in a sandboxed runtime. Unlike agent conversations, workflows encode fixed phase sequences with bounded retries and automatic parallelization — fire-and-forget execution with no user interaction required.

对话式(build + /compose-next)Workflow
控制流模型决定脚本写死
交互可中途改方向无需交互
并行靠模型派子 agent自动并行化
重试模型自己判断有界重试
适合需要中途注入判断需求明确、任务可切分

四个内置流水线

Workflow阶段特点
composeBrainstorm → Design → Implement → Verify → Review → Report → Merge把独立任务自动并行到隔离的 git worktree,每个任务用 TDD,阶段间传结构化输出
deep-researchBrief → Plan → Research → Reflect → Write → Review并行子 agent 收集带引用的发现 → 反思缺口 → 写成连贯 Markdown → 冷审引用。靠文件 checkpoint 可续跑
fact-checkPlan → Search → Extract → Group → Crosscheck → Report对抗式事实核验:3 陪审员对抗投票逐条交叉核对
research-experimentBaseline → Loop → Audit → Report面向可机械验证指标的自主优化循环,审计是否在刷指标
两个和本系列其他项目呼应的点 ① fact-check 的 3 陪审员对抗投票和 Open Design 的 Design Jury 是同一族设计,但方向相反——OD 的五陪审员协作评审同一产物,MiMo 的三陪审员对抗核验同一事实。
② research-experiment 明确要求审计指标造假("audits for metric gaming")——这是"别让 Agent 骗自己"主题在实验场景的延伸。

两个防御性细节

细节一
启动即失败
「this throw runs at module init, so a broken built-in fails the whole app boot; the path tells the user which one.」

和 Open Design 的 registry 去重 throw 是同一个哲学:让错误尽早、尽响。
细节二
Null 原型注册表
Object.create(null)——「a lookup like get("constructor")/get("toString") returns undefined, not an inherited Object.prototype member.」

当注册表的键可能来自模型输出时,这是必须的。

放一个 .js 到 .mimocode/workflows/ 或 .claude/workflows/ 就能定义自己的 workflow;用同名文件可以覆盖内置。

Chapter 16

Orchestrator 模式:一个窗口管所有任务

默认关闭,由单一 flag MIMOCODE_EXPERIMENTAL_ORCHESTRATOR 门控。它解决的问题是:

真正的负担不是机器算力,而是你的注意力和精力——上下文在窗口之间反复切换,人被"多路复用"拖垮。

flowchart TB
    U["用户目标(自然语言)"] --> O["Orchestrator 会话(全局唯一)"]
    O -->|session create| A["child A · build · dir=repo1 · --isolate"]
    O -->|session create| B["child B · plan · dir=repo2"]
    O -->|session create| C["child C · compose · dir=repo1 · --isolate"]
    A & B & C -->|完成| INBOX["actor_notification → inbox"]
    INBOX -->|主动唤醒| O
    O --> MERGE["git 合并各 child 的 mimocode/* 分支"]
    MERGE --> REPORT["汇报给用户"]
    
图 6 Orchestrator 自己不做实质工作——不写代码、不做实现规划、不做质量评审,全部委派。"拆分成派发单元"是它的活。每个 child 是独立会话(peer,不是 in-session subagent),用户可以完整 attach 进去接管。

后台会话的权限审批路由

问题很实在:后台跑的 child 没有面对用户的面板,碰到需要 ask 的权限门会被直接拒绝(interactive:false → DeniedError),用户看不到也无从批准。

场景处置(decideAskRouting)
系统 agent(checkpoint-writer / dream / distill)仍自动拒绝
Orchestrator peer child(background + mode:peer + 有父会话)转发审批
其他后台(compose 的 subagent 等)仍自动拒绝
判定条件是"有没有一条通往人的路径" 不是"是不是后台"。Orchestrator 的 child 有父会话和看 TUI 的用户,所以能转发;系统 agent 没有,所以拒绝。
「完成」和「可以删」是两件事 「只在工作已合并、或任务被放弃后才 cancel 一个 isolated child —— cancel 会删 worktree 和分支,对未合并的工作执行会永久丢失该工作。不要因为 child "完成了"就 cancel(完成产生的是它分支上待合并的提交)。」

不轮询:create 立即返回,child 完成时消息进 inbox 唤醒 Orchestrator。「派发后就返回…不要循环 list/查状态空耗轮次。」中断 Orchestrator 不会停掉 child。

和 openworker 的收件箱对照 openworker 的 inbox 是"Agent 需要人时挂起等你",MiMo 的 inbox 是"子会话干完了唤醒父会话"。同一个原语,一个朝人、一个朝内部调度。
Chapter 17

Dream / Distill / Evolve:让 Agent 改自己

能力做什么
/dream扫最近的会话轨迹,把持久知识提取进项目记忆,并删除过时条目
/distill发现最近工作里重复出现的手工流程,把高置信度候选打包成可复用的技能 / 子 agent / 命令
evolve 技能「Total self-modification — rewrite any layer of the agent: tools, behavior hooks, knowledge, workflows, even the UI」
「并删除过时条目」这半句很关键 多数记忆系统只会往里加,越加越乱。会删的记忆系统才有长期可用性。
drive-mimo:让 Agent 驱动 Agent 26 个内置技能里有一个 drive-mimo——用一个 MiMoCode 进程去脚本化、测试、自动化另一个 MiMoCode 进程(headless 或 TUI 模式)。

这是自举测试的基础设施,也是"共进化"能落地的前提——没有它,改完自己没法验证。

一个有意思的反向关系

26 个内置技能里有 claude-code、codex、grok-build 三个——MiMo 可以把任务委派给这些外部 CLI(且只在对应可执行文件已安装时才暴露)。

这个生态里没有绝对的宿主和客体 Open Design 把 mimo 当引擎(它是 OD 26 条适配器之一);MiMo 又能把 Claude Code / Codex / Grok Build 当工具调。
Chapter 18

Token Efficient 模式:把 bash 输出的噪音洗掉

实验功能,默认关闭,单 flag MIMOCODE_EXPERIMENTAL_TOKEN_EFFICIENCY。bash 的 stdout/stderr 经常被 ANSI 色码、\r 进度条多帧、误打印的密钥、minified JS 超长行撑爆上下文。

约束一
仅清 inline,不清落盘
清理只面向 LLM;TUI 实时预览与磁盘归档保持原始字节,便于人工 debug。

给模型看的和给人看的是两份。
约束二
never-worse 守门
管线尾部统一回吐:任何阶段使输出变大都被丢弃,回到 Raw 路径。
约束三
单 flag、默认关
唯一开关,且默认关闭。
flowchart LR
    RAW["bash stdout/stderr"] --> SPLIT{"三路分流"}
    SPLIT -->|落盘归档| DISK["原始字节"]
    SPLIT -->|TUI 预览| TUI2["原始字节"]
    SPLIT -->|inline 给 LLM| P1["① clean_progress
折叠 \\r 进度条"] P1 --> P2["② clean_ansi
剥 CSI/OSC/DCS + 控制字节"] P2 --> P3["③ clean_redact
PEM/Bearer/JWT/AWS/GH/OpenAI 密钥"] P3 --> P4["④ clean_longline
超 500 字符压成 head 160"] P4 --> GUARD{"never-worse 守门
字节数变小了吗?"} GUARD -->|是| OUT["清理后送 LLM"] GUARD -->|否| RAW2["回吐原文"]
图 7 五层管线。顺序约束都有理由:进度条要先折叠,否则 ANSI 剥完会留一堆重叠帧;密钥要在截断前脱敏,否则可能刚好被截在中间躲过正则。脱敏正则共 14 条。
never-worse 守门这个模式值得单独记 任何"优化"管线都应该有一个"如果没变好就回退原样"的兜底。清理规则总有想不到的输入形状,这条守门让最坏情况等于不清理,而不是等于搞砸。
Chapter 19

总结:三个最独特的设计与三处取舍

三个最独特的设计

DESIGN 01
「不信任模型的自我报告」被做成了一整套纵深装置

这是 MiMo Code 真正的主题,而且不是口号,是四个层次、六个互相独立的机制:

层次装置
步级空参数工具调用检测(软→硬 2 级)· 重复动作签名(键序归一化 + 排除叙述文本)· 文本复读 + n-gram
回合级步数上限:禁工具 + 强制交代(「overrides ALL other instructions」)
会话级Goal 独立裁判(temperature=0 · 只读 transcript · 四道逃生口)· Try-Best 检测器
产物级checkpoint-validator + checkpoint-retry:写完的快照要验证、不合格要重写

四个层次各管各的,没有一个能替代另一个。这种纵深是本系列其他项目里没有的。而且每一层都留下了为什么是这个阈值的注释。

DESIGN 02
一模型一提示词 + 一模型一工具 ABI

20 份 system prompt 按模型家族路由,GPT/Codex 家族还额外换一套只有 4 件的工具 ABI,隐藏 read/write/edit/grep/glob。

这是对"通用 harness"这个假设的正面否定。别家的思路是"写一套好提示词让所有模型都能用";MiMo 的思路是"每个模型的训练分布不同,harness 就该为它调形"。口号里的 "Models and Agents Co-Evolve" 说的就是这件事。

代价也很直白:20 份提示词要各自维护,路由是一串字符串 if,而且提示词路由和工具路由是两套独立规则(架构文档自己承认「尚未统一成模型能力协商层」)。

DESIGN 03
exec 微内核:在沙箱里组合工具而不放大权限

一次工具调用里跑一段 QuickJS 脚本,批量调用聚合宿主工具,同时保证:late-bound registry(拿到和外层完全相同的已过滤工具表)· 控制流工具被排除(能改调度状态的不许藏在脚本里)· 活跃计算计时(等宿主工具的时间不计预算)· 两层边界说清楚(QuickJS 只隔离脚本,bash 仍是真实 Shell)。

"模型决定做什么,exec 负责如何组合,宿主决定是否允许"这句话,在代码里是能逐条对上的。

三处必须知道的取舍

取舍 01
prompt.ts 单文件 4 595 行
主循环、提示词组装、goal gate、各种闸门、结构化输出……全在一个 208 KB 的文件里。对比 Open Design 的 system.ts(2 075 行,只是组装)。

注释密度很高,但单文件承载的关注点太多。这是最明显的可维护性债务。
取舍 02
实验功能默认关闭
Orchestrator、Token Efficient 都靠 flag 门控且默认关。好处是主路径稳定;代价是这些设计精良的能力大部分用户根本不会开,而且 flag 分支会让代码路径组合爆炸。

一个半月 12.5k star、838 个 open issue——相对仓库年龄偏高。
取舍 03
fork 的双重成本
继承骨架省下巨量前期工作,但:上游演进要持续合并(包名至今叫 packages/opencode)· Effect 框架的阅读门槛是上游带来的 · 有些遗留(default.old.txt)留在树里。

它在本系列里的位置

维度本系列其他项目MiMo Code
身世多数从零写OpenCode 的深度 fork(如 Raven 之于 nanobot,但改造幅度大得多)
提示词策略一套通用提示词20 份,一模型家族一份
工具面全模型同一套GPT 家族换一套 4 件 ABI
停止判断模型自己说停就停独立裁判模型,四道逃生口
死循环防护通常 1–2 层步级 3 道 + 回合级 1 道 + 会话级 2 道 + 产物级 1 道
上下文压缩为主checkpoint 重建 + 可调压缩点(含计费档位)
编排子 agent确定性 JS workflow + Orchestrator(peer child + worktree)
自我改进hermes 的闭环学习dream / distill / evolve + drive-mimo 自举
许可多为纯 MIT/ApacheMIT + 独立的 USE_RESTRICTIONS
最后一句 opencode 回答的是"怎么让 agent 成为可被任何客户端消费的服务",MiMo Code 在它的骨架上回答了另一个问题——「模型会骗自己,harness 该怎么办?」

它给出的答案不是一个机制,而是四个层次、六道装置:步级抓打转,回合级抓预算,会话级派独立裁判,产物级验快照。再加上"每个模型的 harness 应该为它单独调形"这个主张,构成了一份很不一样的编程 Agent 设计。

至于"共进化"这个口号有没有兑现——evolve 能改自己、drive-mimo 能验证改动、distill 能从行为里长出新技能——基础设施是齐的,剩下的看社区。

继续读

Part VI

与上游逐层差分

前面讲"MiMo 有什么"。这一部分讲其中哪些是它自己加的、哪些是继承的、为什么要这么改——基于真实的文件级对比。

Chapter 20

逐层差分:MiMo 到底改了 OpenCode 什么,以及为什么

方法 把 参考项目/opencode(commit 62e4641,完整检出 6 252 文件)和 MiMo(commit 076b790,Git Tree API 全量 5 222 blob)做文件级尺寸对比 + 关键文件内容比对。所有判定都有可复现的证据,不靠印象。

20.1 先看总量:1.75 倍,但不是均匀长胖的

packages/opencode/src/ 全部 TS/TSX体积比值
OpenCode 62e46412 610 KB1.00x
MiMo Code 076b7904 572 KB1.75x

但增长完全不均匀——这才是有信息量的地方:

flowchart LR
    subgraph NEW["🆕 15 个全新子系统(上游完全没有)"]
        N1["workflow · actor · memory
cron · inbox · task · team"] N2["history · file · flag · global
metrics · npm · pty · shell"] end subgraph BIG["↑ 大幅扩张"] B1["skill/ 2 → 414 文件(207x)"] B2["provider/ 5 → 34(6.8x)"] B3["storage/ 2 → 11(5.5x)"] B4["cli/ 87 → 230(2.6x)"] B5["tool/ 43 → 84(2.0x)"] B6["permission/index.ts 3.4x"] B7["session/ 39 → 68(1.7x)"] end subgraph SHRINK["↓ 收缩"] S1["server/ 72 → 43"] S2["acp/ 12 → 4"] end subgraph DEL["❌ 删除"] D1["background · image
event-manifest · event-v2-bridge"] end
图 8 一个 fork 的"改造重心"就藏在这张图里。加的 15 个子系统里有 7 个(workflow/actor/memory/cron/inbox/task/team)指向同一件事——让 Agent 能长时间、多线程、跨会话地干活;而 server/ 和 acp/ 收缩说明它放弃了上游"服务器即本体"的部分野心,把重心挪回终端。
LAB 05 归属核对台 逐条点开看:这项能力到底是 MiMo 首创、继承后扩张、还是上游本来就有。每条都附两边的真实文件与字节数。

20.2 主循环:session/prompt.ts 从 65 KB 涨到 208 KB

文件OpenCodeMiMo变化
session/prompt.ts64 925 B207 962 B↑ 3.2x
session/llm.ts15 097 B37 119 B↑ 2.5x
session/processor.ts26 497 B41 504 B↑ 1.6x
session/overflow.ts1 313 B4 921 B↑ 3.7x
同时删掉了上游的整个 session/llm/ 子目录(native-request 7 953 B · native-runtime 8 036 B · ai-sdk 9 315 B · request 7 597 B)和 session/tools.ts(23 424 B)
这是一次「反重构」 上游把 LLM 请求构造、原生运行时、工具装配拆在四五个文件里;MiMo 把它们收拢进 prompt.ts,同时往里塞了大量新逻辑(goalGate、四道闸门、结构化输出、预测下一条消息……)。

上游在拆,MiMo 在合。合的好处是主循环控制流一眼看得完(不用在五个文件间跳);坏处就是那条最大技术债——4 595 行单文件。

从 fork 的角度看这个选择可以理解:你要在别人的骨架上塞进七八个新机制,最快的路径是集中改一个文件,而不是先重构上游的分层。代价是这笔债只会越滚越大。

20.3 提示词:8 → 20 份,但有 5 份一个字节没动

这是最需要澄清归属的一处 分家族提示词路由是上游的设计。上游 session/system.ts:27-42 的路由函数和 MiMo 结构一模一样,连 gpt-4/o1/o3 → BEAST 的顺序都相同。
提示词OpenCodeMiMo判定
gemini.txt15 37215 372字节级未改
kimi.txt8 6958 695字节级未改
codex.txt7 3907 390字节级未改
trinity.txt7 7487 749差 1 字节
copilot-gpt-5.txt14 24114 239差 2 字节
anthropic.txt8 21214 281↑ 1.7x 重写
default.txt8 52820 800↑ 2.4x 重写
gpt.txt9 28425 447↑ 2.7x 重写
deepseek · glm · minimax—10 530 / 4 890 / 10 908🆕
orchestrator · compose—21 002 / 6 036🆕
meta.txt(muse-spark)· plan*.txt9 151 / 10 087—❌ 删除
为什么只重写那 3 份? 因为 gpt.txt / default.txt / anthropic.txt 覆盖了绝大多数实际用量——GPT 系、Claude 系、以及所有没匹配上的模型。小米把力气花在了命中率最高的三条路径上,边缘家族原样继承。这是很务实的取舍。

双 ID 兜底为什么重要(session/system.ts:39):上游只看 model.api.id。但同一个模型经不同 provider 暴露时 ID 不同——OpenRouter 上的 xiaomi/mimo-v2.5、自建网关上的 internal/xiaomi/mimo-v2.5。MiMo 要支持的模型接入路径比上游多得多(MiMo Auto / Xiaomi OAuth / Codex OAuth / 从 Claude Code 导入 / 任意 OpenAI 兼容端点),所以必须两个 ID 都试。

20.4 GPT 工具门控:同一行表达式,作用域扩了一个量级

OpenCode
registry.ts:292-296
const usePatch =
  modelID.includes("gpt-") &&
  !modelID.includes("oss") &&
  !modelID.includes("gpt-4")
if (tool.id === ApplyPatch) return usePatch
if (tool.id === Edit || Write) return !usePatch
只管二选一交换。
MiMo
tool/gpt.ts:11-13
export function usesGPTToolset(modelID) {
  return modelID.includes("gpt-") &&
    !modelID.includes("oss") &&
    !modelID.includes("gpt-4")
}
再门控 exec/view_image,隐藏 read/grep/glob/multiedit/notebook_edit。

判定逻辑一字不差。差别在两处:代码形态(内联局部变量 → 独立文件的命名导出)和作用域(二选一 → 整套 ABI 替换)。

为什么要抽出来 上游那个变量叫 usePatch——名字说明它只关心"用不用 patch 工具"。MiMo 要在三处用同一个判定(工具装配、子 agent 提示词拼接 system.ts:44-48、MCP 工具搜索开关 isMcpToolSearchEnabled),内联变量就不够了,必须提取成有名字的概念。

从「换一把螺丝刀」到「换一整个工具箱」——这个作用域扩张才是 MiMo 的实质贡献,而不是那行表达式本身。

20.5 沙箱:从 3 465 行手写解释器换成 QuickJS

上游 @opencode-ai/codemodeMiMo workflow/sandbox.ts
工具名executeexec
入口tool/code-mode.ts 11 808 B ❌删除tool/tool-script.ts 27 188 B 🆕
隔离实现acorn + 手写 AST 解释器
interpreter/runtime.ts 3 465 行
QuickJS-emscripten(真 WASM JS 引擎)
sandbox.ts 15 878 B
包依赖acorn 8.15.0 + typescript(转译)quickjs-emscripten
暴露什么只有 MCP 工具宿主工具(late-bound registry)
控制流工具—显式排除 task/actor/question/skill/workflow/cron/session
计时—活跃计算 60s/600s · wall 30min
已知代价要自己实现 JS 语义,覆盖面有洞句柄必须在 dispose 前全部释放,否则进程 abort
为什么值得换(三个理由,按重要性排) ① 手写 JS 解释器是个无底洞。3 465 行只是起点——JS 的语义面(原型链、闭包、生成器、Proxy、getter/setter、异常语义、this 绑定……)每补一块都要写代码,每一处没实现到的地方是行为差异,每一处实现错的地方可能是逃逸口。

② 代价从「语义正确性」转成「资源管理」。QuickJS 的坑很硬(句柄不释放直接 abort 进程),但它是有确定检查清单的——sandbox.ts:100-105 那三条 2026-06-01 技术验证结论就是这份清单。语义正确性没有清单。

③ 暴露面变了,风险等级跟着变。上游只暴露 MCP 工具——本来就在权限体系外围。MiMo 要暴露宿主工具(bash / edit / read……),权限风险直接抬高一级。所以才有了 late-bound registry、控制流工具排除、"bash 仍是真实 Shell"这句诚实声明——这一整套配套设计,是暴露面扩张倒逼出来的。
通则 当你把一个沙箱的暴露面从「外围能力」扩到「核心能力」时,隔离手段和权限传递机制都要重新设计,不能只是"多注册几个函数"。

20.6 Shell 执行整个重写 · 20.7 权限 3.4 倍

Shell
37 KB → 87 KB
❌ shell.ts 20 439 B · shell/prompt.ts 16 779 B
🆕 bash.ts 29 711 B · shell-tokenize.ts 11 389 B · shell-wrap.ts 11 001 B · bash-interactive.ts 4 914 B · Token Efficient 两件 25 KB

注意 bash.txt 和 bash.gpt.txt 是两份——连工具的描述文本都按模型家族分开了。
权限
index.ts 7 861 → 26 415 B
arity.ts 6 376 = 6 376 字节级未改;permission-forward-ref.ts 7 536 B 🆕

因果链:MiMo 加了 Orchestrator(后台 peer child)→ 后台会话碰到权限询问会被直接拒绝 → 需要转发机制 → permission-forward-ref.ts + decideAskRouting 三分法。新功能倒逼权限系统扩张,这是 3.4 倍的主要来源。

20.9 收缩的地方也有信息量

目录OpenCodeMiMo说明
server/72 文件43上游"服务器即本体",server 层极重;MiMo 重心回到终端
acp/12 文件4ACP 在上游是一等公民;MiMo 保留但不再深耕
background/1❌被 actor/ + cron/ + inbox/ 取代
image/1❌换成 tool/view-image.ts
读法 本系列分析 opencode 时给它的定位是「服务器即本体」——TUI 和同进程服务器也走完整 HTTP/RPC。MiMo 把 server/ 砍掉四成、acp/ 砍掉三分之二,说明它不想做平台,它想做一个好用的终端产品。

这也解释了 cli/ 为什么反向扩张 2.6 倍(87 → 230 文件,含 10 种语言 i18n):砍掉的服务器野心,加倍投在了终端体验上。

20.10 把 15 个新子系统按「为什么加」归类

flowchart TB
    subgraph G1["动机 A:让 Agent 能长时间干活"]
        A1["memory —— 跨会话记忆 FTS5"]
        A2["checkpoint 家族 —— 上下文重建 74 KB"]
        A3["history —— 历史回捞"]
        A4["task —— 树形任务与进度"]
    end
    subgraph G2["动机 B:让 Agent 能多线程干活"]
        B1["actor —— 子 agent 派发与生命周期"]
        B2["workflow —— 确定性 JS 编排"]
        B3["inbox —— 完成通知唤醒"]
        B4["team / worktree —— 多会话隔离"]
    end
    subgraph G3["动机 C:让 Agent 能无人值守"]
        C1["cron —— 定时 + 分布式锁 + 哨兵"]
        C2["flag —— 实验功能门控"]
        C3["metrics —— 可观测"]
    end
    subgraph G4["动机 D:基础设施补齐"]
        D1["file · global · npm · pty · shell"]
    end
    
图 9 A + B + C 是同一个产品判断的三个面:上游面向"一次对话解决一个问题";MiMo 面向"长周期、可并行、能自己跑"。README 里那句 long-horizon(官网博客标题就是 mimo-code-long-horizon)不是营销,是这 12 个子系统的共同注脚。

20.12 这次 fork 给同类项目的三条经验

经验 01
先想清楚你 fork 的是「骨架」还是「产品」
MiMo 继承的是 Effect 服务层、持久化、provider 抽象、TUI 栈——全是骨架。凡是体现产品判断的地方(提示词内容、工具面、权限线、内容库)它都重做了。

骨架可以借,产品判断不能借。
经验 02
改造重心暴露产品定位
看一眼哪些目录膨胀、哪些收缩,就知道这个 fork 想变成什么。

server/ 砍四成 + cli/ 涨 2.6 倍 = "不做平台,做终端产品";加 12 个长周期/并行子系统 = "从一次对话变成一个工期"。
经验 03
集中改一个文件是合理战术,但要记账
prompt.ts 涨到 4 595 行是有原因的——在别人的骨架上塞新机制,集中改一处比先重构上游分层快得多。

但这笔债会滚,而且 fork 越久越难还(上游还在动)。MiMo 在注释里留了很多 TODO: lift to mimocode.json config,说明团队自己知道。
一句话 MiMo 对 OpenCode 的改造,不是把它变好用一点,而是换了一个产品命题。上游问的是"怎么让 agent 成为可被任何客户端消费的服务";MiMo 问的是"怎么让 agent 能一个人干几个小时的活而不跑偏"。

13 项首创里有 6 项在防自欺、7 项在支撑长周期与并行——这两组加起来,就是那个新命题的全部答案。
↑