树状任务

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

作者 David更新于 第 13 篇(共 33 篇)

在上游 OpenCode v1.14.19 里,task 工具用来派生子会话,另有 todowrite 每次整表覆盖一份待办清单(见子代理、待办与提问)。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:

# 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 精简。其余用法:

  • 跨会话:每个动作都可以带 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):

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):

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),免得把主智能体的任务揽到自己名下,再被下面的闸门困住。主智能体把任务派给子智能体时,流程是这样串起来的:

图表加载中…
  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):

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。

与检查点、进展文件联动

任务表是检查点体系里的一等数据,最直接的接缝是进展文件。绑定了任务的子智能体交付之后,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):

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)。写手本身见检查点写手。
  • 上下文重建: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)。

记忆目录的整体布局见分层记忆。提示词里有两处和代码对不上:写手的任务说明还列着 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——看 MiMo 怎样发现模型在原地打转,并让用户把活交给 Codex CLI 或 Claude Code CLI。

本页目录