dsh-tree-task-flow 0.3.12 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/plan.js CHANGED
@@ -7,8 +7,10 @@
7
7
  * **谁是"当前节点"是推导出来的,不是存的**(`activePath`)——
8
8
  * 少一个需要同步的状态,就少一类 bug。
9
9
  *
10
- * 一个节点完成时必须提交 `result`(这个节点产出了什么)。插件据此把
11
- * 它的执行过程从上下文里折叠掉,换成一条携带该结果的汇总消息。
10
+ * 一个节点完成时必须提交 `result`(这个节点产出了什么)。它提交在这次
11
+ * `tree_task_done` 调用的参数里,而那次调用是折叠边界、不会被收走,所以
12
+ * 结果**留得住**;插件收走的是它前面那段执行过程。树渲染里也会把结果原样
13
+ * 列出来(**不截断**),让 `tree_task_status` 随时能把交付内容重新摆到眼前。
12
14
  *
13
15
  * **子节点全部结束时,父节点不自动完成。** 那一刻转入**检查点**:
14
16
  * 模型要么继续给父节点拆新的子节点,要么提交父节点的结果并完成它。
@@ -460,54 +462,118 @@ function checkpointNote(checkpoint) {
460
462
  );
461
463
  }
462
464
 
463
- /** 节点状态的中文短标记。 */
465
+ /**
466
+ * 节点状态的短标记。
467
+ *
468
+ * 写成 `[ ]` / `[x]` 是**故意的**:整棵树用 Markdown 的嵌套任务清单渲染,
469
+ * 这两个标记在界面上会render成可以勾选的方框。`[-]` 不是标准任务清单标记,
470
+ * 会原样显示成 `[-]`——丢弃的节点本来就该看着"不是个正常的框"。
471
+ */
464
472
  function mark(node) {
465
473
  if (node.status === STATUS.DONE) return "[x]";
466
474
  if (node.status === STATUS.DROPPED) return "[-]";
467
475
  return "[ ]";
468
476
  }
469
477
 
470
- /** 结果取首行,超长截断——树上只报"产出了什么",全文在上下文里的汇总消息里。 */
471
- function firstLine(text, limit = 120) {
472
- const line = String(text ?? "").split("\n")[0].trim();
473
- return line.length > limit ? `${line.slice(0, limit)}…` : line;
478
+ /** 每一层的缩进宽度:两个空格,Markdown 嵌套列表的常规写法。 */
479
+ const INDENT = " ";
480
+
481
+ /**
482
+ * 一个节点的「说明 / 结果」字段,作为该列表项的下一层条目挂进去。
483
+ *
484
+ * **全文输出,一个字都不截。** `result` 是这个节点交付的全部文字,截掉半句
485
+ * 就等于没交付。多行文本逐行输出;空行也要带上缩进,否则 Markdown 会认为
486
+ * 列表项到此为止,后面的内容会被甩出列表。
487
+ *
488
+ * @param lines - 输出行数组,就地追加。
489
+ * @param ownerIndent - 所属节点那一行的缩进层数。
490
+ * @param label - 「说明」或「结果」。
491
+ * @param text - 正文;空值不输出。
492
+ */
493
+ function pushField(lines, ownerIndent, label, text) {
494
+ const rows = String(text ?? "")
495
+ .replace(/\r\n?/gu, "\n")
496
+ .split("\n");
497
+ while (rows.length > 0 && rows[rows.length - 1].trim() === "") rows.pop();
498
+ if (rows.length === 0) return;
499
+
500
+ const pad = INDENT.repeat(ownerIndent + 1);
501
+ lines.push(`${pad}- **${label}**:${rows[0]}`);
502
+ for (const row of rows.slice(1)) {
503
+ lines.push(row === "" ? pad : `${pad}${INDENT}${row}`);
504
+ }
474
505
  }
475
506
 
476
507
  /**
477
- * 把整棵树渲染成给模型看的文本。
508
+ * 渲染一个目标。
478
509
  *
479
- * @param plan - 计划树;null 表示还没有计划。
480
- * @returns 多行文本。
510
+ * 输出是**规范的 Markdown 嵌套任务清单**(`- [ ]` 一层两个空格),复制出去
511
+ * 就能正常预览——之前的写法靠纯空格缩进,四空格起就被 Markdown 当成代码块。
512
+ *
513
+ * @param goal - 目标节点。
514
+ * @param plan - 整棵计划树,用来标出"当前"落在哪。
515
+ * @param expand - `false` 时只渲染目标自己这一级(不含它下面的任务与子任务)。
516
+ * @returns 多行 Markdown 文本。
481
517
  */
482
- export function renderTree(plan) {
483
- const goals = goalsOf(plan);
484
- if (goals.length === 0) return "(还没有计划。先用 tree_task_create 建立目标与任务。)";
518
+ export function renderGoal(goal, plan, expand = true) {
485
519
  const active = activePath(plan);
486
520
  const checkpoint = checkpointOf(plan);
487
521
  const lines = [];
488
522
 
489
- for (const goal of goals) {
490
- lines.push(`${mark(goal)} 目标 ${goal.id} ${goal.title}`);
491
- if (goal.detail) lines.push(` ${goal.detail}`);
492
- if (goal.result) lines.push(` 结果: ${firstLine(goal.result.text)}`);
493
-
494
- for (const task of goal.tasks ?? []) {
495
- const taskHere = active?.task?.id === task.id || checkpoint?.node?.id === task.id;
496
- lines.push(` ${mark(task)} 任务 ${task.id} ${task.title}${taskHere ? " ← 当前" : ""}`);
497
- if (task.result) lines.push(` 结果: ${firstLine(task.result.text)}`);
498
- const steps = task.steps ?? [];
499
- if (steps.length === 0 && isOpen(task)) {
500
- lines.push(" (还没有子节点 → 调用 tree_task_plan)");
501
- }
502
- for (const step of steps) {
503
- lines.push(` ${mark(step)} 子任务 ${step.id} ${step.title}${active?.step?.id === step.id ? " ← 当前" : ""}`);
504
- if (step.result) lines.push(` 结果: ${firstLine(step.result.text)}`);
505
- }
523
+ const goalHere = active?.goal?.id === goal.id;
524
+ lines.push(`- ${mark(goal)} **目标** \`${goal.id}\` ${goal.title}${goalHere ? " ← 当前" : ""}`);
525
+ pushField(lines, 0, "说明", goal.detail);
526
+ pushField(lines, 0, "结果", goal.result?.text);
527
+ if (expand !== true) return lines.join("\n");
528
+
529
+ for (const task of goal.tasks ?? []) {
530
+ const taskHere = active?.task?.id === task.id || checkpoint?.node?.id === task.id;
531
+ lines.push(
532
+ `${INDENT}- ${mark(task)} **任务** \`${task.id}\` ${task.title}${taskHere ? " ← 当前" : ""}`,
533
+ );
534
+ pushField(lines, 1, "结果", task.result?.text);
535
+
536
+ const steps = task.steps ?? [];
537
+ if (steps.length === 0 && isOpen(task)) {
538
+ lines.push(`${INDENT.repeat(2)}- 还没有子节点,用 tree_task_plan 补上`);
539
+ }
540
+ for (const step of steps) {
541
+ const stepHere = active?.step?.id === step.id;
542
+ lines.push(
543
+ `${INDENT.repeat(2)}- ${mark(step)} **子任务** \`${step.id}\` ${step.title}${stepHere ? " ← 当前" : ""}`,
544
+ );
545
+ pushField(lines, 2, "结果", step.result?.text);
506
546
  }
507
547
  }
508
548
  return lines.join("\n");
509
549
  }
510
550
 
551
+ /**
552
+ * 渲染计划树。
553
+ *
554
+ * 默认把**已经结束的目标**收成目标那一级:一个会话跑久了会有好几个做完的目标,
555
+ * 每个都摊开会把树变成一大坨没人看的文字。要某个目标的完整树就指名它。
556
+ *
557
+ * @param plan - 计划树;null 表示还没有计划。
558
+ * @param only - 只渲染这个 id 所在的目标,并展开到底;不传就是全部目标。
559
+ * @returns 多行 Markdown 文本。
560
+ */
561
+ export function renderTree(plan, only = null) {
562
+ const goals = goalsOf(plan);
563
+ if (goals.length === 0) return "(还没有计划。用 tree_task_create 建立目标与任务。)";
564
+
565
+ const wanted = typeof only === "string" && only.trim() !== "" ? only.trim() : null;
566
+ if (wanted !== null) {
567
+ const found = findNode(plan, wanted);
568
+ if (found === null) {
569
+ return `找不到节点 ${wanted}。不带 id 调用可以看到本会话的全部目标。`;
570
+ }
571
+ return renderGoal(found.goal, plan, true);
572
+ }
573
+
574
+ return goals.map((goal) => renderGoal(goal, plan, isOpen(goal))).join("\n");
575
+ }
576
+
511
577
  /**
512
578
  * 注入消息里的收尾提示。它接在完成通告后面,所以自带前置标点(`lead`)。
513
579
  *
package/lib/region.js CHANGED
@@ -3,16 +3,21 @@
3
3
  *
4
4
  * 规则只有一条,**只看表层,与计划树无关**:
5
5
  *
6
- * 从「上一次 `tree_task_done` 之后的第一个 `tree_task_create`」之后,
6
+ * 从「上一次 `tree_task_done` 之后的第一条 `tree_task_create` 调用」之后,
7
7
  * 到本次 `tree_task_done` 之前,中间那一段过程收走。
8
8
  *
9
- * 更早没有 `tree_task_done` 时,起点取表层第一个 `tree_task_create`;区间里一次
9
+ * 更早没有 `tree_task_done` 时,起点取表层第一条 `tree_task_create`;区间里一次
10
10
  * `create` 都没有时,回退到上一次 `tree_task_done` 之后(更早没有 `done` 则是
11
11
  * 表层开头)。起点那次调用、以及本次 `done` 的调用与返回,都留在表层上——
12
12
  * 那是模型看"计划是什么、下一步干什么"的地方。
13
13
  *
14
- * 边界只有 `create` 与 `done` 两个:**`tree_task_plan` 不是边界**,它的调用与返回
15
- * 跟普通过程一样,落在区间里就被收走。
14
+ * 边界只有 `create` 与 `done` 两个:写树的 `tree_task_plan` **不是边界**,
15
+ * 它跟普通过程一样落在区间里就被收走。
16
+ *
17
+ * **为什么不能把 `create` 并进 `plan`**:起点取的是"上一次 done 之后最早的
18
+ * 那一次开工调用",而 `plan` 每补一次步骤就调一次;并成一个工具之后,
19
+ * 每一次补步骤都会把起点往后推,`done` 与它之间那段过程就永远收不走。
20
+ * 靠"实参里有没有 `parentId`"去区分,也只是把认错起点的机会交给模型。
16
21
  *
17
22
  * 判定表见 `docs/06-折叠范围规则.md`。
18
23
  *
@@ -25,7 +30,7 @@
25
30
  *
26
31
  * [节点 0] 系统提示词
27
32
  * [用户消息]
28
- * [tree_task_create 调用] [返回:计划已建立 + 整棵树]
33
+ * [tree_task_create 调用] [返回:计划已建立 + 这个目标的树]
29
34
  * [tree_task消息:隐藏了 12 条过程上下文。读取:… 写入:…]
30
35
  * [tree_task_done 调用] [返回:接下来需要进行步骤:「二」(s-4)。]
31
36
  * [当前这一段执行过程] ← 下一次折叠的对象
@@ -62,17 +67,52 @@ export const PLUGIN = "dsh-tree-task-flow";
62
67
  */
63
68
  export const PLUGIN_SOURCE_KIND = `plugin:${PLUGIN}`;
64
69
 
70
+ /**
71
+ * 界面上那一行摘要的统一前缀。
72
+ *
73
+ * 折叠、压缩、恢复续行、计划续行、中断拉起、中断重试——本插件往会话里放的通知不止一种,
74
+ * 界面上都是一行摘要。**前缀只在这里加一次**:调用点写自己的裸标签
75
+ * (「恢复续行」「中断拉起」),谁都别再手写它,否则迟早有一条漏掉或者写成两遍。
76
+ */
77
+ export const SUMMARY_PREFIX = "树形任务流 · ";
78
+
79
+ /**
80
+ * 给一个裸标签配上统一前缀,得到界面上真正显示的那一行。
81
+ *
82
+ * @param label - 裸标签,例如「恢复续行」。
83
+ * @returns 带前缀的摘要文本。
84
+ */
85
+ export function noticeSummary(label) {
86
+ return `${SUMMARY_PREFIX}${label}`;
87
+ }
88
+
89
+ /**
90
+ * 这条 source 的摘要是不是某个标签。
91
+ *
92
+ * **带不带前缀都认**:升级之前落在会话里的那几条摘要没有前缀,
93
+ * 而按摘要做匹配的地方(收走失败尝试时找那条拉起消息)得能认出它们。
94
+ *
95
+ * @param source - 消息的 source。
96
+ * @param label - 裸标签。
97
+ * @returns 命中为 true。
98
+ */
99
+ export function hasSummary(source, label) {
100
+ if (typeof source?.summary !== "string") return false;
101
+ return source.summary === label || source.summary === noticeSummary(label);
102
+ }
103
+
65
104
  /**
66
105
  * 本插件一条 notice 消息的 source。
67
106
  *
68
107
  * 字段与老写法保持一致:`form: "notice"` 让界面渲染成可展开的一行说明,
69
- * `summary` 是那一行的文字。
108
+ * `summary` 是那一行的文字。**摘要一律走 {@link noticeSummary} 加前缀**——
109
+ * 界面上那一行必须自己说清是哪个插件发的,不能只写「恢复续行」。
70
110
  *
71
- * @param summary - 界面上显示的一行摘要。
111
+ * @param label - 界面上显示的那一行(不含前缀)。
72
112
  * @returns 消息 source。
73
113
  */
74
- export function pluginSource(summary) {
75
- return { kind: PLUGIN_SOURCE_KIND, form: "notice", summary };
114
+ export function pluginSource(label) {
115
+ return { kind: PLUGIN_SOURCE_KIND, form: "notice", summary: noticeSummary(label) };
76
116
  }
77
117
 
78
118
  /**
@@ -96,7 +136,7 @@ export function isPluginSource(source) {
96
136
  const LEVELS = ["goal", "task", "step"];
97
137
 
98
138
  /**
99
- * 边界调用:只有 `tree_task_create` 与 `tree_task_done`。它们的调用与返回不进替换区间,
139
+ * 边界调用:`tree_task_create` 与 `tree_task_done`。它们的调用与返回不进替换区间,
100
140
  * 是折叠的起止点。**`tree_task_plan` 不在其中**——它跟别的过程一样,落在区间里就被收走。
101
141
  */
102
142
  const BOUNDARY_TOOLS = new Set(["tree_task_create", "tree_task_done"]);
@@ -228,7 +268,7 @@ function renderSummary(count, files) {
228
268
  }
229
269
 
230
270
  /**
231
- * 汇总消息的一行摘要,给界面折叠标题用。
271
+ * 汇总消息的一行摘要(**不含统一前缀**——前缀由 `pluginSource` 加)。
232
272
  *
233
273
  * @param count - 被这次替换收走的节点条数。
234
274
  * @param files - `touchedFiles()` 的结果。
@@ -236,11 +276,22 @@ function renderSummary(count, files) {
236
276
  */
237
277
  function summaryLine(count, files) {
238
278
  const touched = files.read.length + files.write.length;
239
- const base = `树形任务流 · 隐藏了${count}条过程上下文`;
279
+ const base = `隐藏了${count}条过程上下文`;
240
280
  return touched === 0 ? base : `${base} · 涉及 ${touched} 个文件`;
241
281
  }
242
282
 
243
- /** 这条 assistant 消息里有没有 `tree_task_create`。 */
283
+ /**
284
+ * 这条 assistant 消息里有没有**新建目标**的那次 `tree_task_create`。
285
+ *
286
+ * 折叠起点只认这一种调用。**不能拿 `tree_task_plan` 顶替**:`plan` 在任务中途
287
+ * 反复调用(每补一次步骤就调一次),而起点取的是"上一次 done 之后最早的那次",
288
+ * 于是每一次补步骤都会把起点往后推,`done` 与它之间那段过程永远收不走。
289
+ * 靠实参里有没有 `parentId` 去区分也只是把"认错起点"的机会交给了模型——
290
+ * 专门一个工具、按名字认,才是可靠的。
291
+ *
292
+ * @param event - 表层上的事件。
293
+ * @returns 是否新建了目标。
294
+ */
244
295
  function isCreateCall(event) {
245
296
  return toolNamesOf(event).includes("tree_task_create");
246
297
  }
@@ -250,8 +301,7 @@ function isCreateCall(event) {
250
301
  *
251
302
  * 从末尾往前找最近的一次边界调用,**它必须是 `tree_task_done`**(它就是这次完成的
252
303
  * 那一下);再往前找上一次 `tree_task_done`,起点就是**它之后最早的那次
253
- * `tree_task_create`**——更早没有 `done` 时,起点是表层第一个 `tree_task_create`。
254
- * `tree_task_plan` 不是边界:它跟过程、工具结果一样落在区间里被收走。
304
+ * `tree_task_create`**——更早没有 `done` 时,起点是表层第一条 `tree_task_create`。
255
305
  *
256
306
  * 区间里一次 `create` 都没有时,起点退回上一次 `done` 之后(更早没有 `done`
257
307
  * 则是表层第一个节点之后)——那时区间多半会撞上真人消息,于是整段不折,这是安全的那一侧。
package/lib/store.js CHANGED
@@ -6,6 +6,7 @@
6
6
  * - `plans/<sessionId>.json` 计划树
7
7
  * - `surfaces/<sessionId>.json` 三层的折叠游标(只给界面看,不参与折叠)
8
8
  * - `held/<sessionId>.json` 「被按住」:暂停 / 停止,**要跨重启保留**
9
+ * - `revive/<sessionId>.json` 意外中断的拉起重试链,**要跨重启保留**
9
10
  *
10
11
  * 计划属于建立它的那个会话。放到会话无关的位置,任何一个会话的计划都会被
11
12
  * 所有会话的 `agent/pre-step` 读到——那等于把不相干的节点指派塞进别人的上下文,
@@ -85,6 +86,9 @@ export function createStore(root) {
85
86
  /** 「被按住」状态的文件路径。 */
86
87
  const heldFile = (sessionId) => file("held", `${sessionId}.json`);
87
88
 
89
+ /** 意外中断重试链的文件路径。 */
90
+ const reviveFile = (sessionId) => file("revive", `${sessionId}.json`);
91
+
88
92
  /** 读「被按住」状态;读不到或文件坏了,一律按"没被按住"处理。 */
89
93
  function readHeldState(sessionId) {
90
94
  if (!sessionId) return { paused: false, stopped: false, at: null };
@@ -216,5 +220,49 @@ export function createStore(root) {
216
220
  });
217
221
  return readHeldState(sessionId);
218
222
  },
223
+
224
+ // ---- 意外中断的拉起重试链:**要跨重启保留** ----
225
+
226
+ /**
227
+ * 读这个会话的重试链状态。
228
+ *
229
+ * 为什么不放内存:网络类中断的时刻表横跨 30 分钟,而这台机器上重启 dsh 是常事。
230
+ * 只留内存的话,重启一次这条链就断了,正好落在最需要它的场景上。
231
+ *
232
+ * @param sessionId - 会话 id。
233
+ * @returns `{ sessionId, turn, class, code, pending, attempts, hidden, updatedAt }`,
234
+ * 没有链时为 null。`class` 是 `network` / `tool` / `malformed` 之一。
235
+ */
236
+ readRevive(sessionId) {
237
+ if (!sessionId) return null;
238
+ const value = readJson(reviveFile(sessionId), null);
239
+ if (value === null || typeof value !== "object") return null;
240
+ return value;
241
+ },
242
+
243
+ /**
244
+ * 写重试链状态。
245
+ *
246
+ * @param sessionId - 会话 id。
247
+ * @param value - 完整状态;`null` 等同于 {@link clearRevive}。
248
+ */
249
+ writeRevive(sessionId, value) {
250
+ if (!sessionId) return;
251
+ if (value === null || typeof value !== "object") {
252
+ rmSync(reviveFile(sessionId), { force: true });
253
+ return;
254
+ }
255
+ writeJson(reviveFile(sessionId), { sessionId, ...value, updatedAt: Date.now() });
256
+ },
257
+
258
+ /**
259
+ * 丢掉这个会话的重试链。链正常结束(这一轮跑完了)或该停手时调用。
260
+ *
261
+ * @param sessionId - 会话 id。
262
+ */
263
+ clearRevive(sessionId) {
264
+ if (!sessionId) return;
265
+ rmSync(reviveFile(sessionId), { force: true });
266
+ },
219
267
  };
220
268
  }
package/lib/tools.js CHANGED
@@ -1,6 +1,14 @@
1
1
  /**
2
2
  * 六个 `tree_task_*` 工具。
3
3
  *
4
+ * **写树分成两个工具,不是可以合并的重复。** `tree_task_create` 与 `tree_task_done`
5
+ * 是折叠区间的两个边界(见 `lib/region.js`):`create` 是"开工"那一下,`done` 是
6
+ * "收工"那一下。`create` 只在新建目标时调用一次,`plan` 却在任务中途反复调用——
7
+ * 把两者并成一个工具,折叠起点就只能靠实参里有没有 `parentId` 去猜,
8
+ * 模型一次写漏或多写就会把起点挪走,那段过程就再也收不走了。
9
+ * 代价是两个工具的实参形状有点像(都是一串 `{title, detail}`),
10
+ * 所以 `plan` 的说明里专门写了一句"建新目标不归它管"。
11
+ *
4
12
  * 工具本身**只做状态读写**——不注入消息、不动 surface,也不拦任何别的工具。
5
13
  * 「完成时把这个节点的执行过程从上下文里折起来、换成一条携带它结果的汇总」
6
14
  * 那件事完全由 `agent/pre-step` 监听器负责(它比对游标与 `activePath` 就知道
@@ -24,7 +32,9 @@ import {
24
32
  createPlan,
25
33
  doneReply,
26
34
  dropNode,
35
+ findNode,
27
36
  renderContinue,
37
+ renderGoal,
28
38
  renderTree,
29
39
  updateNode,
30
40
  } from "./plan.js";
@@ -65,21 +75,35 @@ export function registerTools(ctx, { store, cfg }) {
65
75
  ctx.tools.register({
66
76
  name: "tree_task_status",
67
77
  description:
68
- "查看当前计划树:目标、任务、子任务各自的状态与已提交的结果,以及现在该推进哪个节点。" +
69
- "任务开头、上下文被压缩之后、或者不确定下一步做什么时,调用它。",
70
- parameters: { type: "object", additionalProperties: false, properties: {} },
78
+ "查看计划树:目标、任务、子任务各自的状态与已提交的结果,以及现在该推进哪个节点。" +
79
+ "任务开头、上下文被压缩之后、或者不确定下一步做什么时,调用它。" +
80
+ "默认只展开还没结束的目标——已经做完的目标只留目标那一行,要看它的完整树就把 id 填上。",
81
+ parameters: {
82
+ type: "object",
83
+ additionalProperties: false,
84
+ properties: {
85
+ id: {
86
+ type: "string",
87
+ description:
88
+ "可选。填目标 id 就返回那个目标的完整树(已完成的目标默认不展开);" +
89
+ "填任务或子任务 id,就按它所在的那个目标展开。",
90
+ },
91
+ },
92
+ },
71
93
  output: OUTPUT,
72
- async execute(_args, exec) {
94
+ async execute(args, exec) {
73
95
  const plan = store.readPlan(sessionOf(exec));
74
- return ok(renderTree(plan));
96
+ return ok(renderTree(plan, args?.id ?? null));
75
97
  },
76
98
  });
77
99
 
78
100
  ctx.tools.register({
79
101
  name: "tree_task_create",
80
102
  description:
81
- "新增一个目标:这个目标 + 它下面的若干任务。一个会话可以有多个目标," +
82
- "按调用顺序排在计划里——已经建过的目标和它们的历史都留着,不会被这次调用替换掉。" +
103
+ "新建一个目标:这个目标 + 它下面的若干任务。**它是「开工」那一下**——" +
104
+ "折叠上下文时,插件就认这次调用作为「这一轮从哪儿开始」。" +
105
+ "一个会话可以有多个目标,按调用顺序排在计划里——已经建过的目标和它们的历史都留着," +
106
+ "不会被这次调用替换掉。" +
83
107
  "这里的任务是交付节点——一件能独立验收的交付内容,可以是需要完成或验证的某件事," +
84
108
  "也可以是一个检查点,不是步骤本身。" +
85
109
  "建完记得用 tree_task_plan 给这个目标下的第一个任务拆子任务。",
@@ -124,8 +148,14 @@ export function registerTools(ctx, { store, cfg }) {
124
148
  }
125
149
  store.writePlan(sessionId, plan);
126
150
  return ok(
127
- `${existing === null ? "计划已建立。" : `已新增目标「${goal.title}」。`}\n\n${renderTree(plan)}\n\n` +
128
- "下一步:调用 tree_task_plan 给这个目标下的第一个任务拆子任务。",
151
+ [
152
+ existing === null ? `计划已建立,目标「${goal.title}」。` : `已新增目标「${goal.title}」。`,
153
+ "",
154
+ renderGoal(goal, plan, true),
155
+ "",
156
+ "下一步:给这个目标下的第一个任务拆子任务——用 tree_task_plan," +
157
+ "parentId 填那个任务的 id。",
158
+ ].join("\n"),
129
159
  );
130
160
  },
131
161
  });
@@ -135,7 +165,8 @@ export function registerTools(ctx, { store, cfg }) {
135
165
  description:
136
166
  "给一个节点追加子节点:给目标追加任务,给任务追加子任务。" +
137
167
  "给任务追加的是步骤(需要一次性做完的事情)。" +
138
- "子任务是最低一级,不能再往下拆。",
168
+ "子任务是最低一级,不能再往下拆。" +
169
+ "**建新目标不归它管,那是 tree_task_create**——它只往已有的节点上接子节点。",
139
170
  parameters: {
140
171
  type: "object",
141
172
  additionalProperties: false,
@@ -164,12 +195,13 @@ export function registerTools(ctx, { store, cfg }) {
164
195
  const result = addChildren(plan, args?.parentId, args?.children);
165
196
  if (!result.ok) return ok(`没有添加:${result.reason}`);
166
197
  store.writePlan(sessionOf(exec), plan);
198
+ const found = findNode(plan, args?.parentId);
167
199
  return ok(
168
200
  [
169
201
  `已添加 ${result.added.length} 个${childLabel(result.level)}:`,
170
- ...result.added.map((node) => ` ${node.id} ${node.title}`),
202
+ ...result.added.map((node) => `- \`${node.id}\` ${node.title}`),
171
203
  "",
172
- renderTree(plan),
204
+ renderGoal(found.goal, plan, true),
173
205
  ].join("\n"),
174
206
  );
175
207
  },
@@ -230,7 +262,8 @@ export function registerTools(ctx, { store, cfg }) {
230
262
  const result = updateNode(plan, args?.id, args ?? {});
231
263
  if (!result.ok) return ok(`没有修改:${result.reason}`);
232
264
  store.writePlan(sessionOf(exec), plan);
233
- return ok(`已更新 ${result.changed.join(" / ")}。\n\n${renderTree(plan)}`);
265
+ const found = findNode(plan, args?.id);
266
+ return ok(`已更新 ${result.changed.join(" / ")}。\n\n${renderGoal(found.goal, plan, true)}`);
234
267
  },
235
268
  });
236
269
 
@@ -255,10 +288,15 @@ export function registerTools(ctx, { store, cfg }) {
255
288
  const result = dropNode(plan, args?.id);
256
289
  if (!result.ok) return ok(`没有丢弃:${result.reason}`);
257
290
  store.writePlan(sessionOf(exec), plan);
291
+ const found = findNode(plan, args?.id);
258
292
  return ok(
259
- [`已丢弃:${result.dropped.join("、")}`, "", renderTree(plan), "", renderContinue(plan)].join(
260
- "\n",
261
- ),
293
+ [
294
+ `已丢弃:${result.dropped.join("、")}`,
295
+ "",
296
+ renderGoal(found.goal, plan, true),
297
+ "",
298
+ renderContinue(plan),
299
+ ].join("\n"),
262
300
  );
263
301
  },
264
302
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-tree-task-flow",
3
- "version": "0.3.12",
3
+ "version": "0.3.13",
4
4
  "description": "树形任务流:把多步任务组织成目标→任务→子任务三级树,节点完成时由 AI 提交结果,插件随即把该节点的执行过程从上下文里折叠掉,只留下结果。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",