@agwab/pi-workflow 0.2.1 → 0.4.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 +3 -1
- package/dist/artifact-graph-runtime.d.ts +1 -1
- package/dist/artifact-graph-runtime.js +10 -5
- package/dist/artifact-graph-schema.js +127 -5
- package/dist/compiler.js +52 -19
- package/dist/dynamic-generated-task-runtime.js +3 -1
- package/dist/dynamic-profiles.d.ts +1 -1
- package/dist/engine-run-graph.d.ts +3 -0
- package/dist/engine-run-graph.js +194 -4
- package/dist/engine.d.ts +5 -0
- package/dist/engine.js +389 -41
- package/dist/extension.d.ts +2 -1
- package/dist/extension.js +30 -8
- package/dist/index.d.ts +11 -3
- package/dist/index.js +6 -1
- package/dist/prompt-json.d.ts +7 -0
- package/dist/prompt-json.js +13 -0
- package/dist/roles.d.ts +1 -1
- package/dist/roles.js +5 -8
- package/dist/store.d.ts +20 -1
- package/dist/store.js +139 -35
- package/dist/strings.d.ts +11 -0
- package/dist/strings.js +24 -0
- package/dist/subagent-backend.js +710 -40
- package/dist/types.d.ts +107 -1
- package/dist/verification-ontology.d.ts +31 -0
- package/dist/verification-ontology.js +66 -0
- package/dist/workflow-artifact-tool.js +5 -6
- package/dist/workflow-artifacts.d.ts +7 -0
- package/dist/workflow-artifacts.js +55 -4
- package/dist/workflow-fetch-cache-extension.d.ts +1 -0
- package/dist/workflow-fetch-cache-extension.js +57 -9
- package/dist/workflow-metrics.d.ts +113 -0
- package/dist/workflow-metrics.js +272 -0
- package/dist/workflow-output-artifacts.js +5 -3
- package/dist/workflow-partial-output.d.ts +45 -0
- package/dist/workflow-partial-output.js +205 -0
- package/dist/workflow-progress-health.js +42 -10
- package/dist/workflow-runtime.js +10 -1
- package/dist/workflow-view.js +3 -1
- package/dist/workflow-web-source-extension.js +194 -52
- package/dist/workflow-web-source.d.ts +2 -1
- package/dist/workflow-web-source.js +109 -30
- package/docs/usage.md +76 -29
- package/node_modules/@agwab/pi-subagent/README.md +3 -3
- package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
- package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
- package/node_modules/@agwab/pi-subagent/package.json +2 -2
- package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
- package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
- package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
- package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
- package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
- package/node_modules/@agwab/pi-subagent/src/index.ts +1046 -576
- package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
- package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
- package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
- package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
- package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
- package/node_modules/@agwab/pi-subagent/src/panel.ts +1356 -560
- package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
- package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
- package/package.json +2 -2
- package/skills/workflow-guide/SKILL.md +1 -0
- package/src/artifact-graph-runtime.ts +19 -13
- package/src/artifact-graph-schema.ts +143 -3
- package/src/cli.mjs +52 -0
- package/src/compiler.ts +63 -18
- package/src/dynamic-generated-task-runtime.ts +3 -1
- package/src/dynamic-profiles.ts +1 -1
- package/src/engine-run-graph.ts +246 -4
- package/src/engine.ts +545 -38
- package/src/extension.ts +36 -6
- package/src/index.ts +52 -1
- package/src/prompt-json.ts +13 -0
- package/src/roles.ts +6 -9
- package/src/store.ts +194 -42
- package/src/strings.ts +38 -0
- package/src/subagent-backend.ts +921 -62
- package/src/types.ts +116 -2
- package/src/verification-ontology.ts +88 -0
- package/src/workflow-artifact-tool.ts +5 -7
- package/src/workflow-artifacts.ts +83 -3
- package/src/workflow-fetch-cache-extension.ts +78 -13
- package/src/workflow-metrics.ts +478 -0
- package/src/workflow-output-artifacts.ts +5 -3
- package/src/workflow-partial-output.ts +299 -0
- package/src/workflow-progress-health.ts +47 -15
- package/src/workflow-runtime.ts +18 -2
- package/src/workflow-view.ts +2 -1
- package/src/workflow-web-source-extension.ts +654 -232
- package/src/workflow-web-source.ts +153 -39
- package/workflows/README.md +7 -25
- package/workflows/deep-research/batched-verification.spec.json +253 -0
- package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
- package/workflows/deep-research/helpers/claim-evidence-gate.mjs +229 -36
- package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +81 -2
- package/workflows/deep-research/helpers/render-executive.mjs +40 -26
- package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
- package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
- package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
- package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -3
- package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
- package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
- package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
- package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +13 -3
- package/workflows/deep-research/spec.json +32 -12
- package/workflows/impact-review/spec.json +3 -3
- package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
- package/dist/dynamic-loader.d.ts +0 -25
- package/dist/dynamic-loader.js +0 -13
- package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
- package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
- package/src/dynamic-loader.ts +0 -49
- package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
- package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
package/src/engine.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
compiledWorkflowPath,
|
|
12
12
|
fromProjectPath,
|
|
13
13
|
indexSupervisorErrorPath,
|
|
14
|
+
isBlockedTaskResumableForResume,
|
|
14
15
|
isTerminalWorkflowStatus,
|
|
15
16
|
isTerminalTaskStatus,
|
|
16
17
|
listRunRecords,
|
|
@@ -33,6 +34,7 @@ import { resolveWorkflowBackend } from "./backend.js";
|
|
|
33
34
|
import { ensureManagedWorktree } from "./worktree.js";
|
|
34
35
|
import { resolveWorkflowHelperRef } from "./workflow-helpers.js";
|
|
35
36
|
import { buildAvailableToolView } from "./tool-metadata.js";
|
|
37
|
+
import { summarizeWorkflowTelemetry } from "./workflow-artifacts.js";
|
|
36
38
|
import {
|
|
37
39
|
workflowBundleFingerprint,
|
|
38
40
|
workflowBundleSpecPath,
|
|
@@ -93,9 +95,12 @@ import {
|
|
|
93
95
|
assertRunTaskPositionalAlignment,
|
|
94
96
|
buildForeachGeneratedTasks,
|
|
95
97
|
dependenciesReady,
|
|
98
|
+
foreachStreamingEnabled,
|
|
99
|
+
foreachStreamingMinChunk,
|
|
96
100
|
markDagDependentsSkipped,
|
|
97
101
|
nextTaskRecordIndex,
|
|
98
102
|
reconcileDynamicGeneratedRunRecords,
|
|
103
|
+
reconcileForeachGeneratedRunRecords,
|
|
99
104
|
recoverStaleRunningDynamicControllers,
|
|
100
105
|
replaceDependencyList,
|
|
101
106
|
sourceStageIdsForFrom,
|
|
@@ -120,6 +125,12 @@ import {
|
|
|
120
125
|
DIRECT_DYNAMIC_RUNTIME_VERSION,
|
|
121
126
|
ensureDirectDynamicRuntimeBundle,
|
|
122
127
|
} from "./dynamic-runtime-bundle.js";
|
|
128
|
+
import {
|
|
129
|
+
hasFatalPartialOutputIssue,
|
|
130
|
+
readWorkflowPartialOutputLedger,
|
|
131
|
+
writeWorkflowPartialOutputLedgerFromFile,
|
|
132
|
+
type WorkflowPartialOutputItem,
|
|
133
|
+
} from "./workflow-partial-output.js";
|
|
123
134
|
import {
|
|
124
135
|
type CompiledDynamicWorkflowTask,
|
|
125
136
|
type CompiledTask,
|
|
@@ -147,6 +158,8 @@ const DYNAMIC_CONTROLLER_ENGINE_INTEGRITY_ERROR_MESSAGE =
|
|
|
147
158
|
"incompatible or stale pi-workflow engine: dynamic controller context is missing runDecisionLoop (rebuild dist / reload workflow engine)";
|
|
148
159
|
const supervisorTimers = new Map<string, ReturnType<typeof setInterval>>();
|
|
149
160
|
const supervisorRunMtimes = new Map<string, number>();
|
|
161
|
+
const supervisorErrorCounts = new Map<string, number>();
|
|
162
|
+
const MAX_SUPERVISOR_CONSECUTIVE_ERRORS = 3;
|
|
150
163
|
|
|
151
164
|
export interface WorkflowRunOptions {
|
|
152
165
|
task?: string;
|
|
@@ -228,7 +241,7 @@ async function runLoadedWorkflowSpec(
|
|
|
228
241
|
const scheduled =
|
|
229
242
|
(await scheduleRun(cwd, run.runId, compiled, scheduleOptions)) ??
|
|
230
243
|
(await readRunRecord(cwd, run.runId));
|
|
231
|
-
if (scheduled
|
|
244
|
+
if (shouldWatchRun(scheduled))
|
|
232
245
|
watchRun(cwd, scheduled.runId, scheduleOptions);
|
|
233
246
|
return scheduled;
|
|
234
247
|
}
|
|
@@ -245,6 +258,22 @@ export async function refreshRun(
|
|
|
245
258
|
return refreshed ?? current;
|
|
246
259
|
}
|
|
247
260
|
|
|
261
|
+
function hasActiveSchedulerWork(
|
|
262
|
+
run: Pick<WorkflowRunRecord, "status" | "taskSummary">,
|
|
263
|
+
): boolean {
|
|
264
|
+
return (
|
|
265
|
+
run.status === "running" ||
|
|
266
|
+
run.taskSummary.running > 0 ||
|
|
267
|
+
run.taskSummary.pending > 0
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function shouldWatchRun(
|
|
272
|
+
run: Pick<WorkflowRunRecord, "status" | "taskSummary">,
|
|
273
|
+
): boolean {
|
|
274
|
+
return hasActiveSchedulerWork(run);
|
|
275
|
+
}
|
|
276
|
+
|
|
248
277
|
export async function waitForRun(
|
|
249
278
|
cwd: string,
|
|
250
279
|
runIdOrPrefix: string,
|
|
@@ -255,7 +284,7 @@ export async function waitForRun(
|
|
|
255
284
|
const deadline = Date.now() + timeout;
|
|
256
285
|
let run = await refreshRun(cwd, runIdOrPrefix);
|
|
257
286
|
|
|
258
|
-
while (run
|
|
287
|
+
while (hasActiveSchedulerWork(run)) {
|
|
259
288
|
const beforeScheduleRemaining = deadline - Date.now();
|
|
260
289
|
if (beforeScheduleRemaining <= 0)
|
|
261
290
|
throw new Error(
|
|
@@ -265,7 +294,7 @@ export async function waitForRun(
|
|
|
265
294
|
run = await refreshRun(cwd, run.runId);
|
|
266
295
|
const remaining = deadline - Date.now();
|
|
267
296
|
if (remaining <= 0) {
|
|
268
|
-
if (run
|
|
297
|
+
if (!hasActiveSchedulerWork(run)) return run;
|
|
269
298
|
throw new Error(
|
|
270
299
|
`Flow run still running after ${timeout}ms: ${run.runId}`,
|
|
271
300
|
);
|
|
@@ -282,6 +311,64 @@ export interface ResumeRunSummary {
|
|
|
282
311
|
resetTaskIds: string[];
|
|
283
312
|
}
|
|
284
313
|
|
|
314
|
+
export interface StopRunSummary {
|
|
315
|
+
run: WorkflowRunRecord;
|
|
316
|
+
interruptedTaskIds: string[];
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function assertBlockedRunResumable(run: WorkflowRunRecord): void {
|
|
320
|
+
if (run.status !== "blocked") return;
|
|
321
|
+
const blockers = run.tasks.filter(
|
|
322
|
+
(task) =>
|
|
323
|
+
task.status === "blocked" && !isBlockedTaskResumableForResume(task),
|
|
324
|
+
);
|
|
325
|
+
if (blockers.length === 0) return;
|
|
326
|
+
const details = blockers
|
|
327
|
+
.slice(0, 3)
|
|
328
|
+
.map((task) => `${task.specId} statusDetail=${task.statusDetail}`)
|
|
329
|
+
.join(", ");
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Cannot resume blocked run ${run.runId}: non-resumable blocked task(s): ${details}. Resolve the attention/approval blocker before resuming.`,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export async function stopRun(
|
|
336
|
+
cwd: string,
|
|
337
|
+
runIdOrPrefix: string,
|
|
338
|
+
): Promise<StopRunSummary> {
|
|
339
|
+
const current = await readRunRecord(cwd, runIdOrPrefix);
|
|
340
|
+
const stopped = await withRunLease(cwd, current.runId, async () => {
|
|
341
|
+
const run = await readRunRecord(cwd, current.runId);
|
|
342
|
+
if (isTerminalWorkflowStatus(run.status)) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
`stop requires a non-terminal run; ${run.runId} is ${run.status}`,
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
await resolveWorkflowBackend(run)
|
|
348
|
+
.cleanupRun(cwd, run)
|
|
349
|
+
.catch(() => undefined);
|
|
350
|
+
const interruptedTaskIds: string[] = [];
|
|
351
|
+
for (const task of run.tasks) {
|
|
352
|
+
if (
|
|
353
|
+
setTaskTerminal(task, "interrupted", "workflow_stopped", {
|
|
354
|
+
exitCode: 130,
|
|
355
|
+
lastMessage: "Workflow stopped by user request",
|
|
356
|
+
})
|
|
357
|
+
) {
|
|
358
|
+
interruptedTaskIds.push(task.taskId);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
await writeRunRecord(cwd, run);
|
|
362
|
+
unwatchRun(cwd, run.runId);
|
|
363
|
+
return { run, interruptedTaskIds };
|
|
364
|
+
});
|
|
365
|
+
if (!stopped)
|
|
366
|
+
throw new Error(
|
|
367
|
+
`Could not acquire workflow run lease for ${current.runId}`,
|
|
368
|
+
);
|
|
369
|
+
return stopped;
|
|
370
|
+
}
|
|
371
|
+
|
|
285
372
|
export async function resumeRun(
|
|
286
373
|
cwd: string,
|
|
287
374
|
runIdOrPrefix: string,
|
|
@@ -297,6 +384,7 @@ export async function resumeRun(
|
|
|
297
384
|
`resume requires a failed, interrupted, or resumable blocked run; ${current.runId} is ${current.status}`,
|
|
298
385
|
);
|
|
299
386
|
}
|
|
387
|
+
assertBlockedRunResumable(current);
|
|
300
388
|
const compiledFlow = await readCompiledWorkflow(cwd, current.runId);
|
|
301
389
|
const hasLoopTasks =
|
|
302
390
|
compiledFlow?.tasks.some(
|
|
@@ -314,6 +402,10 @@ export async function resumeRun(
|
|
|
314
402
|
const resetTaskIds: string[] = [];
|
|
315
403
|
const updated = await withRunLease(cwd, current.runId, async () => {
|
|
316
404
|
const run = await readRunRecord(cwd, current.runId);
|
|
405
|
+
assertBlockedRunResumable(run);
|
|
406
|
+
await resolveWorkflowBackend(run)
|
|
407
|
+
.cleanupRun(cwd, run)
|
|
408
|
+
.catch(() => undefined);
|
|
317
409
|
for (const task of run.tasks) {
|
|
318
410
|
if (resetTaskForResume(task)) resetTaskIds.push(task.taskId);
|
|
319
411
|
}
|
|
@@ -332,7 +424,7 @@ export async function resumeRun(
|
|
|
332
424
|
const scheduled =
|
|
333
425
|
(await scheduleRun(cwd, current.runId, undefined, options)) ??
|
|
334
426
|
(await readRunRecord(cwd, current.runId));
|
|
335
|
-
if (scheduled
|
|
427
|
+
if (shouldWatchRun(scheduled)) watchRun(cwd, scheduled.runId, options);
|
|
336
428
|
return { run: scheduled, resetTaskIds };
|
|
337
429
|
}
|
|
338
430
|
|
|
@@ -343,7 +435,7 @@ export async function resumeSupervisors(
|
|
|
343
435
|
try {
|
|
344
436
|
const runs = await listRunRecords(cwd);
|
|
345
437
|
for (const run of runs) {
|
|
346
|
-
if (run
|
|
438
|
+
if (hasActiveSchedulerWork(run)) {
|
|
347
439
|
await scheduleRun(cwd, run.runId, undefined, options).catch((error) =>
|
|
348
440
|
recordSupervisorError(cwd, run.runId, error),
|
|
349
441
|
);
|
|
@@ -358,6 +450,15 @@ export async function resumeSupervisors(
|
|
|
358
450
|
}
|
|
359
451
|
}
|
|
360
452
|
|
|
453
|
+
function unwatchRun(cwd: string, runId: string): void {
|
|
454
|
+
const key = `${cwd}\0${runId}`;
|
|
455
|
+
const existing = supervisorTimers.get(key);
|
|
456
|
+
if (existing) clearInterval(existing);
|
|
457
|
+
supervisorTimers.delete(key);
|
|
458
|
+
supervisorRunMtimes.delete(key);
|
|
459
|
+
supervisorErrorCounts.delete(key);
|
|
460
|
+
}
|
|
461
|
+
|
|
361
462
|
export function watchRun(
|
|
362
463
|
cwd: string,
|
|
363
464
|
runId: string,
|
|
@@ -375,8 +476,9 @@ export function watchRun(
|
|
|
375
476
|
const currentMtime = afterMtime ?? beforeMtime;
|
|
376
477
|
if (currentMtime !== undefined)
|
|
377
478
|
supervisorRunMtimes.set(key, currentMtime);
|
|
479
|
+
supervisorErrorCounts.delete(key);
|
|
378
480
|
|
|
379
|
-
if (refreshed
|
|
481
|
+
if (hasActiveSchedulerWork(refreshed)) {
|
|
380
482
|
const unchanged =
|
|
381
483
|
previousMtime !== undefined &&
|
|
382
484
|
currentMtime !== undefined &&
|
|
@@ -385,12 +487,18 @@ export function watchRun(
|
|
|
385
487
|
return;
|
|
386
488
|
}
|
|
387
489
|
|
|
388
|
-
|
|
389
|
-
if (existing) clearInterval(existing);
|
|
390
|
-
supervisorTimers.delete(key);
|
|
391
|
-
supervisorRunMtimes.delete(key);
|
|
490
|
+
unwatchRun(cwd, runId);
|
|
392
491
|
})().catch((error) => {
|
|
393
|
-
|
|
492
|
+
if (isMissingRunError(error)) {
|
|
493
|
+
unwatchRun(cwd, runId);
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
const failures = (supervisorErrorCounts.get(key) ?? 0) + 1;
|
|
497
|
+
supervisorErrorCounts.set(key, failures);
|
|
498
|
+
void recordSupervisorError(cwd, runId, error).finally(() => {
|
|
499
|
+
if (failures >= MAX_SUPERVISOR_CONSECUTIVE_ERRORS)
|
|
500
|
+
unwatchRun(cwd, runId);
|
|
501
|
+
});
|
|
394
502
|
});
|
|
395
503
|
}, POLL_INTERVAL_MS);
|
|
396
504
|
|
|
@@ -405,21 +513,49 @@ async function readRunMtimeMs(
|
|
|
405
513
|
try {
|
|
406
514
|
return (await stat(workflowRunPath(cwd, runId))).mtimeMs;
|
|
407
515
|
} catch (error) {
|
|
408
|
-
if ((error
|
|
516
|
+
if (isEnoentError(error)) return undefined;
|
|
409
517
|
throw error;
|
|
410
518
|
}
|
|
411
519
|
}
|
|
412
520
|
|
|
521
|
+
function isEnoentError(error: unknown): boolean {
|
|
522
|
+
return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function isMissingRunError(error: unknown): boolean {
|
|
526
|
+
return (
|
|
527
|
+
isEnoentError(error) ||
|
|
528
|
+
(error instanceof Error && /^Flow run not found: /.test(error.message))
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function assertScheduleLeaseActive(signal?: AbortSignal): void {
|
|
533
|
+
if (!signal?.aborted) return;
|
|
534
|
+
const reason = (signal as AbortSignal & { reason?: unknown }).reason;
|
|
535
|
+
if (reason instanceof Error) throw reason;
|
|
536
|
+
throw new Error(
|
|
537
|
+
reason === undefined
|
|
538
|
+
? "Lost supervisor lease"
|
|
539
|
+
: `Lost supervisor lease: ${String(reason)}`,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
|
|
413
543
|
export async function scheduleRun(
|
|
414
544
|
cwd: string,
|
|
415
545
|
runId: string,
|
|
416
546
|
compiled?: CompiledWorkflow,
|
|
417
547
|
options: WorkflowScheduleOptions = {},
|
|
418
548
|
): Promise<WorkflowRunRecord | undefined> {
|
|
419
|
-
return withRunLease(cwd, runId, async () => {
|
|
549
|
+
return withRunLease(cwd, runId, async (leaseSignal) => {
|
|
550
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
420
551
|
let run = await readRunRecord(cwd, runId);
|
|
421
552
|
run = await resolveWorkflowBackend(run).refreshRun(cwd, run);
|
|
422
|
-
if (
|
|
553
|
+
if (isTerminalWorkflowStatus(run.status)) return run;
|
|
554
|
+
if (
|
|
555
|
+
run.taskSummary.blocked > 0 &&
|
|
556
|
+
run.taskSummary.pending === 0 &&
|
|
557
|
+
run.taskSummary.running === 0
|
|
558
|
+
)
|
|
423
559
|
return run;
|
|
424
560
|
|
|
425
561
|
const compiledFlow =
|
|
@@ -431,7 +567,8 @@ export async function scheduleRun(
|
|
|
431
567
|
`unsupported compiled workflow type: ${compiledFlow.type}`,
|
|
432
568
|
);
|
|
433
569
|
}
|
|
434
|
-
await scheduleDag(cwd, run, compiledFlow, options);
|
|
570
|
+
await scheduleDag(cwd, run, compiledFlow, options, leaseSignal);
|
|
571
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
435
572
|
|
|
436
573
|
run = await readRunRecord(cwd, run.runId);
|
|
437
574
|
return run;
|
|
@@ -444,13 +581,13 @@ export async function formatStatus(cwd: string): Promise<string> {
|
|
|
444
581
|
await reconcileIndexedActiveRuns(cwd, cached);
|
|
445
582
|
const refreshed = (await readIndex(cwd).catch(() => cached)) ?? cached;
|
|
446
583
|
if (refreshed.runs.length === 0) return "No workflow runs found.";
|
|
447
|
-
return formatIndex(refreshed);
|
|
584
|
+
return formatIndex(cwd, refreshed);
|
|
448
585
|
}
|
|
449
586
|
|
|
450
587
|
await reconcileActiveRuns(cwd);
|
|
451
588
|
const rebuilt = await updateIndex(cwd).catch(() => readIndex(cwd));
|
|
452
589
|
if (!rebuilt || rebuilt.runs.length === 0) return "No workflow runs found.";
|
|
453
|
-
return formatIndex(rebuilt);
|
|
590
|
+
return formatIndex(cwd, rebuilt);
|
|
454
591
|
}
|
|
455
592
|
|
|
456
593
|
export async function formatRunDetails(
|
|
@@ -502,10 +639,12 @@ export function formatRun(
|
|
|
502
639
|
run: WorkflowRunRecord,
|
|
503
640
|
detail: "summary" | "full" = "summary",
|
|
504
641
|
): string {
|
|
642
|
+
const telemetry = summarizeWorkflowTelemetry(run);
|
|
505
643
|
const lines = [
|
|
506
644
|
`${run.runId} [${run.status}] type=${run.type} backend=${run.backend.type}/${run.backend.mode}`,
|
|
507
645
|
`created=${run.createdAt} updated=${run.updatedAt}`,
|
|
508
646
|
`tasks=${run.taskSummary.completed}/${run.taskSummary.total} completed, running=${run.taskSummary.running}, pending=${run.taskSummary.pending}, blocked=${run.taskSummary.blocked}, failed=${run.taskSummary.failed}, interrupted=${run.taskSummary.interrupted}`,
|
|
647
|
+
`completion=${telemetry.completion.health}, outputRetries=${telemetry.retryCounts.output}, launchRetries=${telemetry.retryCounts.launch}, resumeEvents=${telemetry.resumeCounts.events}, contextLimitFailures=${telemetry.completion.contextLimitFailures}`,
|
|
509
648
|
];
|
|
510
649
|
|
|
511
650
|
for (const task of run.tasks) {
|
|
@@ -518,7 +657,7 @@ export function formatRun(
|
|
|
518
657
|
async function reconcileActiveRuns(cwd: string): Promise<void> {
|
|
519
658
|
const runs = await listRunRecords(cwd);
|
|
520
659
|
for (const run of runs) {
|
|
521
|
-
if (run
|
|
660
|
+
if (hasActiveSchedulerWork(run))
|
|
522
661
|
await refreshRun(cwd, run.runId).catch((error) =>
|
|
523
662
|
recordSupervisorError(cwd, run.runId, error),
|
|
524
663
|
);
|
|
@@ -530,7 +669,7 @@ async function reconcileIndexedActiveRuns(
|
|
|
530
669
|
index: WorkflowIndexRecord,
|
|
531
670
|
): Promise<void> {
|
|
532
671
|
for (const run of index.runs) {
|
|
533
|
-
if (run
|
|
672
|
+
if (hasActiveSchedulerWork(run))
|
|
534
673
|
await refreshRun(cwd, run.runId).catch((error) =>
|
|
535
674
|
recordSupervisorError(cwd, run.runId, error),
|
|
536
675
|
);
|
|
@@ -561,7 +700,9 @@ async function scheduleDag(
|
|
|
561
700
|
run: WorkflowRunRecord,
|
|
562
701
|
compiledFlow: CompiledWorkflow,
|
|
563
702
|
options: WorkflowScheduleOptions = {},
|
|
703
|
+
leaseSignal?: AbortSignal,
|
|
564
704
|
): Promise<void> {
|
|
705
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
565
706
|
if (compiledFlow.type === WORKFLOW_RUN_TYPE) {
|
|
566
707
|
const loopReconciled = await reconcileLoopTaskMaterialization(
|
|
567
708
|
cwd,
|
|
@@ -569,6 +710,16 @@ async function scheduleDag(
|
|
|
569
710
|
compiledFlow,
|
|
570
711
|
);
|
|
571
712
|
if (loopReconciled) return;
|
|
713
|
+
const foreachReconciled = reconcileForeachGeneratedRunRecords(
|
|
714
|
+
cwd,
|
|
715
|
+
run,
|
|
716
|
+
compiledFlow,
|
|
717
|
+
);
|
|
718
|
+
if (foreachReconciled) {
|
|
719
|
+
await writeJsonAtomic(compiledWorkflowPath(cwd, run.runId), compiledFlow);
|
|
720
|
+
await writeRunRecord(cwd, run);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
572
723
|
const dynamicReconciled = reconcileDynamicGeneratedRunRecords(
|
|
573
724
|
cwd,
|
|
574
725
|
run,
|
|
@@ -601,6 +752,7 @@ async function scheduleDag(
|
|
|
601
752
|
index < run.tasks.length && running < maxConcurrency;
|
|
602
753
|
index += 1
|
|
603
754
|
) {
|
|
755
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
604
756
|
const task = run.tasks[index];
|
|
605
757
|
const compiledTask = compiledFlow.tasks[index];
|
|
606
758
|
if (!task || !compiledTask || task.status !== "pending") continue;
|
|
@@ -632,6 +784,7 @@ async function scheduleDag(
|
|
|
632
784
|
compiledTask,
|
|
633
785
|
);
|
|
634
786
|
if (changed) return;
|
|
787
|
+
if (foreachStreamingEnabled(compiledTask)) continue;
|
|
635
788
|
}
|
|
636
789
|
|
|
637
790
|
if (compiledTask.stageMaxConcurrency !== undefined) {
|
|
@@ -647,6 +800,7 @@ async function scheduleDag(
|
|
|
647
800
|
continue;
|
|
648
801
|
}
|
|
649
802
|
|
|
803
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
650
804
|
const launched = await launchPendingTaskAt(
|
|
651
805
|
cwd,
|
|
652
806
|
run,
|
|
@@ -654,7 +808,8 @@ async function scheduleDag(
|
|
|
654
808
|
index,
|
|
655
809
|
options,
|
|
656
810
|
);
|
|
657
|
-
|
|
811
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
812
|
+
if (launched && run.tasks[index]?.status === "running") running += 1;
|
|
658
813
|
}
|
|
659
814
|
}
|
|
660
815
|
|
|
@@ -780,12 +935,14 @@ async function materializeForeachTask(
|
|
|
780
935
|
const sourceTasks = run.tasks.filter((task) =>
|
|
781
936
|
sourceStageIds.includes(task.stageId ?? ""),
|
|
782
937
|
);
|
|
938
|
+
const streaming = foreachStreamingEnabled(template);
|
|
783
939
|
const extracted = await extractArtifactGraphForeachItems(
|
|
784
940
|
cwd,
|
|
785
941
|
{
|
|
786
942
|
from: template.foreach.from,
|
|
787
943
|
sourcePolicy: stageSourcePolicy(compiledFlow, template.stageId),
|
|
788
944
|
maxItems: template.foreach.maxItems,
|
|
945
|
+
streaming,
|
|
789
946
|
},
|
|
790
947
|
sourceTasks,
|
|
791
948
|
);
|
|
@@ -813,7 +970,33 @@ async function materializeForeachTask(
|
|
|
813
970
|
}
|
|
814
971
|
|
|
815
972
|
const placeholderSpecId = template.id;
|
|
973
|
+
if (streaming) {
|
|
974
|
+
return await materializeStreamingForeachTask({
|
|
975
|
+
cwd,
|
|
976
|
+
run,
|
|
977
|
+
compiledFlow,
|
|
978
|
+
index,
|
|
979
|
+
templateRunTask,
|
|
980
|
+
placeholderSpecId,
|
|
981
|
+
sourceTaskSpecIds: sourceTasks.map((task) => task.specId),
|
|
982
|
+
itemMetas: extracted.itemMetas ?? [],
|
|
983
|
+
generatedTasks: generated.tasks,
|
|
984
|
+
waitingForSources: extracted.waitingForSources ?? false,
|
|
985
|
+
minChunk: foreachStreamingMinChunk(template),
|
|
986
|
+
});
|
|
987
|
+
}
|
|
816
988
|
const generatedSpecIds = generated.tasks.map((task) => task.id);
|
|
989
|
+
const hasDownstreamDependents = compiledFlow.tasks.some(
|
|
990
|
+
(task, taskIndex) =>
|
|
991
|
+
taskIndex !== index && (task.dependsOn ?? []).includes(placeholderSpecId),
|
|
992
|
+
);
|
|
993
|
+
if (generatedSpecIds.length === 0 && !hasDownstreamDependents) {
|
|
994
|
+
setTaskTerminal(templateRunTask, "completed", "foreach_empty", {
|
|
995
|
+
lastMessage: "foreach produced 0 item(s)",
|
|
996
|
+
});
|
|
997
|
+
await writeRunRecord(cwd, run);
|
|
998
|
+
return true;
|
|
999
|
+
}
|
|
817
1000
|
compiledFlow.tasks.splice(index, 1, ...generated.tasks);
|
|
818
1001
|
updateDownstreamDependencies(
|
|
819
1002
|
compiledFlow,
|
|
@@ -840,20 +1023,279 @@ async function materializeForeachTask(
|
|
|
840
1023
|
return true;
|
|
841
1024
|
}
|
|
842
1025
|
|
|
1026
|
+
async function materializeStreamingForeachTask(input: {
|
|
1027
|
+
cwd: string;
|
|
1028
|
+
run: WorkflowRunRecord;
|
|
1029
|
+
compiledFlow: CompiledWorkflow;
|
|
1030
|
+
index: number;
|
|
1031
|
+
templateRunTask: WorkflowTaskRunRecord;
|
|
1032
|
+
placeholderSpecId: string;
|
|
1033
|
+
sourceTaskSpecIds: string[];
|
|
1034
|
+
itemMetas: ForeachExtractedItemMeta[];
|
|
1035
|
+
generatedTasks: CompiledTask[];
|
|
1036
|
+
waitingForSources: boolean;
|
|
1037
|
+
minChunk: number;
|
|
1038
|
+
}): Promise<boolean> {
|
|
1039
|
+
const sourceTaskSpecIdSet = new Set(input.sourceTaskSpecIds);
|
|
1040
|
+
const generatedTasksWithItemDeps = input.generatedTasks.map((task, index) => {
|
|
1041
|
+
const itemMeta = input.itemMetas[index];
|
|
1042
|
+
if (!itemMeta) return task;
|
|
1043
|
+
return {
|
|
1044
|
+
...task,
|
|
1045
|
+
dependsOn: replaceSourceDependenciesWithItemSource(
|
|
1046
|
+
task.dependsOn ?? [],
|
|
1047
|
+
sourceTaskSpecIdSet,
|
|
1048
|
+
itemMeta,
|
|
1049
|
+
{
|
|
1050
|
+
keepPartialSourceDependency:
|
|
1051
|
+
partialGeneratedTaskNeedsCompletedSourceContext(task),
|
|
1052
|
+
},
|
|
1053
|
+
),
|
|
1054
|
+
foreachGenerated: {
|
|
1055
|
+
...(task.foreachGenerated ?? {
|
|
1056
|
+
placeholderSpecId: input.placeholderSpecId,
|
|
1057
|
+
}),
|
|
1058
|
+
itemHash: itemMeta.itemHash,
|
|
1059
|
+
itemSourceSpecId: itemMeta.sourceSpecId,
|
|
1060
|
+
itemSourceKind: itemMeta.sourceKind,
|
|
1061
|
+
itemRef: itemMeta.itemRef,
|
|
1062
|
+
},
|
|
1063
|
+
};
|
|
1064
|
+
});
|
|
1065
|
+
const existingGeneratedTasks = input.compiledFlow.tasks.filter(
|
|
1066
|
+
(task) =>
|
|
1067
|
+
task.foreachGenerated?.placeholderSpecId === input.placeholderSpecId,
|
|
1068
|
+
);
|
|
1069
|
+
const existingGeneratedSpecIds = existingGeneratedTasks.map(
|
|
1070
|
+
(task) => task.id,
|
|
1071
|
+
);
|
|
1072
|
+
const existingGeneratedTaskBySpecId = new Map(
|
|
1073
|
+
existingGeneratedTasks.map((task) => [task.id, task]),
|
|
1074
|
+
);
|
|
1075
|
+
for (const task of generatedTasksWithItemDeps) {
|
|
1076
|
+
const existing = existingGeneratedTaskBySpecId.get(task.id);
|
|
1077
|
+
const existingHash = existing?.foreachGenerated?.itemHash;
|
|
1078
|
+
const nextHash = task.foreachGenerated?.itemHash;
|
|
1079
|
+
if (existing && existingHash && nextHash && existingHash !== nextHash) {
|
|
1080
|
+
setTaskTerminal(
|
|
1081
|
+
input.templateRunTask,
|
|
1082
|
+
"blocked",
|
|
1083
|
+
"foreach_expansion_blocked",
|
|
1084
|
+
{
|
|
1085
|
+
lastMessage: `foreach streaming item ${task.id} changed after materialization`,
|
|
1086
|
+
},
|
|
1087
|
+
);
|
|
1088
|
+
await writeRunRecord(input.cwd, input.run);
|
|
1089
|
+
return true;
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const existingGeneratedSpecIdSet = new Set(existingGeneratedSpecIds);
|
|
1093
|
+
const finalGeneratedSpecIdSet = new Set(
|
|
1094
|
+
generatedTasksWithItemDeps.map((task) => task.id),
|
|
1095
|
+
);
|
|
1096
|
+
if (!input.waitingForSources) {
|
|
1097
|
+
const withdrawn = existingGeneratedTasks.find(
|
|
1098
|
+
(task) =>
|
|
1099
|
+
task.foreachGenerated?.itemSourceKind === "partial" &&
|
|
1100
|
+
!finalGeneratedSpecIdSet.has(task.id),
|
|
1101
|
+
);
|
|
1102
|
+
if (withdrawn) {
|
|
1103
|
+
setTaskTerminal(
|
|
1104
|
+
input.templateRunTask,
|
|
1105
|
+
"blocked",
|
|
1106
|
+
"foreach_expansion_blocked",
|
|
1107
|
+
{
|
|
1108
|
+
lastMessage: `foreach streaming item ${withdrawn.id} was published as partial output but is missing from final control`,
|
|
1109
|
+
},
|
|
1110
|
+
);
|
|
1111
|
+
await writeRunRecord(input.cwd, input.run);
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
const newGeneratedTasks = generatedTasksWithItemDeps.filter(
|
|
1116
|
+
(task) => !existingGeneratedSpecIdSet.has(task.id),
|
|
1117
|
+
);
|
|
1118
|
+
const allGeneratedSpecIds = [
|
|
1119
|
+
...existingGeneratedSpecIds,
|
|
1120
|
+
...newGeneratedTasks.map((task) => task.id),
|
|
1121
|
+
];
|
|
1122
|
+
const shouldHoldForMinChunk =
|
|
1123
|
+
input.waitingForSources &&
|
|
1124
|
+
newGeneratedTasks.length > 0 &&
|
|
1125
|
+
newGeneratedTasks.length < input.minChunk;
|
|
1126
|
+
if (shouldHoldForMinChunk) return false;
|
|
1127
|
+
|
|
1128
|
+
let changed = false;
|
|
1129
|
+
if (newGeneratedTasks.length > 0) {
|
|
1130
|
+
let compiledInsertIndex = input.index + 1;
|
|
1131
|
+
while (
|
|
1132
|
+
input.compiledFlow.tasks[compiledInsertIndex]?.foreachGenerated
|
|
1133
|
+
?.placeholderSpecId === input.placeholderSpecId
|
|
1134
|
+
) {
|
|
1135
|
+
compiledInsertIndex += 1;
|
|
1136
|
+
}
|
|
1137
|
+
input.compiledFlow.tasks.splice(
|
|
1138
|
+
compiledInsertIndex,
|
|
1139
|
+
0,
|
|
1140
|
+
...newGeneratedTasks,
|
|
1141
|
+
);
|
|
1142
|
+
|
|
1143
|
+
let runInsertIndex = input.index + 1;
|
|
1144
|
+
while (
|
|
1145
|
+
input.run.tasks[runInsertIndex]?.foreachGenerated?.placeholderSpecId ===
|
|
1146
|
+
input.placeholderSpecId
|
|
1147
|
+
) {
|
|
1148
|
+
runInsertIndex += 1;
|
|
1149
|
+
}
|
|
1150
|
+
const nextIndex = nextTaskRecordIndex(input.run);
|
|
1151
|
+
const generatedRunTasks = newGeneratedTasks.map((task, offset) =>
|
|
1152
|
+
createTaskRunRecord(input.cwd, input.run.runId, task, nextIndex + offset),
|
|
1153
|
+
);
|
|
1154
|
+
input.run.tasks.splice(runInsertIndex, 0, ...generatedRunTasks);
|
|
1155
|
+
changed = true;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const dependencyTargets = [input.placeholderSpecId, ...allGeneratedSpecIds];
|
|
1159
|
+
for (const task of input.compiledFlow.tasks) {
|
|
1160
|
+
if (!task.dependsOn) continue;
|
|
1161
|
+
const replaced = replaceDependencyList(
|
|
1162
|
+
task.dependsOn,
|
|
1163
|
+
input.placeholderSpecId,
|
|
1164
|
+
dependencyTargets,
|
|
1165
|
+
);
|
|
1166
|
+
if (JSON.stringify(task.dependsOn) !== JSON.stringify(replaced)) {
|
|
1167
|
+
task.dependsOn = replaced;
|
|
1168
|
+
changed = true;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
for (const task of input.run.tasks) {
|
|
1172
|
+
if (!task.dependsOn) continue;
|
|
1173
|
+
const replaced = replaceDependencyList(
|
|
1174
|
+
task.dependsOn,
|
|
1175
|
+
input.placeholderSpecId,
|
|
1176
|
+
dependencyTargets,
|
|
1177
|
+
);
|
|
1178
|
+
if (JSON.stringify(task.dependsOn) !== JSON.stringify(replaced)) {
|
|
1179
|
+
task.dependsOn = replaced;
|
|
1180
|
+
changed = true;
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
if (!input.waitingForSources) {
|
|
1185
|
+
const statusDetail =
|
|
1186
|
+
allGeneratedSpecIds.length === 0
|
|
1187
|
+
? "foreach_empty"
|
|
1188
|
+
: "foreach_streaming_complete";
|
|
1189
|
+
const lastMessage =
|
|
1190
|
+
allGeneratedSpecIds.length === 0
|
|
1191
|
+
? "foreach produced 0 item(s)"
|
|
1192
|
+
: `foreach streaming materialized ${allGeneratedSpecIds.length} item(s)`;
|
|
1193
|
+
setTaskTerminal(input.templateRunTask, "completed", statusDetail, {
|
|
1194
|
+
lastMessage,
|
|
1195
|
+
});
|
|
1196
|
+
changed = true;
|
|
1197
|
+
} else if (newGeneratedTasks.length > 0) {
|
|
1198
|
+
input.templateRunTask.statusDetail = "foreach_streaming_waiting";
|
|
1199
|
+
input.templateRunTask.lastMessage = `foreach streaming materialized ${allGeneratedSpecIds.length} item(s); waiting for more source tasks`;
|
|
1200
|
+
changed = true;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
if (!changed) return false;
|
|
1204
|
+
await writeJsonAtomic(
|
|
1205
|
+
compiledWorkflowPath(input.cwd, input.run.runId),
|
|
1206
|
+
input.compiledFlow,
|
|
1207
|
+
);
|
|
1208
|
+
await writeRunRecord(input.cwd, input.run);
|
|
1209
|
+
return true;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
function replaceSourceDependenciesWithItemSource(
|
|
1213
|
+
dependsOn: string[],
|
|
1214
|
+
sourceTaskSpecIds: Set<string>,
|
|
1215
|
+
itemMeta: ForeachExtractedItemMeta,
|
|
1216
|
+
options: { keepPartialSourceDependency?: boolean } = {},
|
|
1217
|
+
): string[] {
|
|
1218
|
+
const replaced: string[] = [];
|
|
1219
|
+
let inserted = false;
|
|
1220
|
+
const shouldReplaceWithSource =
|
|
1221
|
+
itemMeta.sourceKind !== "partial" ||
|
|
1222
|
+
options.keepPartialSourceDependency === true;
|
|
1223
|
+
for (const dep of dependsOn) {
|
|
1224
|
+
if (!sourceTaskSpecIds.has(dep)) {
|
|
1225
|
+
replaced.push(dep);
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
if (!shouldReplaceWithSource) continue;
|
|
1229
|
+
if (!inserted) {
|
|
1230
|
+
replaced.push(itemMeta.sourceSpecId);
|
|
1231
|
+
inserted = true;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
if (!inserted && shouldReplaceWithSource) {
|
|
1235
|
+
replaced.push(itemMeta.sourceSpecId);
|
|
1236
|
+
}
|
|
1237
|
+
return [...new Set(replaced)];
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
function partialGeneratedTaskNeedsCompletedSourceContext(
|
|
1241
|
+
task: CompiledTask,
|
|
1242
|
+
): boolean {
|
|
1243
|
+
const artifactGraph = task.artifactGraph;
|
|
1244
|
+
if (artifactGraph?.artifactAccess === "none") return false;
|
|
1245
|
+
return Boolean(
|
|
1246
|
+
artifactGraph?.sourceProjection !== undefined ||
|
|
1247
|
+
(artifactGraph?.requiredReads?.length ?? 0) > 0,
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
interface ForeachExtractedItemMeta {
|
|
1252
|
+
sourceSpecId: string;
|
|
1253
|
+
sourceKind: "control" | "partial";
|
|
1254
|
+
itemHash: string;
|
|
1255
|
+
itemRef: string;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
843
1258
|
async function extractArtifactGraphForeachItems(
|
|
844
1259
|
cwd: string,
|
|
845
|
-
stage: {
|
|
1260
|
+
stage: {
|
|
1261
|
+
from: unknown;
|
|
1262
|
+
sourcePolicy?: string;
|
|
1263
|
+
maxItems?: number;
|
|
1264
|
+
streaming?: boolean;
|
|
1265
|
+
},
|
|
846
1266
|
sourceTasks: WorkflowTaskRunRecord[],
|
|
847
|
-
): Promise<{
|
|
1267
|
+
): Promise<{
|
|
1268
|
+
items?: unknown[];
|
|
1269
|
+
itemMetas?: ForeachExtractedItemMeta[];
|
|
1270
|
+
error?: string;
|
|
1271
|
+
waitingForSources?: boolean;
|
|
1272
|
+
}> {
|
|
848
1273
|
const items: unknown[] = [];
|
|
1274
|
+
const itemMetas: ForeachExtractedItemMeta[] = [];
|
|
849
1275
|
const path = (stage.from as any)?.path;
|
|
850
1276
|
if (typeof path !== "string" || !path.startsWith("$.")) {
|
|
851
1277
|
return {
|
|
852
1278
|
error: "foreach.from.path must be a control JSONPath like $.items",
|
|
853
1279
|
};
|
|
854
1280
|
}
|
|
1281
|
+
let waitingForSources = false;
|
|
855
1282
|
for (const task of sourceTasks) {
|
|
856
1283
|
if (task.status !== "completed") {
|
|
1284
|
+
if (stage.streaming && !isTerminalTaskStatus(task.status)) {
|
|
1285
|
+
const partial = await extractPartialForeachItems(cwd, task, path);
|
|
1286
|
+
if (partial.error) return { error: partial.error };
|
|
1287
|
+
for (const item of partial.items) {
|
|
1288
|
+
items.push(item.item);
|
|
1289
|
+
itemMetas.push({
|
|
1290
|
+
sourceSpecId: task.specId,
|
|
1291
|
+
sourceKind: "partial",
|
|
1292
|
+
itemHash: item.itemHash,
|
|
1293
|
+
itemRef: `${task.specId}:${item.itemRef}`,
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
waitingForSources = true;
|
|
1297
|
+
continue;
|
|
1298
|
+
}
|
|
857
1299
|
if (stage.sourcePolicy !== "partial")
|
|
858
1300
|
return { error: `${task.taskId} did not complete` };
|
|
859
1301
|
continue;
|
|
@@ -869,7 +1311,15 @@ async function extractArtifactGraphForeachItems(
|
|
|
869
1311
|
}
|
|
870
1312
|
continue;
|
|
871
1313
|
}
|
|
872
|
-
|
|
1314
|
+
for (const [index, item] of value.entries()) {
|
|
1315
|
+
items.push(item);
|
|
1316
|
+
itemMetas.push({
|
|
1317
|
+
sourceSpecId: task.specId,
|
|
1318
|
+
sourceKind: "control",
|
|
1319
|
+
itemHash: hashDynamicRequest(item),
|
|
1320
|
+
itemRef: `${task.specId}:control:${path}[${index}]`,
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
873
1323
|
} catch (error) {
|
|
874
1324
|
if (stage.sourcePolicy !== "partial") {
|
|
875
1325
|
return {
|
|
@@ -883,7 +1333,31 @@ async function extractArtifactGraphForeachItems(
|
|
|
883
1333
|
error: `foreach extracted ${items.length} items, exceeding maxItems=${stage.maxItems}`,
|
|
884
1334
|
};
|
|
885
1335
|
}
|
|
886
|
-
return { items };
|
|
1336
|
+
return { items, itemMetas, waitingForSources };
|
|
1337
|
+
}
|
|
1338
|
+
|
|
1339
|
+
async function extractPartialForeachItems(
|
|
1340
|
+
cwd: string,
|
|
1341
|
+
task: WorkflowTaskRunRecord,
|
|
1342
|
+
path: string,
|
|
1343
|
+
): Promise<{ items: WorkflowPartialOutputItem[]; error?: string }> {
|
|
1344
|
+
const partialPaths = task.artifactGraph?.output.partial?.paths ?? [];
|
|
1345
|
+
if (!partialPaths.includes(path)) return { items: [] };
|
|
1346
|
+
const taskDir = dirname(fromProjectPath(cwd, task.files.result));
|
|
1347
|
+
let ledger = await readWorkflowPartialOutputLedger(taskDir).catch(
|
|
1348
|
+
() => undefined,
|
|
1349
|
+
);
|
|
1350
|
+
if (!ledger) {
|
|
1351
|
+
ledger = await writeWorkflowPartialOutputLedgerFromFile({
|
|
1352
|
+
taskDir,
|
|
1353
|
+
outputFile: fromProjectPath(cwd, task.files.output),
|
|
1354
|
+
allowedPaths: partialPaths,
|
|
1355
|
+
}).catch(() => undefined);
|
|
1356
|
+
}
|
|
1357
|
+
if (!ledger) return { items: [] };
|
|
1358
|
+
const fatal = hasFatalPartialOutputIssue(ledger);
|
|
1359
|
+
if (fatal) return { items: [], error: fatal.message };
|
|
1360
|
+
return { items: ledger.items.filter((item) => item.path === path) };
|
|
887
1361
|
}
|
|
888
1362
|
|
|
889
1363
|
async function launchPendingTaskAt(
|
|
@@ -906,12 +1380,15 @@ async function launchPendingTaskAt(
|
|
|
906
1380
|
return false;
|
|
907
1381
|
}
|
|
908
1382
|
|
|
909
|
-
let launchTask
|
|
910
|
-
|
|
911
|
-
launchTask = await prepareArtifactGraphRetryTask(cwd, task, launchTask);
|
|
912
|
-
}
|
|
913
|
-
|
|
1383
|
+
let launchTask: CompiledWorkflow["tasks"][number] | undefined;
|
|
1384
|
+
let prepareComplete = false;
|
|
914
1385
|
try {
|
|
1386
|
+
launchTask = await prepareDagTask(cwd, run, compiledFlow, index);
|
|
1387
|
+
if (task.outputRetry) {
|
|
1388
|
+
launchTask = await prepareArtifactGraphRetryTask(cwd, task, launchTask);
|
|
1389
|
+
}
|
|
1390
|
+
prepareComplete = true;
|
|
1391
|
+
|
|
915
1392
|
if (launchTask.kind === "support") {
|
|
916
1393
|
return await executeSupportTask(cwd, run, task, launchTask);
|
|
917
1394
|
}
|
|
@@ -939,10 +1416,11 @@ async function launchPendingTaskAt(
|
|
|
939
1416
|
if (launch.kind === "fatal") throw new Error(launch.message);
|
|
940
1417
|
return launch.kind === "launched";
|
|
941
1418
|
} catch (error) {
|
|
942
|
-
const statusDetail =
|
|
943
|
-
|
|
1419
|
+
const statusDetail = !prepareComplete
|
|
1420
|
+
? "prepare_failed"
|
|
1421
|
+
: launchTask?.kind === "support"
|
|
944
1422
|
? "support_failed"
|
|
945
|
-
: launchTask
|
|
1423
|
+
: launchTask?.safety.requiresWorktree
|
|
946
1424
|
? "worktree_failed"
|
|
947
1425
|
: "launch_failed";
|
|
948
1426
|
setTaskTerminal(task, "failed", statusDetail, {
|
|
@@ -2692,14 +3170,17 @@ async function readCompiledWorkflow(
|
|
|
2692
3170
|
return readJson<CompiledWorkflow>(compiledWorkflowPath(cwd, runId));
|
|
2693
3171
|
}
|
|
2694
3172
|
|
|
2695
|
-
function formatIndex(
|
|
2696
|
-
|
|
2697
|
-
|
|
3173
|
+
async function formatIndex(
|
|
3174
|
+
cwd: string,
|
|
3175
|
+
index: WorkflowIndexRecord,
|
|
3176
|
+
): Promise<string> {
|
|
3177
|
+
const blocks = await Promise.all(
|
|
3178
|
+
index.runs.map(async (run) => {
|
|
2698
3179
|
const lines = [
|
|
2699
3180
|
`${run.runId} [${run.status}] type=${run.type} updated=${run.updatedAt}`,
|
|
2700
3181
|
`tasks=${run.taskSummary.completed}/${run.taskSummary.total} completed, running=${run.taskSummary.running}, pending=${run.taskSummary.pending}, blocked=${run.taskSummary.blocked}, failed=${run.taskSummary.failed}, skipped=${run.taskSummary.skipped}, interrupted=${run.taskSummary.interrupted}`,
|
|
2701
3182
|
];
|
|
2702
|
-
for (const task of run
|
|
3183
|
+
for (const task of await indexTasksForStatus(cwd, run)) {
|
|
2703
3184
|
const message = task.lastMessage ? ` — ${task.lastMessage}` : "";
|
|
2704
3185
|
const kind = task.kind && task.kind !== "main" ? ` ${task.kind}` : "";
|
|
2705
3186
|
lines.push(
|
|
@@ -2707,8 +3188,34 @@ function formatIndex(index: WorkflowIndexRecord): string {
|
|
|
2707
3188
|
);
|
|
2708
3189
|
}
|
|
2709
3190
|
return lines.join("\n");
|
|
2710
|
-
})
|
|
2711
|
-
|
|
3191
|
+
}),
|
|
3192
|
+
);
|
|
3193
|
+
return blocks.join("\n\n");
|
|
3194
|
+
}
|
|
3195
|
+
|
|
3196
|
+
type WorkflowIndexTaskEntry = NonNullable<
|
|
3197
|
+
WorkflowIndexRecord["runs"][number]["tasks"]
|
|
3198
|
+
>[number];
|
|
3199
|
+
|
|
3200
|
+
async function indexTasksForStatus(
|
|
3201
|
+
cwd: string,
|
|
3202
|
+
run: WorkflowIndexRecord["runs"][number],
|
|
3203
|
+
): Promise<WorkflowIndexTaskEntry[]> {
|
|
3204
|
+
if (Array.isArray(run.tasks)) return run.tasks;
|
|
3205
|
+
const fullRun = await readRunRecord(cwd, run.runId).catch(() => undefined);
|
|
3206
|
+
return (
|
|
3207
|
+
fullRun?.tasks.map((task) => ({
|
|
3208
|
+
taskId: task.taskId,
|
|
3209
|
+
displayName: task.displayName,
|
|
3210
|
+
agent: task.agent,
|
|
3211
|
+
kind: task.kind,
|
|
3212
|
+
stageId: task.stageId,
|
|
3213
|
+
backendHandle: task.backendHandle,
|
|
3214
|
+
status: task.status,
|
|
3215
|
+
statusDetail: task.statusDetail,
|
|
3216
|
+
lastMessage: task.lastMessage,
|
|
3217
|
+
})) ?? []
|
|
3218
|
+
);
|
|
2712
3219
|
}
|
|
2713
3220
|
|
|
2714
3221
|
function formatTask(
|