它是什么
先把身世说清楚:这是一个 fork。读 fork 的正确方式是读差分。
一个 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 路由。bash / apply_patch / view_image / exec 四件,后者是完整套装。数字化的项目形状
| 字节 | 文件 | 是什么 |
|---|---|---|
| 207 962 | session/prompt.ts | 主循环(4 595 行)——Agent 心脏 |
| 128 317 | cli/cmd/tui/routes/session/index.tsx | TUI 会话主视图 |
| 84 552 | workflow/runtime.ts | 确定性工作流运行时(1 607 行) |
| 74 327 | session/checkpoint.ts | Checkpoint 系统(1 648 行) |
| 71 103 | provider/transform.ts | provider 请求变换 |
| 53 710 | tool/session.ts | Orchestrator 的 session 工具 |
| 48 853 | actor/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 自欺"做成核心设计的项目,在许可条款里也写下了"不许无监督地自主执行高风险动作"——这两件事是一致的。
全景架构: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
每个子系统都是一个 Context.Service + Layer,靠依赖注入拼起来。好处在测试和替换上很明显(换一个 Provider.layer 就能把整棵依赖树换掉),代价是阅读门槛陡——yield* 满屏。这是上游的选择,MiMo 全盘继承。
与上游 OpenCode 的差分:一张增量清单
这一章是全文的地图。读 MiMo Code = 读这张表。
| # | 小米新增 | 一句话 | 章节 |
|---|---|---|---|
| 1 | Goal 停止条件 + 独立裁判 | Agent 想停时,另一个模型读 transcript 判它是不是真做完了 | 4 |
| 2 | 四道死循环闸门 | 空参数 / 重复动作 / 文本复读 / 步数上限,各有软硬两级 | 5 |
| 3 | Try-Best 检测器 + 产物验证 | 抓"我尽力了"式假性完成;checkpoint 写完要验 | 7 |
| 4 | 20 份分家族提示词 | 一个模型家族一份,按 ID 路由 | 8 |
| 5 | Checkpoint 系统 | 上下文快到顶时从快照+记忆+任务进度重建,而非简单压缩 | 9 |
| 6 | FTS5 记忆 + BM25 相对地板 | SQLite 全文检索,按相对分数过滤常见词噪音 | 10 |
| 7 | 可调压缩点 | /context-limit 让模型比自己的窗口更早压缩 | 11 |
| 8 | GPT 微内核 | 给 Codex 家族一套只有 4 件的工具 ABI | 12 |
| 9 | exec + QuickJS | 在沙箱里组合宿主工具,但不放大权限 | 13 |
| 10 | FORCED_ASK | 通配符 allow 压不住删除类操作 | 14 |
| 11 | Workflow 运行时 | 确定性 JS 脚本编排多 Agent,4 个内置流水线 | 15 |
| 12 | Orchestrator 模式 | 一个窗口管所有任务,子会话跑在独立 worktree | 16 |
| 13 | Dream / Distill / Evolve | 沉淀知识、蒸馏技能、改自己 | 17 |
| 14 | Token Efficient 管线 | 洗 bash 输出的 ANSI/进度条/密钥/超长行 | 18 |
| 15 | 26 内置技能 · cron · inbox · 语音 · 任务树 | 生态面 | 散见 |
如果说 opencode 的主题是"协议即产品"、hermes 是"闭环学习"、CodeWhale 是"安全即机制",那 MiMo Code 的主题就是「不信任模型的自我报告」。
别让 Agent 骗自己
四个层次、六道装置。这是整个项目最独特的地方。
心脏地带: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.
最后半句是设计精髓:裁判不干活,所以它的判断相对于干活者的乐观是"冷"的。
裁判提示词里最关键的一句
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{"ok": false, "reason": "insufficient evidence"}。默认不放行。goalGate 的四道逃生口(prompt.ts:2505-2600)。注意每一条都是为了「让用户不被困住」而存在的。
四道死循环闸门:空步 / 重复步 / 文本环 / 步数上限
Goal 管的是"该不该停",这一章管的是"别原地转圈"。四道闸门各抓一种病,而且每道都是软→硬两级。
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 排序键{url,format} vs {format,url})— without this the signatures would differ and the repeated-step check would miss real loops.」prompt.ts:177-196闸门三:剥掉开场白才能看出复读
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
Agent 模式与子 Agent:13 个内置 agent 的三种身份
agent/agent.ts:35 定义三种身份:subagent / primary / all。内置 13 个 agent:
| mode | agent | 用途 |
|---|---|---|
| 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: 一旦进入即隔离
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_REACT | 12 | 主会话的 goal 再入 |
MAX_PRE_REACT / MAX_POST_REACT | 3 | 子 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"])
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 写完快照,不代表快照是对的。
verify-scorecard,宿主程序化检查卡片存在性。Open Design:五个陪审员在同一会话里打分。
MiMo Code:独立裁判 + 措辞启发式 + 产物验证三管齐下。
上下文工程
一模型一提示词、快照重建、FTS5 检索、可调压缩点。
一模型一提示词:20 份 prompt 与路由规则
if 怎么选中提示词,以及 tool/gpt.ts:11-13 怎么另外决定工具面。两套规则各判各的字符串——架构文档自己承认这是尚未统一的地方。
① 提示词路由(system.ts)
② 工具面(tool/gpt.ts)
判定轨迹(顺序有意义)
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。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。
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"]
超长用户消息的截断:一个小而完整的工程样本
// 保留头 ~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")
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
记忆:SQLite FTS5 + BM25 相对地板
progress.md 都会因为匹配 "checkpoint" 这个常见词而进候选集。拖动 floorRatio 看噪音怎么被裁掉。
四个检索工程细节
- " * ( ) 在 MATCH 里都有含义)。memory/fts-query.tsmin(limit*3, 50)bm25() 越小越好,对外统一成越大越好(取负)。这类"外部约定与内部实现方向相反"的地方最容易出错,代码里显式注明了。懒重建与跨产品索引
每次 search 前先 reconcile 一遍(可配置关闭),理由是「covers off-tool writes」——用户可能直接用编辑器改了 MEMORY.md,没走工具。fingerprint 字段让重建是增量的。
memory.cc_index 配置打开后,会把 ~/.claude/projects 也纳入索引——读 Claude Code 的项目记忆。(MiMo 在多处主动兼容 Claude Code 的目录约定:技能、workflow、记忆索引都是。)
压缩点可调:/context-limit 与成本档位
这是一个纯产品驱动的工程功能。README 给了三条动机,每条都很实在:
{
"compaction": {
"max_context": {
"openai/gpt-5.6": "272K", // token 数、"300K"、"1M"、或窗口的 "50%"
"anthropic/*": "300K" // 允许通配符,最长模式胜出
}
}
}
0 恢复模型自己的窗口。提示符页脚显示
33.0K/260K↓ (13%)——那个 ↓ 表示当前有预算在生效,用户一眼知道自己不是在用完整窗口。
工具与权限
给不同模型不同的工具面,在沙箱里组合工具但不放大权限。
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
oss(开放权重模型工具行为不同)和 gpt-4(老模型走 beast 路线)。为什么要给 GPT 更少的工具
表面看是"减法",实际是对齐模型的训练分布。Codex 系模型在 OpenAI 自家的 harness 里就是用 bash + apply_patch 这套 ABI 训练的,给它一套 Claude 风格的 read/write/edit/grep/glob,它反而更容易调错。
apply_patch:「它会预验证全部 patch,但多文件写入不是事务性的,中途失败不会自动回滚已写文件。」view_image:detail 只写 metadata 不改处理;没有独立的图片大小限制;exec 不能透传图片附件。在架构文档里列出自己的未修复缺陷,是这份仓库文档质量的一个信号。
exec 与 QuickJS:组合工具但不放大权限
模型提交一段 async function body,通过 tools.<name>() 调用宿主工具,一次 exec 批量调用聚合多个工具。危险显而易见:如果 exec 里能调到外层被禁的工具,权限体系就穿了。
三道防线
exec 取得和外层相同、已过滤的 Tool.Def:「外层不可见的 read、write、edit 不会在 exec 内重新出现」;MCP 子调用仍逐次执行 ctx.ask()。tool/tool-script-ref.ts:1task、actor、question、skill、workflow、cron、session 被排除,因为它们改变对话或调度状态,不适合隐藏在一次脚本调用中。process、fetch、timer、模块加载);真正副作用仍由宿主工具执行并过权限闸门。但 bash 仍是真实 Shell,不是容器 sandbox。"活跃计算":一个避免误杀的计时器
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 clock | 30 分钟 |
| Guest 内存 | 默认 64 MiB |
| 代码 / 返回值 / 日志 | 128 KiB / 256 KiB / 64 KiB |
files.* 单文件 | 10 MiB(只能读 worktree 或 OS tmp;只能写 OS tmp) |
① sync-promise bridge(
newPromise + executePendingJobs),NOT asyncify② 需要一个并发 pump 配合
resolvePromise,让 host-promise 能结算③ 每个
QuickJSHandle 必须在 context dispose 之前释放(否则进程 abort)——包括未结算的 deferred,否则 vm.dispose() 会因为活着的 GC 对象硬崩进程
exec 是这份仓库里工程密度最高的地方——它要同时满足"能组合"、"不放大权限"、"不误杀正常脚本"、"不崩进程"四个约束,每一个都留下了具体的实现痕迹。
权限系统: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——要绕过必须显式、且在另一个层面绕 |
工具组别名与 findLast
permission/index.ts:591-614 用数组顺序表达优先级:EDIT_TOOLS(edit/write/apply_patch/multiedit)额外按 edit 这个组别名匹配,这样写 edit: "deny" 就能一次禁掉整个编辑家族。
findLastreturns the last-merged matching rule, so a tool-specific rule placed after a group rule wins naturally. This preserves the convenience ofedit: "deny"covering all edit-family tools while letting an explicitwrite: "allow"take precedence when present.
用数组顺序表达优先级,用 findLast 让后写的赢——不需要额外的优先级字段,且符合"配置文件里后写的覆盖先写的"这个直觉。
MiMo 走的是"通用规则 + 少数不可协商的例外"——规则可以宽,但有些线通配符跨不过去。
编排与生态
确定性工作流、一个窗口管所有任务、让 Agent 改自己。
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 | 阶段 | 特点 |
|---|---|---|
compose | Brainstorm → Design → Implement → Verify → Review → Report → Merge | 把独立任务自动并行到隔离的 git worktree,每个任务用 TDD,阶段间传结构化输出 |
deep-research | Brief → Plan → Research → Reflect → Write → Review | 并行子 agent 收集带引用的发现 → 反思缺口 → 写成连贯 Markdown → 冷审引用。靠文件 checkpoint 可续跑 |
fact-check | Plan → Search → Extract → Group → Crosscheck → Report | 对抗式事实核验:3 陪审员对抗投票逐条交叉核对 |
research-experiment | Baseline → Loop → Audit → Report | 面向可机械验证指标的自主优化循环,审计是否在刷指标 |
fact-check 的 3 陪审员对抗投票和 Open Design 的 Design Jury 是同一族设计,但方向相反——OD 的五陪审员协作评审同一产物,MiMo 的三陪审员对抗核验同一事实。②
research-experiment 明确要求审计指标造假("audits for metric gaming")——这是"别让 Agent 骗自己"主题在实验场景的延伸。
两个防御性细节
和 Open Design 的 registry 去重
throw 是同一个哲学:让错误尽早、尽响。Object.create(null)——「a lookup like get("constructor")/get("toString") returns undefined, not an inherited Object.prototype member.」当注册表的键可能来自模型输出时,这是必须的。
放一个 .js 到 .mimocode/workflows/ 或 .claude/workflows/ 就能定义自己的 workflow;用同名文件可以覆盖内置。
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["汇报给用户"]
后台会话的权限审批路由
问题很实在:后台跑的 child 没有面对用户的面板,碰到需要 ask 的权限门会被直接拒绝(interactive:false → DeniedError),用户看不到也无从批准。
| 场景 | 处置(decideAskRouting) |
|---|---|
| 系统 agent(checkpoint-writer / dream / distill) | 仍自动拒绝 |
Orchestrator peer child(background + mode:peer + 有父会话) | 转发审批 |
| 其他后台(compose 的 subagent 等) | 仍自动拒绝 |
cancel 一个 isolated child —— cancel 会删 worktree 和分支,对未合并的工作执行会永久丢失该工作。不要因为 child "完成了"就 cancel(完成产生的是它分支上待合并的提交)。」
不轮询:create 立即返回,child 完成时消息进 inbox 唤醒 Orchestrator。「派发后就返回…不要循环 list/查状态空耗轮次。」中断 Orchestrator 不会停掉 child。
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(且只在对应可执行文件已安装时才暴露)。
mimo 当引擎(它是 OD 26 条适配器之一);MiMo 又能把 Claude Code / Codex / Grok Build 当工具调。
Token Efficient 模式:把 bash 输出的噪音洗掉
实验功能,默认关闭,单 flag MIMOCODE_EXPERIMENTAL_TOKEN_EFFICIENCY。bash 的 stdout/stderr 经常被 ANSI 色码、\r 进度条多帧、误打印的密钥、minified JS 超长行撑爆上下文。
给模型看的和给人看的是两份。
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["回吐原文"]
总结:三个最独特的设计与三处取舍
三个最独特的设计
这是 MiMo Code 真正的主题,而且不是口号,是四个层次、六个互相独立的机制:
| 层次 | 装置 |
|---|---|
| 步级 | 空参数工具调用检测(软→硬 2 级)· 重复动作签名(键序归一化 + 排除叙述文本)· 文本复读 + n-gram |
| 回合级 | 步数上限:禁工具 + 强制交代(「overrides ALL other instructions」) |
| 会话级 | Goal 独立裁判(temperature=0 · 只读 transcript · 四道逃生口)· Try-Best 检测器 |
| 产物级 | checkpoint-validator + checkpoint-retry:写完的快照要验证、不合格要重写 |
四个层次各管各的,没有一个能替代另一个。这种纵深是本系列其他项目里没有的。而且每一层都留下了为什么是这个阈值的注释。
20 份 system prompt 按模型家族路由,GPT/Codex 家族还额外换一套只有 4 件的工具 ABI,隐藏 read/write/edit/grep/glob。
这是对"通用 harness"这个假设的正面否定。别家的思路是"写一套好提示词让所有模型都能用";MiMo 的思路是"每个模型的训练分布不同,harness 就该为它调形"。口号里的 "Models and Agents Co-Evolve" 说的就是这件事。
代价也很直白:20 份提示词要各自维护,路由是一串字符串 if,而且提示词路由和工具路由是两套独立规则(架构文档自己承认「尚未统一成模型能力协商层」)。
exec 微内核:在沙箱里组合工具而不放大权限一次工具调用里跑一段 QuickJS 脚本,批量调用聚合宿主工具,同时保证:late-bound registry(拿到和外层完全相同的已过滤工具表)· 控制流工具被排除(能改调度状态的不许藏在脚本里)· 活跃计算计时(等宿主工具的时间不计预算)· 两层边界说清楚(QuickJS 只隔离脚本,bash 仍是真实 Shell)。
"模型决定做什么,exec 负责如何组合,宿主决定是否允许"这句话,在代码里是能逐条对上的。
三处必须知道的取舍
prompt.ts 单文件 4 595 行system.ts(2 075 行,只是组装)。注释密度很高,但单文件承载的关注点太多。这是最明显的可维护性债务。
一个半月 12.5k star、838 个 open issue——相对仓库年龄偏高。
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/Apache | MIT + 独立的 USE_RESTRICTIONS |
它给出的答案不是一个机制,而是四个层次、六道装置:步级抓打转,回合级抓预算,会话级派独立裁判,产物级验快照。再加上"每个模型的 harness 应该为它单独调形"这个主张,构成了一份很不一样的编程 Agent 设计。
至于"共进化"这个口号有没有兑现——
evolve 能改自己、drive-mimo 能验证改动、distill 能从行为里长出新技能——基础设施是齐的,剩下的看社区。
继续读
与上游逐层差分
前面讲"MiMo 有什么"。这一部分讲其中哪些是它自己加的、哪些是继承的、为什么要这么改——基于真实的文件级对比。
逐层差分: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 62e4641 | 2 610 KB | 1.00x |
MiMo Code 076b790 | 4 572 KB | 1.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
server/ 和 acp/ 收缩说明它放弃了上游"服务器即本体"的部分野心,把重心挪回终端。20.2 主循环:session/prompt.ts 从 65 KB 涨到 208 KB
| 文件 | OpenCode | MiMo | 变化 |
|---|---|---|---|
session/prompt.ts | 64 925 B | 207 962 B | ↑ 3.2x |
session/llm.ts | 15 097 B | 37 119 B | ↑ 2.5x |
session/processor.ts | 26 497 B | 41 504 B | ↑ 1.6x |
session/overflow.ts | 1 313 B | 4 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) | |||
prompt.ts,同时往里塞了大量新逻辑(goalGate、四道闸门、结构化输出、预测下一条消息……)。上游在拆,MiMo 在合。合的好处是主循环控制流一眼看得完(不用在五个文件间跳);坏处就是那条最大技术债——4 595 行单文件。
从 fork 的角度看这个选择可以理解:你要在别人的骨架上塞进七八个新机制,最快的路径是集中改一个文件,而不是先重构上游的分层。代价是这笔债只会越滚越大。
20.3 提示词:8 → 20 份,但有 5 份一个字节没动
session/system.ts:27-42 的路由函数和 MiMo 结构一模一样,连 gpt-4/o1/o3 → BEAST 的顺序都相同。
| 提示词 | OpenCode | MiMo | 判定 |
|---|---|---|---|
gemini.txt | 15 372 | 15 372 | 字节级未改 |
kimi.txt | 8 695 | 8 695 | 字节级未改 |
codex.txt | 7 390 | 7 390 | 字节级未改 |
trinity.txt | 7 748 | 7 749 | 差 1 字节 |
copilot-gpt-5.txt | 14 241 | 14 239 | 差 2 字节 |
anthropic.txt | 8 212 | 14 281 | ↑ 1.7x 重写 |
default.txt | 8 528 | 20 800 | ↑ 2.4x 重写 |
gpt.txt | 9 284 | 25 447 | ↑ 2.7x 重写 |
deepseek · glm · minimax | — | 10 530 / 4 890 / 10 908 | 🆕 |
orchestrator · compose | — | 21 002 / 6 036 | 🆕 |
meta.txt(muse-spark)· plan*.txt | 9 151 / 10 087 | — | ❌ 删除 |
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 工具门控:同一行表达式,作用域扩了一个量级
registry.ts:292-296const usePatch =
modelID.includes("gpt-") &&
!modelID.includes("oss") &&
!modelID.includes("gpt-4")
if (tool.id === ApplyPatch) return usePatch
if (tool.id === Edit || Write) return !usePatch只管二选一交换。tool/gpt.ts:11-13export 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/codemode | MiMo workflow/sandbox.ts | |
|---|---|---|
| 工具名 | execute | exec |
| 入口 | 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 |
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.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 Barity.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 收缩的地方也有信息量
| 目录 | OpenCode | MiMo | 说明 |
|---|---|---|---|
server/ | 72 文件 | 43 | 上游"服务器即本体",server 层极重;MiMo 重心回到终端 |
acp/ | 12 文件 | 4 | ACP 在上游是一等公民;MiMo 保留但不再深耕 |
background/ | 1 | ❌ | 被 actor/ + cron/ + inbox/ 取代 |
image/ | 1 | ❌ | 换成 tool/view-image.ts |
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
long-horizon(官网博客标题就是 mimo-code-long-horizon)不是营销,是这 12 个子系统的共同注脚。20.12 这次 fork 给同类项目的三条经验
骨架可以借,产品判断不能借。
server/ 砍四成 + cli/ 涨 2.6 倍 = "不做平台,做终端产品";加 12 个长周期/并行子系统 = "从一次对话变成一个工期"。prompt.ts 涨到 4 595 行是有原因的——在别人的骨架上塞新机制,集中改一处比先重构上游分层快得多。但这笔债会滚,而且 fork 越久越难还(上游还在动)。MiMo 在注释里留了很多
TODO: lift to mimocode.json config,说明团队自己知道。13 项首创里有 6 项在防自欺、7 项在支撑长周期与并行——这两组加起来,就是那个新命题的全部答案。