dsh-tree-task-flow 0.1.1 → 0.3.0

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/cordis.patch.yml CHANGED
@@ -13,7 +13,7 @@
13
13
  # enabled 总开关 默认 false(装上 ≠ 启用)
14
14
  # autoContinue 是否允许自动续行 默认 false(开着会让人停不下来)
15
15
  # maxAutoRounds 连续自动续行轮次上限 默认 5
16
- # promptOrder 静态说明段顺序 默认 2000
16
+ # promptOrder 工具说明段顺序 默认 1800
17
17
  # rootDir 插件状态目录 默认 $DSH_HOME/dsh-task-tree
18
18
  - insert:
19
19
  - id: task-tree
@@ -9,10 +9,8 @@
9
9
  *
10
10
  * 三条安全阀缺一不可(方案 §7.1):
11
11
  *
12
- * 1. **有活跃的内置 goal 时完全不插手**——不只是"别抢方向盘"。
13
- * `dsh-goal-round-driver` 每推进一轮都设 `startsRequestSeries: true`,
14
- * 那会让系统提示词走归一化分支;节点 0 一旦被判定需要重写,
15
- * 整个上下文从第一个 token 起失去缓存。
12
+ * 1. **有活跃的内置 goal 时完全不插手**——内置 goal 由 `dsh-goal-round-driver`
13
+ * 自己推进轮次,插件再续一次就成了两套机制同时推同一个会话。
16
14
  * 2. **队列里还有人在等的时候绝不抢**:`inbox.nextTurn` / `nextStep` 非空就退出。
17
15
  * 3. **连续续行轮次有上限**,达到就停,避免把 token 烧在一个卡住的任务上。
18
16
  *
@@ -64,7 +62,7 @@ export function registerAutoContinue(ctx, { store, cfg }) {
64
62
  );
65
63
  if (fromHuman) {
66
64
  rounds.set(sessionId, 0);
67
- // 人回来了,解除中断闸门,允许新一轮的续行。
65
+ // 人回来了,解除中断标记,允许新一轮的续行。
68
66
  stopped.delete(sessionId);
69
67
  }
70
68
  return decision;
package/lib/commands.js CHANGED
@@ -2,12 +2,9 @@
2
2
  * `/task-tree` 斜杠命令:给人用的入口。
3
3
  *
4
4
  * 命令**不经过模型**——`handler` 直接执行、直接返回文本。所以它是查看和管理
5
- * 任务树最直接的方式,也是"把自动执行停下来"的那个开关:
5
+ * 任务树最直接的方式,也是"暂停自动执行"的那个开关:
6
6
  * 任务自动执行必须是**受控**的,人得有一句话就能接管的手段。
7
7
  *
8
- * 命令同时是**检查点的旁路**:检查点闸门只拦模型(`tools/pre-execute`),
9
- * 拦不住人。人在检查点里照样能提交结果、丢弃节点、或者把自动续行停掉。
10
- *
11
8
  * @module dsh-tree-task-flow/commands
12
9
  */
13
10
 
@@ -18,8 +15,8 @@ const USAGE = [
18
15
  "/task-tree 查看本会话的任务树与检查点状态",
19
16
  "/task-tree done <id> <结果> 提交某个节点的结果并完成它",
20
17
  "/task-tree drop <id> 丢弃某个节点",
21
- "/task-tree stop 停止本会话的自动续行",
22
- "/task-tree resume 重新允许自动续行(需 config 里 autoContinue 为 true)",
18
+ "/task-tree pause 暂停:当前工具跑完后停住,不再往下推进",
19
+ "/task-tree resume 继续:从停住的地方接着跑,不插消息",
23
20
  "/task-tree reset 清掉本会话的计划树",
24
21
  ].join("\n");
25
22
 
@@ -29,7 +26,7 @@ const DISABLED_NOTE =
29
26
  "也不会折叠任何上下文。要启用请在 profile 的 cordis.patch.yml 里把 enabled 设为 true。";
30
27
 
31
28
  /**
32
- * 把检查点那句话拼出来。措辞与 `plan.js` 的检查点提示、`tools.js` 的闸门拒绝文案一致。
29
+ * 把检查点那句话拼出来。措辞与 `plan.js` 的检查点提示一致。
33
30
  *
34
31
  * @param checkpoint - `checkpointOf(plan)` 的结果。
35
32
  * @returns 两行文本。
@@ -38,8 +35,8 @@ function checkpointLines(checkpoint) {
38
35
  const label = levelLabel(checkpoint.level);
39
36
  return [
40
37
  `检查点:${label}「${checkpoint.node.title}」(${checkpoint.node.id}) 的子节点已全部结束。`,
41
- `此刻只有两条路:tree_task_plan 继续给 ${checkpoint.node.id} 拆子节点,` +
42
- `或者 tree_task_done(id="${checkpoint.node.id}", result="…") 提交这个${label}的结果并完成它。`,
38
+ `此刻轮到你的判断:用 tree_task_plan 继续给 ${checkpoint.node.id} 拆子节点,` +
39
+ `或者用 tree_task_done(id="${checkpoint.node.id}", result="…") 提交这个${label}的结果并完成它。`,
43
40
  ];
44
41
  }
45
42
 
@@ -47,10 +44,10 @@ function checkpointLines(checkpoint) {
47
44
  * 注册命令。
48
45
  *
49
46
  * @param ctx - 挂载本行的 Cordis 上下文。
50
- * @param options - `{ store, cfg, autoContinue, enabled }`;`autoContinue` 是
51
- * `registerAutoContinue` 返回的控制接口,未启用时为 null。
47
+ * @param options - `{ store, cfg, pause, enabled }`;`pause` 是 `registerPause`
48
+ * 返回的控制接口,未启用时是 null。
52
49
  */
53
- export function registerCommands(ctx, { store, cfg, autoContinue, enabled = false }) {
50
+ export function registerCommands(ctx, { store, cfg, pause = null, enabled = false }) {
54
51
  // 和 tools 一样,命令服务不是每个部署都有;用 get 探测而不是写进 inject。
55
52
  const commands = ctx.get?.("commands");
56
53
  if (commands === undefined || commands === null) {
@@ -60,8 +57,8 @@ export function registerCommands(ctx, { store, cfg, autoContinue, enabled = fals
60
57
 
61
58
  commands.register({
62
59
  name: "task-tree",
63
- description: "查看与管理任务树:status / done <id> <结果> / drop <id> / stop / resume / reset",
64
- input: { hint: "status | done <id> <结果> | drop <id> | stop | resume | reset" },
60
+ description: "查看与管理任务树:status / done <id> <结果> / drop <id> / pause / resume / reset",
61
+ input: { hint: "status | done <id> <结果> | drop <id> | pause | resume | reset" },
65
62
  handler: (invocation) => {
66
63
  const sessionId = invocation?.agent?.session?.id;
67
64
  if (sessionId === undefined) {
@@ -84,22 +81,25 @@ export function registerCommands(ctx, { store, cfg, autoContinue, enabled = fals
84
81
  };
85
82
  }
86
83
 
87
- if (verb === "stop") {
88
- autoContinue?.stop?.(sessionId);
84
+ // `stop` 是旧写法,含义与 `pause` 完全一样,留着免得习惯改不过来。
85
+ if (verb === "pause" || verb === "stop") {
86
+ pause?.pause?.(sessionId);
89
87
  return {
90
88
  kind: "success",
91
- text: "已停止本会话的自动续行。任务树的进度没有变,随时可以继续。",
89
+ text:
90
+ "已暂停本会话:正在执行的那个工具会正常跑完,之后不再往下推进。" +
91
+ "任务树的进度没有变,点继续就从停住的地方接着跑。",
92
92
  };
93
93
  }
94
94
 
95
95
  if (verb === "resume") {
96
- autoContinue?.resume?.(sessionId);
96
+ pause?.resume?.(sessionId);
97
97
  return {
98
98
  kind: "success",
99
99
  text:
100
100
  cfg.autoContinue === true
101
- ? "已重新允许本会话自动续行。"
102
- : "已解除中断标记,但配置里 autoContinue 是 false,所以仍然不会自动续行。",
101
+ ? "已继续:停在岔口上的那一步已经放行,自动续行也重新允许了。"
102
+ : "已继续:停在岔口上的那一步已经放行。配置里 autoContinue 是 false,所以模型自己停下后不会再被自动拉起。",
103
103
  };
104
104
  }
105
105
 
package/lib/http.js CHANGED
@@ -1,26 +1,26 @@
1
1
  /**
2
- * 设置面板用的 HTTP 接口。
2
+ * 任务树条目用的 HTTP 接口。
3
3
  *
4
- * 面板跑在浏览器里,拿不到宿主内存中的 store,所以数据要经由这里出去。
4
+ * 条目跑在浏览器里,拿不到宿主内存中的 store,所以数据要经由这里出去。
5
5
  * 全部挂在 `/dsh-tree-task-flow/` 前缀下;只读写插件**自己的**状态目录,
6
6
  * 不碰会话日志(那是后端内部格式)。
7
7
  *
8
- * 面板要显示两样东西,都在这里算好给它:
8
+ * 条目要显示三样东西,都在这里算好给它:
9
9
  *
10
- * - **每层的结果**:节点完成时提交的文本。面板不自己去挖计划树的深层结构,
10
+ * - **每层的结果**:节点完成时提交的文本。条目不自己去挖计划树的深层结构,
11
11
  * 接口直接把三层各自的结果摆出来。
12
12
  * - **检查点状态**:一个节点自己的子节点全部结束、而它自己还开着的那一刻。
13
13
  * 此刻只有两条路——继续拆子节点,或者提交结果并完成它。
14
+ * - **暂停状态**:条目上的暂停/继续按钮写的就是这个。暂停挂的是
15
+ * `agent/pre-step`(见 `pause.js`),不插消息、不改计划,所以它不属于计划树,
16
+ * 单独回传。
14
17
  *
15
- * 面板上的按钮都写计划树,所以写完要把**新的**活动路径、结果与检查点一并回传,
16
- * 免得面板还要再发一次请求才知道自己按出了什么效果。
18
+ * 条目上的按钮都写计划树,所以写完要把**新的**活动路径、结果与检查点一并回传,
19
+ * 免得条目还要再发一次请求才知道自己按出了什么效果。
17
20
  *
18
21
  * @module dsh-tree-task-flow/http
19
22
  */
20
23
 
21
- import { readdirSync } from "node:fs";
22
- import { join } from "node:path";
23
-
24
24
  import { activePath, checkpointOf, complete, dropNode, levelLabel } from "./plan.js";
25
25
 
26
26
  /** 统一的 JSON 响应。 */
@@ -97,63 +97,15 @@ function afterWrite(plan) {
97
97
  };
98
98
  }
99
99
 
100
- /** 列出状态目录里所有**有计划的会话**。 */
101
- function listSessions(store) {
102
- let names = [];
103
- try {
104
- names = readdirSync(join(store.root, "plans"));
105
- } catch {
106
- return [];
107
- }
108
-
109
- const out = [];
110
- for (const name of names) {
111
- if (!name.endsWith(".json")) continue;
112
- const sessionId = name.slice(0, -".json".length);
113
- const plan = store.readPlan(sessionId);
114
- if (plan === null) continue;
115
- const steps = (plan.goal?.tasks ?? []).flatMap((task) => task.steps ?? []);
116
- out.push({
117
- sessionId,
118
- goal: plan.goal?.title ?? "(未命名目标)",
119
- status: plan.goal?.status ?? "pending",
120
- updatedAt: plan.updatedAt ?? 0,
121
- steps: steps.length,
122
- done: steps.filter((step) => step.status === "done").length,
123
- result: plan.goal?.result?.text ?? null,
124
- checkpoint: checkpointInfo(plan),
125
- });
126
- }
127
- out.sort((a, b) => b.updatedAt - a.updatedAt);
128
- return out;
129
- }
130
-
131
100
  /**
132
101
  * 注册全部面板接口。
133
102
  *
134
103
  * @param ctx - 挂载本行的 Cordis 上下文。
135
- * @param options - `{ store, cfg, autoContinue }`。
104
+ * @param options - `{ store, autoContinue, pause }`;未启用时后两者是 null。
136
105
  */
137
- export function registerHttp(ctx, { store, cfg, autoContinue }) {
106
+ export function registerHttp(ctx, { store, autoContinue, pause = null }) {
138
107
  ctx.inject(["webServer"], (host) => {
139
108
  host.effect(() => {
140
- // 会话列表:面板顶部的下拉框。
141
- host.webServer.register(
142
- {
143
- kind: "exact",
144
- path: "/dsh-tree-task-flow/sessions",
145
- handler: async (request, response) => {
146
- if (request.method !== "GET") return sendJson(response, 405, { ok: false, error: "只支持 GET" });
147
- sendJson(response, 200, {
148
- ok: true,
149
- enabled: cfg.enabled === true,
150
- sessions: listSessions(store),
151
- });
152
- },
153
- },
154
- "dsh-tree-task-flow: sessions",
155
- );
156
-
157
109
  // 一棵树的全部信息:结构 + 每层结果 + 检查点 + 当前路径 + 三层折叠游标。
158
110
  host.webServer.register(
159
111
  {
@@ -177,6 +129,7 @@ export function registerHttp(ctx, { store, cfg, autoContinue }) {
177
129
  step: cursors.step ?? null,
178
130
  },
179
131
  stopped: autoContinue?.isStopped?.(sessionId) === true,
132
+ paused: pause?.isPaused?.(sessionId) === true,
180
133
  });
181
134
  },
182
135
  },
@@ -194,10 +147,13 @@ export function registerHttp(ctx, { store, cfg, autoContinue }) {
194
147
  const sessionId = String(body.sessionId ?? "");
195
148
  const action = String(body.action ?? "");
196
149
 
197
- if (action === "stop" || action === "resume") {
198
- autoContinue?.[action]?.(sessionId);
150
+ // 暂停 / 继续。暂停会同时压住自动续行兜底,两个状态一起回传,
151
+ // 免得条目还要再问一次。
152
+ if (action === "pause" || action === "resume") {
153
+ pause?.[action]?.(sessionId);
199
154
  return sendJson(response, 200, {
200
155
  ok: true,
156
+ paused: pause?.isPaused?.(sessionId) === true,
201
157
  stopped: autoContinue?.isStopped?.(sessionId) === true,
202
158
  });
203
159
  }
package/lib/index.js CHANGED
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * ```
7
7
  * 一个节点的上下文 =
8
- * ① 节点 0:会话级稳定前缀(身份 / 工具 / 静态说明) ← 永不变化
8
+ * ① 节点 0:会话级稳定前缀(身份 / 工具 / 工具说明) ← 不随计划变化
9
9
  * + ② 这一层已有的汇总消息 ← 同级逐条累积,不被卷走
10
10
  * + ③ 当前节点的执行过程 ← 完成时整段折叠成一条汇总
11
11
  * ```
@@ -20,6 +20,7 @@
20
20
  import { registerAutoContinue } from "./auto-continue.js";
21
21
  import { registerCommands } from "./commands.js";
22
22
  import { registerHttp } from "./http.js";
23
+ import { registerPause } from "./pause.js";
23
24
  import { DEFAULT_ORDER, registerPromptSection } from "./prompt.js";
24
25
  import { registerRegion } from "./region.js";
25
26
  import { createStore, defaultRoot } from "./store.js";
@@ -57,7 +58,7 @@ export const DEFAULT_CONFIG = {
57
58
  autoContinue: false,
58
59
  /** 连续自动续行的轮次上限,达到就停。 */
59
60
  maxAutoRounds: 5,
60
- /** 静态说明段落的排序。 */
61
+ /** 工具说明段落的排序。 */
61
62
  promptOrder: DEFAULT_ORDER,
62
63
  /** 状态目录;null 表示 $DSH_HOME/dsh-task-tree。 */
63
64
  rootDir: null,
@@ -77,20 +78,21 @@ export function apply(ctx, config) {
77
78
  // 总开关默认**关**。未启用时一个字节都不碰会话:不注册工具、不加提示词段落、
78
79
  // 不折叠上下文、不续行——只留一条命令,让人能确认"装了但没开"以及怎么开。
79
80
  if (cfg.enabled !== true) {
80
- registerCommands(ctx, { store, cfg, autoContinue: null, enabled: false });
81
- // 面板接口照挂:设置页要能显示"装了但没启用"以及启用后的影响。
82
- // 只读自己的状态目录,不碰任何会话。
83
- registerHttp(ctx, { store, cfg, autoContinue: null });
81
+ registerCommands(ctx, { store, cfg, pause: null, enabled: false });
82
+ // 条目接口照挂:客户端条目与 enabled 无关,照样会来读。读不到计划它就返回
83
+ // null,一个像素都不占;而既有的计划文件仍然能看到。
84
+ // 只读插件自己的状态目录,不碰任何会话。
85
+ registerHttp(ctx, { store, autoContinue: null, pause: null });
84
86
  ctx.logger?.warn?.(
85
87
  "dsh-tree-task-flow: 已装载但**未启用**(enabled=false)。要启用请在 profile 的 cordis.patch.yml 里设为 true。",
86
88
  );
87
89
  return;
88
90
  }
89
91
 
90
- // 静态说明段。必须包在 effect 里,否则热更新后会重复注册而抛错。
92
+ // 工具说明段。必须包在 effect 里,否则热更新后会重复注册而抛错。
91
93
  ctx.effect(registerPromptSection(ctx, cfg.promptOrder), "dsh-tree-task-flow.section");
92
94
 
93
- // 六个工具,以及检查点上的工具闸门。tools 已在 inject 里声明,这里必定就绪。
95
+ // 六个工具。tools 已在 inject 里声明,这里必定就绪。
94
96
  registerTools(ctx, { store, cfg });
95
97
 
96
98
  // 节点完成时把它的执行过程折叠成一条携带结果的汇总消息。
@@ -100,11 +102,15 @@ export function apply(ctx, config) {
100
102
  // 让人随时能一句话停下来。
101
103
  const autoContinue = registerAutoContinue(ctx, { store, cfg });
102
104
 
103
- // /task-tree 命令:查看与管理任务树,也是"停止自动执行"的开关。
104
- registerCommands(ctx, { store, cfg, autoContinue, enabled: true });
105
+ // 暂停闸门:挂在 `agent/pre-step` 上,人点暂停就停在"上一个工具跑完、
106
+ // 下一次模型请求还没发出"那个岔口。它同时压住上面的自动续行兜底。
107
+ const pause = registerPause(ctx, { autoContinue });
105
108
 
106
- // 任务树条目与设置页要用的 HTTP 接口。
107
- registerHttp(ctx, { store, cfg, autoContinue });
109
+ // /task-tree 命令:查看与管理任务树,也是"暂停 / 继续"的开关。
110
+ registerCommands(ctx, { store, cfg, pause, enabled: true });
111
+
112
+ // 任务树条目要用的 HTTP 接口。
113
+ registerHttp(ctx, { store, autoContinue, pause });
108
114
 
109
115
  ctx.logger?.info?.(`dsh-tree-task-flow: 已启用(状态目录 ${root})`);
110
116
  }
package/lib/pause.js ADDED
@@ -0,0 +1,138 @@
1
+ /**
2
+ * 暂停 / 继续。
3
+ *
4
+ * 人按下暂停时想要的是"**现在就停,别再往下跑**",而且**不要留下痕迹**:
5
+ * 不要插一条消息进对话,也不要凭空多出一轮模型请求。
6
+ *
7
+ * 能同时满足这两点的位置只有一个:**`agent/pre-step`**。
8
+ *
9
+ * 一个 step 走完、工具结果都已落盘之后,驱动循环会回到起点,向 `agent/pre-step`
10
+ * 询问"下一步进不进、带哪些消息进去"(`agent-loop` 的 `turn()` 里那次
11
+ * `await this.preStep(target, …)`)。此刻的状态恰好是:
12
+ *
13
+ * - 正在执行的工具**已经正常跑完**,结果也在会话里了——不打断它;
14
+ * - 模型**还没**发起下一次请求——拦在这里就真的什么都不会发生;
15
+ * - 消息已被 claim 但尚未提交,轮次也还开着——放行后模型从原处接着请求,
16
+ * 上下文里**零新增消息**。
17
+ *
18
+ * 所以本模块做的事只有一件:在这个岔口上挂住,等人点继续。
19
+ *
20
+ * 相比之下另外两个候选都不合适:
21
+ * - `agent/turn-stopping` 虽然也是被 await 的,但轮次紧接着就会关闭
22
+ * (`agent-loop` 里 `if (turnEnds && this.inbox.nextStep.length === 0) break;`),
23
+ * 想让它继续跑就必须 `steer()` 一条消息进去——那就留下了痕迹。
24
+ * - 工具结果里的 `concludesTurn: true` 只是提前收尾,同样得靠新输入才能再启动。
25
+ *
26
+ * ★ **闸门只拦自动推进,不拦真人发言。** 暂停期间人自己发了消息,说明人要它跑,
27
+ * 这时既不放闸也不该继续端着:直接放行,并顺手解除暂停。
28
+ *
29
+ * @module dsh-tree-task-flow/pause
30
+ */
31
+
32
+ /**
33
+ * 注册暂停闸门。
34
+ *
35
+ * @param ctx - 挂载本行的 Cordis 上下文。
36
+ * @param options - `{ autoContinue }`:自动续行兜底的控制接口,可为 null。
37
+ * 暂停要同时压住它,否则模型刚停下、兜底就把会话又 followup 拉起来了。
38
+ */
39
+ export function registerPause(ctx, { autoContinue = null } = {}) {
40
+ /** 处于暂停态的会话。 */
41
+ const paused = new Set();
42
+ /**
43
+ * 正挂在 pre-step 上的会话 → 唤醒它们的那些函数。
44
+ *
45
+ * 存成集合而不是单个函数:驱动循环虽然串行,一个会话同一时刻只会有一个
46
+ * pre-step 在等,但"继续"必须是幂等且彻底的——万一有第二个挂上来,
47
+ * 后一个把前一个的唤醒函数盖掉,先挂住的那次就永远醒不过来了。
48
+ */
49
+ const waiters = new Map();
50
+
51
+ /**
52
+ * 挂起当前会话,直到有人点继续、或这一轮被取消。
53
+ *
54
+ * @param sessionId - 要挂起的会话。
55
+ * @param signal - 本轮的取消信号。人在界面上按停止时它会 abort,
56
+ * 必须跟着醒来——否则轮次会被永远挂住,连停都停不下来。
57
+ * @returns 可以放行时兑现的 promise(不传递任何值)。
58
+ */
59
+ function waitForResume(sessionId, signal) {
60
+ return new Promise((resolve) => {
61
+ let settled = false;
62
+ const release = () => {
63
+ if (settled) return;
64
+ settled = true;
65
+ const pending = waiters.get(sessionId);
66
+ if (pending !== undefined) {
67
+ pending.delete(release);
68
+ if (pending.size === 0) waiters.delete(sessionId);
69
+ }
70
+ signal?.removeEventListener?.("abort", release);
71
+ resolve();
72
+ };
73
+ const pending = waiters.get(sessionId) ?? new Set();
74
+ pending.add(release);
75
+ waiters.set(sessionId, pending);
76
+ if (signal?.aborted === true) {
77
+ release();
78
+ return;
79
+ }
80
+ signal?.addEventListener?.("abort", release, { once: true });
81
+ });
82
+ }
83
+
84
+ // waterfall:必须 await next() 并把它返回,否则会截断监听器链。
85
+ // auto-continue.js 也注册了同一个事件(重置续行配额),两条互不干扰。
86
+ ctx.on("agent/pre-step", async (payload, next) => {
87
+ const sessionId = payload?.agent?.session?.id;
88
+ if (sessionId === undefined || !paused.has(sessionId)) return next();
89
+
90
+ // 真人发言 = 人要它继续跑。放行,并且不再端着暂停态。
91
+ const fromHuman = (payload?.messages ?? []).some(
92
+ (message) => message?.source?.kind === "user",
93
+ );
94
+ if (fromHuman) {
95
+ paused.delete(sessionId);
96
+ return next();
97
+ }
98
+
99
+ await waitForResume(sessionId, payload?.signal);
100
+ return next();
101
+ });
102
+
103
+ return {
104
+ /**
105
+ * 暂停某个会话:之后的每一次自动推进都停在 pre-step。
106
+ * 正在执行的工具不受影响,它跑完才会走到这里。
107
+ *
108
+ * @param sessionId - 要暂停的会话。
109
+ */
110
+ pause(sessionId) {
111
+ if (sessionId === undefined) return;
112
+ paused.add(sessionId);
113
+ autoContinue?.stop?.(sessionId);
114
+ },
115
+ /**
116
+ * 继续某个会话:放行挂在 pre-step 上的那一步。
117
+ * 不推送任何消息——模型只是从它原本要请求的地方接着请求。
118
+ *
119
+ * @param sessionId - 要放行的会话。
120
+ */
121
+ resume(sessionId) {
122
+ if (sessionId === undefined) return;
123
+ paused.delete(sessionId);
124
+ const pending = waiters.get(sessionId);
125
+ if (pending !== undefined) for (const release of [...pending]) release();
126
+ autoContinue?.resume?.(sessionId);
127
+ },
128
+ /**
129
+ * 这个会话当前是不是暂停着。
130
+ *
131
+ * @param sessionId - 要查询的会话。
132
+ * @returns 暂停中为 true。
133
+ */
134
+ isPaused(sessionId) {
135
+ return paused.has(sessionId);
136
+ },
137
+ };
138
+ }
package/lib/plan.js CHANGED
@@ -323,7 +323,7 @@ export function updateNode(plan, id, patch, now = Date.now()) {
323
323
  }
324
324
 
325
325
  /**
326
- * 层级的显示名。树渲染、检查点提示、闸门拒绝文案都用它,保证三处称呼一致。
326
+ * 层级的显示名。树渲染、检查点提示、命令回执都用它,保证几处称呼一致。
327
327
  *
328
328
  * @param level - `goal` / `task` / `step`。
329
329
  * @returns 中文称呼。
@@ -334,12 +334,12 @@ export function levelLabel(level) {
334
334
  return "子任务";
335
335
  }
336
336
 
337
- /** 检查点那句话:只放行拆子节点与提交结果两条路。 */
337
+ /** 检查点那句话:把判断权交还给模型,不假装替它规定了出路。 */
338
338
  function checkpointNote(checkpoint) {
339
339
  const label = levelLabel(checkpoint.level);
340
340
  return (
341
341
  `${label}「${checkpoint.node.title}」的子节点已全部结束,转入检查点。` +
342
- `此刻只有两条路:用 tree_task_plan 继续给 ${checkpoint.node.id} 拆子节点,` +
342
+ `此刻轮到你的判断:用 tree_task_plan 继续给 ${checkpoint.node.id} 拆子节点,` +
343
343
  `或者用 tree_task_done(id="${checkpoint.node.id}", result="…") 提交这个${label}的结果并完成它。`
344
344
  );
345
345
  }
@@ -402,9 +402,8 @@ export function renderContinue(plan) {
402
402
  const label = levelLabel(checkpoint.level);
403
403
  return [
404
404
  `检查点:${label}「${checkpoint.node.title}」(${checkpoint.node.id}) 的子节点已全部结束。`,
405
- `此刻只放行两个工具:tree_task_plan(继续给 ${checkpoint.node.id} 拆子节点)与 ` +
406
- `tree_task_done(提交这个${label}的结果并完成它)。`,
407
- `不要再调用别的工具。`,
405
+ `此刻轮到你的判断:用 tree_task_plan 继续给 ${checkpoint.node.id} 拆子节点,` +
406
+ `或者用 tree_task_done(id="${checkpoint.node.id}", result="…") 提交这个${label}的结果并完成它。`,
408
407
  ].join("\n");
409
408
  }
410
409
 
package/lib/prompt.js CHANGED
@@ -1,13 +1,11 @@
1
1
  /**
2
- * 系统提示词段落:**只有一段,静态,会话内逐字节不变**。
2
+ * 系统提示词段落:**只有一段**,讲的是"什么时候用这组工具、完成时守什么规矩"。
3
3
  *
4
- * 这里只说明"这组工具是什么、怎么配合用"。计划的当前状态由 `tree_task_status`
5
- * 现取;已完成节点的产出一部分在树上(首行),全文在完成时注入的那条汇总消息里
6
- * ——都不放进这里。
4
+ * 工具清单**不在这里**——每个工具的用法由它自己的 `description` 承担。同一句话
5
+ * 写在两处,只会让它在每一次请求里占两份位置,而模型真要调用时读的始终是
6
+ * `description`。计划当前的状态同样不在这里,由 `tree_task_status` 现取。
7
7
  *
8
- * 之所以坚持静态:提示词一变,DSH 就会往 surface 上追加一整份新副本
9
- * (默认 in-history 路由);万一撞上归一化,还会重写节点 0,
10
- * 让整个上下文从第一个 token 起失去缓存。所以这里一个字都不动。
8
+ * 写法与内置工具段落一致:`text` 是个函数,工具不在当前作用域里就输出空串。
11
9
  *
12
10
  * @module dsh-tree-task-flow/prompt
13
11
  */
@@ -15,34 +13,28 @@
15
13
  /** 段落名。注册表要求全局唯一。 */
16
14
  export const SECTION_NAME = "dsh-tree-task-flow";
17
15
 
18
- /** 默认排序。放在部署人设(0)、计划策略(500)之后,所有内置工具指引之前。 */
19
- export const DEFAULT_ORDER = 2000;
16
+ /**
17
+ * 默认排序。
18
+ *
19
+ * 落在 TOOL_PTY(1700)与 TOOL_WEB_SEARCH(2000)之间的空档:与内置工具指引同区,
20
+ * 又不与任何已占用的号相撞。内置贡献方靠 `getSectionOrder` 取中央分配的位置,
21
+ * 外部插件没有这个待遇,只能自己挑一个不冲突的有限值。
22
+ */
23
+ export const DEFAULT_ORDER = 1800;
20
24
 
21
25
  /** 段落正文。 */
22
26
  export const SECTION_TEXT = `## 树形任务流(tree_task_* 工具组)
23
27
 
24
- 一件事需要多步才能做完时,用 tree_task_* 这组工具把它组织成一棵树,
25
- 而不是把计划写在回复正文里。层级固定三级:**目标 → 任务 → 子任务**;
28
+ 一件事需要多步才能做完时,用这组工具把它组织成**目标 → 任务 → 子任务**三级树,
29
+ 而不是把计划写在回复正文里,也不要再用 todo_write 另记一份。层级固定三级,
26
30
  子任务是最低一级,不能再往下拆。
27
31
 
28
- - **tree_task_create** —— 建立一个计划:一个目标 + 若干任务。会替换掉现有计划,只在开始新目标时用。
29
- - **tree_task_plan** —— 给一个节点追加子节点:给目标追加任务,给任务追加子任务。
30
- - **tree_task_status** —— 查看整棵树、各节点状态与已提交的结果,以及现在该推进哪个节点。
31
- - **tree_task_done** —— 完成一个节点并提交它的结果。**result 必填。**
32
- - **tree_task_update** —— 修改一个节点的标题或说明。
33
- - **tree_task_drop** —— 丢弃一个节点;丢弃任务会连同它的子任务一起丢弃。改需求用它,不要重建整棵树。
34
-
35
- 三条规则:
36
-
37
- 1. **完成必须写 result。** 一个节点完成时,插件会把它从开始到现在的执行过程
38
- 从上下文里折叠掉,换成一条只携带这段结果的汇总消息。所以 result 要写清它
39
- 究竟产出了什么,写成能独立看懂的样子——过程会被丢掉,别写在里面。
40
- 2. **子任务全部结束,不会让父节点自动完成**,而是转入**检查点**。那一刻只剩两条路:
41
- 用 tree_task_plan 继续给这个节点拆子节点,或者用 tree_task_done 提交它的结果。
42
- 检查点期间其它工具一律被拒绝——"这一层到底做完没有"要由你判断,不能被计数规则替你定死。
43
- 3. 折叠只发生在完成的那一刻:已完成节点的执行过程不再可见,只留下它提交的结果,
44
- 而同级节点各自的结果都会保留下来。需要看全貌——哪些做完了、各自产出了什么——
45
- 调用 tree_task_status,不要凭印象猜。`;
32
+ - **完成必须提交能独立看懂的 result。** 这个节点从开始到现在的执行过程会被整段
33
+ 折叠掉,只留下这条结果;同级已完成的汇总不会被后来的折叠卷走。要看全貌就调
34
+ tree_task_status,不要凭印象猜。
35
+ - **子任务全部结束后,父节点不会自动完成。** 那一刻轮到你的判断:继续用
36
+ tree_task_plan 拆子节点,还是用 tree_task_done 提交结果收尾。插件不会替你决定
37
+ 这一层做完没有。`;
46
38
 
47
39
  /**
48
40
  * 注册段落。调用方必须包在 `ctx.effect()` 里,否则热更新后会重复注册而抛错。
@@ -52,5 +44,11 @@ export const SECTION_TEXT = `## 树形任务流(tree_task_* 工具组)
52
44
  * @returns 注册返回的 disposer(由 ctx.effect 持有)。
53
45
  */
54
46
  export function registerPromptSection(ctx, order) {
55
- return () => ctx.systemPrompt.section({ name: SECTION_NAME, order, text: SECTION_TEXT });
47
+ return () =>
48
+ ctx.systemPrompt.section({
49
+ name: SECTION_NAME,
50
+ order,
51
+ text: ({ scope }) =>
52
+ ctx.tools.get("tree_task_status", scope) === undefined ? "" : SECTION_TEXT,
53
+ });
56
54
  }