# 树状任务

> task 工具在 MiMo 里改成了持久化工作项：T1、T1.1 式编号，SQLite 两张表，宽松的状态机，子智能体完成闸门，以及与检查点、进展文件的联动。

- 作者：David（道雾轩）
- 专栏：MiMo Code 源码解读（https://daiw.org/manual/mimo-code.md）
- 最后更新：2026-09-17
- 原文：https://daiw.org/manual/mimo-code/tasks
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

在上游 OpenCode v1.14.19 里，`task` 工具用来派生子会话，另有 `todowrite` 每次整表覆盖一份待办清单（见[子代理、待办与提问](https://daiw.org/manual/opencode/task-and-todo)）。MiMo 把这两件事都改了：派子智能体交给新的 `actor` 工具（`packages/opencode/src/tool/registry.ts:263`），`task` 这个工具 ID 留给了一套工作项系统（`packages/opencode/src/tool/registry.ts:276`），`todowrite` 从工具表里消失。上游附在 `task` 描述末尾的子代理清单，现在改附在 `actor` 后面（`packages/opencode/src/tool/registry.ts:489`），生成它的函数仍叫 `describeTask`。

工作项和整表覆盖的待办清单不同：每个任务有稳定编号、有归属、有事件日志，上下文重建之后还能按编号找回来。工具说明的第一句（`packages/opencode/src/tool/task.txt:1`）：

> Persistent work-item tool. Tasks are bounded, referenceable entities with IDs (T1, T2, ...; subtasks T1.1, T1.2, ...).

## 怎么用

任务主要由模型操作，用户侧没有斜杠命令。默认是 JSON 调用：参数外面必须包一层 `operation`，`action` 取 `create`、`list`、`get`、`start`、`block`、`unblock`、`done`、`abandon`、`rename` 之一，例如 `{"operation":{"action":"create","summary":"Lexer","parent_id":"T1"}}`（`task.txt:28`）。源码注释提到，这层 schema 必须标上 `type: "object"`，否则有的模型会把整个 `operation` 序列化成字符串（`packages/opencode/src/tool/task.ts:112`）。

把配置 `tool.invocation_style` 设为 `shell`，或只对这个工具设 `tool.invocation_style_by_tool.task`，模型就改写脚本，一行一条命令，说明见 `packages/opencode/src/tool/task.shell.txt:29`：

```text
# state-machine transitions:
    task start T1 [--reason "<note>"]  # mark in_progress (working on it now)
    task block T1 "waiting on spec"
    task unblock T1 "spec resolved"
    task done T1 "all tests pass"
    task abandon T1 "out of scope"
```

两种调用形式的实现见[命令行式工具调用与 Token 精简](https://daiw.org/manual/mimo-code/token-efficient-shell)。其余用法：

- **跨会话**：每个动作都可以带 `session_id`（命令行里是 `--session`）去操作别的会话。
- **绑定子智能体**：`actor spawn general "Implement auth" "build the login flow" --task T1`，JSON 形式是 `task_id` 字段（`packages/opencode/src/tool/actor.shell.txt:67`）。
- **配置**：`checkpoint.task_archive_days`（旧名 `task_cleanup_days`）决定已完成或已放弃的任务多少天后从默认列表里隐藏，默认 7 天（`packages/opencode/src/task/registry.ts:100`）。
- **查看**：TUI 侧栏的 Tasks 面板；服务端 `GET /session/:sessionID/task` 返回会话的全部任务，含已结束的（`packages/opencode/src/server/routes/instance/session.ts:259`）。

## 两张表

`packages/opencode/src/task/task.sql.ts:5` 定义 `task` 表：主键是 `(session_id, id)`，编号只在会话内唯一；另有 `parent_task_id`、`status`、`summary`、`owner` 和几个时间戳，会话删除时级联删除。`task_event` 表（`task.sql.ts:30`）是追加式日志，每次迁移记一行 `kind` 和可选说明。写库之后，注册表在总线上发布 `task.created` 或带迁移种类的 `task.updated`（`packages/opencode/src/task/events.ts:6`）。

迁移目录 `packages/opencode/migration/` 记录了这套表的演化：`20260515020000_user_task` 建表时还有 `kind` 和 `focus` 两列，外加“每个会话至多一个焦点任务”的唯一索引；`20260529000000_task_todo_redesign` 删掉这两列，把旧状态 `proposed`、`active` 并入 `open`，顺带删掉 todo 表的 `priority`；`20260603000000_task_in_progress_owner` 加上 `owner`。命令行说明末尾还列着几个已删除的动词（`task.shell.txt:48`），其中 `approve` 的说明正是“proposed state was removed”。

## 编号怎么来

`create` 先查出同一会话、同一父任务下的兄弟任务，再交给 `nextChildId`（`packages/opencode/src/task/registry.ts:48`）：

```ts
function nextChildId(parentId: string | undefined, siblings: string[]): string {
  const prefix = parentId ? `${parentId}.` : "T"
  const used = siblings
    .filter((s) => (parentId ? s.startsWith(prefix) : /^T\d+$/.test(s)))
    .map((s) => {
      const tail = s.slice(prefix.length)
      return /^\d+$/.test(tail) ? Number(tail) : 0
    })
  const next = used.length > 0 ? Math.max(...used) + 1 : 1
  return `${prefix}${next}`
}
```

顶层编号是 `T` 加最大序号再加一，子任务在父编号后面加点再编号，所以 T1 下面是 T1.1、T1.2，还能继续嵌套出 T1.1.1。任务行从不删除，最大序号只增不减，编号不会复用。`create` 不检查父任务是否存在，传一个不存在的父编号也会建出孤儿任务。格式校验 `^T\d+(\.\d+)*$` 定义在 `packages/opencode/src/task/schema.ts:4`，`actor` 工具用它校验 `task_id`，`task` 工具自己的参数只要求非空字符串。

## 状态机：写在说明里，代码只守一条

给模型的说明是这样描述生命周期的（`task.txt:21`）：

```text
Status lifecycle: open ⇄ in_progress, either → blocked → open, either → done | abandoned.
```

注册表的实现要宽松得多：

- `block`、`unblock`、`done`、`abandon`、`rename` 直接改写，不看当前状态（`packages/opencode/src/task/registry.ts:224` 起）。`unblock` 一律改回 `open`，对进行中的任务调用也一样。
- 唯一的硬约束在 `start`（`packages/opencode/src/task/registry.ts:281`）：对 `done` 或 `abandoned` 的任务只记一条警告、原样返回，不会让它“复活”；同一 owner 重复 start 是空操作，换了 owner 才算交接，会更新 owner 并再记一条 `started`。
- `done` 和 `abandon` 写入 `ended_at`，并把 `cleanup_after` 设为当前时间加归档天数。`list` 默认只返回 open、in_progress、blocked，且过滤掉 `cleanup_after` 已过的行（`packages/opencode/src/task/registry.ts:180`）。
- 配置注释写明“Rows are NOT deleted”（`packages/opencode/src/config/config.ts:342`），给模型的说明却是“Terminal states clean up automatically after 7 days.”（`task.txt:23`），实际只是隐藏。
- `schema.ts` 定义了 `unstarted` 事件种类，但没有任何代码产生它。

## owner 与子智能体的完成闸门

`owner` 让任务有了归属。模型建任务时，owner 取工具上下文里的 `actorID`，没有就退回 agent 名（`task.ts:343`）。子智能体对别人名下的任务调用 `start` 不会改写 owner（`task.ts:393`），免得把主智能体的任务揽到自己名下，再被下面的闸门困住。主智能体把任务派给子智能体时，流程是这样串起来的：

```mermaid
sequenceDiagram
  participant M as 主智能体
  participant A as actor 运行时
  participant S as 子智能体
  participant W as 检查点写手
  M->>M: task create 建出 T1
  M->>A: actor spawn，带 task_id T1
  A->>A: 校验编号，start T1，owner 记为新 actor
  A->>S: 运行子智能体
  S-->>A: 结束
  A->>S: 完成闸门，名下仍有未完成任务就再跑，最多 2 轮
  A-->>M: 交付结果，按任务表修正返回状态
  A->>S: postStop 检查 tasks/T1/progress.md，不合格就要求补写
  W->>W: 下次写检查点时并入新增或变更的进展文件
```

1. **校验绑定**：`actor` 工具执行时检查 `task_id`，格式不对或本会话里没有这个任务，就丢掉绑定按临时任务运行，并在工具输出里说明原因（`packages/opencode/src/tool/actor.ts:801`）。
2. **自动开始**：`Actor.spawn` 替子智能体 `start` 绑定的任务，owner 记为新 actor（`packages/opencode/src/actor/spawn.ts:413`）。注释的说法是模型维护任务状态并不可靠，所以“开始”作为派生的副作用由运行时完成，`done` 仍交给模型。
3. **完成闸门**：子智能体结束后，`TaskGate.decide` 列出它名下状态为 open 或 in_progress 的任务，blocked 的不算（催了也没法推进），有就注入提醒再跑一轮，最多 `MAX_TASK_GATE_SUBAGENT_REACT` 即 2 轮（`packages/opencode/src/task/gate.ts:10`）。
4. **以表为准**：交付前再查一次任务表，仍有 open 或 in_progress 的，返回状态降为 `partial`；只剩 blocked 的降为 `blocked`，并在结果末尾追加未完成任务的编号，子智能体自报的 `**Status**` 被覆盖（`spawn.ts:599`）。

闸门的提醒（`gate.ts:24`）：

```ts
const buildReentryText = (incomplete: { id: string; status: string; summary: string }[]): string =>
  [
    "<system-reminder>",
    "You are about to finish, but these tasks you own are still unfinished:",
    ...incomplete.map((t) => `- ${t.id} (${t.status}): ${t.summary}`),
    "For EACH: complete the work then `task done <id> <summary>`, or `task abandon <id> <reason>` if it is genuinely not needed.",
    "Then re-emit your final message starting with the **Status**/**Summary** header.",
    "</system-reminder>",
  ].join("\n")
```

闸门只对“受生命周期管理”的子智能体生效（`spawn.ts:854`）：模式为 subagent，并且声明了 `completionGate`，或者没有自带提示词且不是检查点写手。内置的 `general` 声明了 `completionGate: true`（`packages/opencode/src/agent/agent.ts:263`），`explore` 自带提示词，不受约束。主智能体没有这道闸门，与它对应的是 [Goal](https://daiw.org/manual/mimo-code/goal)。

## 与检查点、进展文件联动

任务表是检查点体系里的一等数据，最直接的接缝是进展文件。绑定了任务的子智能体交付之后，`SubagentProgressCheckerPlugin` 在 `actor.postStop` 钩子里检查 `tasks/<TID>/progress.md`（`packages/opencode/src/plugin/subagent-progress-checker.ts:102`；路径由 `packages/opencode/src/session/checkpoint-paths.ts:84` 拼出，在会话的记忆目录下）。文件缺失或缺少固定小节，就要求子智能体写好或补全，postStop 最多重入 `MAX_POST_REACT` 即 3 次（`spawn.ts:41`）；通过后由运行时写入 `written-at` frontmatter。写工具被权限禁用的只读子智能体（如 `explore`）和设置了 `memory.disable_write` 时跳过检查。必备小节是（`subagent-progress-checker.ts:11`）：

```ts
const REQUIRED_SECTIONS = [
  "## §1 Task identity",
  "## §2 Subagent intent",
  "## §3 Files and code sections",
  "## §4 Verbatim commands",
  "## §5 Outcome and discoveries",
] as const
```

另外三处接缝：

- **检查点写手**：下次写检查点时，`buildProgressDiffItems`（`packages/opencode/src/session/checkpoint-progress-reconcile.ts:45`）比较每个进展文件的 `written-at` 与 checkpoint.md 里记下的 `last-reconciled-written-at`，把新增或变更的列成“SUBAGENT PROGRESS to integrate”块放进写手提示词。写手提示词规定 checkpoint.md 第 4 节 Task tree 以任务表为准（`packages/opencode/src/agent/prompt/checkpoint-writer.txt:16`）。写手本身见[检查点写手](https://daiw.org/manual/mimo-code/checkpoint-writer)。
- **上下文重建**：`renderRebuildContext` 注入的第一节就是 Tasks ledger（`packages/opencode/src/session/checkpoint.ts:1424`）：顶层任务各占一行，带状态和摘要；直接子任务只列状态图标和编号，孙任务不出现。整节受 `checkpoint.push_caps.tasks_ledger` 限制，配置注释说它是 token 上限、默认 2000（`packages/opencode/src/config/config.ts:299`），但 `truncate` 比较的是字符串长度（`checkpoint.ts:50`）。
- **回忆提醒**：只要会话有记忆文件或任何任务（`checkpoint.ts:1202`），主循环每一步都会在发给模型的最后一条用户消息上临时附一段提醒，列出 `memory` 搜索、`task list` 等查询方式（`packages/opencode/src/session/prompt.ts:153`）。

记忆目录的整体布局见[分层记忆](https://daiw.org/manual/mimo-code/memory)。提示词里有两处和代码对不上：写手的任务说明还列着 `progress`、`approve`、`batch_create` 这些已删除的动词（`checkpoint.ts:400`）；系统提示词里的记忆说明把 per-task progress.md 描述为“writer-derived splitover … (not LLM-written)”（`packages/opencode/src/session/llm.ts:144`），而实际是子智能体按上面的插件要求亲手写。

## 界面

TUI 进入会话时用 `sdk.client.session.task` 拉全量任务（`packages/opencode/src/cli/cmd/tui/context/sync.tsx:1007`），之后按 `task.created`、`task.updated` 增量更新（`sync.tsx:440`）。侧栏插件以 `order: 400` 渲染 Tasks 面板（`packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/task.tsx:7`）：活动任务按 in_progress、open、blocked 排序，同状态按编号；已完成的只显示最近 3 条，其余折叠成一行可点开的“N more done”；已放弃的不显示；缩进层数等于编号里点的个数。每行由 `task-item.tsx` 绘制，方括号里是状态标记，进行中的任务显示转圈动画，关闭动效时换成静态圆点（`packages/opencode/src/cli/cmd/tui/component/task-item.tsx:22`）。

## 上游的 todo 去哪了

`packages/opencode/src/session/todo.ts` 还在，和上游相比只删掉了 `priority` 字段。但 `todowrite` 已经不在工具表里，`packages/opencode/src` 中也找不到调用 `Todo.update` 的地方，代码里还会写 todo 表的只剩旧版 JSON 存储迁移（`packages/opencode/src/storage/json-migration.ts`）。留下来的是一层兼容：

- `GET /session/:sessionID/todo` 先查任务表，有任务就映射成 todo 的形状返回（`packages/opencode/src/server/routes/instance/session.ts:48`）：in_progress 不变，done 变 completed，abandoned 变 cancelled，open 和 blocked 都变 pending；没有任务才读旧的 todo 表（`packages/opencode/src/server/routes/instance/session.ts:251`）。
- TUI 的 Todo 侧栏只在会话没有任何任务、又有未完成的旧待办时显示（`packages/opencode/src/cli/cmd/tui/feature-plugins/sidebar/todo.tsx:11`）。

下一篇：[Try-Best 检测与 Hand-off](https://daiw.org/manual/mimo-code/try-best-handoff)——看 MiMo 怎样发现模型在原地打转，并让用户把活交给 Codex CLI 或 Claude Code CLI。
