@mzzsfy/dsh-rs-workflow 0.2.4 → 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.
@@ -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
+ }