dsh-tree-task-flow 0.1.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/LICENSE +21 -0
- package/README.md +250 -0
- package/client.js +921 -0
- package/cordis.patch.yml +20 -0
- package/enable.example.yml +4 -0
- package/lib/auto-continue.js +132 -0
- package/lib/commands.js +189 -0
- package/lib/http.js +243 -0
- package/lib/index.js +110 -0
- package/lib/plan.js +430 -0
- package/lib/prompt.js +56 -0
- package/lib/region.js +345 -0
- package/lib/store.js +126 -0
- package/lib/tools.js +308 -0
- package/package.json +68 -0
package/lib/tools.js
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 六个 `tree_task_*` 工具。
|
|
3
|
+
*
|
|
4
|
+
* 工具本身**只做状态读写**——不注入消息、不动 surface。
|
|
5
|
+
* 「完成时把这个节点的执行过程从上下文里折起来、换成一条携带它结果的汇总」
|
|
6
|
+
* 那件事完全由 `agent/pre-step` 监听器负责(它比对游标与 `activePath` 就知道
|
|
7
|
+
* 哪一层刚结束)。
|
|
8
|
+
*
|
|
9
|
+
* 这边唯一的 hook 是**检查点闸门**:它也不碰会话、不碰 surface,只是在
|
|
10
|
+
* `tools/pre-execute` 上拦一道——检查点期间除了拆子节点和提交结果,别的工具一律
|
|
11
|
+
* 拒掉。判断本身在纯函数 `checkpointGate` 里,方便单测。
|
|
12
|
+
*
|
|
13
|
+
* 工具能决定的只有一件事:**这个节点算不算做完了**。而它做完了就必须交出
|
|
14
|
+
* `result`——插件正是拿这段文字去顶替它整段执行过程。没有结果的完成会让那段
|
|
15
|
+
* 上下文白白消失,所以 `result` 是必填的。
|
|
16
|
+
*
|
|
17
|
+
* 所有工具共用同一套契约:
|
|
18
|
+
* - `parameters` 用**原始 JSON Schema**(不能 import 宿主的 schemastery)
|
|
19
|
+
* - 返回 `{ text }`,由 `output.render` 渲染成一个文本块
|
|
20
|
+
*
|
|
21
|
+
* @module dsh-tree-task-flow/tools
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
addChildren,
|
|
26
|
+
checkpointOf,
|
|
27
|
+
complete,
|
|
28
|
+
createPlan,
|
|
29
|
+
dropNode,
|
|
30
|
+
levelLabel,
|
|
31
|
+
renderContinue,
|
|
32
|
+
renderTree,
|
|
33
|
+
updateNode,
|
|
34
|
+
} from "./plan.js";
|
|
35
|
+
|
|
36
|
+
/** 所有工具统一的返回形状与渲染。 */
|
|
37
|
+
const OUTPUT = {
|
|
38
|
+
schema: {
|
|
39
|
+
type: "object",
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
properties: { text: { type: "string" } },
|
|
42
|
+
},
|
|
43
|
+
render: (_args, value) => [{ type: "text", text: String(value?.text ?? "") }],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** 统一包装返回值。 */
|
|
47
|
+
const ok = (text) => ({ text });
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 追加子节点时,父节点层级对应的子节点称呼。
|
|
51
|
+
* @param level - `addChildren` 报回来的父节点层级。
|
|
52
|
+
* @returns 中文称呼。
|
|
53
|
+
*/
|
|
54
|
+
function childLabel(level) {
|
|
55
|
+
return level === "goal" ? "任务" : "子任务";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 检查点期间放行的工具,就这两个。
|
|
60
|
+
*
|
|
61
|
+
* 检查点是"这一层只剩一个判断"的时刻:继续往下拆,或者交出结果收尾。
|
|
62
|
+
* 那一刻再多放行任何一个工具,模型都可以绕开这个判断继续埋头干活,
|
|
63
|
+
* 检查点也就成了摆设。所以这里是白名单,不是黑名单。
|
|
64
|
+
*/
|
|
65
|
+
export const CHECKPOINT_ALLOWED = Object.freeze(["tree_task_plan", "tree_task_done"]);
|
|
66
|
+
|
|
67
|
+
/** 在检查点里被挡下时,给模型看的话:局面是什么、只剩哪两条路。 */
|
|
68
|
+
function checkpointDenyReason(toolName, checkpoint) {
|
|
69
|
+
const label = levelLabel(checkpoint.level);
|
|
70
|
+
const id = checkpoint.node.id;
|
|
71
|
+
return [
|
|
72
|
+
`现在是检查点:${label}「${checkpoint.node.title}」(${id}) 的子节点已全部结束。`,
|
|
73
|
+
`此刻只放行两个工具:tree_task_plan 继续给 ${id} 拆子节点,` +
|
|
74
|
+
`或者 tree_task_done(id="${id}", result="…") 提交这个${label}的结果并完成它。`,
|
|
75
|
+
`工具 "${toolName}" 不在放行范围内,已被拒绝。`,
|
|
76
|
+
].join("\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 检查点闸门的判定。
|
|
81
|
+
*
|
|
82
|
+
* **纯函数**——不读磁盘、不碰会话,只看出入参,所以可以直接单测。
|
|
83
|
+
* 注册处只负责把当前计划读出来、把 `checkpointOf` 的结果递进来。
|
|
84
|
+
*
|
|
85
|
+
* @param toolName - 模型这次要调的工具名。
|
|
86
|
+
* @param checkpoint - `checkpointOf(plan)` 的结果;`null` 表示不在检查点。
|
|
87
|
+
* @returns `{ kind: "allow" }`,或 `{ kind: "deny", reason }`。
|
|
88
|
+
*/
|
|
89
|
+
export function checkpointGate(toolName, checkpoint) {
|
|
90
|
+
if (checkpoint === null || checkpoint === undefined) return { kind: "allow" };
|
|
91
|
+
if (CHECKPOINT_ALLOWED.includes(toolName)) return { kind: "allow" };
|
|
92
|
+
return { kind: "deny", reason: checkpointDenyReason(toolName, checkpoint) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 注册全部工具。
|
|
97
|
+
*
|
|
98
|
+
* @param ctx - 挂载本行的 Cordis 上下文。
|
|
99
|
+
* @param options - `{ store, cfg }`。
|
|
100
|
+
*/
|
|
101
|
+
export function registerTools(ctx, { store, cfg }) {
|
|
102
|
+
const sessionOf = (exec) => exec?.agent?.session?.id ?? null;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* 检查点的硬闸门。
|
|
106
|
+
*
|
|
107
|
+
* 先跑完下游,别人已经拒了就尊重别人的决定——那多半是审批或 hook;
|
|
108
|
+
* 只有一路放行到这里,才轮到用检查点收敛。
|
|
109
|
+
*
|
|
110
|
+
* 计划按会话取,所以闸门只对"正处于检查点的那个会话"生效,
|
|
111
|
+
* 别的会话照常使用全部工具。
|
|
112
|
+
*/
|
|
113
|
+
ctx.on("tools/pre-execute", async (exec, next) => {
|
|
114
|
+
const decision = await next();
|
|
115
|
+
if (decision?.kind !== "allow") return decision;
|
|
116
|
+
|
|
117
|
+
const checkpoint = checkpointOf(store.readPlan(sessionOf(exec)));
|
|
118
|
+
const verdict = checkpointGate(exec?.name, checkpoint);
|
|
119
|
+
return verdict.kind === "allow" ? decision : verdict;
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------- 计划树
|
|
123
|
+
|
|
124
|
+
ctx.tools.register({
|
|
125
|
+
name: "tree_task_status",
|
|
126
|
+
description:
|
|
127
|
+
"查看当前计划树:目标、任务、子任务各自的状态与已提交的结果,以及现在该推进哪个节点。" +
|
|
128
|
+
"任务开头、上下文被压缩之后、或者不确定下一步做什么时,调用它。",
|
|
129
|
+
parameters: { type: "object", additionalProperties: false, properties: {} },
|
|
130
|
+
output: OUTPUT,
|
|
131
|
+
async execute(_args, exec) {
|
|
132
|
+
const plan = store.readPlan(sessionOf(exec));
|
|
133
|
+
return ok(renderTree(plan));
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
ctx.tools.register({
|
|
138
|
+
name: "tree_task_create",
|
|
139
|
+
description:
|
|
140
|
+
"建立一个新计划:一个目标 + 若干任务。这会替换掉现有计划,所以只在开始一个新目标时调用。" +
|
|
141
|
+
"建完记得用 tree_task_plan 给目标下的第一个任务拆子任务。",
|
|
142
|
+
parameters: {
|
|
143
|
+
type: "object",
|
|
144
|
+
additionalProperties: false,
|
|
145
|
+
properties: {
|
|
146
|
+
title: { type: "string", description: "目标:一句话说清最终要达成什么" },
|
|
147
|
+
detail: { type: "string", description: "目标的补充说明" },
|
|
148
|
+
tasks: {
|
|
149
|
+
type: "array",
|
|
150
|
+
description: "目标下的任务列表,按建议的执行顺序排列",
|
|
151
|
+
items: {
|
|
152
|
+
type: "object",
|
|
153
|
+
additionalProperties: false,
|
|
154
|
+
properties: {
|
|
155
|
+
title: { type: "string", description: "任务标题" },
|
|
156
|
+
detail: { type: "string", description: "任务的补充说明" },
|
|
157
|
+
},
|
|
158
|
+
required: ["title"],
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
required: ["title", "tasks"],
|
|
163
|
+
},
|
|
164
|
+
output: OUTPUT,
|
|
165
|
+
async execute(args, exec) {
|
|
166
|
+
let plan;
|
|
167
|
+
try {
|
|
168
|
+
plan = createPlan(args);
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return ok(`建立计划失败:${String(error?.message ?? error)}`);
|
|
171
|
+
}
|
|
172
|
+
store.writePlan(sessionOf(exec), plan);
|
|
173
|
+
return ok(
|
|
174
|
+
`计划已建立。\n\n${renderTree(plan)}\n\n下一步:调用 tree_task_plan 给目标下的第一个任务拆子任务。`,
|
|
175
|
+
);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
ctx.tools.register({
|
|
180
|
+
name: "tree_task_plan",
|
|
181
|
+
description:
|
|
182
|
+
"给一个节点追加子节点:给目标追加任务,给任务追加子任务。子任务是最低一级,不能再往下拆。" +
|
|
183
|
+
"子任务全部结束后父节点不会自动完成,而是转入检查点——那时再决定继续拆还是提交结果。",
|
|
184
|
+
parameters: {
|
|
185
|
+
type: "object",
|
|
186
|
+
additionalProperties: false,
|
|
187
|
+
properties: {
|
|
188
|
+
parentId: { type: "string", description: "父节点 id(目标或任务),例如 t-abc123" },
|
|
189
|
+
children: {
|
|
190
|
+
type: "array",
|
|
191
|
+
description: "要追加的子节点,按执行顺序;父节点是目标时是任务,是任务时是子任务",
|
|
192
|
+
items: {
|
|
193
|
+
type: "object",
|
|
194
|
+
additionalProperties: false,
|
|
195
|
+
properties: {
|
|
196
|
+
title: { type: "string", description: "子节点标题" },
|
|
197
|
+
detail: { type: "string", description: "补充说明" },
|
|
198
|
+
},
|
|
199
|
+
required: ["title"],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
required: ["parentId", "children"],
|
|
204
|
+
},
|
|
205
|
+
output: OUTPUT,
|
|
206
|
+
async execute(args, exec) {
|
|
207
|
+
const plan = store.readPlan(sessionOf(exec));
|
|
208
|
+
if (!plan) return ok("还没有计划。先用 tree_task_create 建立目标与任务。");
|
|
209
|
+
const result = addChildren(plan, args?.parentId, args?.children);
|
|
210
|
+
if (!result.ok) return ok(`没有添加:${result.reason}`);
|
|
211
|
+
store.writePlan(sessionOf(exec), plan);
|
|
212
|
+
return ok(
|
|
213
|
+
[
|
|
214
|
+
`已添加 ${result.added.length} 个${childLabel(result.level)}:`,
|
|
215
|
+
...result.added.map((node) => ` ${node.id} ${node.title}`),
|
|
216
|
+
"",
|
|
217
|
+
renderTree(plan),
|
|
218
|
+
].join("\n"),
|
|
219
|
+
);
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
ctx.tools.register({
|
|
224
|
+
name: "tree_task_done",
|
|
225
|
+
description:
|
|
226
|
+
"完成一个节点并提交它的结果(目标 / 任务 / 子任务都给它的 id)。**必须写 result**:" +
|
|
227
|
+
"插件会把这个节点从开始到现在的执行过程从上下文里折叠掉,换成一条只携带这段结果的汇总消息," +
|
|
228
|
+
"所以 result 要写清它究竟产出了什么,写成能独立看懂的样子。" +
|
|
229
|
+
"子任务全部结束后父节点不会自动完成,而是转入检查点;那时再决定继续拆子任务还是提交父节点的结果。",
|
|
230
|
+
parameters: {
|
|
231
|
+
type: "object",
|
|
232
|
+
additionalProperties: false,
|
|
233
|
+
properties: {
|
|
234
|
+
id: { type: "string", description: "要完成的节点 id" },
|
|
235
|
+
result: {
|
|
236
|
+
type: "string",
|
|
237
|
+
description: "这个节点产出了什么。它整段执行过程会被这条结果顶替,所以要能独立看懂。",
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
required: ["id", "result"],
|
|
241
|
+
},
|
|
242
|
+
output: OUTPUT,
|
|
243
|
+
async execute(args, exec) {
|
|
244
|
+
const plan = store.readPlan(sessionOf(exec));
|
|
245
|
+
if (!plan) return ok("还没有计划。");
|
|
246
|
+
const result = complete(plan, args?.id, args?.result);
|
|
247
|
+
if (!result.ok) return ok(`没有完成:${result.reason}`);
|
|
248
|
+
store.writePlan(sessionOf(exec), plan);
|
|
249
|
+
return ok(
|
|
250
|
+
[...result.events, "", renderTree(plan), "", renderContinue(plan)].join("\n"),
|
|
251
|
+
);
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
ctx.tools.register({
|
|
256
|
+
name: "tree_task_update",
|
|
257
|
+
description: "修改一个节点的标题或说明。",
|
|
258
|
+
parameters: {
|
|
259
|
+
type: "object",
|
|
260
|
+
additionalProperties: false,
|
|
261
|
+
properties: {
|
|
262
|
+
id: { type: "string", description: "节点 id" },
|
|
263
|
+
title: { type: "string", description: "新的标题" },
|
|
264
|
+
detail: { type: "string", description: "新的说明" },
|
|
265
|
+
},
|
|
266
|
+
required: ["id"],
|
|
267
|
+
},
|
|
268
|
+
output: OUTPUT,
|
|
269
|
+
async execute(args, exec) {
|
|
270
|
+
const plan = store.readPlan(sessionOf(exec));
|
|
271
|
+
if (!plan) return ok("还没有计划。");
|
|
272
|
+
const result = updateNode(plan, args?.id, args ?? {});
|
|
273
|
+
if (!result.ok) return ok(`没有修改:${result.reason}`);
|
|
274
|
+
store.writePlan(sessionOf(exec), plan);
|
|
275
|
+
return ok(`已更新 ${result.changed.join(" / ")}。\n\n${renderTree(plan)}`);
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
ctx.tools.register({
|
|
280
|
+
name: "tree_task_drop",
|
|
281
|
+
description:
|
|
282
|
+
"丢弃一个节点。丢弃任务会连同它的子任务一起丢弃。改需求用这个,不要重建整棵树——" +
|
|
283
|
+
"已丢弃的节点会留在树里可查。",
|
|
284
|
+
parameters: {
|
|
285
|
+
type: "object",
|
|
286
|
+
additionalProperties: false,
|
|
287
|
+
properties: {
|
|
288
|
+
id: { type: "string", description: "要丢弃的节点 id" },
|
|
289
|
+
},
|
|
290
|
+
required: ["id"],
|
|
291
|
+
},
|
|
292
|
+
output: OUTPUT,
|
|
293
|
+
async execute(args, exec) {
|
|
294
|
+
const plan = store.readPlan(sessionOf(exec));
|
|
295
|
+
if (!plan) return ok("还没有计划。");
|
|
296
|
+
const result = dropNode(plan, args?.id);
|
|
297
|
+
if (!result.ok) return ok(`没有丢弃:${result.reason}`);
|
|
298
|
+
store.writePlan(sessionOf(exec), plan);
|
|
299
|
+
return ok(
|
|
300
|
+
[`已丢弃:${result.dropped.join("、")}`, "", renderTree(plan), "", renderContinue(plan)].join(
|
|
301
|
+
"\n",
|
|
302
|
+
),
|
|
303
|
+
);
|
|
304
|
+
},
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
void cfg;
|
|
308
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-tree-task-flow",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "树形任务流:把多步任务组织成目标→任务→子任务三级树,节点完成时由 AI 提交结果,插件随即把该节点的执行过程从上下文里折叠掉,只留下结果。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./lib/index.js",
|
|
9
|
+
"./client": "./client.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"lib",
|
|
14
|
+
"client.js",
|
|
15
|
+
"cordis.patch.yml",
|
|
16
|
+
"enable.example.yml",
|
|
17
|
+
"README.md"
|
|
18
|
+
],
|
|
19
|
+
"dsh": {
|
|
20
|
+
"bundle": {
|
|
21
|
+
"patch": "./cordis.patch.yml"
|
|
22
|
+
},
|
|
23
|
+
"client": {
|
|
24
|
+
"platform": "web",
|
|
25
|
+
"inject": [
|
|
26
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
27
|
+
"@deepseek-ai/dsh-client-locale",
|
|
28
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
29
|
+
"@deepseek-ai/dsh-client-ui-sidebar-right",
|
|
30
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
31
|
+
]
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"registry": "https://registry.npmjs.org",
|
|
36
|
+
"access": "public"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": "^22.19.0 || >=24.0.0"
|
|
40
|
+
},
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"author": "viyiviyi",
|
|
43
|
+
"repository": {
|
|
44
|
+
"type": "git",
|
|
45
|
+
"url": "git+https://github.com/viyiviyi/dsh-tree-task-flow.git"
|
|
46
|
+
},
|
|
47
|
+
"homepage": "https://github.com/viyiviyi/dsh-tree-task-flow#readme",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/viyiviyi/dsh-tree-task-flow/issues"
|
|
50
|
+
},
|
|
51
|
+
"keywords": [
|
|
52
|
+
"deepseek",
|
|
53
|
+
"deepseek-harness",
|
|
54
|
+
"dsh",
|
|
55
|
+
"dsh-plugin",
|
|
56
|
+
"cordis",
|
|
57
|
+
"plan",
|
|
58
|
+
"task-tree"
|
|
59
|
+
],
|
|
60
|
+
"peerDependencies": {
|
|
61
|
+
"@deepseek-ai/cordis": ">=4.0.0 <5"
|
|
62
|
+
},
|
|
63
|
+
"peerDependenciesMeta": {
|
|
64
|
+
"@deepseek-ai/cordis": {
|
|
65
|
+
"optional": true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|