dsh-workplan 0.0.0-stage → 1.4.3

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.
@@ -0,0 +1,4 @@
1
+ - insert:
2
+ - id: dsh-workplan-plugin
3
+ name: './lib/host.js'
4
+ config: {}
package/icon.svg ADDED
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64" role="img" aria-label="Task Workplan">
2
+ <rect width="64" height="64" rx="14" fill="#4F6BED"/>
3
+ <rect x="13" y="15" width="38" height="7" rx="3.5" fill="#FFFFFF" opacity="0.95"/>
4
+ <rect x="13" y="28" width="26" height="7" rx="3.5" fill="#FFFFFF" opacity="0.82"/>
5
+ <rect x="13" y="41" width="33" height="7" rx="3.5" fill="#FFFFFF" opacity="0.68"/>
6
+ <circle cx="47" cy="45" r="4" fill="#FFFFFF"/>
7
+ </svg>
package/lib/host.js ADDED
@@ -0,0 +1,301 @@
1
+ import { readFileSync } from 'node:fs';
2
+ /**
3
+ * Host half of dsh-workplan.
4
+ *
5
+ * Registers:
6
+ * - the agent/pre-step listener that commits a plan before step 1; dispatch is a model tool;
7
+ * - a short system-prompt section for the main agent's integration duties;
8
+ * - dsh_workplan_update and dsh_workplan_dispatch model tools;
9
+ * - the read-only dashboard route.
10
+ */
11
+ import { createWorkplanRuntime, dispatchToolOutputSchema, plannerOutputSchema, updateToolOutputSchema, } from './runtime.js';
12
+ import { isRecord } from './plan.js';
13
+ function phaseLabel(phase) {
14
+ if (phase === 'paused')
15
+ return '本轮预算用尽,可续跑';
16
+ if (phase === 'blocked')
17
+ return '卡住了';
18
+ if (phase === 'done')
19
+ return '已完成';
20
+ if (phase === 'executing')
21
+ return '正在做';
22
+ if (phase === 'planning')
23
+ return '正在安排';
24
+ if (phase === 'integrating')
25
+ return '正在收尾';
26
+ if (phase === 'cancelled')
27
+ return '已取消';
28
+ return String(phase ?? '');
29
+ }
30
+ export const name = 'dsh-workplan';
31
+ export const inject = ['tools', 'systemPrompt', 'llm', 'subagents', 'sessions'];
32
+ export const PROMPT_ORDER = 40;
33
+ const PROMPT_SECTION = [
34
+ '本会话已启用自动任务分解。系统会在每个新用户任务的第一个模型步骤前生成计划,并先把计划显示在对话里;收到【任务分解】后,必须先调用 dsh_workplan_dispatch,等它返回部门执行结果,再做任何其他工具调用或回答。',
35
+ '如果当前是 plan 模式:不要调用 dsh_workplan_dispatch;先把计划展示给用户,等待用户审阅并退出 plan 模式后,再调用 dsh_workplan_dispatch 执行。',
36
+ '纯问候、寒暄、致谢、告别、简单确认(如「你好」「谢谢」「好的」「再见」)不生成计划,也不启动子 Agent;直接自然、简短地回答。只有包含实际目标、问题或交付物的消息才进行分解。',
37
+ '单独的「继续」「接着」「然后呢」「下一步」等没有新增任务内容的追问,不生成计划;由主 Agent 结合会话上下文继续,或先询问用户要延续哪件事。',
38
+ '询问进度、是否完成、还剩多少、哪里卡住等状态型问题,不生成计划、不启动子 Agent;主 Agent 直接读取当前计划/会话记录回答。只有要求继续修复或新增工作量时,才重新分解。',
39
+ '当上下文出现来源为 workplan 的「任务分解」或「部门执行结果」时:',
40
+ '1. 先核对计划中的事实、未知项与假设,不得把假设当成已验证事实;',
41
+ '2. 核对每个完成子任务的证据与验收标准,不合格的要用 dsh_workplan_update 标记为 blocked 或 in_progress;',
42
+ '3. 对受阻项向用户说明,必要时先询问再继续;',
43
+ '4. 不要声称未经验证的完成,不要重复派发已完成的子任务。',
44
+ '如果自动分解降级,先向用户说明失败原因并询问是否重试,在用户确认前不要开始执行任务。',
45
+ ].join('\n');
46
+ function textContent(text) {
47
+ return [{ type: 'text', text }];
48
+ }
49
+ function asObject(value) {
50
+ if (isRecord(value))
51
+ return value;
52
+ if (typeof value === 'string' && value.trim() !== '') {
53
+ try {
54
+ const parsed = JSON.parse(value);
55
+ return isRecord(parsed) ? parsed : null;
56
+ }
57
+ catch {
58
+ return null;
59
+ }
60
+ }
61
+ return null;
62
+ }
63
+ function createUpdateTool(runtime) {
64
+ return {
65
+ name: 'dsh_workplan_update',
66
+ description: '更新当前任务分解计划中一个子任务的状态与证据。仅用于本会话已生成的主计划;子 Agent 不可调用。'
67
+ + '当主 Agent 核对子任务结果、发现完成不实或需要修正时使用。',
68
+ parameters: {
69
+ type: 'object',
70
+ properties: {
71
+ planId: {
72
+ type: 'string',
73
+ description: '计划 ID;省略时使用当前会话最新计划。',
74
+ },
75
+ subtaskId: {
76
+ type: 'string',
77
+ description: '要更新的子任务 ID,例如 T1。',
78
+ },
79
+ status: {
80
+ type: 'string',
81
+ enum: ['pending', 'in_progress', 'done', 'blocked'],
82
+ description: '新的状态。',
83
+ },
84
+ progress: {
85
+ type: 'integer',
86
+ minimum: 0,
87
+ maximum: 100,
88
+ description: '进度百分比;done 会自动置为 100。',
89
+ },
90
+ evidence: {
91
+ type: 'array',
92
+ items: { type: 'string' },
93
+ description: '支持该状态的证据(文件、命令输出、验收结果)。',
94
+ },
95
+ note: {
96
+ type: 'string',
97
+ description: '补充说明;会显示在任务卡片中。',
98
+ },
99
+ },
100
+ required: ['subtaskId', 'status'],
101
+ additionalProperties: false,
102
+ },
103
+ output: {
104
+ schema: updateToolOutputSchema(),
105
+ render(_args, value) {
106
+ const result = isRecord(value) ? value : {};
107
+ const stats = isRecord(result.stats) ? result.stats : {};
108
+ const text = result.ok
109
+ ? `已更新子任务 ${String(result.subtaskId ?? '')}:${String(result.status ?? '')}`
110
+ + `(完成 ${Number(stats.done ?? 0)}/${Number(stats.total ?? 0)},受阻 ${Number(stats.blocked ?? 0)})`
111
+ : `更新失败:${String(result.error ?? '未知错误')}`;
112
+ return textContent(text);
113
+ },
114
+ },
115
+ async execute(args, exec) {
116
+ const parsed = asObject(args);
117
+ if (!parsed)
118
+ throw new Error('参数必须是 JSON 对象');
119
+ const result = await runtime.updateStatus(exec.agent, parsed);
120
+ return result;
121
+ },
122
+ };
123
+ }
124
+ function createCancelTool(runtime) {
125
+ return {
126
+ name: 'dsh_workplan_cancel',
127
+ description: '取消当前会话尚未派发/正在等待派发的任务分解计划。仅取消计划记录,不伪造任何完成;'
128
+ + '取消后计划在面板中显示为“已取消”,不会启动任何部门 Agent。',
129
+ parameters: {
130
+ type: 'object',
131
+ properties: {
132
+ planId: {
133
+ type: 'string',
134
+ description: '计划 ID;省略时使用当前会话最新计划。',
135
+ },
136
+ reason: {
137
+ type: 'string',
138
+ description: '取消原因,会写入会话记录。',
139
+ },
140
+ },
141
+ additionalProperties: false,
142
+ },
143
+ output: {
144
+ schema: {
145
+ type: 'object',
146
+ properties: {
147
+ ok: { type: 'boolean' },
148
+ planId: { type: 'string' },
149
+ phase: { type: 'string' },
150
+ reason: { type: 'string' },
151
+ },
152
+ required: ['ok'],
153
+ additionalProperties: true,
154
+ },
155
+ render(_args, value) {
156
+ const result = isRecord(value) ? value : {};
157
+ return textContent(result.ok
158
+ ? `已取消计划 ${String(result.planId ?? '')}:${String(result.reason ?? '用户取消')}`
159
+ : `取消失败:${String(result.error ?? '未知错误')}`);
160
+ },
161
+ },
162
+ async execute(args, exec) {
163
+ const parsed = asObject(args);
164
+ if (!parsed)
165
+ throw new Error('参数必须是 JSON 对象');
166
+ return runtime.cancelPlan(exec.agent, parsed);
167
+ },
168
+ };
169
+ }
170
+ function createDispatchTool(runtime) {
171
+ return {
172
+ name: 'dsh_workplan_dispatch',
173
+ description: '派发当前会话尚未执行的任务分解计划。计划显示后必须先调用本工具;它会等待各部门 Agent 完成,'
174
+ + '把 durable 结果写入会话并返回每个子任务的状态/摘要。重复调用是幂等的。',
175
+ parameters: {
176
+ type: 'object',
177
+ properties: {
178
+ planId: {
179
+ type: 'string',
180
+ description: '计划 ID;省略时使用当前会话最新计划。',
181
+ },
182
+ },
183
+ additionalProperties: false,
184
+ },
185
+ output: {
186
+ schema: dispatchToolOutputSchema(),
187
+ render(_args, value) {
188
+ const result = isRecord(value) ? value : {};
189
+ const stats = isRecord(result.stats) ? result.stats : {};
190
+ return textContent(result.ok
191
+ ? `计划 ${String(result.planId ?? '')} 当前阶段:${phaseLabel(result.phase)}`
192
+ + `(完成 ${Number(stats.done ?? 0)}/${Number(stats.total ?? 0)},受阻 ${Number(stats.blocked ?? 0)})`
193
+ + (result.alreadyRunning ? `|仍在执行(已 ${Math.round(Number(result.elapsedMs ?? 0) / 1000)}s)` : '')
194
+ // The hint carries the recovery instruction (resume, or take the work
195
+ // over directly); showing it in the rendered line keeps it visible
196
+ // even when only the summary is read.
197
+ : `派发失败:${String(result.error ?? '未知错误')}${result.hint ? `|下一步:${String(result.hint)}` : ''}`);
198
+ },
199
+ },
200
+ /*
201
+ * The host tool timeout must outlive the plan budget, otherwise the model
202
+ * call is killed while the dispatcher is still legitimately working and the
203
+ * plan looks stuck. Derive it from the resolved limits.
204
+ */
205
+ timeoutMs: Number(runtime?.limits?.totalTimeoutMs ?? 1200000) + 20000,
206
+ async execute(args, exec) {
207
+ const parsed = asObject(args) ?? {};
208
+ return runtime.dispatchTool(exec.agent, parsed, exec.signal);
209
+ },
210
+ };
211
+ }
212
+ /*
213
+ * Read the limits from the environment when the config does not set them.
214
+ *
215
+ * The plugin used to hard-code a 300 s child ceiling with no way to raise it
216
+ * short of editing the source, so any machine with slower subagents hit the
217
+ * wall on every plan. These knobs let operations tune it without a rebuild.
218
+ */
219
+ function numericSetting(configValue, envName) {
220
+ if (configValue !== undefined && configValue !== null && configValue !== '') {
221
+ const numeric = Number(configValue);
222
+ if (Number.isFinite(numeric) && numeric > 0)
223
+ return numeric;
224
+ }
225
+ const raw = typeof process !== 'undefined' ? process.env?.[envName] : undefined;
226
+ if (raw === undefined || raw === '')
227
+ return undefined;
228
+ const numeric = Number(raw);
229
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : undefined;
230
+ }
231
+ export function apply(ctx, config = {}) {
232
+ const runtime = createWorkplanRuntime(ctx, {
233
+ cwd: typeof config.cwd === 'string' ? config.cwd : undefined,
234
+ childTimeoutMs: numericSetting(config.childTimeoutMs, 'DSH_WORKPLAN_CHILD_TIMEOUT_MS'),
235
+ idleTimeoutMs: numericSetting(config.idleTimeoutMs, 'DSH_WORKPLAN_IDLE_TIMEOUT_MS'),
236
+ totalTimeoutMs: numericSetting(config.totalTimeoutMs, 'DSH_WORKPLAN_TOTAL_TIMEOUT_MS'),
237
+ maxResumes: numericSetting(config.maxResumes, 'DSH_WORKPLAN_MAX_RESUMES'),
238
+ });
239
+ ctx.effect(() => ctx.systemPrompt.section({
240
+ name: 'dsh-workplan',
241
+ order: PROMPT_ORDER,
242
+ text: PROMPT_SECTION,
243
+ }));
244
+ ctx.effect(() => ctx.tools.register(createUpdateTool(runtime)));
245
+ ctx.effect(() => ctx.tools.register(createCancelTool(runtime)));
246
+ ctx.effect(() => ctx.tools.register(createDispatchTool(runtime)));
247
+ /*
248
+ * Plan mode produces a plan for review, not department execution. Denying
249
+ * only the dispatch tool keeps every other tool available (no deadlock)
250
+ * and gives the model a precise reason instead of a silent no-op.
251
+ */
252
+ ctx.effect(() => ctx.tools.guard((exec) => {
253
+ if (exec?.name !== 'dsh_workplan_dispatch')
254
+ return undefined;
255
+ const session = exec?.agent?.session;
256
+ if (session && runtime.planModeActive(session)) {
257
+ return '当前是 plan 模式:计划已生成,请先让用户审阅/退出 plan 模式,再调用 dsh_workplan_dispatch。';
258
+ }
259
+ return undefined;
260
+ }));
261
+ ctx.effect(() => ctx.on('agent/pre-step', runtime.onPreStep));
262
+ ctx.inject(['webServer', 'connection'], (scope) => {
263
+ scope.effect(() => scope.webServer.register({
264
+ kind: 'exact',
265
+ path: '/dsh-workplan/plans',
266
+ handler: (req, res) => runtime.route(req, res, scope),
267
+ }));
268
+ /*
269
+ * Chart.js is vendored (vendor/chart.umd.js, MIT) instead of bundled: the
270
+ * client half is a hand-written module-registry factory with no bundler, so
271
+ * the page loads the library as a same-origin script and falls back to the
272
+ * built-in CSS charts when it cannot load.
273
+ */
274
+ scope.effect(() => scope.webServer.register({
275
+ kind: 'exact',
276
+ path: '/dsh-workplan/vendor/chart.umd.js',
277
+ handler: (req, res) => {
278
+ try {
279
+ const body = readFileSync(new URL('../vendor/chart.umd.js', import.meta.url));
280
+ res.statusCode = 200;
281
+ res.setHeader('content-type', 'text/javascript; charset=utf-8');
282
+ res.setHeader('cache-control', 'public, max-age=3600');
283
+ res.end(body);
284
+ }
285
+ catch (error) {
286
+ res.statusCode = 404;
287
+ res.setHeader('content-type', 'text/plain; charset=utf-8');
288
+ res.end('chart.js not found');
289
+ }
290
+ },
291
+ }));
292
+ });
293
+ ctx.effect(() => () => runtime.dispose());
294
+ }
295
+ export const meta = {
296
+ name,
297
+ version: '1.4.3',
298
+ promptOrder: PROMPT_ORDER,
299
+ routes: ['/dsh-workplan/plans', '/dsh-workplan/vendor/chart.umd.js'],
300
+ };
301
+ export { PROMPT_SECTION, createCancelTool, createDispatchTool, createUpdateTool, plannerOutputSchema };