Dream 与 Distill
/dream 与 /distill 从哪里触发、自动运行的真实条件、两个隐藏智能体的权限与写入沙箱、提示词要点,以及整理出的记忆和打包出的资产落在哪里。
检查点写手只盯着当前会话。跨会话的整理交给两个隐藏智能体:dream 回看最近的会话,把经得起核对的知识并进项目 MEMORY.md;distill 回看更长的时间窗,找出反复出现的手工流程,打包成 skill、子智能体或命令。官方博客把它们称为进化层。拆开看,每一个都只是三样东西的组合:一条斜杠命令、一个带写入沙箱的隐藏 agent、一份很长的提示词。上游 v1.14.19 一样都没有,它的内置命令只有 init 和 review 两条。
怎么用
- 手动:
/dream [关注点]、/distill [关注点]。两条命令登记在packages/opencode/src/command/index.ts:130和index.ts:150,参数填进模板里的$ARGUMENTS,作为用户给出的关注点或约束。 - 两条命令都声明了
subtask: false。按SessionPrompt.command的规则(packages/opencode/src/session/prompt.ts:5185),subagent 模式的 agent 遇到显式的false就不包成子任务,模板直接作为当前会话的一条用户消息,由dream或distill自己处理,整理过程和最终汇总都出现在当前对话里。 - 模型:手动命令优先用 agent 配置里的
provider/model形式的模型,否则沿用当前会话的模型(prompt.ts:5163);自动运行则固定沿用触发那一轮用户消息的模型(prompt.ts:3779),显式传入的模型优先于 agent 配置(prompt.ts:2449)。 - 自动运行:在配置里写
"dream": { "auto": true }、"distill": { "auto": true },间隔用interval_days调,默认 7 天和 30 天(packages/opencode/src/session/auto-dream.ts:11)。 memory.disable_write为真时两种自动运行都跳过(auto-dream.ts:113)。手动命令不受这个开关拦截,但 Dream 写MEMORY.md会被写入闸门拒绝;Distill 写.mimocode/不在记忆树里,不受影响,闸门细节见分层记忆。- 自动运行建出的会话标题固定为
Auto Dream、Auto Distill(auto-dream.ts:15),会话列表里带一个“自动”徽标(packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx:201),mimo run --continue和 TUI 的-c找“上一个会话”时都会跳过它们(packages/opencode/src/cli/cmd/run.ts:389、packages/opencode/src/cli/cmd/tui/app.tsx:441)。
官方博客写 Dream 每 7 天、Distill 每 30 天自动触发,内置文档技能 mimocode-docs 的配置表也把 dream.auto、distill.auto 的默认值写成 true(packages/opencode/src/skill/builtin/.bundle/mimocode-docs/reference/config.md:136)。代码里 7 和 30 只是间隔的默认值,开关必须是字面量 true 才算打开(auto-dream.ts:114、auto-dream.ts:127),配置 schema 也注明默认 false(packages/opencode/src/config/config.ts:375)。不改配置,两者都不会自动运行。
自动触发
检查发生在主循环里:顶层会话(没有父会话)每次启动 runLoop 后的第一步,依次调用 shouldAutoDream 与 shouldAutoDistill(prompt.ts:3775)。配置说明称在新会话开始时触发,实际上每轮第一步都会检查,只是被间隔挡住了。Dream 这一侧的判断(auto-dream.ts:110):
export function shouldAutoDream(cfg: Config.Info) {
// Memory writing off → the consolidation pass that rewrites project memory
// must not run either.
if (!isMemoryWriteEnabled(cfg)) return Effect.succeed(false)
const enabled = cfg.dream?.auto === true
if (!enabled) return Effect.succeed(false)
const now = Date.now()
if (now - lastDreamSpawnTime < MIN_SPAWN_GAP_MS) return Effect.succeed(false)
lastDreamSpawnTime = now
const intervalDays = cfg.dream?.interval_days ?? DEFAULT_DREAM_INTERVAL_DAYS
return shouldAutoRun({ enabled, intervalDays, title: AUTO_DREAM_TITLE, label: "dream" })
}shouldAutoRun(auto-dream.ts:45)里有几处值得注意:
- 会话标题就是运行记录:上次运行时间取标题等于
Auto Dream的最新会话的创建时间(auto-dream.ts:56),没有单独的锁或状态表。会话在 agent 开始干活之前就建好了,所以失败或被中断的一次同样算数。 - 第一次运行要等库够老:从没运行过时,数据库里最早的顶层会话必须创建于一个间隔之前,否则记一条
project too young日志后跳过(auto-dream.ts:73)。 - 范围是整台机器:两条查询都没有按
project_id过滤,而所有项目共用一个mimocode.db。注释和日志里说的都是项目,实际效果却是:间隔到期后,哪个项目先有会话跑到这一步,就给哪个项目整理一次,其他项目要等下一个间隔。 - 进程内 10 秒闸门:
MIN_SPAWN_GAP_MS(auto-dream.ts:13)挡住同时启动的多个会话重复触发。时间戳在开关检查通过后就更新,新建的 Auto Dream 会话自己跑到第一步时,也会被这道闸门或间隔检查挡住,不会递归。
两边都通过后,主循环动态导入 AppRuntime(避开模块初始化的循环依赖),建会话、以 dream agent 发出 DREAM_TASK,不等待结果,出错只记日志(prompt.ts:3784)。Dream 与 Distill 各判各的,同一轮里可能一起触发。
两个隐藏智能体
dream 与 distill 定义在 packages/opencode/src/agent/agent.ts:376 与 agent.ts:415,都是 mode: "subagent"、hidden: true,按 Tab 切换不到。两者的权限和工具表完全相同,区别只在提示词。以 dream 为例(agent.ts:383):
permission: Permission.merge(
defaults,
Permission.fromConfig({
"*": "deny",
read: "allow",
write: "allow",
edit: "allow",
glob: "allow",
grep: "allow",
memory: "allow",
bash: "allow",
exec: "allow",
external_directory: {
[path.join(Global.Path.data, "memory")]: "allow",
[path.join(Global.Path.data, "memory", "*")]: "allow",
},
}),
user,
),紧接着的 toolAllowlist(agent.ts:402)把模型能看到的工具收窄到 read、write、edit、apply_patch、view_image、glob、grep、memory、bash、exec 十个,没有 actor、task、skill,也没有联网和 history;其中 apply_patch、view_image 分别归入 edit、read 两组权限(packages/opencode/src/permission/index.ts:700)。和其他内置 agent 一样,配置文件 agent 段里同名的 dream、distill 条目可以改模型、提示词、工具白名单,也可以直接禁用(agent.ts:456)。
更硬的约束来自“系统派生 agent”这个身份。SYSTEM_SPAWNED_AGENT_TYPES 只有 checkpoint-writer、dream、distill 三个成员(packages/opencode/src/agent/config.ts:7),带来三点不同:
- 权限询问一律非交互:
decideAskRouting对它们直接返回interactive: false(packages/opencode/src/agent/config.ts:72),需要询问的操作不弹框,当场以DeniedError失败(packages/opencode/src/permission/index.ts:370)。即便是你在自己会话里手动敲的/dream,它也没法停下来问你。 - 写入沙箱:
write、edit、apply_patch、notebook_edit只能写记忆树或<worktree>/.mimocode/(packages/opencode/src/tool/memory-path-guard.ts:9)。bash不受这道闸门约束,提示词反复要求只做只读查询,靠的是模型自律。 - 无效输出的纠正提示:回复只有思考或为空时,追加的纠正提示按 actor 口径写,要求向父 agent 交付结果,而不是面向用户的主 agent 口径(
packages/opencode/src/agent/config.ts:30、prompt.ts:3384)。
Dream 的提示词
packages/opencode/src/agent/prompt/dream.txt 共 155 行,默认回看最近 7 天(dream.txt:8),注意这个时间窗与自动触发的间隔是两回事。核心立场是原始轨迹优先(dream.txt:20):
Raw trajectory is authoritative; memory files are a structured index/cache.
流程分六个阶段:
- 定位数据:先用
memory工具搜几个宽泛的词,再从记忆根目录推出数据库<DATA>/mimocode.db,留意MIMOCODE_DB覆盖;记忆为空且没有本项目会话就直接报告无事可做。 - 定向:读项目
MEMORY.md和当前会话的notes.md,Glob 出最近的checkpoint.md,用 SQLite 列出本项目最近的会话,记下MEMORY.md现有的小节结构以免重复。 - 从记忆文件收集候选:检查点里的已发现知识、错误与修复、设计决策,
tasks/*/progress.md,以及还没并入项目记忆的笔记。 - 对照原始轨迹核实:提示词给出了表结构说明和查询模板,让模型在用户发言里搜 always、never、remember、decided、every time 这类词,用户用别的语言时也搜对应的说法;只有明确的用户陈述、清楚的设计决策或跨会话重复出现的证据才能提升(
dream.txt:101)。 - 整合:写入
MEMORY.md的## Rules、## Architecture decisions、## Discovered durable knowledge、## Patterns、## Gotchas(dream.txt:119),后两节是模板之外新加的。合并重复条目,相对日期换成绝对日期,每条一到三行,末尾保留来源会话 ID,如[ses_xxx]。 - 修剪与核验(
dream.txt:137):
- Keep `MEMORY.md` under 200 lines and 10KB when possible. Prefer fewer, denser entries over exhaustive notes.
- Remove entries superseded by newer decisions.
- Remove details that mattered only to one session.
- Remove low-signal memory files or entries that are redundant with stronger project memory.
- Clear current `notes.md` entries only when fully integrated.
- Verify mentioned file paths with Glob.
- Verify mentioned function/class names with Grep.
- Mark unverifiable-but-plausible claims `[unverified]`.分工写得很清楚:发现值得打包的流程,最多留一行指向 /distill 的提示,不要自己去建 skill 或命令(dream.txt:26)。跨项目的偏好可以提升到 global/MEMORY.md(dream.txt:27)。最后按固定格式汇报新增、更新、删除、跳过的条目,以及项目记忆的行数与大小。
Distill 的提示词
packages/opencode/src/agent/prompt/distill.txt 默认回看最近 30 天(distill.txt:7),证据优先级依次是原始轨迹、记忆文件、已有资产。它最强调的是克制(distill.txt:39):
If nothing has actually been repeated, create nothing. Doing zero packaging is a valid and expected outcome; just say so in the summary rather than manufacturing an asset to justify the run.
流程是先盘点已有资产,再找候选、核实、列清单、选形式、落盘。盘点用的 Glob 模式和 MiMo 的加载器一致:skill 是 {skill,skills}/**/SKILL.md(packages/opencode/src/skill/index.ts:25),命令是 {command,commands}/**/*.md(packages/opencode/src/config/command.ts:29),agent 是 {agent,agents}/**/*.md 与 {mode,modes}/*.md(packages/opencode/src/config/agent.ts:129),外加 .claude、.agents、.codex、.opencode 这几个外部目录里的 skill。候选至少出现两次,或者明显还会重复且每次代价高,才算数(distill.txt:126)。清单里每个候选要附证据和日期(引用会话 ID)、频率与置信度、推荐形式和理由。形式按从小到大挑(distill.txt:152):
| 形式 | 落点 | 要点 |
|---|---|---|
| skill | .mimocode/skills/<name>/SKILL.md | frontmatter 写 name、description,描述要可被检索到 |
| 自定义子智能体 | .mimocode/agent/<name>.md | frontmatter 写 description,可选 mode、model、工具与权限,正文是系统提示词 |
| 命令 | .mimocode/command/<name>.md | frontmatter 写 description,可选 agent,正文用 $ARGUMENTS、$1 占位 |
| 自动化 | .mimocode/plugins/ 下的插件钩子 | 只有理由确凿时才写,否则打包成可重复执行的命令 |
| 扩展已有 | 原文件 | 改已有的 skill、agent 或命令,不新建近似副本 |
| 跳过 | 无 | 太一次性、太模糊、太敏感或证据不足 |
落盘后要用 Glob、Grep 核对引用的路径和符号,不做建账号、发消息、改权限这类不可逆的外部操作。
提示词与代码对不上的地方
两份提示词开头都写着(dream.txt:10、distill.txt:10):
This command is manual. The user intentionally started it and is watching.
但自动运行用的是同一个 agent、同一份系统提示词,只是用户消息换成了写着 automatic 的 DREAM_TASK 或 DISTILL_TASK(auto-dream.ts:27),这时并没有人在看。
两份提示词的表结构说明都写着(dream.txt:76、distill.txt:102):
Empty
agent_idmeans main agent; non-emptyagent_idmeans subagent.
而 message.agent_id 早已是非空列、默认值 main(packages/opencode/src/session/session.sql.ts:91),迁移时还把旧数据里的 NULL 统一回填成 main(packages/opencode/migration/20260526000000_agent_id_main/migration.sql:1)。照字面去查 agent_id 为空的消息,一条主 agent 消息也查不到。内置技能 memory-search 的说明倒是对的(packages/opencode/src/skill/builtin/.bundle/memory-search/SKILL.md:49)。
Distill 的自动化一节说 mimocode 没有内置调度器(distill.txt:163),可定时任务与循环调度默认开着,关掉要显式设 MIMOCODE_EXPERIMENTAL_CRON=false(packages/opencode/src/flag/flag.ts:366),见定时任务与循环。它还说用户要求全局范围时可以不写项目 .mimocode/(distill.txt:177),但写入沙箱只放行记忆树和工作区里的 .mimocode/,用 write、edit 往全局配置目录写会被拒绝。
产物落在哪里
| 来源 | 产物 | 位置 | 之后谁会读到 |
|---|---|---|---|
| Dream | 项目记忆条目 | <data>/memory/projects/<pid>/MEMORY.md | 重建注入、检查点写手、memory 检索 |
| Dream | 跨项目偏好 | <data>/memory/global/MEMORY.md | 重建注入 |
| Dream | 清空已并入的笔记 | 当前会话的 notes.md | 无 |
| Distill | skill、子智能体、命令、插件钩子 | 工作区根目录的 .mimocode/ 下 | 对应的加载器 |
两者的产物都是普通文件:Dream 改的记忆下次重建时被注入,Distill 建的资产和手写的没有区别,可以审阅、修改、提交进仓库。自动运行不会征求确认,打开 auto 之前最好先手动跑一两次,看看它整理出来的东西合不合心意。
evolve:在工作中随手进化
内置技能包里有个 evolve 技能(packages/opencode/src/skill/builtin/.bundle/evolve/SKILL.md:3),思路相近而形态不同。它不是定期批量回看,而是一条常驻指令:主 agent 在工作中看到信号就立刻改造自己,比如同一串 bash 或 API 调用跑了 3 次以上就包成工具(SKILL.md:24),同样的纠正反复出现就加钩子,值得长期保留的项目知识写成 skill,产物同样落在 .mimocode/ 下。Distill 事后从轨迹数据库里找证据,受写入沙箱约束;evolve 靠模型当下的判断,用的是主 agent 的普通权限。技能包整体见内置技能包与技能检索。
下一篇:Goal:让独立裁判决定何时停——进入第 3 部分,看 /goal 怎样让另一次模型调用判定任务是否真的完成。