@mzzsfy/dsh-rs-workflow 1.0.0 → 1.0.2
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/engine/flow-exec.js +23 -0
- package/lib/board.mjs +28 -14
- package/lib/driver/approve.mjs +3 -1
- package/lib/driver/index.mjs +47 -15
- package/lib/driver/runner.mjs +11 -5
- package/lib/driver/scheduler.mjs +2 -1
- package/lib/guard.mjs +67 -0
- package/lib/orchestrator.mjs +511 -428
- package/lib/storage.mjs +13 -4
- package/lib/store.mjs +28 -10
- package/lib/template.mjs +7 -6
- package/package.json +6 -2
- package/preset/rs-workflow/agent.cordis.yml +181 -0
- package/preset/rs-workflow/preset.yml +5 -0
- package/src/client.js +0 -1
package/lib/orchestrator.mjs
CHANGED
|
@@ -1,428 +1,511 @@
|
|
|
1
|
-
// orchestrator — 主循环工具行:rs_workflow_start/status/resume/cancel/message/resume_from/verdict 七工具
|
|
2
|
-
// 编排以分段 continuable job 推进:每段 = jobs.start 包装 driver.runSegment,settle 负载经 tool-jobs
|
|
3
|
-
// 完成通知唤醒主循环;推进责任唯一在 rs_workflow_resume(页签 control 仅清 paused/裁决)
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
//
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
if (record ===
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (args.
|
|
396
|
-
return { ok: false, error: '
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
if (
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
}
|
|
1
|
+
// orchestrator — 主循环工具行:rs_workflow_start/status/resume/cancel/message/resume_from/verdict 七工具
|
|
2
|
+
// 编排以分段 continuable job 推进:每段 = jobs.start 包装 driver.runSegment,settle 负载经 tool-jobs
|
|
3
|
+
// 完成通知唤醒主循环;推进责任唯一在 rs_workflow_resume(页签 control 仅清 paused/裁决)
|
|
4
|
+
// 另有会话守门:轮次将停时(agent/turn-stopping)判定欠动作并 steer 拉回,防弱模型提前退出
|
|
5
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
6
|
+
import { gate } from './planner-gate.mjs'
|
|
7
|
+
import { validateTemplate } from './template.mjs'
|
|
8
|
+
import { startRun, buildSeed } from './driver/index.mjs'
|
|
9
|
+
import { registry, registerInitiator, registerResumer } from './driver/control.mjs'
|
|
10
|
+
import { reportStore } from './store.mjs'
|
|
11
|
+
import { loadJson } from './storage.mjs'
|
|
12
|
+
import { normalizeConfig } from './settings-schema.mjs'
|
|
13
|
+
import { pendingOf, createNudgeGate, nudgeText } from './guard.mjs'
|
|
14
|
+
|
|
15
|
+
const MAX_REJECTS = 3
|
|
16
|
+
const SEGMENT_KIND = 'rsww-segment'
|
|
17
|
+
const SETTLE_OUTPUT_LIMIT = 64 * 1024
|
|
18
|
+
|
|
19
|
+
// 已启用模板表(自有存储 → {entry, parsed};gate/驱动共用此形态)
|
|
20
|
+
export function enabledTemplates() {
|
|
21
|
+
const out = []
|
|
22
|
+
for (const entry of loadJson('templates.json', [])) {
|
|
23
|
+
if (entry.enabled === false || typeof entry.json !== 'string') continue
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(entry.json)
|
|
26
|
+
if (validateTemplate(parsed).length === 0) out.push({ entry, parsed })
|
|
27
|
+
} catch { /* 损坏模板不进候选 */ }
|
|
28
|
+
}
|
|
29
|
+
return out
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** 模式行激活入口(config: { role:'orchestrator', templateId });宿主服务缺一目工具即报错 */
|
|
33
|
+
export function registerOrchestrator(ctx, config) {
|
|
34
|
+
const expectedTemplateId = config.templateId
|
|
35
|
+
// 连续拒单计数与现役 run:行实例内存态,进程重启清零
|
|
36
|
+
const rejectCounts = new Map()
|
|
37
|
+
const activeRuns = new Map()
|
|
38
|
+
|
|
39
|
+
// 会话现役 run:同会话同一时刻至多一个未终态 run(守门与工具同源,故在 inject 外共享)
|
|
40
|
+
const agentIdOf = (agent) => String(agent?.id ?? '')
|
|
41
|
+
const currentRunId = (agent) => {
|
|
42
|
+
const runId = activeRuns.get(agentIdOf(agent))
|
|
43
|
+
if (runId === undefined) return undefined
|
|
44
|
+
// run 可能已被页签删除:get 抛异常须自愈清位,不能让编排入口变砖;
|
|
45
|
+
// driver 不在内存(行重建/模块重载)的僵尸 running 同样清位:resume/cancel 对它均无力,只会永久阻塞
|
|
46
|
+
let record
|
|
47
|
+
try { record = reportStore().get(runId) } catch { record = undefined }
|
|
48
|
+
if (record === undefined || record.finishedAt !== undefined || !registry.drivers.has(runId)) {
|
|
49
|
+
activeRuns.delete(agentIdOf(agent))
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
return runId
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const registered = ctx.inject(['tools', 'workflowEngine', 'jobs'], (tctx) => {
|
|
56
|
+
const engine = tctx.workflowEngine
|
|
57
|
+
const jobs = tctx.jobs
|
|
58
|
+
|
|
59
|
+
const configOf = () => normalizeConfig(loadJson('config.json', undefined))
|
|
60
|
+
|
|
61
|
+
// 段 job:jobs.start 包装 runSegment;settle 负载进 output,经 tool-jobs 完成通知唤醒主循环。
|
|
62
|
+
// jobs.start 同步抛错(无控制器/owner 超上限/stale owner)时 run 已落盘而段未起,必须收敛为 failed,
|
|
63
|
+
// 否则本会话被未终态 run 永久阻塞(start 与页签 resume-from 均拒)
|
|
64
|
+
function startSegmentJob(agent, driver) {
|
|
65
|
+
const promise = driver.runSegment()
|
|
66
|
+
return jobs.start({
|
|
67
|
+
kind: SEGMENT_KIND,
|
|
68
|
+
label: `若水编排段推进 ${driver.runId}`,
|
|
69
|
+
outputLimitBytes: SETTLE_OUTPUT_LIMIT,
|
|
70
|
+
owner: agent,
|
|
71
|
+
run: () => ({
|
|
72
|
+
cancel: () => driver.cancel(),
|
|
73
|
+
done: promise.then((payload) => ({ status: 'completed', output: JSON.stringify(payload) })),
|
|
74
|
+
}),
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
// 包装失败收敛:jobs.start 抛错 → run 收敛 failed + 注销驱动器,保持记录终态一致
|
|
78
|
+
function startSegmentJobSafe(agent, driver) {
|
|
79
|
+
try {
|
|
80
|
+
return startSegmentJob(agent, driver)
|
|
81
|
+
} catch (e) {
|
|
82
|
+
driver.finish('failed', `段任务启动失败:${e?.message ?? e}`)
|
|
83
|
+
try { registry.drivers.delete(driver.runId) } catch { /* 内存表清理,不影响收敛结果 */ }
|
|
84
|
+
throw e
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 断点续跑挂靠:board resume-from 经此回调在原会话重建种子 run(engine/parent 闭包自本域)
|
|
89
|
+
function startSeedRun(agent, record, fromStepId, inputs) {
|
|
90
|
+
const templates = enabledTemplates()
|
|
91
|
+
const template = templates.find((t) => t.entry.id === record.templateId)
|
|
92
|
+
if (template === undefined) return { ok: false, error: `模板不存在或已禁用: ${record.templateId}` }
|
|
93
|
+
// 单活跃约束:种子 run 与 start 工具同守卫;主循环成为其推进者(activeRuns 注册,补拉/守卫生效)
|
|
94
|
+
const current = currentRunId(agent)
|
|
95
|
+
if (current !== undefined) return { ok: false, error: `本会话已有进行中的编排 ${current},不可续跑` }
|
|
96
|
+
const seed = buildSeed(record, record.plan, fromStepId, inputs)
|
|
97
|
+
const driver = startRun({
|
|
98
|
+
template: template.parsed, templateSet: templates.map((t) => t.parsed),
|
|
99
|
+
plan: record.plan, warnings: record.warnings ?? [],
|
|
100
|
+
request: record.request, inputs: seed.inputs, state: seed,
|
|
101
|
+
sessionId: agentIdOf(agent), workspace: agent.session?.header?.cwd ?? process.cwd(),
|
|
102
|
+
engine, slots: configOf().slots ?? {}, budgets: configOf().budgets ?? {},
|
|
103
|
+
parent: agent,
|
|
104
|
+
})
|
|
105
|
+
// 先拉段后注册:jobs.start 抛错时 activeRuns 不占位(段收敛 failed 已终态,不阻塞后续 start);
|
|
106
|
+
// 拉段成功但未注册的窗口内 resume 幂等(skipped),无漂移
|
|
107
|
+
startSegmentJobSafe(agent, driver)
|
|
108
|
+
activeRuns.set(agentIdOf(agent), driver.runId)
|
|
109
|
+
return { ok: true, runId: driver.runId }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function finishRun(agent, runId) {
|
|
113
|
+
if (currentRunId(agent) === runId) activeRuns.delete(agentIdOf(agent))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 段拉起挂靠:页签裁决受理后 board 经此回到本域拉下一段(幂等:活跃段不重拉);点位与 initiator 挂靠一致
|
|
117
|
+
function ensureResumer(agent) {
|
|
118
|
+
registerResumer(agentIdOf(agent), (runId) => {
|
|
119
|
+
const driver = registry.drivers.get(runId)
|
|
120
|
+
if (driver === undefined || driver.active) return false
|
|
121
|
+
startSegmentJobSafe(agent, driver)
|
|
122
|
+
return true
|
|
123
|
+
})
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const tools = [
|
|
127
|
+
defineTool({
|
|
128
|
+
name: 'rs_workflow_start',
|
|
129
|
+
description: [
|
|
130
|
+
'启动若水工作流编排:提交用户请求原文与结构化规划,受理后引擎分段推进。',
|
|
131
|
+
'受理返回 { ok, runId, status };规划被拒返回 { ok:false, errors }(按 errors 修正重交;连续 3 次被拒须回退直接答复并说明)。',
|
|
132
|
+
'templateId 必须等于本组合锚定的模板 id;plan 缺省 = 保底全序(不推荐,多步骤任务应提交 steps/brief)。',
|
|
133
|
+
].join(''),
|
|
134
|
+
parameters: {
|
|
135
|
+
request: { type: 'string', required: true, description: '用户请求原文(逐字,不转写)' },
|
|
136
|
+
templateId: { type: 'string', required: true, description: '本组合锚定的模板 id' },
|
|
137
|
+
inputs: { type: 'object', additionalProperties: true, description: '模板顶层 inputs 的键值,值必须为字符串' },
|
|
138
|
+
plan: {
|
|
139
|
+
type: 'object',
|
|
140
|
+
additionalProperties: true,
|
|
141
|
+
description: '结构化规划 { steps: [{ref, note?, done?}], brief? }:steps 各步要点(note)与验收口径(done);缺省 = 保底全序',
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
output: {
|
|
145
|
+
schema: {
|
|
146
|
+
type: 'object',
|
|
147
|
+
additionalProperties: false,
|
|
148
|
+
properties: {
|
|
149
|
+
ok: { type: 'boolean', required: true },
|
|
150
|
+
runId: { type: 'string' },
|
|
151
|
+
status: { type: 'string' },
|
|
152
|
+
errors: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
153
|
+
hint: { type: 'string' },
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
157
|
+
},
|
|
158
|
+
async execute(args, exec) {
|
|
159
|
+
const agent = exec.agent
|
|
160
|
+
// 工具参数扁平:gate 入参 = 顶层三字段 + plan(steps/brief)组装(见 feat/orchestrator.md)
|
|
161
|
+
const gatePlan = {
|
|
162
|
+
request: args.request, templateId: args.templateId, inputs: args.inputs,
|
|
163
|
+
brief: args.plan?.brief, steps: args.plan?.steps,
|
|
164
|
+
}
|
|
165
|
+
const rejects = (id) => {
|
|
166
|
+
const next = (rejectCounts.get(id) ?? 0) + 1
|
|
167
|
+
rejectCounts.set(id, next)
|
|
168
|
+
return next
|
|
169
|
+
}
|
|
170
|
+
const rejected = (errors) => {
|
|
171
|
+
const count = rejects(agentIdOf(agent))
|
|
172
|
+
if (count >= MAX_REJECTS) return { ok: false, errors, hint: `已连续 ${MAX_REJECTS} 次规划被拒,本会话编排入口关闭:回退直接答复并向用户说明` }
|
|
173
|
+
return { ok: false, errors, hint: `修正 plan 后重新调用;连续 ${MAX_REJECTS} 次失败将回退直接答复` }
|
|
174
|
+
}
|
|
175
|
+
// 连续拒单达上限:本会话工具恒失败
|
|
176
|
+
if ((rejectCounts.get(agentIdOf(agent)) ?? 0) >= MAX_REJECTS) {
|
|
177
|
+
return { ok: false, errors: [], hint: `已连续 ${MAX_REJECTS} 次规划被拒,本会话编排入口关闭:回退直接答复并向用户说明` }
|
|
178
|
+
}
|
|
179
|
+
if (currentRunId(agent) !== undefined) {
|
|
180
|
+
return { ok: false, errors: [], hint: `本会话已有进行中的编排 ${currentRunId(agent)},先等待终态或取消` }
|
|
181
|
+
}
|
|
182
|
+
const templates = enabledTemplates()
|
|
183
|
+
const template = templates.find((t) => t.entry.id === args.templateId)
|
|
184
|
+
if (template === undefined) {
|
|
185
|
+
// 组合/环境错误而非规划错误:不计连续拒单,hint 指向组合而非 plan
|
|
186
|
+
return { ok: false, errors: [{ target: 'templateId', message: `模板不存在或未启用: ${args.templateId}` }], hint: '本组合未启用该模板:检查组合锚定与模板启用状态,勿修改 plan' }
|
|
187
|
+
}
|
|
188
|
+
const outcome = gate(gatePlan, template, { expectedTemplateId })
|
|
189
|
+
if (!outcome.ok) {
|
|
190
|
+
// 锚定不符与模板缺失同为组合/环境错误:模板集是环境给定,模型改 plan 无济于事,不计拒单
|
|
191
|
+
const anchoring = outcome.errors.some((e) => e.target === 'templateId' && String(e.message).includes('与本组合锚定模板不符'))
|
|
192
|
+
if (anchoring) {
|
|
193
|
+
return { ok: false, errors: outcome.errors, hint: '提交的 templateId 与本组合锚定不符:改用组合锚定模板,勿修改 plan' }
|
|
194
|
+
}
|
|
195
|
+
return rejected(outcome.errors)
|
|
196
|
+
}
|
|
197
|
+
const driver = startRun({
|
|
198
|
+
template: template.parsed, templateSet: templates.map((t) => t.parsed),
|
|
199
|
+
plan: outcome.planScript, warnings: outcome.warnings ?? [],
|
|
200
|
+
request: args.request, inputs: args.inputs ?? {},
|
|
201
|
+
engine, slots: configOf().slots ?? {}, budgets: configOf().budgets ?? {},
|
|
202
|
+
// 会话工作区取自会话 header(子代理继承同源);宿主 cwd 仅兜底
|
|
203
|
+
sessionId: agentIdOf(agent), workspace: agent.session?.header?.cwd ?? process.cwd(),
|
|
204
|
+
parent: agent,
|
|
205
|
+
})
|
|
206
|
+
rejectCounts.delete(agentIdOf(agent))
|
|
207
|
+
// 断点续跑挂靠:本会话 agent 成为推进器,board resume-from 据此重建种子 run
|
|
208
|
+
registerInitiator(agentIdOf(agent), (record, fromStepId, inputs) => startSeedRun(agent, record, fromStepId, inputs))
|
|
209
|
+
ensureResumer(agent)
|
|
210
|
+
// 先拉段后注册现役:jobs.start 抛错时 run 已收敛 failed,不占 activeRuns 阻塞本会话
|
|
211
|
+
startSegmentJobSafe(agent, driver)
|
|
212
|
+
activeRuns.set(agentIdOf(agent), driver.runId)
|
|
213
|
+
return { ok: true, runId: driver.runId, status: driver.state.status }
|
|
214
|
+
},
|
|
215
|
+
}),
|
|
216
|
+
defineTool({
|
|
217
|
+
name: 'rs_workflow_status',
|
|
218
|
+
description: '查询若水编排状态:runId 缺省 = 本会话现役 run。返回 status/awaitingResume(补拉判定信号)/steps 账目/waiting 待裁决摘要。',
|
|
219
|
+
parameters: {
|
|
220
|
+
runId: { type: 'string', description: '缺省 = 本会话现役 run' },
|
|
221
|
+
},
|
|
222
|
+
output: {
|
|
223
|
+
schema: { type: 'object', additionalProperties: true },
|
|
224
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
225
|
+
},
|
|
226
|
+
async execute(args, exec) {
|
|
227
|
+
const store = reportStore()
|
|
228
|
+
const runId = typeof args.runId === 'string' && args.runId !== '' ? args.runId : currentRunId(exec.agent)
|
|
229
|
+
if (runId === undefined) return { ok: false, error: '本会话无现役 run(编排未启动)' }
|
|
230
|
+
let record
|
|
231
|
+
try { record = store.get(runId) } catch { record = undefined }
|
|
232
|
+
if (record === undefined) return { ok: false, error: '运行记录不存在:' + runId }
|
|
233
|
+
// 唤醒轮挂靠:主循环任一唤醒轮必先 status,借机注册断点续跑推进器(进程重启后表空,
|
|
234
|
+
// 页签 resume-from 恰是重启中断的唯一恢复路径,不能依赖 start/resume 才有挂靠)
|
|
235
|
+
registerInitiator(agentIdOf(exec.agent), (record2, fromStepId, inputs) => startSeedRun(exec.agent, record2, fromStepId, inputs))
|
|
236
|
+
ensureResumer(exec.agent)
|
|
237
|
+
const driver = registry.drivers.get(runId)
|
|
238
|
+
const steps = {}
|
|
239
|
+
for (const [id, s] of Object.entries(record.state?.steps ?? {})) {
|
|
240
|
+
steps[id] = { status: s.status, failCount: s.failCount ?? 0 }
|
|
241
|
+
}
|
|
242
|
+
const waiting = []
|
|
243
|
+
if (record.status === 'waiting_approval') {
|
|
244
|
+
const stepId = record.state?.waitingApproval
|
|
245
|
+
const planStep = (record.plan?.steps ?? []).find((p) => p.ref === stepId)
|
|
246
|
+
waiting.push({ stepId, note: planStep?.note ?? '', done: planStep?.done ?? '', autoApprove: record.state?.waitingAutoApprove === true })
|
|
247
|
+
}
|
|
248
|
+
// paused+已入队裁决:主循环据 pendingVerdicts 可知 resume 拉段后裁决将生效
|
|
249
|
+
const pendingVerdicts = (record.state?.pendingApprovals ?? []).length
|
|
250
|
+
return {
|
|
251
|
+
ok: true, runId, status: record.status,
|
|
252
|
+
awaitingResume: record.status === 'paused' && driver?.awaitingResume === true,
|
|
253
|
+
// 段在飞信号:模型据此区分"running 且段在飞(勿拉)"与"running 无段(须 resume 补拉)"
|
|
254
|
+
active: driver?.active === true,
|
|
255
|
+
steps,
|
|
256
|
+
// 宿主校验工具输出须为纯 JSON:undefined 值键会被判无效输出
|
|
257
|
+
...(waiting.length > 0 ? { waiting } : {}),
|
|
258
|
+
...(pendingVerdicts > 0 ? { pendingVerdicts } : {}),
|
|
259
|
+
summary: record.summary ?? '',
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
}),
|
|
263
|
+
defineTool({
|
|
264
|
+
name: 'rs_workflow_resume',
|
|
265
|
+
description: [
|
|
266
|
+
'拉起若水编排下一段(推进责任唯一在此):paused 拉起时同步置 running;已推进/终态返回 skipped。',
|
|
267
|
+
'裁决回写后、页签恢复后、running 无活跃段(待拉起)时调用。',
|
|
268
|
+
].join(''),
|
|
269
|
+
parameters: {
|
|
270
|
+
runId: { type: 'string', required: true, description: '要推进的 run' },
|
|
271
|
+
},
|
|
272
|
+
output: {
|
|
273
|
+
schema: { type: 'object', additionalProperties: true },
|
|
274
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
275
|
+
},
|
|
276
|
+
async execute(args, exec) {
|
|
277
|
+
const store = reportStore()
|
|
278
|
+
let record
|
|
279
|
+
try { record = store.get(args.runId) } catch { record = undefined }
|
|
280
|
+
if (record === undefined) return { ok: false, error: '运行记录不存在:' + args.runId }
|
|
281
|
+
if (record.sessionId !== undefined && record.sessionId !== agentIdOf(exec.agent)) {
|
|
282
|
+
return { ok: false, error: '拒绝操作:该 run 属于其他会话' }
|
|
283
|
+
}
|
|
284
|
+
if (record.finishedAt !== undefined) {
|
|
285
|
+
finishRun(exec.agent, args.runId)
|
|
286
|
+
return { ok: true, runId: args.runId, status: record.status, summary: record.summary }
|
|
287
|
+
}
|
|
288
|
+
const driver = registry.drivers.get(args.runId)
|
|
289
|
+
if (driver === undefined) {
|
|
290
|
+
return { ok: false, error: '编排驱动器未注册(进程重启后 run 已收敛为终态,请 rs_workflow_status 确认)' }
|
|
291
|
+
}
|
|
292
|
+
// 页签已推进(有活跃段):不重复拉起
|
|
293
|
+
if (driver.active) return { ok: true, runId: args.runId, status: record.status, skipped: true }
|
|
294
|
+
if (record.status === 'paused') {
|
|
295
|
+
driver.state.status = 'running'
|
|
296
|
+
driver.awaitingResume = false
|
|
297
|
+
driver.persistState()
|
|
298
|
+
}
|
|
299
|
+
if (record.status === 'waiting_approval') {
|
|
300
|
+
return { ok: true, runId: args.runId, status: record.status, skipped: true, hint: '先裁决(control approve/reject 或经会话页签),再 resume 拉起下一段' }
|
|
301
|
+
}
|
|
302
|
+
startSegmentJobSafe(exec.agent, driver)
|
|
303
|
+
registerInitiator(agentIdOf(exec.agent), (record2, fromStepId, inputs) => startSeedRun(exec.agent, record2, fromStepId, inputs))
|
|
304
|
+
ensureResumer(exec.agent)
|
|
305
|
+
return { ok: true, runId: args.runId, status: record.status }
|
|
306
|
+
},
|
|
307
|
+
}),
|
|
308
|
+
defineTool({
|
|
309
|
+
name: 'rs_workflow_cancel',
|
|
310
|
+
description: '取消若水编排(幂等):running 有活跃段则 abort 收敛;waiting_approval/paused 即时终态;已终态返回摘要无副作用。',
|
|
311
|
+
parameters: {
|
|
312
|
+
runId: { type: 'string', required: true, description: '要取消的 run' },
|
|
313
|
+
},
|
|
314
|
+
output: {
|
|
315
|
+
schema: { type: 'object', additionalProperties: true },
|
|
316
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
317
|
+
},
|
|
318
|
+
async execute(args, exec) {
|
|
319
|
+
const store = reportStore()
|
|
320
|
+
let record
|
|
321
|
+
try { record = store.get(args.runId) } catch { record = undefined }
|
|
322
|
+
if (record === undefined) return { ok: false, error: '运行记录不存在:' + args.runId }
|
|
323
|
+
if (record.sessionId !== undefined && record.sessionId !== agentIdOf(exec.agent)) {
|
|
324
|
+
return { ok: false, error: '拒绝操作:该 run 属于其他会话' }
|
|
325
|
+
}
|
|
326
|
+
if (record.finishedAt !== undefined) {
|
|
327
|
+
finishRun(exec.agent, args.runId)
|
|
328
|
+
return { ok: true, runId: args.runId, status: record.status, summary: record.summary }
|
|
329
|
+
}
|
|
330
|
+
const driver = registry.drivers.get(args.runId)
|
|
331
|
+
if (driver === undefined) return { ok: false, error: '编排驱动器未注册' }
|
|
332
|
+
await driver.cancelAndSettle()
|
|
333
|
+
let after
|
|
334
|
+
try { after = store.get(args.runId) } catch { after = undefined }
|
|
335
|
+
finishRun(exec.agent, args.runId)
|
|
336
|
+
const hint = '已完成步骤保留,可在会话页签断点续跑'
|
|
337
|
+
return { ok: true, runId: args.runId, status: after?.status ?? 'cancelled', hint }
|
|
338
|
+
},
|
|
339
|
+
}),
|
|
340
|
+
defineTool({
|
|
341
|
+
name: 'rs_workflow_message',
|
|
342
|
+
description: '向进行中的若水编排转达用户纠偏:下一步骤边界进入编排(纠偏类经主循环转译后 inject 注入后续指令)。',
|
|
343
|
+
parameters: {
|
|
344
|
+
runId: { type: 'string', required: true, description: '目标 run' },
|
|
345
|
+
text: { type: 'string', required: true, description: '用户纠偏内容(转译后)' },
|
|
346
|
+
inject: { type: 'boolean', description: 'true = 注入后续指令;缺省 false 排队' },
|
|
347
|
+
},
|
|
348
|
+
output: {
|
|
349
|
+
schema: { type: 'object', additionalProperties: true },
|
|
350
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
351
|
+
},
|
|
352
|
+
async execute(args, exec) {
|
|
353
|
+
const driver = registry.drivers.get(args.runId)
|
|
354
|
+
if (driver === undefined) return { ok: false, error: '编排驱动器未注册(run 未终态但不在内存,或已终态)' }
|
|
355
|
+
// 会话归属守卫:driver 与 record 同 runId,直接以 driver.sessionId 校验
|
|
356
|
+
if (driver.sessionId !== undefined && driver.sessionId !== agentIdOf(exec.agent)) {
|
|
357
|
+
return { ok: false, error: '拒绝操作:该 run 属于其他会话' }
|
|
358
|
+
}
|
|
359
|
+
if (typeof args.text !== 'string' || args.text.trim() === '') {
|
|
360
|
+
return { ok: false, error: 'text 必须为非空字符串' }
|
|
361
|
+
}
|
|
362
|
+
const accepted = driver.handlePost({ kind: 'message', text: args.text, inject: args.inject === true })
|
|
363
|
+
return accepted ? { ok: true } : { ok: false, error: '消息未被受理(run 已终态或内容为空)' }
|
|
364
|
+
},
|
|
365
|
+
}),
|
|
366
|
+
defineTool({
|
|
367
|
+
name: 'rs_workflow_resume_from',
|
|
368
|
+
description: [
|
|
369
|
+
'断点续跑/从头重跑(主循环通道,与页签 ResumePicker 同能力):目标 run 须终态或已收敛。',
|
|
370
|
+
'经挂靠机制在原会话重建种子 run:done 步骤产出继承,fromStepId 及其后代闭包重置(缺省 = 从第一个未完成步续跑);',
|
|
371
|
+
'inputs 可选覆盖模板入参。旧 run 记录保留作审计,旧 run 未消费的纠偏消息不带入新 run。',
|
|
372
|
+
].join(''),
|
|
373
|
+
parameters: {
|
|
374
|
+
runId: { type: 'string', required: true, description: '要续跑的 run(终态)' },
|
|
375
|
+
fromStepId: { type: 'string', description: '从该步骤重来(其后代全部重置);缺省 = 断点续跑' },
|
|
376
|
+
inputs: { type: 'object', additionalProperties: true, description: '覆盖模板顶层 inputs,值必须为字符串' },
|
|
377
|
+
},
|
|
378
|
+
output: {
|
|
379
|
+
schema: { type: 'object', additionalProperties: true },
|
|
380
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
381
|
+
},
|
|
382
|
+
async execute(args, exec) {
|
|
383
|
+
const store = reportStore()
|
|
384
|
+
let record
|
|
385
|
+
try { record = store.get(args.runId) } catch { record = undefined }
|
|
386
|
+
if (record === undefined) return { ok: false, error: '运行记录不存在:' + args.runId }
|
|
387
|
+
// 活跃守卫:未终态(或 driver 仍在内存)不可续跑,与页签 resume-from 同口径
|
|
388
|
+
if (registry.drivers.has(args.runId) || record.finishedAt === undefined) {
|
|
389
|
+
return { ok: false, error: '运行进行中,不可续跑' }
|
|
390
|
+
}
|
|
391
|
+
// 会话归属守卫:仅原 run 所属会话的主循环可续跑,防跨会话接管他人 run
|
|
392
|
+
if (record.sessionId !== agentIdOf(exec.agent)) {
|
|
393
|
+
return { ok: false, error: '该 run 属于其他会话,本会话不可续跑' }
|
|
394
|
+
}
|
|
395
|
+
if (args.fromStepId !== undefined && (record.plan?.steps?.some((p) => p.ref === args.fromStepId)) !== true) {
|
|
396
|
+
return { ok: false, error: 'fromStepId 不在剧本中:' + args.fromStepId }
|
|
397
|
+
}
|
|
398
|
+
// 与模板入参契约同口径:值必须为字符串(数组/对象在种子展开中失真)
|
|
399
|
+
if (args.inputs !== undefined && (typeof args.inputs !== 'object' || args.inputs === null || Array.isArray(args.inputs)
|
|
400
|
+
|| Object.values(args.inputs).some((v) => typeof v !== 'string'))) {
|
|
401
|
+
return { ok: false, error: 'inputs 必须为字符串值对象(键→字符串)' }
|
|
402
|
+
}
|
|
403
|
+
if (currentRunId(exec.agent) !== undefined) {
|
|
404
|
+
return { ok: false, error: `本会话已有进行中的编排 ${currentRunId(exec.agent)},不可续跑` }
|
|
405
|
+
}
|
|
406
|
+
// 唤醒轮挂靠兜底:重启后 initiator 表空,续跑前借本调用重注册(status/resume 同款)
|
|
407
|
+
registerInitiator(agentIdOf(exec.agent), (record2, fromStepId, inputs) => startSeedRun(exec.agent, record2, fromStepId, inputs))
|
|
408
|
+
ensureResumer(exec.agent)
|
|
409
|
+
const outcome = startSeedRun(exec.agent, record, args.fromStepId, args.inputs)
|
|
410
|
+
if (!outcome.ok) return { ok: false, error: outcome.error }
|
|
411
|
+
return { ok: true, runId: outcome.runId, hint: '旧 run 未消费的纠偏消息不带入新 run;rs_workflow_status 跟踪推进' }
|
|
412
|
+
},
|
|
413
|
+
}),
|
|
414
|
+
defineTool({
|
|
415
|
+
name: 'rs_workflow_verdict',
|
|
416
|
+
description: [
|
|
417
|
+
'回写若水编排裁决(主循环裁决通道,与页签先到先得):run 处于 waiting_approval 时受理。',
|
|
418
|
+
'autoApprove=true 代审:by=main-agent + 代审意见;ask_user 转呈真人:by=user + 用户意见。裁决后 rs_workflow_resume 拉起下一段。',
|
|
419
|
+
].join(''),
|
|
420
|
+
parameters: {
|
|
421
|
+
runId: { type: 'string', description: '要裁决的 run(缺省 = 本会话现役 run)' },
|
|
422
|
+
verdict: { type: 'string', required: true, description: 'approve = 通过;reject = 驳回(重做)' },
|
|
423
|
+
reason: { type: 'string', required: true, description: '裁决意见(审计必填):驳回=重做口径,通过=放行依据' },
|
|
424
|
+
by: { type: 'string', description: '裁决来源:main-agent=代审(缺省);user=真人转呈后的用户裁决' },
|
|
425
|
+
},
|
|
426
|
+
output: {
|
|
427
|
+
schema: { type: 'object', additionalProperties: true },
|
|
428
|
+
render: (_args, value) => [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
429
|
+
},
|
|
430
|
+
async execute(args, exec) {
|
|
431
|
+
if (args.verdict !== 'approve' && args.verdict !== 'reject') {
|
|
432
|
+
return { ok: false, error: 'verdict 须为 approve|reject' }
|
|
433
|
+
}
|
|
434
|
+
if (typeof args.reason !== 'string' || args.reason.trim() === '') {
|
|
435
|
+
return { ok: false, error: 'reason 必填(审计口径与页签一致)' }
|
|
436
|
+
}
|
|
437
|
+
const by = args.by === 'user' ? 'user' : 'main-agent'
|
|
438
|
+
const runId = typeof args.runId === 'string' && args.runId !== '' ? args.runId : currentRunId(exec.agent)
|
|
439
|
+
if (runId === undefined) return { ok: false, error: '本会话无现役 run(编排未启动)' }
|
|
440
|
+
const driver = registry.drivers.get(runId)
|
|
441
|
+
if (driver === undefined) return { ok: false, error: '编排驱动器未注册(run 未终态但不在内存,或已终态)' }
|
|
442
|
+
// 会话归属守卫:显式 runId 可指向任意会话的 run,凭 runId 不可代裁
|
|
443
|
+
if (driver.sessionId !== undefined && driver.sessionId !== agentIdOf(exec.agent)) {
|
|
444
|
+
return { ok: false, error: '拒绝操作:该 run 属于其他会话' }
|
|
445
|
+
}
|
|
446
|
+
const accepted = driver.handlePost({ kind: args.verdict, by, reason: args.reason })
|
|
447
|
+
const record = reportStore().get(runId)
|
|
448
|
+
if (accepted) {
|
|
449
|
+
return { ok: true, runId, status: record?.status, hint: '裁决已受理;rs_workflow_resume 拉起下一段' }
|
|
450
|
+
}
|
|
451
|
+
// 竞态口径:页签先裁时后到方不受理,但 run 可能已翻 running 且无活跃段(等 resume 拉起),
|
|
452
|
+
// 主循环须继续承担推进责任,否则 run 挂死在 running 无段状态
|
|
453
|
+
if (!driver.active && record?.status === 'running') {
|
|
454
|
+
startSegmentJobSafe(exec.agent, driver)
|
|
455
|
+
return { ok: true, runId, status: record.status, hint: '裁决已被页签先裁(先到先得);已代为拉起下一段' }
|
|
456
|
+
}
|
|
457
|
+
return { ok: false, error: '裁决未受理(状态不符或已被页签先裁)' }
|
|
458
|
+
},
|
|
459
|
+
}),
|
|
460
|
+
]
|
|
461
|
+
for (const tool of tools) tctx.effect(() => tctx.tools.register(tool), 'rs-workflow orchestrator: ' + tool.name)
|
|
462
|
+
// 行上下文(tool context)无 agent 属性,此处若直接 return undefined,register 的 disposer 不登记
|
|
463
|
+
// (cordis 静默接受 undefined):行销毁时七工具残留,重载后重复注册抛错致全量回滚(43267fc 实机定位)。
|
|
464
|
+
// 现行为返回注册 disposer,行销毁即注销;挂靠表(initiator/resumer)以 agentId 为键,行销毁后的
|
|
465
|
+
// stale 表项由下次 status/start 受理覆盖;run 终态经 finishRun 清 activeRuns。
|
|
466
|
+
})
|
|
467
|
+
registerTurnGuard(ctx, { currentRunId })
|
|
468
|
+
return { rejectCounts, activeRuns }
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// 会话守门注册:轮次将停时判定欠动作并 steer 拉回(宿主机器重读 inbox,有 steering 即续跑一步)。
|
|
472
|
+
// 消息构造依赖宿主 llm 包,缺失即降级不守门(规约条款 1:编排能力不得被增强功能拖垮)。
|
|
473
|
+
function registerTurnGuard(ctx, { currentRunId }) {
|
|
474
|
+
// 事件面缺失的上下文(旧宿主 / 精简组合)不注册守门——编排本身照常可用
|
|
475
|
+
if (typeof ctx.on !== 'function') return
|
|
476
|
+
const nudgeGate = createNudgeGate()
|
|
477
|
+
let createUserMessage
|
|
478
|
+
ctx.on('agent/turn-stopping', async ({ agent }) => {
|
|
479
|
+
const runId = currentRunId(agent)
|
|
480
|
+
if (runId === undefined) { return }
|
|
481
|
+
let record
|
|
482
|
+
try { record = reportStore().get(runId) } catch { return }
|
|
483
|
+
const pending = pendingOf(record, registry.drivers.get(runId))
|
|
484
|
+
if (pending === undefined) {
|
|
485
|
+
// 无欠动作(段在飞/终态):清计数并回收,防跨 run 累积
|
|
486
|
+
nudgeGate.clear(runId)
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
if (createUserMessage === undefined) {
|
|
490
|
+
// 消息构造器来自宿主 llm 包:解析失败即降级不守门,但须留痕(否则守门静默消失无从诊断)
|
|
491
|
+
try { ({ createUserMessage } = await import('@deepseek-ai/dsh-llm')) } catch (e) {
|
|
492
|
+
ctx.logger?.warn?.(`[rsww-guard] 消息构造器不可用,守门降级:${e?.message ?? e}`)
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (!nudgeGate.take(runId)) {
|
|
497
|
+
// 提醒额度耗尽仍未处置:留痕供诊断,不再 steer(防烧 token 死循环)
|
|
498
|
+
if (nudgeGate.exhausted(runId)) ctx.logger?.warn?.(`[rsww-guard] ${runId} 提醒已达上限仍未处置(欠${pending.need}),停止守门`)
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
agent.steer(createUserMessage({
|
|
503
|
+
content: [{ type: 'text', text: nudgeText(runId, pending) }],
|
|
504
|
+
source: { kind: 'plugin', plugin: 'rs-workflow' },
|
|
505
|
+
}))
|
|
506
|
+
} catch (e) {
|
|
507
|
+
// 轮次已中止 / agent 已停:守门无从施加,不影响编排本体
|
|
508
|
+
ctx.logger?.warn?.(`[rsww-guard] ${runId} 守门提醒投递失败:${e?.message ?? e}`)
|
|
509
|
+
}
|
|
510
|
+
})
|
|
511
|
+
}
|