dsh-collab-workbench 0.0.0-stage → 0.1.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.

Potentially problematic release.


This version of dsh-collab-workbench might be problematic. Click here for more details.

@@ -0,0 +1,417 @@
1
+ import { randomUUID } from 'node:crypto'
2
+
3
+ /** Replay only admission facts when recovering a cold Session. */
4
+ export function executionAdmission(events = []) {
5
+ const inbox = {'next-turn': [], 'next-step': []}, read = new Set(), legacy = new Set()
6
+ for (const event of events) {
7
+ if (event.type === 'user/message') {
8
+ const id = event.data?.source?.executionInputId
9
+ if (id) read.add(id)
10
+ } else if (event.type === 'agent/inbox/spliced' && inbox[event.data?.target]) {
11
+ const data = event.data
12
+ inbox[data.target].splice(data.start, data.removedCount ?? 0, ...(data.inserted ?? []))
13
+ } else if (event.type === 'inbox/inserted') {
14
+ const id = event.message?.source?.executionInputId
15
+ if (id) legacy.add(id)
16
+ }
17
+ }
18
+ const pending = new Set([...Object.values(inbox).flat().map(m => m.source?.executionInputId).filter(Boolean), ...legacy])
19
+ return {read, pending}
20
+ }
21
+
22
+ const TERMINAL = new Set(['completed', 'cancelled'])
23
+ const ACTIVE = new Set(['starting', 'running', 'approval-wait'])
24
+ const REPORTS = new Set(['running', 'waiting', 'blocked', 'completed', 'failed'])
25
+ const PRIORITY = { 高: 0, high: 0, 中: 1, medium: 1, 低: 2, low: 2 }
26
+ const bounded = value => String(value ?? '').slice(0, 4000)
27
+ const clone = value => structuredClone(value)
28
+ const revision = task => JSON.stringify([task.title ?? '', task.description ?? '', task.dependencies ?? [], task.revision ?? 1])
29
+
30
+ /**
31
+ * Durable scheduler. All mutations are serialized; transport callbacks must not
32
+ * await calls back into this engine. persist() must durably save getState().
33
+ * transport.deliver(run,input) must attach source.executionInputId = input.id to
34
+ * the admitted user message/inbox event. Unknown admission is at-least-once:
35
+ * after a crash an unlogged input may be sent again with the SAME id. The
36
+ * transport should also deduplicate this id where possible. ensureSession(run)
37
+ * should be idempotent by run.id (a crash can precede persisting its result).
38
+ * maxAttempts counts execution starts per automatic retry budget. Explicit
39
+ * retry resets it. canRun is a synchronous scheduling/delivery gate.
40
+ */
41
+ export class ExecutionEngine {
42
+ constructor({ getState, persist, transport, now = Date.now, maxConcurrent = 3,
43
+ maxAttempts = 3, retryBaseMs = 1000, staleMs = 15 * 60 * 1000,
44
+ canRun = () => true } = {}) {
45
+ if (typeof getState !== 'function' || typeof persist !== 'function' || !transport)
46
+ throw new TypeError('getState, persist and transport are required')
47
+ for (const [key, value] of Object.entries({ maxConcurrent, maxAttempts })) {
48
+ if (!Number.isSafeInteger(value) || value < 1) throw new TypeError(`${key} must be a positive integer`)
49
+ }
50
+ for (const [key, value] of Object.entries({ retryBaseMs, staleMs })) {
51
+ if (!Number.isFinite(value) || value < 0) throw new TypeError(`${key} must be nonnegative`)
52
+ }
53
+ Object.assign(this, { getState, persist, transport, now, maxConcurrent, maxAttempts, retryBaseMs, staleMs, canRun })
54
+ this.tail = Promise.resolve()
55
+ }
56
+
57
+ _serial(fn) {
58
+ const result = this.tail.then(fn)
59
+ this.tail = result.catch(() => {})
60
+ return result
61
+ }
62
+ _state() {
63
+ const state = this.getState()
64
+ state.runs ??= []; state.inputs ??= []; state.version ??= 1
65
+ return state
66
+ }
67
+ _touch(run) { run.updatedAt = this.now() }
68
+ _heartbeat(run) {
69
+ this._touch(run); run.lastActivityAt = this.now(); delete run.attention
70
+ }
71
+ _inputs(run) { return this._state().inputs.filter(input => input.runId === run.id) }
72
+ _addInput(run, text, kind, eventId) {
73
+ const inputs = this._state().inputs
74
+ if (eventId !== undefined) {
75
+ const prior = inputs.find(i => i.runId === run.id && i.eventId === eventId)
76
+ if (prior) return prior
77
+ }
78
+ const input = { id: randomUUID(), runId: run.id, taskId: run.taskId,
79
+ text: String(text ?? ''), kind, status: 'pending', attempts: 0,
80
+ nextAt: this.now(), createdAt: this.now() }
81
+ if (eventId !== undefined) input.eventId = eventId
82
+ inputs.push(input)
83
+ return input
84
+ }
85
+ _enqueue(task, { legacy = false, generation: requestedGeneration } = {}) {
86
+ const runs = this._state().runs
87
+ const previous = runs.filter(r => r.taskId === task.id)
88
+ const generation = requestedGeneration ?? Math.max(1, ...previous.map(r => r.generation || 1))
89
+ const created = []
90
+ for (const roleId of new Set(task.assignees ?? [])) {
91
+ if (previous.some(r => r.roleId === roleId && r.generation === generation && r.status !== 'cancelled')) continue
92
+ const run = { id: randomUUID(), taskId: task.id, groupId: task.groupId,
93
+ roleId, generation, sessionId: null, status: legacy ? 'paused' : 'queued',
94
+ attempts: 0, createdAt: this.now(), updatedAt: this.now(), lastActivityAt: this.now(),
95
+ summary: '', priority: task.priority, revision: revision(task), taskRevision: task.revision ?? 1,
96
+ title: String(task.title ?? ''), description: String(task.description ?? '') }
97
+ runs.push(run); created.push(run)
98
+ this._addInput(run, [task.title, task.description].filter(Boolean).join('\n'), 'assignment')
99
+ }
100
+ return created
101
+ }
102
+ enqueue(task, options = {}) {
103
+ return this._serial(async () => {
104
+ const created = this._enqueue(task, options)
105
+ await this.persist(); return clone(created)
106
+ })
107
+ }
108
+ list(taskId) {
109
+ return clone(this._state().runs.filter(r => taskId === undefined || r.taskId === taskId))
110
+ }
111
+ input(taskId, text, { eventId, roleIds, kind = 'followup' } = {}) {
112
+ return this._serial(async () => {
113
+ const created = []
114
+ for (const run of this._state().runs) {
115
+ // Paused/blocked/failed runs need explicit retry, including user stops.
116
+ if (run.taskId !== taskId || !['queued', 'retry', 'starting', 'running', 'approval-wait', 'waiting'].includes(run.status)
117
+ || (roleIds && !roleIds.includes(run.roleId))) continue
118
+ created.push(this._addInput(run, text, kind, eventId))
119
+ if (run.status === 'waiting') { run.status = 'queued'; run.attempts = 0; run.nextAt = this.now(); run.reminders = 0; this._heartbeat(run) }
120
+ }
121
+ await this.persist(); return clone(created)
122
+ })
123
+ }
124
+ _finish(run, status) {
125
+ run.approvalIds = []; run.status = status; this._touch(run)
126
+ for (const input of this._inputs(run)) {
127
+ if (status === 'cancelled' || !['read', 'cancelled'].includes(input.status)) input.status = 'cancelled'
128
+ }
129
+ }
130
+ async _cancelSessions(runs) {
131
+ for (const run of runs) {
132
+ if (!run.sessionId) continue
133
+ try { await this.transport.cancel(clone(run)) }
134
+ catch (error) { run.cancelError = bounded(error?.message ?? error); await this.persist() }
135
+ }
136
+ }
137
+ cancelTask(taskId) {
138
+ return this._serial(async () => {
139
+ const runs = this._state().runs.filter(r => r.taskId === taskId && !TERMINAL.has(r.status))
140
+ for (const run of runs) this._finish(run, 'cancelled')
141
+ await this.persist(); await this._cancelSessions(runs)
142
+ return clone(runs)
143
+ })
144
+ }
145
+ pauseRole(groupId, roleId) {
146
+ return this._serial(async () => {
147
+ const runs = this._state().runs.filter(r => (!groupId || r.groupId === groupId) && r.roleId === roleId && !TERMINAL.has(r.status))
148
+ for (const run of runs) { run.approvalIds = []; run.status = 'paused'; run.summary = '用户停止或成员已停用'; this._touch(run) }
149
+ await this.persist(); await this._cancelSessions(runs)
150
+ return clone(runs)
151
+ })
152
+ }
153
+ resolveDecision(taskId, text, {eventId, runId, commit = () => {}} = {}) {
154
+ return this._serial(async () => {
155
+ for (const run of this._state().runs.filter(r => r.taskId === taskId && (!runId || r.id === runId) && ['blocked','waiting'].includes(r.status))) {
156
+ this._addInput(run, text, 'decision', eventId)
157
+ run.status = 'queued'; run.nextAt = this.now(); run.attempts = 0; run.reported = false
158
+ this._heartbeat(run)
159
+ }
160
+ commit()
161
+ await this.persist()
162
+ })
163
+ }
164
+ retryTask(taskId) {
165
+ return this._serial(async () => {
166
+ const runs = this._state().runs.filter(r => r.taskId === taskId && ['failed', 'blocked', 'paused', 'waiting'].includes(r.status))
167
+ for (const run of runs) {
168
+ run.status = 'queued'; run.attempts = 0; run.nextAt = this.now(); run.reminders = 0
169
+ delete run.error; this._heartbeat(run)
170
+ for (const input of this._inputs(run)) {
171
+ if (['failed','sent','delivering'].includes(input.status)) { input.status = 'pending'; input.attempts = 0; input.nextAt = this.now() }
172
+ }
173
+ this._addInput(run, 'Explicit retry: resume this task and report status with evidence when complete.', 'recovery')
174
+ }
175
+ await this.persist(); return clone(runs)
176
+ })
177
+ }
178
+ report(runId, { status, summary, evidence = [] } = {}) {
179
+ return this._serial(async () => {
180
+ const run = this._state().runs.find(r => r.id === runId)
181
+ if (!run || TERMINAL.has(run.status) || run.status === 'paused') return false
182
+ if (run.status === 'approval-wait') throw new Error('Cannot report while approval is pending')
183
+ if (!REPORTS.has(status)) throw new TypeError('Unsupported execution report status')
184
+ if (status === 'completed' && (!String(summary ?? '').trim() || !Array.isArray(evidence)
185
+ || !evidence.some(item => item != null && (typeof item !== 'string' || item.trim()))))
186
+ throw new TypeError('completed requires nonempty summary and evidence')
187
+ if (!Array.isArray(evidence)) throw new TypeError('evidence must be an array')
188
+ if (status === 'completed' && this._inputs(run).some(i => !['read','cancelled'].includes(i.status))) throw new Error('仍有未处理输入,请先处理新消息再完成任务')
189
+ run.status = status; run.summary = bounded(summary); run.evidence = clone(evidence)
190
+ run.reported = status !== 'running'; this._heartbeat(run)
191
+ if (status === 'completed') this._finish(run, status)
192
+ await this.persist(); return true
193
+ })
194
+ }
195
+ async _failure(run, error, input) {
196
+ run.error = bounded(error?.message ?? error)
197
+ run.summary = bounded(`Execution needs retry: ${run.error}`)
198
+ this._touch(run)
199
+ if (run.attempts >= this.maxAttempts) {
200
+ run.status = 'failed'
201
+ for (const i of this._inputs(run)) if (['pending', 'delivering'].includes(i.status)) i.status = 'failed'
202
+ } else {
203
+ run.status = 'retry'
204
+ run.nextAt = this.now() + this.retryBaseMs * 2 ** Math.min(30, Math.max(0, run.attempts - 1))
205
+ if (input) { input.status = 'pending'; input.nextAt = run.nextAt; input.error = run.error }
206
+ }
207
+ await this.persist()
208
+ }
209
+ async _deliver(run) {
210
+ if (run.status === 'approval-wait') return
211
+ for (const input of this._inputs(run)) {
212
+ if (input.status !== 'pending' || input.nextAt > this.now()) continue
213
+ input.status = 'delivering'; input.attempts++; this._touch(run)
214
+ await this.persist()
215
+ try { await this.transport.deliver(clone(run), clone(input)) }
216
+ catch (error) { await this._failure(run, error, input); return }
217
+ input.status = 'sent'; run.status = 'running'; this._heartbeat(run)
218
+ await this.persist()
219
+ }
220
+ }
221
+ tick() {
222
+ return this._serial(async () => {
223
+ const runs = this._state().runs
224
+ let changed = false
225
+ for (const run of runs) {
226
+ if (run.status === 'approval-wait' || this.now() - run.lastActivityAt < this.staleMs) continue
227
+ if (ACTIVE.has(run.status)) {
228
+ if (run.attention === 'stale') continue
229
+ changed = true
230
+ run.attention = 'stale'; run.summary = 'No recent activity; worker may still be active. Review required.'
231
+ } else if (run.status === 'waiting' && !run.reported) {
232
+ changed = true
233
+ if ((run.reminders || 0) >= 1) {
234
+ run.status = 'blocked'; run.summary = 'No execution report after one continuation reminder.'
235
+ } else {
236
+ run.reminders = 1; run.status = 'queued'
237
+ this._addInput(run, 'Please continue or report waiting/blocked/completed with summary and evidence.', 'continuation')
238
+ }
239
+ this._heartbeat(run)
240
+ }
241
+ }
242
+ if (changed) await this.persist()
243
+ const candidates = runs.filter(r => ['queued', 'retry'].includes(r.status) && (r.nextAt ?? 0) <= this.now())
244
+ .sort((a, b) => (PRIORITY[a.priority] ?? 1) - (PRIORITY[b.priority] ?? 1) || a.createdAt - b.createdAt)
245
+ for (const run of candidates) {
246
+ const active = runs.filter(r => ACTIVE.has(r.status))
247
+ if (active.length >= this.maxConcurrent) break
248
+ if (active.some(r => r.roleId === run.roleId) || !this.canRun(clone(run))) continue
249
+ if (run.attempts >= this.maxAttempts) { run.status = 'failed'; await this.persist(); continue }
250
+ run.status = 'starting'; run.attempts++; run.reported = false; this._heartbeat(run)
251
+ await this.persist()
252
+ if (!run.sessionId) {
253
+ let sessionId
254
+ try { sessionId = await this.transport.ensureSession(clone(run)) }
255
+ catch (error) { await this._failure(run, error); continue }
256
+ if (!sessionId || runs.some(r => r.id !== run.id && r.sessionId === sessionId)) {
257
+ await this._failure(run, new Error('ensureSession must return an independent session ID')); continue
258
+ }
259
+ run.sessionId = sessionId
260
+ await this.persist()
261
+ }
262
+ run.status = 'running'; await this.persist()
263
+ }
264
+ for (const run of runs) {
265
+ if (run.status === 'running' && this.canRun(clone(run))) await this._deliver(run)
266
+ }
267
+ return this.list()
268
+ })
269
+ }
270
+ reconcileTask(task) {
271
+ return this._serial(async () => {
272
+ const old = this._state().runs.filter(r => r.taskId === task.id)
273
+ const cancel = []
274
+ if (task.status === '已完成') {
275
+ for (const run of old.filter(r => !TERMINAL.has(r.status))) {
276
+ this._finish(run, 'completed'); run.completedExternally = true
277
+ run.summary = 'Task marked completed externally.'; cancel.push(run)
278
+ }
279
+ } else {
280
+ const generation = Math.max(1, ...old.map(r => r.generation || 1))
281
+ const current = old.filter(r => r.generation === generation)
282
+ const revised = current.some(r => r.revision !== revision(task)) || (current.length > 0 && current.every(r => r.status === 'completed'))
283
+ for (const run of old) {
284
+ if (!TERMINAL.has(run.status) && (revised || !(task.assignees ?? []).includes(run.roleId))) {
285
+ this._finish(run, 'cancelled'); cancel.push(run)
286
+ }
287
+ if (!revised) run.priority = task.priority
288
+ }
289
+ if (revised) {
290
+ this._enqueue(task, { generation: generation + 1 })
291
+ } else this._enqueue(task)
292
+ }
293
+ await this.persist(); await this._cancelSessions(cancel)
294
+ return this.list(task.id)
295
+ })
296
+ }
297
+ _markers(event) {
298
+ // Durable inbox events may wrap the admitted message in payload/messages.
299
+ const ids = new Set()
300
+ const visit = (node, depth) => {
301
+ if (!node || typeof node !== 'object' || depth > 10) return
302
+ if (node.source?.executionInputId) ids.add(node.source.executionInputId)
303
+ for (const value of Object.values(node)) {
304
+ if (value && typeof value === 'object') visit(value, depth + 1)
305
+ }
306
+ }
307
+ visit(event, 0); return ids
308
+ }
309
+ async _event(run, event, admissionOnly = false) {
310
+ if (TERMINAL.has(run.status) || ['paused', 'blocked', 'failed'].includes(run.status)) return false
311
+ const type = event.type ?? event.kind
312
+ const markers = this._markers(event)
313
+ let admitted = false
314
+ const read = type === 'user/message'
315
+ for (const input of this._inputs(run)) {
316
+ if (markers.has(input.id) && input.status !== 'cancelled') {
317
+ if (read) { input.status = 'read'; input.readAt = event.time ?? this.now() }
318
+ else if (input.status !== 'read') input.status = 'sent'
319
+ admitted = true
320
+ }
321
+ }
322
+ const replayed = Number.isInteger(event.seq) && event.seq <= (run.lastEventSeq ?? -1)
323
+ if (admissionOnly || replayed) return admitted
324
+ if (Number.isInteger(event.seq)) run.lastEventSeq = event.seq
325
+ if (admitted) { if (run.status !== 'approval-wait') run.status = 'running'; this._heartbeat(run) }
326
+ const data = event.data ?? {}
327
+ if (type === 'approval/asked' && typeof data.id === 'string' && data.id) {
328
+ run.approvalIds = [...new Set([...(run.approvalIds ?? []), data.id])]
329
+ run.status = 'approval-wait'; this._heartbeat(run)
330
+ } else if (type === 'approval/decided') {
331
+ run.approvalIds = (run.approvalIds ?? []).filter(id => id !== data.id)
332
+ if (run.status === 'approval-wait' && !run.approvalIds.length) run.status = 'running'
333
+ }
334
+ if (type === 'tool/result' || type === 'tool/call') this._heartbeat(run)
335
+ if (type === 'turn/end') {
336
+ run.approvalIds = []
337
+ if (run.status === 'approval-wait') run.status = 'running'
338
+ const status = event.data?.status ?? event.status ?? event.result?.status ?? event.outcome
339
+ const cause = event.data?.cause ?? event.cause ?? event.reason ?? event.result?.cause
340
+ if (status === 'aborted' && (cause === 'user' || cause === 'user-stop' || cause?.type === 'user')) {
341
+ run.status = 'paused'; run.summary = 'Stopped by user.'; this._heartbeat(run)
342
+ } else if (status === 'blocked') {
343
+ run.status = 'blocked'; this._heartbeat(run)
344
+ } else if (['error', 'interrupted', 'aborted', 'failed'].includes(status)) {
345
+ await this._failure(run, new Error(bounded(event.error?.message ?? event.error ?? status)))
346
+ if (run.status === 'retry') this._addInput(run, 'Continue interrupted work; report status and evidence.', 'recovery', `recovery:${run.attempts}`)
347
+ } else if (status === 'completed' || status === 'success' || status === undefined) {
348
+ // An ended turn is not a proof-backed task completion.
349
+ const pending = this._inputs(run).some(i => ['pending','delivering'].includes(i.status))
350
+ run.status = pending ? 'queued' : 'waiting'
351
+ if (pending) {run.attempts = 0;run.nextAt = this.now();run.reported = false}
352
+ this._heartbeat(run)
353
+ }
354
+ }
355
+ return true
356
+ }
357
+ event(sessionId, event) {
358
+ return this._serial(async () => {
359
+ const run = this._state().runs.find(r => r.sessionId === sessionId)
360
+ if (!run) return false
361
+ const accepted = await this._event(run, event)
362
+ if (accepted) await this.persist()
363
+ return accepted
364
+ })
365
+ }
366
+ recover(observations = new Map()) {
367
+ return this._serial(async () => {
368
+ for (const run of this._state().runs) {
369
+ if (TERMINAL.has(run.status) || ['paused', 'blocked', 'failed'].includes(run.status)) continue
370
+ const observation = observations.get(run.sessionId)
371
+ // Audit history is authoritative for pending IDs, not the persisted cache.
372
+ // This rebuild deliberately starts at the beginning, independent of lastEventSeq.
373
+ const pendingApprovals = new Set(), seenSeq = new Set()
374
+ for (const event of observation?.events ?? []) {
375
+ if (Number.isInteger(event.seq)) {
376
+ if (seenSeq.has(event.seq)) continue
377
+ seenSeq.add(event.seq)
378
+ }
379
+ if (event.type === 'approval/asked' && typeof event.data?.id === 'string' && event.data.id) pendingApprovals.add(event.data.id)
380
+ else if (event.type === 'approval/decided') pendingApprovals.delete(event.data?.id)
381
+ else if (event.type === 'turn/end') pendingApprovals.clear()
382
+ }
383
+ run.approvalIds = observation?.running ? [...pendingApprovals] : []
384
+ if (run.status === 'approval-wait') run.status = 'running'
385
+ for (const event of observation?.events ?? []) {
386
+ await this._event(run, event, true)
387
+ if (event.type === 'turn/end' && Number.isInteger(event.seq) && event.seq > (run.lastEventSeq ?? -1)) await this._event(run, event)
388
+ }
389
+ // Approval-only observations must also advance the live replay watermark.
390
+ for (const event of observation?.events ?? []) {
391
+ if (Number.isInteger(event.seq)) run.lastEventSeq = Math.max(run.lastEventSeq ?? -1, event.seq)
392
+ }
393
+ if (TERMINAL.has(run.status) || ['paused', 'blocked', 'failed'].includes(run.status)) continue
394
+ run.approvalIds = observation?.running ? [...pendingApprovals] : []
395
+ const proof = executionAdmission(observation?.events ?? [])
396
+ for (const input of this._inputs(run)) {
397
+ if (input.status === 'cancelled') continue
398
+ if (proof.read.has(input.id)) input.status = 'read'
399
+ else if (proof.pending.has(input.id)) input.status = 'sent'
400
+ else if (['delivering','sent'].includes(input.status)) { input.status = 'pending'; input.nextAt = this.now() }
401
+ }
402
+ if (observation?.running) {
403
+ run.status = run.approvalIds.length ? 'approval-wait' : 'running'; this._heartbeat(run)
404
+ } else if (ACTIVE.has(run.status)) {
405
+ if (run.attempts >= this.maxAttempts) {
406
+ run.status = 'failed'; run.error = 'Restart recovery attempt budget exhausted.'
407
+ } else {
408
+ run.status = 'queued'; run.nextAt = this.now()
409
+ this._addInput(run, 'Recover interrupted execution; inspect prior work before continuing. Report status and evidence.', 'recovery', `recovery:${run.attempts}`)
410
+ }
411
+ this._touch(run)
412
+ }
413
+ }
414
+ await this.persist(); return this.list()
415
+ })
416
+ }
417
+ }
@@ -0,0 +1,65 @@
1
+ import { defineTool } from '@deepseek-ai/dsh-tools'
2
+ const OUTPUT = {schema: {type: 'string'}, render: (_args, value) => [{type: 'text', text: String(value)}]}
3
+
4
+ export function install(rt) {
5
+ const register = definition => rt.ctx.tools.register(defineTool({...definition, output: OUTPUT}))
6
+ register({
7
+ name: 'dwb_task_execution',
8
+ description: '查看任务执行状态、结果依据和近期输入;按输入 id 分页读取完整内容。',
9
+ parameters: {
10
+ taskId: {type: 'string', description: '任务 id;省略时读取当前执行任务'},
11
+ offset: {type: 'integer', description: '执行记录偏移,默认 0;每页最多 20 条'},
12
+ inputId: {type: 'string', description: '从执行输入摘要取得的 id;设置后读取该输入正文'},
13
+ inputOffset: {type: 'integer', description: '输入正文的字符偏移,默认 0;每页最多 4000 字符'},
14
+ },
15
+ async execute(args) {
16
+ await rt.executionReady
17
+ const caller = rt.currentExecution(), taskId = args.taskId ?? caller?.taskId
18
+ if (caller && taskId !== caller.taskId) throw new Error('当前执行只能读取自己的任务')
19
+ const inputs = rt.state.execution.inputs ?? []
20
+ if (args.inputId) {
21
+ const input = inputs.find(i => i.id === args.inputId && (!taskId || i.taskId === taskId))
22
+ if (!input || (caller && input.runId !== caller.id)) throw new Error('输入不存在或不属于当前执行')
23
+ const offset = Math.max(0, Number(args.inputOffset) || 0)
24
+ return JSON.stringify({id: input.id, taskId: input.taskId, text: input.text.slice(offset, offset + 4000), nextInputOffset: offset + 4000 < input.text.length ? offset + 4000 : null})
25
+ }
26
+ const runs = rt.executionRuns(taskId), offset = Math.max(0, Number(args.offset) || 0)
27
+ const items = runs.slice(offset, offset + 20).map(run => ({
28
+ id: run.id, taskId: run.taskId, roleId: run.roleId, generation: run.generation,
29
+ sessionId: run.sessionId, status: run.status, attempts: run.attempts, summary: run.summary,
30
+ evidence: run.evidence, error: run.error, attention: run.attention,
31
+ inputs: inputs.filter(i => i.runId === run.id && (!caller || run.id === caller.id)).slice(-10).map(i => ({id: i.id, kind: i.kind, status: i.status, preview: i.text.slice(0, 400), truncated: i.text.length > 400})),
32
+ }))
33
+ return JSON.stringify({error: rt.executionError ?? null, taskId, items, nextOffset: offset + 20 < runs.length ? offset + 20 : null})
34
+ },
35
+ })
36
+ register({
37
+ name: 'dwb_report_execution',
38
+ description: '保存自己的执行状态、完整摘要与证据。系统仅生成简短完成/等待回执或异常提醒,执行中不刷群;不要重复发群消息。多人全部完成后自动关闭任务。',
39
+ parameters: {
40
+ runId: {type: 'string', description: '当前独立执行 id', required: true},
41
+ status: {type: 'string', enum: ['running', 'waiting', 'blocked', 'completed', 'failed'], description: '执行中、等待后续、阻塞、完成或失败', required: true},
42
+ summary: {type: 'string', description: '进展、结果或阻塞原因', required: true},
43
+ evidence: {type: 'array', items: {type: 'string'}, description: '完成时必填:成果路径、验证结果或可核验引用'},
44
+ },
45
+ async execute(args, exec) {
46
+ const result = await rt.reportExecution(args)
47
+ if (args.status !== 'running') exec?.concludeTurn?.()
48
+ return JSON.stringify({runId: result.id, taskId: result.taskId, status: result.status, summary: result.summary})
49
+ },
50
+ })
51
+ register({
52
+ name: 'dwb_manage_execution',
53
+ description: '恢复暂停、阻塞或失败的任务,或停止其全部指派成员;已完成记录不会自动重跑。',
54
+ parameters: {
55
+ taskId: {type: 'string', description: '任务 id', required: true},
56
+ action: {type: 'string', enum: ['retry', 'stop'], description: 'retry 恢复;stop 终止执行,终止后需重新派发任务', required: true},
57
+ },
58
+ async execute(args) {
59
+ await rt.executionReady
60
+ if (rt.currentExecution()) throw new Error('执行成员不能自行重启或停止任务;请向管理员报告')
61
+ const runs = args.action === 'retry' ? await rt.retryExecution(args.taskId) : await rt.stopTaskExecution(args.taskId)
62
+ return JSON.stringify({taskId: args.taskId, runs: runs.map(r => ({runId: r.id, roleId: r.roleId, status: r.status}))})
63
+ },
64
+ })
65
+ }