dsh-tree-task-flow 0.2.0 → 0.3.1
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 +86 -38
- package/client.js +426 -463
- package/config.example.yml +27 -0
- package/cordis.patch.yml +5 -4
- package/lib/auto-continue.js +3 -5
- package/lib/commands.js +26 -26
- package/lib/http.js +12 -5
- package/lib/index.js +23 -16
- package/lib/pause.js +138 -0
- package/lib/plan.js +5 -6
- package/lib/prompt.js +28 -30
- package/lib/tools.js +1 -62
- package/package.json +2 -2
- package/enable.example.yml +0 -4
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# 这个文件只是示例,插件不需要它——**安装即启用**,装完就能用。
|
|
2
|
+
#
|
|
3
|
+
# 想改默认值时,把下面这段抄进 profile 自己的 cordis.patch.yml
|
|
4
|
+
# (通常是 $DSH_HOME/profiles/<名字>/cordis.patch.yml):
|
|
5
|
+
#
|
|
6
|
+
# - id: task-tree
|
|
7
|
+
# config:
|
|
8
|
+
# enabled: false
|
|
9
|
+
#
|
|
10
|
+
# ⚠️ 同 id 的 config 是**整体替换**,不是逐字段深合并:写进去的字段会一次性顶掉
|
|
11
|
+
# 整份默认配置,没写的字段回落到 `lib/index.js` 的 DEFAULT_CONFIG。所以要改
|
|
12
|
+
# 哪一项就把想保留的项一起写全。
|
|
13
|
+
- id: task-tree
|
|
14
|
+
config:
|
|
15
|
+
# 总开关。默认 true(装上即启用);写 false 就一句话关掉,
|
|
16
|
+
# 关掉后插件不注册工具、不加提示词、不折叠上下文、不续行,一个字节都不碰会话。
|
|
17
|
+
enabled: true
|
|
18
|
+
# 模型停下时是否自动续行。默认 false —— 开着意味着 agent 一空下来就被再推一把,
|
|
19
|
+
# 会出现「按下停止却停不下来」,所以它从不默认打开。
|
|
20
|
+
autoContinue: false
|
|
21
|
+
# 连续自动续行的轮次上限,达到就停;只有真人发言才重置配额。
|
|
22
|
+
maxAutoRounds: 5
|
|
23
|
+
# 工具说明段落的排序。1800 落在内置的 TOOL_PTY(1700) 与 TOOL_WEB_SEARCH(2000)
|
|
24
|
+
# 之间的空档,不与任何内置段落撞号。
|
|
25
|
+
promptOrder: 1800
|
|
26
|
+
# 插件状态目录;null 表示 $DSH_HOME/dsh-task-tree。
|
|
27
|
+
rootDir: null
|
package/cordis.patch.yml
CHANGED
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
#
|
|
3
3
|
# package.json 的 dsh.bundle.patch 指向本文件,因此安装成 bundle 后,
|
|
4
4
|
# 这一层会自动挂进 `dsh.profile.bundles` 配置栈(优先级低于 profile 自身的
|
|
5
|
-
# cordis.patch.yml
|
|
5
|
+
# cordis.patch.yml)。安装这个动作本身就把插件行插好了——**装上即启用**,
|
|
6
|
+
# 不需要再改任何配置。
|
|
6
7
|
#
|
|
7
|
-
# 这里刻意不写 config,全部走 lib/index.js 里的 DEFAULT_CONFIG
|
|
8
|
+
# 这里刻意不写 config,全部走 lib/index.js 里的 DEFAULT_CONFIG(enabled 默认 true)。
|
|
8
9
|
# 要临时关掉或改默认值,请在 profile 自己的 cordis.patch.yml 里覆盖这一行。
|
|
9
10
|
#
|
|
10
11
|
# ⚠️ 同 id 的 config 是**整体替换**,不是逐字段深合并——覆盖时要把需要的
|
|
11
12
|
# 字段一次写全。可覆盖的字段见 DEFAULT_CONFIG:
|
|
12
13
|
#
|
|
13
|
-
# enabled 总开关 默认 false
|
|
14
|
+
# enabled 总开关 默认 true(写 false 关掉)
|
|
14
15
|
# autoContinue 是否允许自动续行 默认 false(开着会让人停不下来)
|
|
15
16
|
# maxAutoRounds 连续自动续行轮次上限 默认 5
|
|
16
|
-
# promptOrder
|
|
17
|
+
# promptOrder 工具说明段顺序 默认 1800
|
|
17
18
|
# rootDir 插件状态目录 默认 $DSH_HOME/dsh-task-tree
|
|
18
19
|
- insert:
|
|
19
20
|
- id: task-tree
|
package/lib/auto-continue.js
CHANGED
|
@@ -9,10 +9,8 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 三条安全阀缺一不可(方案 §7.1):
|
|
11
11
|
*
|
|
12
|
-
* 1. **有活跃的内置 goal
|
|
13
|
-
*
|
|
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,18 +15,18 @@ const USAGE = [
|
|
|
18
15
|
"/task-tree 查看本会话的任务树与检查点状态",
|
|
19
16
|
"/task-tree done <id> <结果> 提交某个节点的结果并完成它",
|
|
20
17
|
"/task-tree drop <id> 丢弃某个节点",
|
|
21
|
-
"/task-tree
|
|
22
|
-
"/task-tree resume
|
|
18
|
+
"/task-tree pause 暂停:当前工具跑完后停住,不再往下推进",
|
|
19
|
+
"/task-tree resume 继续:从停住的地方接着跑,不插消息",
|
|
23
20
|
"/task-tree reset 清掉本会话的计划树",
|
|
24
21
|
].join("\n");
|
|
25
22
|
|
|
26
|
-
/**
|
|
23
|
+
/** 插件被配置关掉时唯一要说的话:现在是关的,以及怎么打开。 */
|
|
27
24
|
const DISABLED_NOTE =
|
|
28
|
-
"dsh-tree-task-flow
|
|
29
|
-
"
|
|
25
|
+
"dsh-tree-task-flow 已被配置**关闭**(config 里 enabled 不是 true):它没有注册工具," +
|
|
26
|
+
"也不会折叠任何上下文。要打开请在 profile 的 cordis.patch.yml 里把 enabled 设为 true。";
|
|
30
27
|
|
|
31
28
|
/**
|
|
32
|
-
* 把检查点那句话拼出来。措辞与 `plan.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
|
-
|
|
42
|
-
|
|
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,
|
|
51
|
-
*
|
|
47
|
+
* @param options - `{ store, cfg, pause, enabled }`;`pause` 是 `registerPause`
|
|
48
|
+
* 返回的控制接口,被关闭时是 null。
|
|
52
49
|
*/
|
|
53
|
-
export function registerCommands(ctx, { store, cfg,
|
|
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> /
|
|
64
|
-
input: { hint: "status | done <id> <结果> | drop <id> |
|
|
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) {
|
|
@@ -75,31 +72,34 @@ export function registerCommands(ctx, { store, cfg, autoContinue, enabled = fals
|
|
|
75
72
|
return { kind: "success", text: enabled === true ? USAGE : `${DISABLED_NOTE}\n\n${USAGE}` };
|
|
76
73
|
}
|
|
77
74
|
|
|
78
|
-
//
|
|
79
|
-
//
|
|
75
|
+
// 被关闭时插件没有注册工具,也就不会有计划树;除了确认关闭状态,
|
|
76
|
+
// 其余动词一概不执行——关掉的插件一个字节都不该碰会话。
|
|
80
77
|
if (enabled !== true && verb !== "status") {
|
|
81
78
|
return {
|
|
82
79
|
kind: "error",
|
|
83
|
-
text: `${DISABLED_NOTE}\n\n
|
|
80
|
+
text: `${DISABLED_NOTE}\n\n关闭状态下只有 /task-tree status 与 /task-tree help 可用。`,
|
|
84
81
|
};
|
|
85
82
|
}
|
|
86
83
|
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
96
|
+
pause?.resume?.(sessionId);
|
|
97
97
|
return {
|
|
98
98
|
kind: "success",
|
|
99
99
|
text:
|
|
100
100
|
cfg.autoContinue === true
|
|
101
|
-
? "
|
|
102
|
-
: "
|
|
101
|
+
? "已继续:停在岔口上的那一步已经放行,自动续行也重新允许了。"
|
|
102
|
+
: "已继续:停在岔口上的那一步已经放行。配置里 autoContinue 是 false,所以模型自己停下后不会再被自动拉起。",
|
|
103
103
|
};
|
|
104
104
|
}
|
|
105
105
|
|
package/lib/http.js
CHANGED
|
@@ -5,12 +5,15 @@
|
|
|
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
18
|
* 条目上的按钮都写计划树,所以写完要把**新的**活动路径、结果与检查点一并回传,
|
|
16
19
|
* 免得条目还要再发一次请求才知道自己按出了什么效果。
|
|
@@ -98,9 +101,9 @@ function afterWrite(plan) {
|
|
|
98
101
|
* 注册全部面板接口。
|
|
99
102
|
*
|
|
100
103
|
* @param ctx - 挂载本行的 Cordis 上下文。
|
|
101
|
-
* @param options - `{ store, autoContinue }
|
|
104
|
+
* @param options - `{ store, autoContinue, pause }`;插件被关闭时后两者是 null。
|
|
102
105
|
*/
|
|
103
|
-
export function registerHttp(ctx, { store, autoContinue }) {
|
|
106
|
+
export function registerHttp(ctx, { store, autoContinue, pause = null }) {
|
|
104
107
|
ctx.inject(["webServer"], (host) => {
|
|
105
108
|
host.effect(() => {
|
|
106
109
|
// 一棵树的全部信息:结构 + 每层结果 + 检查点 + 当前路径 + 三层折叠游标。
|
|
@@ -126,6 +129,7 @@ export function registerHttp(ctx, { store, autoContinue }) {
|
|
|
126
129
|
step: cursors.step ?? null,
|
|
127
130
|
},
|
|
128
131
|
stopped: autoContinue?.isStopped?.(sessionId) === true,
|
|
132
|
+
paused: pause?.isPaused?.(sessionId) === true,
|
|
129
133
|
});
|
|
130
134
|
},
|
|
131
135
|
},
|
|
@@ -143,10 +147,13 @@ export function registerHttp(ctx, { store, autoContinue }) {
|
|
|
143
147
|
const sessionId = String(body.sessionId ?? "");
|
|
144
148
|
const action = String(body.action ?? "");
|
|
145
149
|
|
|
146
|
-
|
|
147
|
-
|
|
150
|
+
// 暂停 / 继续。暂停会同时压住自动续行兜底,两个状态一起回传,
|
|
151
|
+
// 免得条目还要再问一次。
|
|
152
|
+
if (action === "pause" || action === "resume") {
|
|
153
|
+
pause?.[action]?.(sessionId);
|
|
148
154
|
return sendJson(response, 200, {
|
|
149
155
|
ok: true,
|
|
156
|
+
paused: pause?.isPaused?.(sessionId) === true,
|
|
150
157
|
stopped: autoContinue?.isStopped?.(sessionId) === true,
|
|
151
158
|
});
|
|
152
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";
|
|
@@ -42,11 +43,13 @@ export const DEFAULT_CONFIG = {
|
|
|
42
43
|
/**
|
|
43
44
|
* 总开关。
|
|
44
45
|
*
|
|
45
|
-
* ★
|
|
46
|
-
*
|
|
47
|
-
*
|
|
46
|
+
* ★ **默认开启**:装上就用。安装这个插件本身就是用户点的头,
|
|
47
|
+
* 再让每个人去改一遍 profile 配置没有道理。
|
|
48
|
+
*
|
|
49
|
+
* 想关掉就在 profile 的 `cordis.patch.yml` 里显式写 `enabled: false`——
|
|
50
|
+
* 关掉之后它一个字节都不碰会话(见下面 apply 里的分支)。
|
|
48
51
|
*/
|
|
49
|
-
enabled:
|
|
52
|
+
enabled: true,
|
|
50
53
|
/**
|
|
51
54
|
* 是否允许在模型停下时自动续行。
|
|
52
55
|
*
|
|
@@ -57,7 +60,7 @@ export const DEFAULT_CONFIG = {
|
|
|
57
60
|
autoContinue: false,
|
|
58
61
|
/** 连续自动续行的轮次上限,达到就停。 */
|
|
59
62
|
maxAutoRounds: 5,
|
|
60
|
-
/**
|
|
63
|
+
/** 工具说明段落的排序。 */
|
|
61
64
|
promptOrder: DEFAULT_ORDER,
|
|
62
65
|
/** 状态目录;null 表示 $DSH_HOME/dsh-task-tree。 */
|
|
63
66
|
rootDir: null,
|
|
@@ -74,24 +77,24 @@ export function apply(ctx, config) {
|
|
|
74
77
|
const root = cfg.rootDir ?? defaultRoot();
|
|
75
78
|
const store = createStore(root);
|
|
76
79
|
|
|
77
|
-
//
|
|
78
|
-
//
|
|
80
|
+
// 被显式关掉时,一个字节都不碰会话:不注册工具、不加提示词段落、不折叠上下文、
|
|
81
|
+
// 不续行——只留一条命令,让人能确认"装了但被关掉了"以及怎么打开。
|
|
79
82
|
if (cfg.enabled !== true) {
|
|
80
|
-
registerCommands(ctx, { store, cfg,
|
|
83
|
+
registerCommands(ctx, { store, cfg, pause: null, enabled: false });
|
|
81
84
|
// 条目接口照挂:客户端条目与 enabled 无关,照样会来读。读不到计划它就返回
|
|
82
85
|
// null,一个像素都不占;而既有的计划文件仍然能看到。
|
|
83
86
|
// 只读插件自己的状态目录,不碰任何会话。
|
|
84
|
-
registerHttp(ctx, { store, autoContinue: null });
|
|
87
|
+
registerHttp(ctx, { store, autoContinue: null, pause: null });
|
|
85
88
|
ctx.logger?.warn?.(
|
|
86
|
-
"dsh-tree-task-flow:
|
|
89
|
+
"dsh-tree-task-flow: 已按配置关闭(enabled=false)。要打开请在 profile 的 cordis.patch.yml 里把它设为 true。",
|
|
87
90
|
);
|
|
88
91
|
return;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
|
-
//
|
|
94
|
+
// 工具说明段。必须包在 effect 里,否则热更新后会重复注册而抛错。
|
|
92
95
|
ctx.effect(registerPromptSection(ctx, cfg.promptOrder), "dsh-tree-task-flow.section");
|
|
93
96
|
|
|
94
|
-
//
|
|
97
|
+
// 六个工具。tools 已在 inject 里声明,这里必定就绪。
|
|
95
98
|
registerTools(ctx, { store, cfg });
|
|
96
99
|
|
|
97
100
|
// 节点完成时把它的执行过程折叠成一条携带结果的汇总消息。
|
|
@@ -101,11 +104,15 @@ export function apply(ctx, config) {
|
|
|
101
104
|
// 让人随时能一句话停下来。
|
|
102
105
|
const autoContinue = registerAutoContinue(ctx, { store, cfg });
|
|
103
106
|
|
|
104
|
-
// /
|
|
105
|
-
|
|
107
|
+
// 暂停闸门:挂在 `agent/pre-step` 上,人点暂停就停在"上一个工具跑完、
|
|
108
|
+
// 下一次模型请求还没发出"那个岔口。它同时压住上面的自动续行兜底。
|
|
109
|
+
const pause = registerPause(ctx, { autoContinue });
|
|
110
|
+
|
|
111
|
+
// /task-tree 命令:查看与管理任务树,也是"暂停 / 继续"的开关。
|
|
112
|
+
registerCommands(ctx, { store, cfg, pause, enabled: true });
|
|
106
113
|
|
|
107
114
|
// 任务树条目要用的 HTTP 接口。
|
|
108
|
-
registerHttp(ctx, { store, autoContinue });
|
|
115
|
+
registerHttp(ctx, { store, autoContinue, pause });
|
|
109
116
|
|
|
110
117
|
ctx.logger?.info?.(`dsh-tree-task-flow: 已启用(状态目录 ${root})`);
|
|
111
118
|
}
|
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
|
-
|
|
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
|
-
|
|
406
|
-
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* 工具清单**不在这里**——每个工具的用法由它自己的 `description` 承担。同一句话
|
|
5
|
+
* 写在两处,只会让它在每一次请求里占两份位置,而模型真要调用时读的始终是
|
|
6
|
+
* `description`。计划当前的状态同样不在这里,由 `tree_task_status` 现取。
|
|
7
7
|
*
|
|
8
|
-
*
|
|
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
|
-
/**
|
|
19
|
-
|
|
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
|
-
|
|
25
|
-
|
|
28
|
+
一件事需要多步才能做完时,用这组工具把它组织成**目标 → 任务 → 子任务**三级树,
|
|
29
|
+
而不是把计划写在回复正文里,也不要再用 todo_write 另记一份。层级固定三级,
|
|
26
30
|
子任务是最低一级,不能再往下拆。
|
|
27
31
|
|
|
28
|
-
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
|
|
33
|
-
|
|
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 () =>
|
|
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
|
}
|