dsh-tree-task-flow 0.3.11 → 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/README.md +98 -20
- package/config.example.yml +12 -4
- package/cordis.patch.yml +9 -5
- package/lib/auto-continue.js +36 -3
- package/lib/commands.js +1 -1
- package/lib/compaction.js +26 -10
- package/lib/index.js +35 -4
- package/lib/interrupt.js +541 -0
- package/lib/pause.js +2 -4
- package/lib/plan.js +96 -30
- package/lib/region.js +115 -19
- package/lib/store.js +48 -0
- package/lib/tools.js +54 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
规则只有一条,**只看表层,与计划树无关**:
|
|
28
28
|
|
|
29
29
|
```
|
|
30
|
-
从「上一次 tree_task_done
|
|
30
|
+
从「上一次 tree_task_done 之后的第一条 tree_task_create 调用」之后
|
|
31
31
|
→ 到本次 tree_task_done 之前,这一段收走
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
起点是上一次 `tree_task_done`
|
|
35
|
-
会话里还没有过 `tree_task_done`
|
|
34
|
+
起点是上一次 `tree_task_done` 之后的**第一条** `tree_task_create`;
|
|
35
|
+
会话里还没有过 `tree_task_done` 时,就是表层第一条 `tree_task_create`;
|
|
36
36
|
区间里一次 `tree_task_create` 都没有时,回退到上一次 `tree_task_done` 之后。
|
|
37
37
|
**`tree_task_plan` 不是边界**——它的调用与返回跟普通过程一样,随这一段一起收走。
|
|
38
38
|
起点那次 `create` 的调用与返回留在上下文里,本次 `tree_task_done` 的调用与返回
|
|
@@ -40,11 +40,17 @@
|
|
|
40
40
|
折叠只收它们**之间**的过程,换成一条计数通告:
|
|
41
41
|
|
|
42
42
|
```
|
|
43
|
-
[tree_task_create 调用] [返回:计划已建立 +
|
|
43
|
+
[tree_task_create 调用] [返回:计划已建立 + 这个目标的树]
|
|
44
44
|
[tree_task消息:隐藏了 12 条过程上下文。读取:… 写入:…]
|
|
45
45
|
[tree_task_done 调用] [返回:接下来需要进行步骤:「二」(s-4)。]
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
> **`tree_task_create` 与 `tree_task_plan` 不能合并成一个工具。**
|
|
49
|
+
> 起点取的是"上一次 `done` 之后最早的**开工**那一次",而 `plan` 每补一次步骤就调一次;
|
|
50
|
+
> 并成一个工具之后,每次补步骤都会把起点往后推,`done` 与它之间那段过程就永远收不走。
|
|
51
|
+
> 靠"实参里有没有 `parentId`"去区分,也只是把认错起点的机会交给模型。
|
|
52
|
+
> 代价是两个工具的实参形状有点像,所以 `plan` 的说明里专门写了一句"建新目标不归它管"。
|
|
53
|
+
|
|
48
54
|
两条硬约束:
|
|
49
55
|
|
|
50
56
|
- 折叠区间里**夹着你的消息就不折**——往前折丢了需求,往后折丢了回应,
|
|
@@ -101,17 +107,39 @@ tree_task消息:接下来需要进行步骤:「步骤二」(s-4)。
|
|
|
101
107
|
|
|
102
108
|
## 六个工具
|
|
103
109
|
|
|
110
|
+
**写树分两个工具,不是可以合并的重复。** `tree_task_create` 与 `tree_task_done` 是折叠的
|
|
111
|
+
两个边界:`create` 是"开工"那一下,`done` 是"收工"那一下。`create` 只在新建目标时调一次,
|
|
112
|
+
`plan` 却在任务中途反复调用——并成一个工具,折叠起点就只能靠实参去猜了(见上一节)。
|
|
113
|
+
|
|
104
114
|
| 工具 | 用途 |
|
|
105
115
|
|---|---|
|
|
106
|
-
| `tree_task_status` |
|
|
107
|
-
| `tree_task_create` |
|
|
108
|
-
| `tree_task_plan` |
|
|
116
|
+
| `tree_task_status` | 查看计划树:默认只展开**还没结束**的目标,已完成的目标只留目标那一行;填 `id` 就返回那个目标(或该节点所在目标)的完整树 |
|
|
117
|
+
| `tree_task_create` | 新建一个目标:这个目标 + 它下面的若干任务。可多次调用,多个目标并存,不替换已有计划。**它也是折叠的起点标记** |
|
|
118
|
+
| `tree_task_plan` | 给一个节点追加子节点:父节点是目标加的是任务,父节点是任务加的是子任务。**建新目标不归它管** |
|
|
109
119
|
| `tree_task_done` | 完成一个节点并提交它的结果,**`result` 必填**;返回的就是"下一步干什么",这次调用连同返回留在上下文里,被收走的只是它前面的执行过程 |
|
|
110
120
|
| `tree_task_update` | 修改一个节点的标题或说明 |
|
|
111
121
|
| `tree_task_drop` | 丢弃一个节点;丢弃目标会连同它下面的任务与子任务一起走,丢弃任务带上它的子任务 |
|
|
112
122
|
|
|
113
123
|
层级固定三级,子任务是最低一级,不能再往下拆。
|
|
114
124
|
|
|
125
|
+
**返回的三条规矩:**
|
|
126
|
+
|
|
127
|
+
- 写树类工具只返回**本次动到的那个目标**的树,不再每次倾倒整棵树;
|
|
128
|
+
- 树用**规范的 Markdown 嵌套任务清单**渲染(`- [ ]` 开头、每层两个空格缩进),
|
|
129
|
+
复制出去能正常预览,不会被当成代码块;
|
|
130
|
+
- **结果全文给出,一个字都不截**。
|
|
131
|
+
|
|
132
|
+
```markdown
|
|
133
|
+
- [x] **目标** `g-mv50h48v1` 安卓端播放
|
|
134
|
+
- **结果**:手机连上 PC 并把声音收下来了。
|
|
135
|
+
验收:模拟器上跑通一次端到端。
|
|
136
|
+
- [x] **任务** `t-mv50h48v2` 工程骨架
|
|
137
|
+
- **结果**:交付物 android\settings.gradle;`cd android; powershell -File tools\build.ps1` 冷构建成功。
|
|
138
|
+
- [ ] **子任务** `s-mv50h48v5` 写 build.gradle ← 当前
|
|
139
|
+
- [ ] **目标** `g-mv50h48v6` 安卓端来源 ← 当前
|
|
140
|
+
- [ ] **任务** `t-mv50h48v7` 采集层
|
|
141
|
+
```
|
|
142
|
+
|
|
115
143
|
### 三层怎么分
|
|
116
144
|
|
|
117
145
|
| 层 | 是什么 | 怎么算分对了 |
|
|
@@ -217,11 +245,49 @@ dsh --profile web --dump-config # 输出里应出现 task-tree 这一
|
|
|
217
245
|
| 字段 | 默认 | 说明 |
|
|
218
246
|
|---|---|---|
|
|
219
247
|
| `enabled` | `true` | 总开关;**装上即启用**,写 `false` 关掉 |
|
|
220
|
-
| `autoContinue` | `
|
|
248
|
+
| `autoContinue` | `true` | 模型停下时是否自动续行(见下节) |
|
|
221
249
|
| `maxAutoRounds` | `5` | 连续自动续行的轮次上限 |
|
|
250
|
+
| `interruptRecovery` | `true` | 意外中断后是否自动拉起(见下节) |
|
|
251
|
+
| `interruptDelaysMs` | `null` | 网络类中断的拉起时刻,`null` = 1/5/10/30 分钟 |
|
|
222
252
|
| `promptOrder` | `1800` | 工具说明段落的排序 |
|
|
223
253
|
| `rootDir` | `null` | 状态目录,默认 `$DSH_HOME/dsh-task-tree` |
|
|
224
254
|
|
|
255
|
+
## 意外中断自动拉起
|
|
256
|
+
|
|
257
|
+
上一节 `autoContinue` 管的是"模型自己正常停下"。这一节管的是**这一轮没能正常
|
|
258
|
+
结束**:网络断了、模型 API 报错、或者工具执行到一半进程没了。这两种停法插件原先不管,
|
|
259
|
+
会话就一直躺在那里。
|
|
260
|
+
|
|
261
|
+
判据全部取自 DSH 自己写下的数据,不看文本:
|
|
262
|
+
|
|
263
|
+
| 中断 | 怎么认 | 什么时候拉起 |
|
|
264
|
+
|---|---|---|
|
|
265
|
+
| 网络 / API | `turn/end` 的原因是 `error`,错误码是 `TRANSPORT`、`TIMEOUT`、`SERVER`、`RATE_LIMIT`、`EMPTY_RESPONSE` | 中断后 **1 分钟、5 分钟、10 分钟、30 分钟**各一次 |
|
|
266
|
+
| 模型输出损坏 | `turn/end` 的原因是 `error`,错误码是 `MALFORMED_RESPONSE`(模型给出的工具入参不是合法 JSON) | **立即**一次 |
|
|
267
|
+
| 工具执行 | 这一轮出现过 `TOOL_OUTCOME_UNKNOWN` / `TOOL_NOT_STARTED` 的合成工具结果(意思是"这次调用没留下结果") | **立即**一次 |
|
|
268
|
+
|
|
269
|
+
后两类虽然都"立即一次",处置并不一样——差别在于**有没有东西要勘察、有没有东西可收**:
|
|
270
|
+
|
|
271
|
+
- **工具执行类**:那次调用确实发生过,只是结果没落盘。拉起消息会明确说"它到底做没做
|
|
272
|
+
没人知道,先确认那一步的实际影响再决定要不要重做";第二次仍然失败时,把那条拉起消息、
|
|
273
|
+
发起该调用的助手消息、以及它的合成错误结果一起**收走**(和折叠同一套机制,界面上仍可
|
|
274
|
+
展开),再拉一次。
|
|
275
|
+
- **模型输出损坏类**:DSH 是在解析响应流时就判了死刑——**整条响应被作废,一个工具都没
|
|
276
|
+
执行,连 `tool/call` 都没落盘**,没有任何副作用要勘察。拉起消息只说"原样重做那一步,
|
|
277
|
+
把引号括号写全,必要时一次只调一个工具";第二次仍然失败时**什么都不收**(没东西可收),
|
|
278
|
+
直接再拉一次。
|
|
279
|
+
|
|
280
|
+
两类都是第三次同类中断就停手,链上记 `exhausted`。
|
|
281
|
+
|
|
282
|
+
几条边界:
|
|
283
|
+
|
|
284
|
+
- **余额不足(`QUOTA`)不拉。** 等再久也不会自己好。
|
|
285
|
+
- **你按下的停止,永远不拉。** 和自动续行的第 1 条闸门一致。
|
|
286
|
+
- **普通工具报错不拉。** 比如"文件没找到""edit 没匹配上""网址被拦"——这些模型看到就能
|
|
287
|
+
自己改道,本来也没中断这一轮。
|
|
288
|
+
- 只在**这个会话的计划树还没走完**时才动手。等待时刻写在状态目录的 `revive/` 下,
|
|
289
|
+
重启 `dsh web` 不会丢。
|
|
290
|
+
|
|
225
291
|
## 暂停与继续
|
|
226
292
|
|
|
227
293
|
界面上的暂停按钮(或 `/task-tree pause`)是**真暂停**,不是"关掉自动续行":
|
|
@@ -249,22 +315,32 @@ dsh --profile web --dump-config # 输出里应出现 task-tree 这一
|
|
|
249
315
|
暂停期间人按界面上的停止(中断本轮)也不会卡住:闸门盯着本轮的取消信号,
|
|
250
316
|
信号一 abort 就立刻放行。
|
|
251
317
|
|
|
252
|
-
##
|
|
318
|
+
## 自动续行
|
|
253
319
|
|
|
254
|
-
|
|
255
|
-
"请继续执行下一个子任务"
|
|
320
|
+
**模型自己停下、计划树却没跑完,插件就推一条"接下来要做 X"把它唤醒。** 这只是**兜底**:
|
|
321
|
+
`tree_task_done` 的返回文本里已经带了"请继续执行下一个子任务",模型通常会在同一轮里接着做。
|
|
256
322
|
|
|
257
|
-
|
|
323
|
+
`autoContinue` **默认开启**。一条总规矩:**插件还没有配置界面,功能就不能默认关**——
|
|
324
|
+
默认关等于这个功能不存在,没人会去翻 YAML 把它打开。等哪天真做了配置界面,再交给用户去配。
|
|
258
325
|
|
|
259
|
-
|
|
260
|
-
2. **中断即停**——用户按下停止(`agent/turn-stopping` 且 `signal.aborted`),
|
|
261
|
-
这个会话就被标记为不再自动续行,直到真人再发一条消息。
|
|
262
|
-
3. **轮次上限**——连续续行达到 `maxAutoRounds` 就停;只有真人发的消息才重置配额。
|
|
326
|
+
开着也不会脱缰,有四道闸门:
|
|
263
327
|
|
|
264
|
-
|
|
328
|
+
1. **中断即停**——你按下停止,落盘的 `turn/end` 会记成 `reason.kind = "aborted"`,
|
|
329
|
+
插件从那里认,把这个会话标记为不再自动续行(落盘在 `held/` 下,跨重启有效),
|
|
330
|
+
直到真人再发一条消息。
|
|
331
|
+
> **这里不能用 `agent/turn-stopping` 的 `signal.aborted` 当判据。** 那个事件只在
|
|
332
|
+
> "这一轮本来就要正常收尾"时才发(`dsh-agent-loop` 里唯一一处 dispatch 前面刚做过
|
|
333
|
+
> `throwIfAborted()`);你按停止会让信号 abort、从别处抛出,catch 里直接记下
|
|
334
|
+
> `turn/end` 就重新抛出,那条 dispatch 走不到——`signal.aborted` 在那里几乎永远是
|
|
335
|
+
> `false`,拿它当闸门等于没闸门。
|
|
336
|
+
2. **轮次上限**——连续续行达到 `maxAutoRounds`(默认 5)就停;只有真人发的消息才重置配额。
|
|
337
|
+
3. **不抢排队输入**——队列里还有你排着的话时,让用户先说。
|
|
338
|
+
4. **暂停压住它**——`/task-tree pause` 之后,即使模型自己停了下来,兜底也不会把它拉起来;
|
|
339
|
+
`/task-tree resume` 再放开。
|
|
265
340
|
|
|
266
|
-
|
|
267
|
-
|
|
341
|
+
> **`agent/status` 只在状态变化时发。** 重启 `dsh web` 之后,一个本来就停在 `idle` 上的
|
|
342
|
+
> 会话不会产生这个事件(新建的 agent 一开始就是 idle),所以它**不会**被自动唤醒——
|
|
343
|
+
> 要它动,你得在那个会话里先说一句话。从下一次模型自己停下开始,续行就接手了。
|
|
268
344
|
|
|
269
345
|
## 状态文件
|
|
270
346
|
|
|
@@ -274,6 +350,8 @@ dsh --profile web --dump-config # 输出里应出现 task-tree 这一
|
|
|
274
350
|
|---|---|
|
|
275
351
|
| `plans/<sessionId>.json` | 计划树(每个会话一棵) |
|
|
276
352
|
| `surfaces/<sessionId>.json` | 三层的进度游标 `{ key, cursor }`(只给界面看,不参与折叠) |
|
|
353
|
+
| `held/<sessionId>.json` | 暂停 / 停止状态(跨重启保留) |
|
|
354
|
+
| `revive/<sessionId>.json` | 意外中断的拉起时刻表(跨重启保留) |
|
|
277
355
|
|
|
278
356
|
> 目录名沿用插件改名前的标识,为的是不让既有的计划文件失联;插件名与它无关。
|
|
279
357
|
|
|
@@ -289,7 +367,7 @@ dsh --profile web --dump-config # 输出里应出现 task-tree 这一
|
|
|
289
367
|
空 `result` 会被拒绝。所以要写清"产出了什么",写成能独立看懂的样子——
|
|
290
368
|
过程会丢,别把过程写进去。
|
|
291
369
|
|
|
292
|
-
2. **折叠只看表层。** 起点是上一次 `done`
|
|
370
|
+
2. **折叠只看表层。** 起点是上一次 `done` 之后的第一条 `tree_task_create`;
|
|
293
371
|
`tree_task_plan` 不是边界,它的调用与返回跟过程一样会被收走;区间里没有过程时
|
|
294
372
|
什么都不注入,夹着你的消息时整段不折——那一段原样留着(往前折丢需求、
|
|
295
373
|
往后折丢回应),代价是上下文不收缩。
|
package/config.example.yml
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# 这个文件只是示例,插件不需要它——**安装即启用**,装完就能用。
|
|
2
2
|
#
|
|
3
|
-
#
|
|
3
|
+
# **默认值一律是"开"。** 插件还没有配置界面,功能默认关等于这个功能不存在。
|
|
4
|
+
# 想改成默认以外的东西时,把下面这段抄进 profile 自己的 cordis.patch.yml
|
|
4
5
|
# (通常是 $DSH_HOME/profiles/<名字>/cordis.patch.yml):
|
|
5
6
|
#
|
|
6
7
|
# - id: task-tree
|
|
@@ -15,11 +16,18 @@
|
|
|
15
16
|
# 总开关。默认 true(装上即启用);写 false 就一句话关掉,
|
|
16
17
|
# 关掉后插件不注册工具、不加提示词、不折叠上下文、不续行,一个字节都不碰会话。
|
|
17
18
|
enabled: true
|
|
18
|
-
# 模型停下时是否自动续行。默认
|
|
19
|
-
#
|
|
20
|
-
|
|
19
|
+
# 模型停下时是否自动续行。默认 true —— 一条总规矩:插件还没有配置界面,
|
|
20
|
+
# 功能就不能默认关(默认关等于这个功能不存在)。怕它停不下来不必靠这里:
|
|
21
|
+
# 你按下停止会落盘 held/stopped,续行自己就被拦住,真人再发一条消息才解除。
|
|
22
|
+
autoContinue: true
|
|
21
23
|
# 连续自动续行的轮次上限,达到就停;只有真人发言才重置配额。
|
|
22
24
|
maxAutoRounds: 5
|
|
25
|
+
# 意外中断后是否自动拉起(见 README「意外中断自动拉起」)。默认 true:
|
|
26
|
+
# 它只在「这一轮不是正常结束」时动手,正常停下不受影响。
|
|
27
|
+
interruptRecovery: true
|
|
28
|
+
# 网络/API 类中断的拉起时刻,单位毫秒,默认 [1分钟, 5分钟, 10分钟, 30分钟];
|
|
29
|
+
# null 表示用内置默认值。工具执行中断不看这张表,它永远立即拉一次。
|
|
30
|
+
interruptDelaysMs: null
|
|
23
31
|
# 工具说明段落的排序。1800 落在内置的 TOOL_PTY(1700) 与 TOOL_WEB_SEARCH(2000)
|
|
24
32
|
# 之间的空档,不与任何内置段落撞号。
|
|
25
33
|
promptOrder: 1800
|
package/cordis.patch.yml
CHANGED
|
@@ -11,11 +11,15 @@
|
|
|
11
11
|
# ⚠️ 同 id 的 config 是**整体替换**,不是逐字段深合并——覆盖时要把需要的
|
|
12
12
|
# 字段一次写全。可覆盖的字段见 DEFAULT_CONFIG:
|
|
13
13
|
#
|
|
14
|
-
# enabled
|
|
15
|
-
# autoContinue
|
|
16
|
-
# maxAutoRounds
|
|
17
|
-
#
|
|
18
|
-
#
|
|
14
|
+
# enabled 总开关 默认 true(写 false 关掉)
|
|
15
|
+
# autoContinue 是否允许自动续行 默认 true
|
|
16
|
+
# maxAutoRounds 连续自动续行轮次上限 默认 5
|
|
17
|
+
# interruptRecovery 意外中断是否自动拉起 默认 true
|
|
18
|
+
# interruptDelaysMs 网络类中断拉起时刻 默认 null(= 1/5/10/30 分钟)
|
|
19
|
+
# promptOrder 工具说明段顺序 默认 1800
|
|
20
|
+
# rootDir 插件状态目录 默认 $DSH_HOME/dsh-task-tree
|
|
21
|
+
#
|
|
22
|
+
# 默认值一律是"开"。插件还没有配置界面,功能默认关等于这个功能不存在。
|
|
19
23
|
- insert:
|
|
20
24
|
- id: task-tree
|
|
21
25
|
name: 'dsh-tree-task-flow'
|
package/lib/auto-continue.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { activePath, renderContinue } from "./plan.js";
|
|
19
|
+
import { pluginSource } from "./region.js";
|
|
19
20
|
|
|
20
21
|
/**
|
|
21
22
|
* 注册续行兜底。
|
|
@@ -42,12 +43,43 @@ export function registerAutoContinue(ctx, { store, cfg }) {
|
|
|
42
43
|
return stopped.has(sessionId) || store?.readHeld?.(sessionId)?.stopped === true;
|
|
43
44
|
}
|
|
44
45
|
|
|
46
|
+
/**
|
|
47
|
+
* 用户按下停止:**从落盘的 `turn/end` 认,不能从 `agent/turn-stopping` 认。**
|
|
48
|
+
*
|
|
49
|
+
* `agent/turn-stopping` 只在"这一轮本来就要正常收尾"时才发——`dsh-agent-loop` 里
|
|
50
|
+
* 唯一一处 dispatch 在 `turn()` 的 try 里,紧挨着前面那句 `signal.throwIfAborted()`。
|
|
51
|
+
* 用户按停止会让信号 abort、从别处抛出,catch 里直接记下
|
|
52
|
+
* `turn/end reason.kind = "aborted"` 并重新抛出,那条 dispatch 根本走不到。
|
|
53
|
+
* 也就是说 `payload.signal.aborted` 在那里几乎永远是 `false`——拿它当闸门等于没闸门:
|
|
54
|
+
* 会话被停掉之后 `agent/status` 一转 `idle`,续行立刻又把它拉起来。
|
|
55
|
+
*
|
|
56
|
+
* 可靠的事实是**落盘的 `turn/end`**:中止一定会在 `finally` 里被记成
|
|
57
|
+
* `reason: { kind: "aborted", reason: { kind: "user" } }`(cause 为 `user` 就是人按的,
|
|
58
|
+
* 另有 `parent` / `hook` / `disposed`)。而且它发生在驱动循环 `setPhase(idle)`
|
|
59
|
+
* 之前——`agent/status` 是之后才发的,所以这个标记一定赶得上那次判断。
|
|
60
|
+
*
|
|
61
|
+
* 中止就是中止:不管是谁按的,插件都不该自作主张把这一轮重新拉起来。
|
|
62
|
+
*/
|
|
63
|
+
ctx.on("session/event", (session, event) => {
|
|
64
|
+
if (event?.type !== "turn/end") return;
|
|
65
|
+
if (event.data?.reason?.kind !== "aborted") return;
|
|
66
|
+
const sessionId = session?.id;
|
|
67
|
+
if (sessionId === undefined) return;
|
|
68
|
+
stopped.add(sessionId);
|
|
69
|
+
// 落盘:重启后条目上仍看得到「继续」,也仍然压得住自动续行。
|
|
70
|
+
store?.patchHeld?.(sessionId, { stopped: true });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 次要路径,留着兜底:如果中止恰好发生在 `agent/turn-stopping` 的 dispatch 过程中
|
|
75
|
+
* (信号在那一刻才变 abort),上面那条 `turn/end` 判据同样会兜住它;这里只是让
|
|
76
|
+
* 标记更早一点落下。**不要把它当成主判据**,原因见上。
|
|
77
|
+
*/
|
|
45
78
|
ctx.on("agent/turn-stopping", (payload) => {
|
|
46
79
|
const sessionId = payload?.agent?.session?.id;
|
|
47
80
|
if (sessionId === undefined) return;
|
|
48
81
|
if (payload?.signal?.aborted === true) {
|
|
49
82
|
stopped.add(sessionId);
|
|
50
|
-
// 落盘:重启后条目上仍看得到「继续」,也仍然压得住自动续行。
|
|
51
83
|
store?.patchHeld?.(sessionId, { stopped: true });
|
|
52
84
|
}
|
|
53
85
|
});
|
|
@@ -57,7 +89,8 @@ export function registerAutoContinue(ctx, { store, cfg }) {
|
|
|
57
89
|
// 必须按来源区分,不能按"这一步有没有消息":`agent.followup()` 推的消息同样会
|
|
58
90
|
// 从 inbox 进入下一步的 `payload.messages`,那样每一轮都会把自己刚用掉的配额
|
|
59
91
|
// 重置回去,`maxAutoRounds` 也就失去了意义。
|
|
60
|
-
// `source.kind === 'user'` 才是真人,插件推的是 `
|
|
92
|
+
// `source.kind === 'user'` 才是真人,插件推的是 `plugin:dsh-tree-task-flow`
|
|
93
|
+
// (v3 老会话里是 `{ kind: 'plugin', plugin: 'dsh-tree-task-flow' }`;两种都不是 `'user'`)。
|
|
61
94
|
//
|
|
62
95
|
// 注意这是 waterfall:必须 await next() 并把它返回,否则会截断监听器链。
|
|
63
96
|
ctx.on("agent/pre-step", async (payload, next) => {
|
|
@@ -106,7 +139,7 @@ export function registerAutoContinue(ctx, { store, cfg }) {
|
|
|
106
139
|
id: `tt-continue-${sessionId}-${Date.now().toString(36)}`,
|
|
107
140
|
role: "user",
|
|
108
141
|
content: [{ type: "text", text: renderContinue(plan) }],
|
|
109
|
-
source:
|
|
142
|
+
source: pluginSource("计划续行"),
|
|
110
143
|
});
|
|
111
144
|
ctx.logger?.info?.(
|
|
112
145
|
`dsh-tree-task-flow: 自动续行 ${used + 1}/${cfg.maxAutoRounds}(步骤 ${active.step.id})`,
|
package/lib/commands.js
CHANGED
|
@@ -161,7 +161,7 @@ export function registerCommands(ctx, { store, cfg, pause = null, enabled = fals
|
|
|
161
161
|
if (enabled !== true) {
|
|
162
162
|
lines.push("", DISABLED_NOTE);
|
|
163
163
|
} else if (plan === null) {
|
|
164
|
-
lines.push("", "本会话还没有计划。用
|
|
164
|
+
lines.push("", "本会话还没有计划。用 tree_task_plan 建立目标与任务。");
|
|
165
165
|
} else {
|
|
166
166
|
// 检查点优先于"当前节点":那一刻要推进的不是某个子任务,
|
|
167
167
|
// 而是"这一层到底做完了没有"这个判断。
|
package/lib/compaction.js
CHANGED
|
@@ -10,8 +10,15 @@
|
|
|
10
10
|
* 又明确要求"不要提及这次压缩",模型不会自己想到去查。
|
|
11
11
|
*
|
|
12
12
|
* 怎么认:压缩留下的表层节点是一条 `user/message`,它的 `source` 由
|
|
13
|
-
* `compactCheckpointSource()`
|
|
14
|
-
*
|
|
13
|
+
* `compactCheckpointSource()` 构造。**两个形状都要认**:
|
|
14
|
+
*
|
|
15
|
+
* - v4 原生:`{ kind: "compact-checkpoint", compactionId, … }`
|
|
16
|
+
* (`COMPACT_CHECKPOINT_MARKER` 就是 `{ kind: "compact-checkpoint" }`);
|
|
17
|
+
* - v3 老写法:`{ kind: "plugin", plugin: "compact" }`——它只在 v3 → v4 迁移里
|
|
18
|
+
* 被改写成前者,迁移前的会话里仍是这个样子。
|
|
19
|
+
*
|
|
20
|
+
* 只认老写法,会让**已经迁到 v4 的会话彻底收不到压缩提示**(实测:本机
|
|
21
|
+
* session-2083c383 在 v4 里连续压缩两次,一条提示都没有)。
|
|
15
22
|
*
|
|
16
23
|
* **不要认 `compaction/summary` 事件**:那是压缩自己的记录,不带 `surfaceOp`,
|
|
17
24
|
* 因此根本不在表层里;真正替代被收走那段的是紧随其后那条带
|
|
@@ -35,14 +42,21 @@
|
|
|
35
42
|
import { summaryMessage } from "./region.js";
|
|
36
43
|
|
|
37
44
|
/**
|
|
38
|
-
*
|
|
45
|
+
* 压缩检查点的插件标记——**只有** v3 老写法用到它。
|
|
39
46
|
*
|
|
40
|
-
* 与 `@deepseek-ai/dsh-compaction` 的
|
|
41
|
-
*
|
|
42
|
-
* 就把一个 DSH 内部包写进自己的依赖里。
|
|
47
|
+
* 与 `@deepseek-ai/dsh-compaction` 的 V3 侧字面量保持一致。这里按形状判断而不是
|
|
48
|
+
* import 那个包:插件不该为了认一个标记,就把一个 DSH 内部包写进自己的依赖里。
|
|
43
49
|
*/
|
|
44
50
|
const COMPACTION_PLUGIN = "compact";
|
|
45
51
|
|
|
52
|
+
/**
|
|
53
|
+
* v4 原生压缩检查点的 source kind。
|
|
54
|
+
*
|
|
55
|
+
* 与 `@deepseek-ai/dsh-compaction` 的 `COMPACT_CHECKPOINT_MARKER` 一致
|
|
56
|
+
* (`{ kind: "compact-checkpoint" }`)。
|
|
57
|
+
*/
|
|
58
|
+
const COMPACTION_KIND = "compact-checkpoint";
|
|
59
|
+
|
|
46
60
|
/**
|
|
47
61
|
* 提示正文。
|
|
48
62
|
*
|
|
@@ -53,8 +67,8 @@ const NOTICE_TEXT =
|
|
|
53
67
|
"tree_task消息:上下文刚被压缩过,计划树没有被压缩。用 tree_task_status 取回当前进度、" +
|
|
54
68
|
"各节点已提交的 result,以及下一步该推进哪个节点。";
|
|
55
69
|
|
|
56
|
-
/**
|
|
57
|
-
const NOTICE_LINE = "
|
|
70
|
+
/** 界面上的一行摘要(**不含统一前缀**——前缀由 `pluginSource` 加)。 */
|
|
71
|
+
const NOTICE_LINE = "上下文被压缩,计划树仍在";
|
|
58
72
|
|
|
59
73
|
/** 提示消息的编号,保证同一毫秒内连发两条也不撞 id。 */
|
|
60
74
|
let noticeSeq = 0;
|
|
@@ -64,11 +78,13 @@ function noticeId() {
|
|
|
64
78
|
return `tt-compaction-${Date.now().toString(36)}-${noticeSeq.toString(36)}`;
|
|
65
79
|
}
|
|
66
80
|
|
|
67
|
-
/**
|
|
81
|
+
/** 这条表层节点是不是压缩落下的检查点(v4 新写法与 v3 老写法都认)。 */
|
|
68
82
|
function isCompactionCheckpoint(event) {
|
|
69
83
|
if (event?.type !== "user/message") return false;
|
|
70
84
|
const source = event.data?.source;
|
|
71
|
-
|
|
85
|
+
if (source === undefined || source === null) return false;
|
|
86
|
+
if (source.kind === COMPACTION_KIND) return true;
|
|
87
|
+
return source.kind === "plugin" && source.plugin === COMPACTION_PLUGIN;
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
/**
|
package/lib/index.js
CHANGED
|
@@ -21,6 +21,7 @@ import { registerAutoContinue } from "./auto-continue.js";
|
|
|
21
21
|
import { registerCommands } from "./commands.js";
|
|
22
22
|
import { registerCompactionNotice } from "./compaction.js";
|
|
23
23
|
import { registerHttp } from "./http.js";
|
|
24
|
+
import { registerInterruptRecovery } from "./interrupt.js";
|
|
24
25
|
import { registerPause } from "./pause.js";
|
|
25
26
|
import { DEFAULT_ORDER, registerPromptSection } from "./prompt.js";
|
|
26
27
|
import { registerRegion } from "./region.js";
|
|
@@ -54,13 +55,36 @@ export const DEFAULT_CONFIG = {
|
|
|
54
55
|
/**
|
|
55
56
|
* 是否允许在模型停下时自动续行。
|
|
56
57
|
*
|
|
57
|
-
* ★
|
|
58
|
-
*
|
|
59
|
-
*
|
|
58
|
+
* ★ **默认开启**。一条总规矩:**插件还没有配置界面,功能就不能默认关**——
|
|
59
|
+
* 默认关等于这个功能不存在,没人会去翻 YAML 把它打开。等哪天真做了配置界面,
|
|
60
|
+
* 再交给用户去配。
|
|
61
|
+
*
|
|
62
|
+
* 曾经担心"开着就停不下来"(用户按下停止、agent 回到 idle,插件立刻又把它拉起来)。
|
|
63
|
+
* **那个担心已经有闸门挡住了**:按停止会写 `held/<sessionId>.json` 的
|
|
64
|
+
* `stopped: true`(见 `auto-continue.js` 的 `agent/turn-stopping`),
|
|
65
|
+
* `agent/status` 一回 idle 就先看这个标记,是就一步都不动;真人再发一条消息才解除。
|
|
66
|
+
* 再加上 `maxAutoRounds` 的轮次上限,兜底不会变成脱缰。
|
|
60
67
|
*/
|
|
61
|
-
autoContinue:
|
|
68
|
+
autoContinue: true,
|
|
62
69
|
/** 连续自动续行的轮次上限,达到就停。 */
|
|
63
70
|
maxAutoRounds: 5,
|
|
71
|
+
/**
|
|
72
|
+
* 意外中断的自动拉起。
|
|
73
|
+
*
|
|
74
|
+
* ★ **默认开启**。它和 `autoContinue` 管的是两件事:`autoContinue` 是"模型自己
|
|
75
|
+
* 正常收尾了、但计划没做完",本项是"模型根本没机会说完就断了"——网络断了、
|
|
76
|
+
* 模型 API 报错、或者进程在工具执行中途没了。后者不加干预就永远停在那儿。
|
|
77
|
+
*
|
|
78
|
+
* 用户按停止(`aborted`)永远不拉起;余额不足(`QUOTA`)也不拉(等再久也不会好)。
|
|
79
|
+
*/
|
|
80
|
+
interruptRecovery: true,
|
|
81
|
+
/**
|
|
82
|
+
* 网络类中断的拉起时刻表,单位毫秒,相对中断那一刻。
|
|
83
|
+
*
|
|
84
|
+
* 默认 `[1 分钟, 5 分钟, 10 分钟, 30 分钟]`——四个**累计时刻**,不是递增间隔。
|
|
85
|
+
* 一条链只跑一遍;跑完还失败就停手,等真人发消息再允许重开。
|
|
86
|
+
*/
|
|
87
|
+
interruptDelaysMs: null,
|
|
64
88
|
/** 工具说明段落的排序。 */
|
|
65
89
|
promptOrder: DEFAULT_ORDER,
|
|
66
90
|
/** 状态目录;null 表示 $DSH_HOME/dsh-task-tree。 */
|
|
@@ -115,6 +139,13 @@ export function apply(ctx, config) {
|
|
|
115
139
|
// 下一次模型请求还没发出"那个岔口。它同时压住上面的自动续行兜底。
|
|
116
140
|
const pause = registerPause(ctx, { store, autoContinue });
|
|
117
141
|
|
|
142
|
+
// 意外中断的拉起:网络类按 1/5/10/30 分钟各一次,工具类(结果未知的合成错误)
|
|
143
|
+
// 立刻一次,再失败就收走那一次尝试再来一次。它与 autoContinue 并存:
|
|
144
|
+
// 那边管"模型正常收尾但计划没做完",这边管"模型没机会说完就断了"。
|
|
145
|
+
if (cfg.interruptRecovery === true) {
|
|
146
|
+
registerInterruptRecovery(ctx, { store, cfg, pause });
|
|
147
|
+
}
|
|
148
|
+
|
|
118
149
|
// /task-tree 命令:查看与管理任务树,也是"暂停 / 继续"的开关。
|
|
119
150
|
registerCommands(ctx, { store, cfg, pause, enabled: true });
|
|
120
151
|
|