dsh-task-tree
给 DeepSeek Harness 的三级任务树:把一件需要多步做完的事组织成
目标 → 任务 → 子任务,每个节点完成时由模型提交结果,插件随即把这个节点的
执行过程从上下文里折叠掉,只留下那条结果。
装上不等于启用。插件默认关闭,不会在没人点头的时候改动任何会话。
它解决什么
长任务跑到后半程,上下文里绝大多数内容是过程:读过哪些文件、试过哪些错、
执行过哪些命令。这些内容对后面没有用,但会一直占着位置——每多一轮,
模型要被喂的就越长,缓存也越容易被顶掉。
真正需要留给后面的只有一句话:这一步做完了,它产出了什么。
所以这里的分工是:
| 内容 | 去向 |
|---|
| 过程 | 读文件、试错、命令输出 | 节点完成时整段折叠掉 |
| 结果 | 模型在 tree_task_done 里提交的 result | 留下来,供后续节点引用 |
上下文怎么长
[节点 0] 系统提示词(身份 / 工具 schema / 静态说明) ← 会话内逐字节不变
[用户消息]
[目标级汇总] ← 换目标时逐条累积
[任务级汇总] ← 换任务时逐条累积
[子任务汇总] [子任务汇总] … ← 同级逐条累积,互不卷走
[当前子任务的执行过程] ← 下一次折叠的对象
每一层有一个游标,记着"这一层总结到哪儿了"。节点完成时,插件把
游标之后到末尾这一整段替换成一条汇总消息:
子任务完成 → 替换 [上个游标之后, 末尾] → 本子任务的汇总
任务完成 → 替换 [进任务时的边界之后, 末尾] → 本任务的汇总
目标完成 → 替换 [进目标时的边界之后, 末尾] → 目标的汇总
游标自己留在替换范围之外,所以同级上一条汇总消息不会被这一次折叠卷走。
这就是"同级完成不互相屏蔽"的来由。
游标失效时(被压缩遮掉、或它之后本来就什么都没有)折叠降级为纯追加:
过程收不起来,但提交的结果照样进上下文。收不起来可以,丢掉结果不行。
折叠发生在 agent/pre-step——上一轮已结束、下一个请求还没构造的那一刻。
工具执行中途不能折叠,那时 surface 还在增长,替换会把自己卷进去。
检查点
子任务全部结束,不会让父节点自动完成。 那一刻树进入检查点:
检查点:任务「重构解析器」(t-3) 的子节点已全部结束。
此刻只放行两个工具:tree_task_plan 与 tree_task_done。
检查点只放行 tree_task_plan(继续拆子节点)和 tree_task_done(提交结果并完成),
其余工具一律被拒。"这一层到底做完了没有"要由模型判断,不能被计数规则替它定死。
检查点只拦模型(tools/pre-execute),拦不住人:/task-tree 命令随时能接管。
六个工具
| 工具 | 用途 |
|---|
tree_task_status | 查看整棵树、各节点状态与已提交的结果,以及现在该推进哪个节点 |
tree_task_create | 建立计划:一个目标 + 若干任务 |
tree_task_plan | 给一个节点追加子节点:给目标追加任务,给任务追加子任务 |
tree_task_done | 完成一个节点并提交它的结果,result 必填 |
tree_task_update | 修改一个节点的标题或说明 |
tree_task_drop | 丢弃一个节点;丢弃任务会连同它的子任务一起丢弃 |
层级固定三级,子任务是最低一级,不能再往下拆。
安装
需要 Node.js 22.19 以上,以及能运行 dsh。
从源码目录安装(装成 link:,改完源码重启即生效):
dsh plugin --profile web add C:\apps\dsh_workspace\dsh-task-tree
dsh --profile web --dump-config # 输出里应出现 task-tree 这一行
从 npm 安装:
dsh plugin --profile web add dsh-task-tree
装完必须重启 dsh web——运行中的会话不追溯。
启用
装上 ≠ 启用,插件默认是关的(enabled: false)。
在 $DSH_HOME/profiles/web/cordis.patch.yml 里加:
- id: task-tree
config:
enabled: true
然后重启 dsh web。仓库里的 enable.example.yml 可以直接抄。
⚠️ profile 这一层按行覆盖时 config 是整体替换,不是逐字段深合并——
覆盖时要把想保留的字段一起写全,没写的会走插件默认值(见下面的配置表)。
没启用时插件一个字节都不碰会话:不注册工具、不加提示词段落、不折叠、不续行。
只留一条 /task-tree 命令让你确认"装了但没开"以及怎么开。
界面
输入框上方的任务树条目(和内置的待办、目标条目并排,宽度与消息正文对齐):
- 折叠时一行:标题 +
已完成/总数 · 当前节点
- 展开后是三级树,当前节点高亮,已完成节点直接显示已提交的结果
- 节点上可以直接完成、丢弃,不用敲命令
- 顶部状态条:「停止自动续行」「清空计划」「刷新」
设置 → 任务树:显示插件当前是启用还是未启用,并逐条列明启用后会影响什么,
以及怎么开、怎么关。
命令
/task-tree 不经过模型,直接执行:
| 命令 | 作用 |
|---|
/task-tree | 查看本会话的任务树与检查点状态 |
/task-tree done <id> <结果> | 提交某个节点的结果并完成它 |
/task-tree drop <id> | 丢弃某个节点 |
/task-tree stop | 停止本会话的自动续行 |
/task-tree resume | 重新允许自动续行(需 config 里 autoContinue 为 true) |
/task-tree reset | 清掉本会话的计划树 |
配置
默认值全在 lib/index.js 的 DEFAULT_CONFIG 里:
| 字段 | 默认 | 说明 |
|---|
enabled | false | 总开关 |
autoContinue | false | 模型停下时是否自动续行 |
maxAutoRounds | 5 | 连续自动续行的轮次上限 |
promptOrder | 2000 | 静态说明段落的排序 |
rootDir | null | 状态目录,默认 $DSH_HOME/dsh-task-tree |
自动续行的四道闸门
autoContinue 默认关闭,开着也只是兜底:tree_task_done 的返回文本里已经带了
"请继续执行下一个子任务",模型通常会在同一轮里接着做。这里兜的是模型仍然停下来的情况。
开启后仍有四道闸门,缺一不可:
- 默认关——
autoContinue: false 时不注册续行逻辑。
- 中断即停——用户按下停止(
agent/turn-stopping 且 signal.aborted),
这个会话就被标记为不再自动续行,直到真人再发一条消息。
- 内置 goal 活跃时不插手——
goals 服务里有活跃 goal 就完全让路。
- 轮次上限——连续续行达到
maxAutoRounds 就停;只有真人发的消息才重置配额。
队列里还有排队输入时也不抢,让用户先说话。
状态文件
全部在 $DSH_HOME/dsh-task-tree/ 下,以 sessionId 为键:
| 文件 | 内容 |
|---|
plans/<sessionId>.json | 计划树(每个会话一棵) |
surfaces/<sessionId>.json | 三层的折叠游标 { key, cursor } |
计划属于建立它的那个会话。放到会话无关的位置,任何一个会话的计划都会被
所有会话读到——那等于把不相干的节点指派塞进别人的上下文。
写入一律走「临时文件 + rename」,进程崩在写入中途也不会留下半截 JSON。
插件绝不直接写会话日志,会话里的一切都走 session.append()。
你会观察到的行为
-
完成必须写 result。 节点的执行过程会被折叠掉,result 是它唯一的出口;
空 result 会被拒绝。所以要写清"产出了什么",写成能独立看懂的样子——
过程会丢,别把过程写进去。
-
折叠是尽力而为的。 游标被压缩遮掉时收不起来,但结果一定进上下文。
面板上看到某段过程还在,通常是这个原因。
-
模型看到的是静态的工具组说明,不随计划变化。 提示词一变,整个上下文
从第一个 token 起失去缓存。要了解计划就让模型调 tree_task_status。
-
每次折叠,界面上的"系统提示词"卡片会重新渲染一次,但提示词一个字都没变。
DSH 把"上下文被替换过"记成一次请求序列的边界,界面在 reason 不是 change
的请求上都会渲染这张卡片,而复用的是同一个锚点,所以卡片不会变多,位置也不动。
这是 DSH 的固有语义,插件只能把替换次数压到最低——一次折叠只替换真正变化的
那一层,典型的完成一个节点只产生一次替换。
-
改需求用「丢弃 + 追加」,不要重建整棵树。 丢掉的节点留在树里可查,
它和完成的节点会合进同一条汇总。
不做的事
| 不做 | 原因 |
|---|
| 资料 / 文件快照 / 记忆 | 上下文里值得留下的只有结果,其余靠模型自己重读 |
| 文件回退 / 撤销 | 改需求用「丢弃 + 追加」 |
| 多目标并行(多个根) | 单活动计划,数据结构已预留扩展 |
| 把计划内容注入提示词 | 提示词必须逐字节不变,否则整个上下文失去缓存 |
自检
三个自检脚本都不 import 任何 @deepseek-ai/* 运行时包——插件以 link: 方式安装时,
从插件目录往上找不到宿主作用域的包,一旦 import 就根本跑不起来。
node verify-entry.mjs # 入口装配:启用/未启用两条路径各注册了什么
node verify-tools.mjs # 工具层:schema、result 必填、检查点闸门、三级约束
node verify-region.mjs # 折叠行为:游标推进、降级追加、检查点提示
node verify-plan.mjs # 计划树纯逻辑:状态流转、自动完成、丢弃级联
verify-region.mjs 用假 session 忠实复刻了 DSH surface 的三条硬规则
(替换保持位置、sourceEventSeqs 必须覆盖全部遮蔽节点、节点 0 重写约束),
否则测出来的东西不算数。
卸载
dsh plugin --profile web remove dsh-task-tree
状态目录不会自动清理,需要的话手动删 $DSH_HOME/dsh-task-tree。
设计文档
docs/02-DSH插件开发方案.md —— 完整设计:上下文组成、游标、折叠、检查点、缓存推理
docs/01-AIDesktop联动设计整理.md —— 本插件最初的设计来源
许可
MIT