@mzzsfy/dsh-rs-workflow 0.2.3 → 1.0.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/README.md +14 -49
- package/cordis.patch.yml +8 -17
- package/lib/board.mjs +355 -0
- package/lib/driver/approve.mjs +126 -0
- package/lib/driver/control.mjs +46 -0
- package/lib/driver/index.mjs +454 -0
- package/lib/driver/prompts.mjs +102 -0
- package/lib/driver/runner.mjs +173 -0
- package/lib/driver/scheduler.mjs +159 -0
- package/lib/index.js +30 -213
- package/lib/orchestrator.mjs +428 -0
- package/lib/planner-gate.mjs +195 -0
- package/lib/release.mjs +183 -0
- package/lib/settings-schema.mjs +73 -0
- package/lib/spec.mjs +161 -0
- package/lib/storage.mjs +35 -0
- package/lib/store.mjs +255 -0
- package/lib/template-tool.mjs +161 -0
- package/lib/template.mjs +341 -0
- package/package.json +8 -6
- package/src/client.js +1269 -0
- package/lib/preset-sync.mjs +0 -227
- package/preset/rs-workflow/agent.cordis.yml +0 -196
- package/preset/rs-workflow/preset.yml +0 -5
- package/preset/rs-workflow/skills/rs-workflow/SKILL.md +0 -110
- package/preset/rs-workflow/skills/rs-workflow/references/engine.js +0 -1446
- package/preset/rs-workflow/skills/rs-workflow/references/templates.md +0 -104
- package/preset/rs-workflow/skills/rs-workflow/slots.json5 +0 -53
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
// RunDriver 聚合(v5):剧本驱动;runSegment 分段推进(审批到达/暂停/终态即返回 settle 负载);
|
|
2
|
+
// 外部裁决回写(waiting_approval 即时应用,paused 入队 resume 生效);取消优先;推进责任在主循环 resume
|
|
3
|
+
import { reportStore } from '../store.mjs'
|
|
4
|
+
import { templateDeps } from '../planner-gate.mjs'
|
|
5
|
+
import { nextBatch, scriptViewOf } from './scheduler.mjs'
|
|
6
|
+
import { applyApproveResult, applyExternalVerdict, waitingPayload } from './approve.mjs'
|
|
7
|
+
import { runBatch } from './runner.mjs'
|
|
8
|
+
import { registerDriver, unregisterDriver } from './control.mjs'
|
|
9
|
+
|
|
10
|
+
const TERMINAL_STATES = new Set(['completed', 'cancelled', 'failed', 'blocked'])
|
|
11
|
+
|
|
12
|
+
const CANCELLED_SUMMARY = '用户取消,已完成步骤保留,可在会话页签断点续跑'
|
|
13
|
+
|
|
14
|
+
// 批次间宏任务让步时长:防止失败重试微任务级联饿死宿主同进程定时器/HTTP
|
|
15
|
+
const BATCH_YIELD_MS = 0
|
|
16
|
+
const yieldToLoop = () => new Promise((resolve) => setTimeout(resolve, BATCH_YIELD_MS))
|
|
17
|
+
|
|
18
|
+
// 运行态初始化:仅剧本内步骤入账(调度域=剧本,被裁剪步骤对引擎不存在)
|
|
19
|
+
function initState(script, request, inputs) {
|
|
20
|
+
const state = {
|
|
21
|
+
status: 'running', request, inputs: inputs ?? {},
|
|
22
|
+
steps: {}, approvals: {}, escalations: 0, queued: [], batchSeq: 0, slotCursor: {}, controlSeq: 0, redoInfo: {},
|
|
23
|
+
}
|
|
24
|
+
for (const step of script.steps) {
|
|
25
|
+
state.steps[step.id] = { status: 'pending', outputs: null, failCount: 0, instances: [] }
|
|
26
|
+
}
|
|
27
|
+
return state
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 子流程产出收编(spec §9:按子步骤扁平挂载;for_each 子步按实例序收编同名字段数组)
|
|
31
|
+
export function collectSubOutputs(subTemplate, subState) {
|
|
32
|
+
const outputs = {}
|
|
33
|
+
for (const child of subTemplate.steps) {
|
|
34
|
+
const cs = subState.steps[child.id]
|
|
35
|
+
if (!cs) continue
|
|
36
|
+
if (cs.outputs) {
|
|
37
|
+
outputs[`${child.id}.${Object.keys(cs.outputs)[0] ?? 'out'}`] = Object.values(cs.outputs)[0]
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
// for_each 子步:步骤级 outputs 恒空,产出在实例上
|
|
41
|
+
const doneInstances = (cs.instances ?? []).filter((i) => i.status === 'done' && i.outputs)
|
|
42
|
+
if (doneInstances.length > 0) {
|
|
43
|
+
outputs[`${child.id}.${Object.keys(doneInstances[0].outputs)[0] ?? 'out'}`] =
|
|
44
|
+
doneInstances.map((i) => Object.values(i.outputs)[0])
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return outputs
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 续跑种子:快照直读;fromStepId 及其 deps 后代全重置(含 done,旧产出作废,与 redo 语义一致)
|
|
51
|
+
export function buildSeed(record, plan, fromStepId, inputs) {
|
|
52
|
+
const state = JSON.parse(JSON.stringify(record.state ?? {}))
|
|
53
|
+
state.status = 'running'
|
|
54
|
+
state.request = record.request
|
|
55
|
+
state.inputs = inputs && Object.keys(inputs).length > 0 ? { ...(record.inputs ?? {}), ...inputs } : (record.inputs ?? {})
|
|
56
|
+
const scriptIds = (plan?.steps ?? []).map((p) => p.ref)
|
|
57
|
+
for (const id of scriptIds) {
|
|
58
|
+
state.steps[id] ??= { status: 'pending', outputs: null, failCount: 0, instances: [] }
|
|
59
|
+
}
|
|
60
|
+
const resetStep = (id) => {
|
|
61
|
+
const s = state.steps[id]
|
|
62
|
+
if (s) Object.assign(s, { status: 'pending', outputs: null, failCount: 0, instances: [] })
|
|
63
|
+
}
|
|
64
|
+
if (fromStepId && state.steps[fromStepId]) {
|
|
65
|
+
resetStep(fromStepId)
|
|
66
|
+
// 重置集合 = fromStepId 在 plan.deps 上的全部依赖闭包(后代),非剧本数组序
|
|
67
|
+
const dependents = new Map(scriptIds.map((id) => [id, []]))
|
|
68
|
+
for (const [id, ds] of Object.entries(plan?.deps ?? {})) {
|
|
69
|
+
for (const d of Array.isArray(ds) ? ds : []) dependents.get(d)?.push(id)
|
|
70
|
+
}
|
|
71
|
+
const descendants = new Set([fromStepId])
|
|
72
|
+
for (const q = [fromStepId]; q.length > 0;) {
|
|
73
|
+
for (const next of dependents.get(q.shift()) ?? []) {
|
|
74
|
+
if (!descendants.has(next)) { descendants.add(next); q.push(next) }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
for (const id of scriptIds) {
|
|
78
|
+
// 后代全重置(含 done):与 redo 路径语义一致——上游重跑后旧产出一律作废
|
|
79
|
+
if (descendants.has(id)) resetStep(id)
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
for (const id of scriptIds) {
|
|
83
|
+
const s = state.steps[id]
|
|
84
|
+
if (!s) continue
|
|
85
|
+
if (s.status === 'running') Object.assign(s, { status: 'pending', outputs: null })
|
|
86
|
+
for (const inst of s.instances ?? []) {
|
|
87
|
+
if (inst.status === 'running') inst.status = 'pending'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
state.pendingApprovals = []
|
|
91
|
+
// 终态污染标志清零:续跑后旧账不得再次收口(blocked)或无耗尽即派发升级步
|
|
92
|
+
state.terminalBlocked = false
|
|
93
|
+
state.escalateReady = []
|
|
94
|
+
state.escalateLimitReached = false
|
|
95
|
+
state.controlSeq = record.controls?.length ?? 0
|
|
96
|
+
return state
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export class RunDriver {
|
|
100
|
+
constructor({ template, templateSet = [], plan, warnings = [], runId, request, inputs, state, engine, slots = {}, budgets = {}, sessionId = '', workspace = '', store = reportStore(), signal, subordinate = false, parent = undefined }) {
|
|
101
|
+
this.template = template
|
|
102
|
+
this.templateSet = templateSet
|
|
103
|
+
this.plan = plan
|
|
104
|
+
this.warnings = warnings
|
|
105
|
+
this.script = scriptViewOf(template, plan)
|
|
106
|
+
this.planStepOf = new Map((plan?.steps ?? []).map((p) => [p.ref, p]))
|
|
107
|
+
this.runId = runId
|
|
108
|
+
this.request = request
|
|
109
|
+
this.inputs = inputs ?? {}
|
|
110
|
+
this.state = state ?? initState(this.script, request, inputs)
|
|
111
|
+
this.engine = engine
|
|
112
|
+
this.slots = slots
|
|
113
|
+
this.budgets = budgets
|
|
114
|
+
this.sessionId = sessionId
|
|
115
|
+
this.workspace = workspace
|
|
116
|
+
this.store = store
|
|
117
|
+
this.controller = new AbortController()
|
|
118
|
+
this.signal = signal ?? this.controller.signal
|
|
119
|
+
this.skipRecorded = new Set()
|
|
120
|
+
this.awaitingResume = false
|
|
121
|
+
this.finished = false
|
|
122
|
+
this.active = false
|
|
123
|
+
this.subordinate = subordinate
|
|
124
|
+
// 编排子代理的挂载父 agent(与 workflow 工具链对齐;undefined 时引擎派发行为未定义)
|
|
125
|
+
this.parent = parent
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// 段推进唯一入口:跑到下一个段边界(审批到达/暂停/终态)返回 settle 负载;orchestrator 包装为 continuable job
|
|
129
|
+
async runSegment() {
|
|
130
|
+
if (this.finished || TERMINAL_STATES.has(this.state.status)) {
|
|
131
|
+
return this.terminalPayload()
|
|
132
|
+
}
|
|
133
|
+
this.active = true
|
|
134
|
+
try {
|
|
135
|
+
this.drainPendingApprovals()
|
|
136
|
+
return await this.loop()
|
|
137
|
+
} catch (e) {
|
|
138
|
+
if (this.signal.aborted) {
|
|
139
|
+
this.finish('cancelled', CANCELLED_SUMMARY)
|
|
140
|
+
return this.terminalPayload()
|
|
141
|
+
}
|
|
142
|
+
this.finish('failed', `驱动器异常:${e?.message ?? e}`)
|
|
143
|
+
return this.terminalPayload()
|
|
144
|
+
} finally {
|
|
145
|
+
this.active = false
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
startPersist() {
|
|
150
|
+
registerDriver(this.runId, this)
|
|
151
|
+
this.store.start({
|
|
152
|
+
runId: this.runId, sessionId: this.sessionId, workspace: this.workspace,
|
|
153
|
+
request: this.request, templateId: this.template.id, inputs: this.inputs,
|
|
154
|
+
plan: this.plan, warnings: this.warnings, state: this.state,
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
pause() {
|
|
159
|
+
if (this.finished || TERMINAL_STATES.has(this.state.status)) return
|
|
160
|
+
// 统一转 paused(与 waiting_approval 互斥,paused 优先);活跃段在飞批次收敛后于段顶 settle
|
|
161
|
+
this.state.status = 'paused'
|
|
162
|
+
this.persistState()
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 页签 control resume:仅标记恢复意图(不翻状态不拉段);主循环 rs_workflow_resume 执行翻转与推进
|
|
166
|
+
tabResume() {
|
|
167
|
+
if (this.finished || this.state.status !== 'paused') return false
|
|
168
|
+
this.awaitingResume = true
|
|
169
|
+
this.store.step({ runId: this.runId, event: 'control', body: { kind: 'resume' } })
|
|
170
|
+
return true
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
cancel() {
|
|
174
|
+
if (this.finished) return
|
|
175
|
+
if (!this.active) {
|
|
176
|
+
// 取消优先:无活跃段(审批等待/暂停/裁决窗口)即时终态,不依赖 abort 传播
|
|
177
|
+
this.finish('cancelled', CANCELLED_SUMMARY)
|
|
178
|
+
return
|
|
179
|
+
}
|
|
180
|
+
this.controller.abort()
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// 控制回写受理:approve/reject 外部裁决 + message 边界消息;返回 false = 未受理(状态不符)
|
|
184
|
+
handlePost(event) {
|
|
185
|
+
if (this.finished || TERMINAL_STATES.has(this.state.status)) return false
|
|
186
|
+
if (event.kind === 'approve' || event.kind === 'reject') {
|
|
187
|
+
return this.applyVerdictPost(event)
|
|
188
|
+
}
|
|
189
|
+
if (event.kind !== 'message') return false
|
|
190
|
+
if (typeof event.text !== 'string' || event.text === '') return false
|
|
191
|
+
// controls 记摘要(≤120 字),全文进 queued(data-design controls[].text 契约)
|
|
192
|
+
this.store.step({ runId: this.runId, event: 'control', body: { kind: 'message', text: event.text.slice(0, 120), inject: !!event.inject } })
|
|
193
|
+
const record = this.store.get(this.runId)
|
|
194
|
+
this.store.update({ runId: this.runId, queued: [...(record.queued ?? []), event.text] })
|
|
195
|
+
return true
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// 裁决回写:waiting_approval 即时应用;paused 入队(resume 段首生效)
|
|
199
|
+
applyVerdictPost(event) {
|
|
200
|
+
const verdict = event.kind === 'approve' ? 'APPROVED' : 'REJECTED'
|
|
201
|
+
if (this.state.status === 'paused') {
|
|
202
|
+
// 无审批窗口(paused 非审批转入)裁决无处挂靠,不受理
|
|
203
|
+
if (this.state.waitingApproval === undefined) return false
|
|
204
|
+
// 先到先得:同一步已有入队裁决则后到不受理(防 rounds 虚增/已 done 步被重开)
|
|
205
|
+
this.state.pendingApprovals ??= []
|
|
206
|
+
const stepId = this.state.waitingApproval
|
|
207
|
+
if (this.state.pendingApprovals.some((p) => p.stepId === stepId)) return false
|
|
208
|
+
this.state.pendingApprovals.push({ stepId, verdict, comments: event.reason ?? '', by: event.by })
|
|
209
|
+
// 受理即记账(与 waiting 分支对称):页签裁决来源回显不因 paused 入队丢失
|
|
210
|
+
this.store.step({ runId: this.runId, event: 'control', body: { kind: event.kind, by: event.by, reason: event.reason } })
|
|
211
|
+
this.persistState()
|
|
212
|
+
return true
|
|
213
|
+
}
|
|
214
|
+
if (this.state.status !== 'waiting_approval') return false
|
|
215
|
+
const approveStep = this.script.steps.find((s) => s.id === this.state.waitingApproval)
|
|
216
|
+
if (!approveStep) return false
|
|
217
|
+
this.store.step({ runId: this.runId, event: 'control', body: { kind: event.kind, by: event.by, reason: event.reason } })
|
|
218
|
+
const { applied, route } = applyExternalVerdict(this.state, this.script, approveStep, { verdict, comments: event.reason ?? '' }, this.budgets)
|
|
219
|
+
if (applied) {
|
|
220
|
+
this.state.waitingApproval = undefined
|
|
221
|
+
if (route.verdict === 'REJECTED' && !route.exhausted) {
|
|
222
|
+
this.state.redoInfo = { [route.redoTarget]: { comments: route.comments, prevOutputs: route.prevOutputs } }
|
|
223
|
+
}
|
|
224
|
+
this.store.update({ runId: this.runId, waiting: null })
|
|
225
|
+
this.persistState()
|
|
226
|
+
}
|
|
227
|
+
return applied
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// 段首:应用 paused 期间入队的裁决(resume 后生效)
|
|
231
|
+
drainPendingApprovals() {
|
|
232
|
+
const pending = this.state.pendingApprovals ?? []
|
|
233
|
+
if (pending.length === 0) return
|
|
234
|
+
this.state.pendingApprovals = []
|
|
235
|
+
for (const p of pending) {
|
|
236
|
+
const approveStep = this.script.steps.find((s) => s.id === p.stepId)
|
|
237
|
+
if (!approveStep) continue
|
|
238
|
+
const verdict = p.verdict === 'APPROVED' ? 'APPROVED' : 'REJECTED'
|
|
239
|
+
this.state.status = 'waiting_approval'
|
|
240
|
+
const { route } = applyExternalVerdict(this.state, this.script, approveStep, { verdict, comments: p.comments }, this.budgets)
|
|
241
|
+
this.state.status = 'running'
|
|
242
|
+
if (route.verdict === 'REJECTED' && !route.exhausted) {
|
|
243
|
+
this.state.redoInfo = { [route.redoTarget]: { comments: route.comments, prevOutputs: route.prevOutputs } }
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
this.store.update({ runId: this.runId, waiting: null })
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// 边界通道 drain:消费 controls 中未消费的 message 事件
|
|
250
|
+
drainControls() {
|
|
251
|
+
const record = this.store.get(this.runId)
|
|
252
|
+
const pending = (record.controls ?? []).slice(this.state.controlSeq ?? 0)
|
|
253
|
+
// 全文以 queued 为准(controls[].text 是 ≤120 字审计摘要);按消息顺序与队列对齐
|
|
254
|
+
const fullTexts = [...(record.queued ?? [])]
|
|
255
|
+
const inject = []
|
|
256
|
+
const queued = []
|
|
257
|
+
for (const c of pending) {
|
|
258
|
+
if (c.kind !== 'message') continue
|
|
259
|
+
const text = fullTexts.shift() ?? c.text
|
|
260
|
+
if (c.inject) inject.push(text)
|
|
261
|
+
else queued.push(text)
|
|
262
|
+
}
|
|
263
|
+
this.state.controlSeq = (record.controls ?? []).length
|
|
264
|
+
this.store.update({ runId: this.runId, queued: [] })
|
|
265
|
+
return { inject, queued }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
persistState() {
|
|
269
|
+
this.store.update({ runId: this.runId, state: this.state, status: this.state.status })
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
finish(status, summary) {
|
|
273
|
+
if (this.finished) return
|
|
274
|
+
this.finished = true
|
|
275
|
+
this.state.status = status
|
|
276
|
+
if (this.subordinate) return
|
|
277
|
+
// waiting 摘要随终态清除,防终态卡残留「待审批」区块
|
|
278
|
+
this.store.update({ runId: this.runId, queued: [], waiting: null })
|
|
279
|
+
this.store.finish({ runId: this.runId, status, summary: summary ?? '' })
|
|
280
|
+
unregisterDriver(this.runId)
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
outputsIndex() {
|
|
284
|
+
const out = {}
|
|
285
|
+
for (const [id, s] of Object.entries(this.state.steps)) {
|
|
286
|
+
if (s.status === 'done' && s.outputs) out[id] = s.outputs
|
|
287
|
+
}
|
|
288
|
+
return out
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
terminalPayload() {
|
|
292
|
+
return { kind: 'terminal', runId: this.runId, status: this.state.status, summary: this.store.get(this.runId)?.summary ?? '', outputsIndex: this.outputsIndex() }
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
async loop() {
|
|
296
|
+
for (;;) {
|
|
297
|
+
if (this.signal.aborted) {
|
|
298
|
+
this.finish('cancelled', CANCELLED_SUMMARY)
|
|
299
|
+
return this.terminalPayload()
|
|
300
|
+
}
|
|
301
|
+
// 暂停即段边界:在飞批次收敛后回到此处,本段以 paused settle
|
|
302
|
+
if (this.state.status === 'paused') {
|
|
303
|
+
this.persistState()
|
|
304
|
+
return { kind: 'paused', runId: this.runId, status: 'paused' }
|
|
305
|
+
}
|
|
306
|
+
await yieldToLoop()
|
|
307
|
+
const batch = nextBatch(this.state, this.script, this.budgets)
|
|
308
|
+
// skipped 落账:页签步骤轨迹补 skip 行;以 stepsTrace 幂等(重启续跑不重复记账)
|
|
309
|
+
for (const step of this.script.steps) {
|
|
310
|
+
const s = this.state.steps[step.id]
|
|
311
|
+
if (s?.status !== 'skipped' || this.skipRecorded.has(step.id)) continue
|
|
312
|
+
const events = this.store.get(this.runId)?.stepsTrace?.[step.id]?.['-'] ?? []
|
|
313
|
+
if (events.some((e) => e.event === 'skip')) { this.skipRecorded.add(step.id); continue }
|
|
314
|
+
this.skipRecorded.add(step.id)
|
|
315
|
+
this.store.step({ runId: this.runId, stepId: step.id, event: 'skip', body: { reason: s.skipReason ?? '' } })
|
|
316
|
+
}
|
|
317
|
+
if (batch.kind === 'terminal') {
|
|
318
|
+
// 升级账/审批耗尽置账后,blocked 终态先于 pending 残留判定
|
|
319
|
+
if (this.state.terminalBlocked) {
|
|
320
|
+
this.finish('blocked', this.state.terminalBlocked)
|
|
321
|
+
} else {
|
|
322
|
+
this.judgeTerminal()
|
|
323
|
+
}
|
|
324
|
+
return this.terminalPayload()
|
|
325
|
+
}
|
|
326
|
+
if (batch.kind === 'blocked') {
|
|
327
|
+
this.finish('blocked', this.state.terminalBlocked ?? batch.reason)
|
|
328
|
+
return this.terminalPayload()
|
|
329
|
+
}
|
|
330
|
+
if (batch.kind === 'idle') {
|
|
331
|
+
this.persistState()
|
|
332
|
+
await new Promise((r) => setTimeout(r, 50))
|
|
333
|
+
continue
|
|
334
|
+
}
|
|
335
|
+
if (batch.kind === 'flow') {
|
|
336
|
+
await this.runFlowStep(batch.step)
|
|
337
|
+
if (this.finished) return this.terminalPayload()
|
|
338
|
+
this.persistState()
|
|
339
|
+
continue
|
|
340
|
+
}
|
|
341
|
+
if (batch.kind === 'approve') {
|
|
342
|
+
// v5 外部裁决:不派发,置 waiting_approval 即段边界
|
|
343
|
+
const step = batch.step
|
|
344
|
+
this.state.status = 'waiting_approval'
|
|
345
|
+
this.state.waitingApproval = step.id
|
|
346
|
+
this.persistState()
|
|
347
|
+
const payload = waitingPayload({ runId: this.runId, state: this.state, script: this.script, planStepOf: this.planStepOf, approveStep: step })
|
|
348
|
+
// 待裁决摘要落盘:页签 /run 路由据此渲染审批上下文
|
|
349
|
+
this.store.update({ runId: this.runId, waiting: payload.waiting })
|
|
350
|
+
return payload
|
|
351
|
+
}
|
|
352
|
+
// 派发时才消费控制消息:approve/flow/idle/terminal 边界不丢不耗,留给真正组装 prompt 的批次
|
|
353
|
+
const { inject, queued } = this.drainControls()
|
|
354
|
+
const outcome = await runBatch({
|
|
355
|
+
state: this.state, script: this.script, template: this.template,
|
|
356
|
+
batch: batch.calls,
|
|
357
|
+
ctx: {
|
|
358
|
+
store: this.store, runId: this.runId, engine: this.engine, parent: this.parent,
|
|
359
|
+
signal: this.signal, slots: this.slots, budgets: this.budgets,
|
|
360
|
+
request: this.request, inputs: this.inputs, injectMessages: inject, queuedMessages: queued,
|
|
361
|
+
readDoc: this.readDoc, planStepOf: this.planStepOf,
|
|
362
|
+
},
|
|
363
|
+
})
|
|
364
|
+
// 重做说明只服务紧邻的重做批次,派发后即清
|
|
365
|
+
this.state.redoInfo = {}
|
|
366
|
+
if (outcome.cancelled) {
|
|
367
|
+
this.finish('cancelled', CANCELLED_SUMMARY)
|
|
368
|
+
return this.terminalPayload()
|
|
369
|
+
}
|
|
370
|
+
this.persistState()
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
judgeTerminal() {
|
|
375
|
+
const failed = Object.values(this.state.steps).some((s) => s.status === 'failed')
|
|
376
|
+
if (failed) {
|
|
377
|
+
this.finish('failed', '存在失败步骤(失败账耗尽),下游已跳过')
|
|
378
|
+
return
|
|
379
|
+
}
|
|
380
|
+
this.finish('completed', '全部步骤完成')
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// 嵌套子流程:解析路由 → 模板集查找 → 递归子循环;route 空即 done
|
|
384
|
+
async runFlowStep(step) {
|
|
385
|
+
const s = this.state.steps[step.id]
|
|
386
|
+
s.status = 'running'
|
|
387
|
+
const route = this.resolveFlowRoute(step)
|
|
388
|
+
if (!route) {
|
|
389
|
+
s.status = 'done'
|
|
390
|
+
s.outputs = {}
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
const sub = this.templateSet?.find((t) => t.id === route)
|
|
394
|
+
if (!sub) {
|
|
395
|
+
s.status = 'failed'
|
|
396
|
+
s.error = `子流程模板不存在:${route}`
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
const { deps: subDeps } = templateDeps(sub)
|
|
400
|
+
const subScript = scriptViewOf(sub, { steps: sub.steps.map((x) => ({ ref: x.id })), deps: subDeps })
|
|
401
|
+
const subState = initState(subScript, this.request, this.resolveFlowInputs(step))
|
|
402
|
+
const subDriver = new RunDriver({
|
|
403
|
+
template: sub, plan: { source: 'fallback', brief: '', steps: sub.steps.map((x) => ({ ref: x.id, note: '', done: '' })), deps: subDeps },
|
|
404
|
+
runId: this.runId, request: this.request,
|
|
405
|
+
inputs: subState.inputs, state: subState, engine: this.engine, parent: this.parent,
|
|
406
|
+
slots: this.slots, budgets: this.budgets, sessionId: this.sessionId, workspace: this.workspace,
|
|
407
|
+
store: this.store, signal: this.signal, subordinate: true,
|
|
408
|
+
})
|
|
409
|
+
this.store.step({ runId: this.runId, stepId: step.id, event: 'dispatch', body: { prompt: `[嵌套子流程] ${route}`, callLabel: route } })
|
|
410
|
+
await subDriver.loop()
|
|
411
|
+
if (this.signal.aborted) return
|
|
412
|
+
if (subState.status === 'completed') {
|
|
413
|
+
s.status = 'done'
|
|
414
|
+
s.outputs = collectSubOutputs(sub, subState)
|
|
415
|
+
this.store.step({ runId: this.runId, stepId: step.id, event: 'submit', body: { outputs: s.outputs } })
|
|
416
|
+
} else {
|
|
417
|
+
s.status = 'failed'
|
|
418
|
+
s.error = `子流程 ${route} 终态 ${subState.status}`
|
|
419
|
+
this.store.step({ runId: this.runId, stepId: step.id, event: 'fail', body: { error: s.error } })
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
resolveFlowRoute(step) {
|
|
424
|
+
const m = /^\{([^{}]+)\}$/.exec(String(step.flow).trim())
|
|
425
|
+
if (!m) return String(step.flow).trim()
|
|
426
|
+
const [refId, refOut] = m[1].split('.')
|
|
427
|
+
const ref = this.state.steps[refId]
|
|
428
|
+
const v = ref?.outputs?.[refOut]
|
|
429
|
+
if (Array.isArray(v)) return v[0] ?? ''
|
|
430
|
+
return typeof v === 'string' ? v.trim() : ''
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
resolveFlowInputs(step) {
|
|
434
|
+
const out = {}
|
|
435
|
+
for (const [k, v] of Object.entries(step.input ?? {})) {
|
|
436
|
+
const m = /^\{([^{}]+)\}$/.exec(String(v).trim())
|
|
437
|
+
if (!m) continue
|
|
438
|
+
const [refId, refOut] = m[1].split('.')
|
|
439
|
+
const ref = this.state.steps[refId]
|
|
440
|
+
out[k] = ref?.outputs?.[refOut] ?? ''
|
|
441
|
+
}
|
|
442
|
+
return out
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// 发起入口(v5):创建 driver 并落盘注册,不启动段;首段由 orchestrator 包装 continuable job 调 runSegment
|
|
447
|
+
export function startRun({ template, templateSet = [], plan, warnings = [], runId, request, inputs, engine, slots, budgets, sessionId, workspace, state, parent }) {
|
|
448
|
+
const store = reportStore()
|
|
449
|
+
RunDriver.seq = (RunDriver.seq ?? 0) + 1
|
|
450
|
+
const id = runId ?? `r-${Date.now().toString(36)}-${RunDriver.seq}`
|
|
451
|
+
const driver = new RunDriver({ template, templateSet, plan, warnings, runId: id, request, inputs, engine, slots, budgets, sessionId, workspace, store, state, parent })
|
|
452
|
+
driver.startPersist()
|
|
453
|
+
return driver
|
|
454
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// 指令组装:节顺序固定 [任务][任务要点][产出要求][参考资料][用户补充][运行中用户消息][重做说明]
|
|
2
|
+
// 剧本 note/done 注入([任务要点] 节与产出要求末尾"本任务口径:");fallback 剧本不注入
|
|
3
|
+
import { buildSchema } from '../template.mjs'
|
|
4
|
+
import { stepTypeOf } from './scheduler.mjs'
|
|
5
|
+
|
|
6
|
+
const PLACEHOLDER_RE = /\{([^{}]+)\}/g
|
|
7
|
+
const DOC_TRUNCATE = 16 * 1024
|
|
8
|
+
export const VERDICT_SCHEMA = buildSchema({ type: 'approve' })
|
|
9
|
+
|
|
10
|
+
// 占位符解析;未满足来源解析空串(静态校验期已拦截非法引用)
|
|
11
|
+
export function resolvePlaceholders(text, ctx) {
|
|
12
|
+
return String(text).replace(PLACEHOLDER_RE, (_, ph) => {
|
|
13
|
+
if (ph === 'request') return ctx.request ?? ''
|
|
14
|
+
if (ph === 'item') return ctx.item !== undefined ? String(ctx.item) : ''
|
|
15
|
+
if (ph === 'item.index') return ctx.index !== undefined ? String(ctx.index) : ''
|
|
16
|
+
if (ph.startsWith('input.')) {
|
|
17
|
+
const v = ctx.inputs?.[ph.slice(6)]
|
|
18
|
+
return v === undefined || v === null ? '' : String(v)
|
|
19
|
+
}
|
|
20
|
+
const dot = ph.indexOf('.')
|
|
21
|
+
if (dot > 0) {
|
|
22
|
+
const refId = ph.slice(0, dot)
|
|
23
|
+
const refOut = ph.slice(dot + 1)
|
|
24
|
+
if (ctx.selfId === refId) {
|
|
25
|
+
if (ctx.carry !== undefined && ctx.carry !== null) return String(ctx.carry[refOut] ?? '')
|
|
26
|
+
return ''
|
|
27
|
+
}
|
|
28
|
+
const ref = ctx.state?.steps?.[refId]
|
|
29
|
+
if (ref?.outputs && refOut in ref.outputs) {
|
|
30
|
+
const v = ref.outputs[refOut]
|
|
31
|
+
return Array.isArray(v) ? v.join('\n') : String(v ?? '')
|
|
32
|
+
}
|
|
33
|
+
return ''
|
|
34
|
+
}
|
|
35
|
+
return ''
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function outputsSection(step, planStep) {
|
|
40
|
+
if (stepTypeOf(step) === 'approve') {
|
|
41
|
+
return [
|
|
42
|
+
'[产出要求]',
|
|
43
|
+
'verdict:审批裁决,只能是 APPROVED(达标放行)或 REJECTED(驳回)',
|
|
44
|
+
'comments:裁决说明;驳回时必须写明逐条修正要求',
|
|
45
|
+
'完成后以 structured_output 工具提交,字段缺失或为空视同本步失败。',
|
|
46
|
+
]
|
|
47
|
+
}
|
|
48
|
+
const lines = ['[产出要求]']
|
|
49
|
+
const listSet = new Set(step.listOutputs ?? [])
|
|
50
|
+
for (const [name, desc] of Object.entries(step.outputs ?? {})) {
|
|
51
|
+
lines.push(`${name}:${desc}${listSet.has(name) ? '(字符串列表,每行一项)' : ''}`)
|
|
52
|
+
}
|
|
53
|
+
lines.push('完成后以 structured_output 工具提交,字段缺失或为空视同本步失败。')
|
|
54
|
+
if (typeof planStep?.done === 'string' && planStep.done.trim() !== '') {
|
|
55
|
+
lines.push(`本任务口径:${planStep.done}`)
|
|
56
|
+
}
|
|
57
|
+
return lines
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function loadSection(step, ctx) {
|
|
61
|
+
if (!Array.isArray(step.load) || step.load.length === 0) return []
|
|
62
|
+
const lines = ['[参考资料]']
|
|
63
|
+
for (const entry of step.load) {
|
|
64
|
+
if (entry.startsWith('skill:')) {
|
|
65
|
+
lines.push(`本步需使用技能 ${entry.slice(6)}(经技能工具加载后按其指引执行)。`)
|
|
66
|
+
} else if (entry.startsWith('doc:')) {
|
|
67
|
+
const text = ctx.readDoc?.(entry.slice(4))
|
|
68
|
+
if (text) lines.push(text.length > DOC_TRUNCATE ? `${text.slice(0, DOC_TRUNCATE)}\n...(已截断)` : text)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return lines.length > 1 ? lines : []
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 批次指令;ctx: {state, request, inputs, step, inst, planStep, injectMessages, queuedMessages, redo, readDoc}
|
|
75
|
+
export function buildPrompt(ctx) {
|
|
76
|
+
const { step, inst, planStep } = ctx
|
|
77
|
+
const phCtx = {
|
|
78
|
+
request: ctx.request, inputs: ctx.inputs, state: ctx.state,
|
|
79
|
+
item: inst?.item, index: inst?.index, carry: inst?.carry, selfId: step.id,
|
|
80
|
+
}
|
|
81
|
+
const lines = [`[任务]`, resolvePlaceholders(step.prompt, phCtx)]
|
|
82
|
+
lines.push('[角色边界]', '你是若水编排的步骤执行者,不是主控:禁止调用 rs_workflow_* 工具、禁止再发起编排或工作流;直接完成本任务并按产出要求提交。')
|
|
83
|
+
if (typeof planStep?.note === 'string' && planStep.note.trim() !== '') {
|
|
84
|
+
lines.push('[任务要点]', planStep.note)
|
|
85
|
+
}
|
|
86
|
+
lines.push(...outputsSection(step, planStep))
|
|
87
|
+
lines.push(...loadSection(step, ctx))
|
|
88
|
+
if (ctx.queuedMessages?.length) {
|
|
89
|
+
lines.push('[用户补充]', ...ctx.queuedMessages.map((m) => `- ${m}`))
|
|
90
|
+
}
|
|
91
|
+
if (ctx.injectMessages?.length) {
|
|
92
|
+
lines.push('[运行中用户消息]', ...ctx.injectMessages.map((m) => `- ${m}`))
|
|
93
|
+
}
|
|
94
|
+
if (ctx.redo) {
|
|
95
|
+
lines.push('[重做说明]', `上一轮产出被驳回,审批意见:${ctx.redo.comments || '(无补充说明)'}`, `上一轮产出:`, ctx.redo.prevOutputs ?? '(无)')
|
|
96
|
+
}
|
|
97
|
+
return lines.join('\n')
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function schemaOf(step) {
|
|
101
|
+
return stepTypeOf(step) === 'approve' ? VERDICT_SCHEMA : buildSchema(step)
|
|
102
|
+
}
|