@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/dist/engine.js
CHANGED
|
@@ -4,11 +4,12 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
4
4
|
import { Worker } from "node:worker_threads";
|
|
5
5
|
import { compileWorkflow } from "./compiler.js";
|
|
6
6
|
import { loadWorkflowSpec } from "./schema.js";
|
|
7
|
-
import { createRunRecord, createTaskRunRecord, compiledWorkflowPath, fromProjectPath, indexSupervisorErrorPath, isTerminalWorkflowStatus, isTerminalTaskStatus, listRunRecords, readIndex, readJson, readRunRecord, resetTaskForResume, setTaskTerminal, supervisorPath, toProjectPath, updateIndex, withRunLease, workflowRunPath, writeJsonAtomic, writeRunRecord, writeCompiledRunArtifact, writeStaticRunArtifacts, } from "./store.js";
|
|
7
|
+
import { createRunRecord, createTaskRunRecord, compiledWorkflowPath, fromProjectPath, indexSupervisorErrorPath, isBlockedTaskResumableForResume, isTerminalWorkflowStatus, isTerminalTaskStatus, listRunRecords, readIndex, readJson, readRunRecord, resetTaskForResume, setTaskTerminal, supervisorPath, toProjectPath, updateIndex, withRunLease, workflowRunPath, writeJsonAtomic, writeRunRecord, writeCompiledRunArtifact, writeStaticRunArtifacts, } from "./store.js";
|
|
8
8
|
import { resolveWorkflowBackend } from "./backend.js";
|
|
9
9
|
import { ensureManagedWorktree } from "./worktree.js";
|
|
10
10
|
import { resolveWorkflowHelperRef } from "./workflow-helpers.js";
|
|
11
11
|
import { buildAvailableToolView } from "./tool-metadata.js";
|
|
12
|
+
import { summarizeWorkflowTelemetry } from "./workflow-artifacts.js";
|
|
12
13
|
import { workflowBundleFingerprint, workflowBundleSpecPath, } from "./workflow-source-context-runtime.js";
|
|
13
14
|
import { readSimpleJsonPath, } from "./workflow-runtime.js";
|
|
14
15
|
import { dynamicRunDir, hashDynamicRequest, readDynamicEvents, } from "./dynamic-events.js";
|
|
@@ -18,10 +19,11 @@ import { assertDynamicRuntimeBudgetAvailable, dynamicRuntimeBudgetExceededMessag
|
|
|
18
19
|
import { assertDynamicGeneratedMetadataMatches, assertDynamicGenerationBudgetAvailable, buildDynamicGeneratedCompiledTask, dynamicGeneratedInsertIndex, isDynamicCompiledTaskPayload, normalizeDynamicAgentRequest, readDynamicGeneratedTaskResult, } from "./dynamic-generated-task-runtime.js";
|
|
19
20
|
import { optionalEventString, runDynamicHelperCall, runDynamicNestedWorkflowCall, } from "./dynamic-controller-calls.js";
|
|
20
21
|
import { normalizeDynamicFanoutPlanRequest, runDynamicDecisionLoopStatusPersistCall, runDynamicDecisionPersistCall, runDynamicFanoutPlanPersistCall, runDynamicResultReadCall, runDynamicStateIndexPersistCall, } from "./dynamic-control-ops.js";
|
|
21
|
-
import { assertRunTaskPositionalAlignment, buildForeachGeneratedTasks, dependenciesReady, markDagDependentsSkipped, nextTaskRecordIndex, reconcileDynamicGeneratedRunRecords, recoverStaleRunningDynamicControllers, replaceDependencyList, sourceStageIdsForFrom, stageSourcePolicy, updateDownstreamDependencies, } from "./engine-run-graph.js";
|
|
22
|
+
import { assertRunTaskPositionalAlignment, buildForeachGeneratedTasks, dependenciesReady, foreachStreamingEnabled, foreachStreamingMinChunk, markDagDependentsSkipped, nextTaskRecordIndex, reconcileDynamicGeneratedRunRecords, reconcileForeachGeneratedRunRecords, recoverStaleRunningDynamicControllers, replaceDependencyList, sourceStageIdsForFrom, stageSourcePolicy, updateDownstreamDependencies, } from "./engine-run-graph.js";
|
|
22
23
|
import { reconcileLoopTaskMaterialization, scheduleLoop, } from "./loop-runtime.js";
|
|
23
24
|
import { executeSupportTask, normalizeDynamicControllerOutput, prepareArtifactGraphRetryTask, prepareDagTask, readArtifactGraphControl, readArtifactGraphSupportSources, readSupportSources, writeArtifactGraphDynamicResult, } from "./artifact-graph-runtime.js";
|
|
24
25
|
import { DIRECT_DYNAMIC_RUNTIME_VERSION, ensureDirectDynamicRuntimeBundle, } from "./dynamic-runtime-bundle.js";
|
|
26
|
+
import { hasFatalPartialOutputIssue, readWorkflowPartialOutputLedger, writeWorkflowPartialOutputLedgerFromFile, } from "./workflow-partial-output.js";
|
|
25
27
|
import { WORKFLOW_RUN_TYPE, } from "./types.js";
|
|
26
28
|
export { buildRunSourceContext } from "./workflow-source-context-runtime.js";
|
|
27
29
|
export { evaluateLoopUntilCondition } from "./loop-runtime.js";
|
|
@@ -37,6 +39,8 @@ const DYNAMIC_CONTROLLER_ENGINE_CAPABILITIES = Object.freeze({
|
|
|
37
39
|
const DYNAMIC_CONTROLLER_ENGINE_INTEGRITY_ERROR_MESSAGE = "incompatible or stale pi-workflow engine: dynamic controller context is missing runDecisionLoop (rebuild dist / reload workflow engine)";
|
|
38
40
|
const supervisorTimers = new Map();
|
|
39
41
|
const supervisorRunMtimes = new Map();
|
|
42
|
+
const supervisorErrorCounts = new Map();
|
|
43
|
+
const MAX_SUPERVISOR_CONSECUTIVE_ERRORS = 3;
|
|
40
44
|
export async function runWorkflowSpec(specPath, cwd, options = {}) {
|
|
41
45
|
const loaded = await loadWorkflowSpec(specPath, cwd);
|
|
42
46
|
return runLoadedWorkflowSpec(cwd, loaded.specPath, loaded.spec, options);
|
|
@@ -83,7 +87,7 @@ async function runLoadedWorkflowSpec(cwd, specPath, spec, options, provenance) {
|
|
|
83
87
|
};
|
|
84
88
|
const scheduled = (await scheduleRun(cwd, run.runId, compiled, scheduleOptions)) ??
|
|
85
89
|
(await readRunRecord(cwd, run.runId));
|
|
86
|
-
if (scheduled
|
|
90
|
+
if (shouldWatchRun(scheduled))
|
|
87
91
|
watchRun(cwd, scheduled.runId, scheduleOptions);
|
|
88
92
|
return scheduled;
|
|
89
93
|
}
|
|
@@ -95,11 +99,19 @@ export async function refreshRun(cwd, runIdOrPrefix) {
|
|
|
95
99
|
});
|
|
96
100
|
return refreshed ?? current;
|
|
97
101
|
}
|
|
102
|
+
function hasActiveSchedulerWork(run) {
|
|
103
|
+
return (run.status === "running" ||
|
|
104
|
+
run.taskSummary.running > 0 ||
|
|
105
|
+
run.taskSummary.pending > 0);
|
|
106
|
+
}
|
|
107
|
+
function shouldWatchRun(run) {
|
|
108
|
+
return hasActiveSchedulerWork(run);
|
|
109
|
+
}
|
|
98
110
|
export async function waitForRun(cwd, runIdOrPrefix, timeoutMs, options = {}) {
|
|
99
111
|
const timeout = clampTimeout(timeoutMs);
|
|
100
112
|
const deadline = Date.now() + timeout;
|
|
101
113
|
let run = await refreshRun(cwd, runIdOrPrefix);
|
|
102
|
-
while (run
|
|
114
|
+
while (hasActiveSchedulerWork(run)) {
|
|
103
115
|
const beforeScheduleRemaining = deadline - Date.now();
|
|
104
116
|
if (beforeScheduleRemaining <= 0)
|
|
105
117
|
throw new Error(`Flow run still running after ${timeout}ms: ${run.runId}`);
|
|
@@ -107,7 +119,7 @@ export async function waitForRun(cwd, runIdOrPrefix, timeoutMs, options = {}) {
|
|
|
107
119
|
run = await refreshRun(cwd, run.runId);
|
|
108
120
|
const remaining = deadline - Date.now();
|
|
109
121
|
if (remaining <= 0) {
|
|
110
|
-
if (run
|
|
122
|
+
if (!hasActiveSchedulerWork(run))
|
|
111
123
|
return run;
|
|
112
124
|
throw new Error(`Flow run still running after ${timeout}ms: ${run.runId}`);
|
|
113
125
|
}
|
|
@@ -116,6 +128,45 @@ export async function waitForRun(cwd, runIdOrPrefix, timeoutMs, options = {}) {
|
|
|
116
128
|
}
|
|
117
129
|
return run;
|
|
118
130
|
}
|
|
131
|
+
function assertBlockedRunResumable(run) {
|
|
132
|
+
if (run.status !== "blocked")
|
|
133
|
+
return;
|
|
134
|
+
const blockers = run.tasks.filter((task) => task.status === "blocked" && !isBlockedTaskResumableForResume(task));
|
|
135
|
+
if (blockers.length === 0)
|
|
136
|
+
return;
|
|
137
|
+
const details = blockers
|
|
138
|
+
.slice(0, 3)
|
|
139
|
+
.map((task) => `${task.specId} statusDetail=${task.statusDetail}`)
|
|
140
|
+
.join(", ");
|
|
141
|
+
throw new Error(`Cannot resume blocked run ${run.runId}: non-resumable blocked task(s): ${details}. Resolve the attention/approval blocker before resuming.`);
|
|
142
|
+
}
|
|
143
|
+
export async function stopRun(cwd, runIdOrPrefix) {
|
|
144
|
+
const current = await readRunRecord(cwd, runIdOrPrefix);
|
|
145
|
+
const stopped = await withRunLease(cwd, current.runId, async () => {
|
|
146
|
+
const run = await readRunRecord(cwd, current.runId);
|
|
147
|
+
if (isTerminalWorkflowStatus(run.status)) {
|
|
148
|
+
throw new Error(`stop requires a non-terminal run; ${run.runId} is ${run.status}`);
|
|
149
|
+
}
|
|
150
|
+
await resolveWorkflowBackend(run)
|
|
151
|
+
.cleanupRun(cwd, run)
|
|
152
|
+
.catch(() => undefined);
|
|
153
|
+
const interruptedTaskIds = [];
|
|
154
|
+
for (const task of run.tasks) {
|
|
155
|
+
if (setTaskTerminal(task, "interrupted", "workflow_stopped", {
|
|
156
|
+
exitCode: 130,
|
|
157
|
+
lastMessage: "Workflow stopped by user request",
|
|
158
|
+
})) {
|
|
159
|
+
interruptedTaskIds.push(task.taskId);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
await writeRunRecord(cwd, run);
|
|
163
|
+
unwatchRun(cwd, run.runId);
|
|
164
|
+
return { run, interruptedTaskIds };
|
|
165
|
+
});
|
|
166
|
+
if (!stopped)
|
|
167
|
+
throw new Error(`Could not acquire workflow run lease for ${current.runId}`);
|
|
168
|
+
return stopped;
|
|
169
|
+
}
|
|
119
170
|
export async function resumeRun(cwd, runIdOrPrefix, options = {}) {
|
|
120
171
|
const current = await readRunRecord(cwd, runIdOrPrefix);
|
|
121
172
|
if (current.status !== "failed" &&
|
|
@@ -123,6 +174,7 @@ export async function resumeRun(cwd, runIdOrPrefix, options = {}) {
|
|
|
123
174
|
current.status !== "blocked") {
|
|
124
175
|
throw new Error(`resume requires a failed, interrupted, or resumable blocked run; ${current.runId} is ${current.status}`);
|
|
125
176
|
}
|
|
177
|
+
assertBlockedRunResumable(current);
|
|
126
178
|
const compiledFlow = await readCompiledWorkflow(cwd, current.runId);
|
|
127
179
|
const hasLoopTasks = compiledFlow?.tasks.some((task) => task.kind === "loop" ||
|
|
128
180
|
task.loopPlaceholder !== undefined ||
|
|
@@ -133,6 +185,10 @@ export async function resumeRun(cwd, runIdOrPrefix, options = {}) {
|
|
|
133
185
|
const resetTaskIds = [];
|
|
134
186
|
const updated = await withRunLease(cwd, current.runId, async () => {
|
|
135
187
|
const run = await readRunRecord(cwd, current.runId);
|
|
188
|
+
assertBlockedRunResumable(run);
|
|
189
|
+
await resolveWorkflowBackend(run)
|
|
190
|
+
.cleanupRun(cwd, run)
|
|
191
|
+
.catch(() => undefined);
|
|
136
192
|
for (const task of run.tasks) {
|
|
137
193
|
if (resetTaskForResume(task))
|
|
138
194
|
resetTaskIds.push(task.taskId);
|
|
@@ -147,7 +203,7 @@ export async function resumeRun(cwd, runIdOrPrefix, options = {}) {
|
|
|
147
203
|
throw new Error(`No failed, interrupted, skipped, or resumable blocked tasks to resume in ${current.runId}`);
|
|
148
204
|
const scheduled = (await scheduleRun(cwd, current.runId, undefined, options)) ??
|
|
149
205
|
(await readRunRecord(cwd, current.runId));
|
|
150
|
-
if (scheduled
|
|
206
|
+
if (shouldWatchRun(scheduled))
|
|
151
207
|
watchRun(cwd, scheduled.runId, options);
|
|
152
208
|
return { run: scheduled, resetTaskIds };
|
|
153
209
|
}
|
|
@@ -155,7 +211,7 @@ export async function resumeSupervisors(cwd, options = {}) {
|
|
|
155
211
|
try {
|
|
156
212
|
const runs = await listRunRecords(cwd);
|
|
157
213
|
for (const run of runs) {
|
|
158
|
-
if (run
|
|
214
|
+
if (hasActiveSchedulerWork(run)) {
|
|
159
215
|
await scheduleRun(cwd, run.runId, undefined, options).catch((error) => recordSupervisorError(cwd, run.runId, error));
|
|
160
216
|
watchRun(cwd, run.runId, options);
|
|
161
217
|
}
|
|
@@ -166,6 +222,15 @@ export async function resumeSupervisors(cwd, options = {}) {
|
|
|
166
222
|
await recordSupervisorError(cwd, "index", error);
|
|
167
223
|
}
|
|
168
224
|
}
|
|
225
|
+
function unwatchRun(cwd, runId) {
|
|
226
|
+
const key = `${cwd}\0${runId}`;
|
|
227
|
+
const existing = supervisorTimers.get(key);
|
|
228
|
+
if (existing)
|
|
229
|
+
clearInterval(existing);
|
|
230
|
+
supervisorTimers.delete(key);
|
|
231
|
+
supervisorRunMtimes.delete(key);
|
|
232
|
+
supervisorErrorCounts.delete(key);
|
|
233
|
+
}
|
|
169
234
|
export function watchRun(cwd, runId, options = {}) {
|
|
170
235
|
const key = `${cwd}\0${runId}`;
|
|
171
236
|
if (supervisorTimers.has(key))
|
|
@@ -179,7 +244,8 @@ export function watchRun(cwd, runId, options = {}) {
|
|
|
179
244
|
const currentMtime = afterMtime ?? beforeMtime;
|
|
180
245
|
if (currentMtime !== undefined)
|
|
181
246
|
supervisorRunMtimes.set(key, currentMtime);
|
|
182
|
-
|
|
247
|
+
supervisorErrorCounts.delete(key);
|
|
248
|
+
if (hasActiveSchedulerWork(refreshed)) {
|
|
183
249
|
const unchanged = previousMtime !== undefined &&
|
|
184
250
|
currentMtime !== undefined &&
|
|
185
251
|
currentMtime <= previousMtime;
|
|
@@ -187,13 +253,18 @@ export function watchRun(cwd, runId, options = {}) {
|
|
|
187
253
|
await scheduleRun(cwd, runId, undefined, options);
|
|
188
254
|
return;
|
|
189
255
|
}
|
|
190
|
-
|
|
191
|
-
if (existing)
|
|
192
|
-
clearInterval(existing);
|
|
193
|
-
supervisorTimers.delete(key);
|
|
194
|
-
supervisorRunMtimes.delete(key);
|
|
256
|
+
unwatchRun(cwd, runId);
|
|
195
257
|
})().catch((error) => {
|
|
196
|
-
|
|
258
|
+
if (isMissingRunError(error)) {
|
|
259
|
+
unwatchRun(cwd, runId);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const failures = (supervisorErrorCounts.get(key) ?? 0) + 1;
|
|
263
|
+
supervisorErrorCounts.set(key, failures);
|
|
264
|
+
void recordSupervisorError(cwd, runId, error).finally(() => {
|
|
265
|
+
if (failures >= MAX_SUPERVISOR_CONSECUTIVE_ERRORS)
|
|
266
|
+
unwatchRun(cwd, runId);
|
|
267
|
+
});
|
|
197
268
|
});
|
|
198
269
|
}, POLL_INTERVAL_MS);
|
|
199
270
|
timer.unref?.();
|
|
@@ -204,16 +275,38 @@ async function readRunMtimeMs(cwd, runId) {
|
|
|
204
275
|
return (await stat(workflowRunPath(cwd, runId))).mtimeMs;
|
|
205
276
|
}
|
|
206
277
|
catch (error) {
|
|
207
|
-
if (error
|
|
278
|
+
if (isEnoentError(error))
|
|
208
279
|
return undefined;
|
|
209
280
|
throw error;
|
|
210
281
|
}
|
|
211
282
|
}
|
|
283
|
+
function isEnoentError(error) {
|
|
284
|
+
return error?.code === "ENOENT";
|
|
285
|
+
}
|
|
286
|
+
function isMissingRunError(error) {
|
|
287
|
+
return (isEnoentError(error) ||
|
|
288
|
+
(error instanceof Error && /^Flow run not found: /.test(error.message)));
|
|
289
|
+
}
|
|
290
|
+
function assertScheduleLeaseActive(signal) {
|
|
291
|
+
if (!signal?.aborted)
|
|
292
|
+
return;
|
|
293
|
+
const reason = signal.reason;
|
|
294
|
+
if (reason instanceof Error)
|
|
295
|
+
throw reason;
|
|
296
|
+
throw new Error(reason === undefined
|
|
297
|
+
? "Lost supervisor lease"
|
|
298
|
+
: `Lost supervisor lease: ${String(reason)}`);
|
|
299
|
+
}
|
|
212
300
|
export async function scheduleRun(cwd, runId, compiled, options = {}) {
|
|
213
|
-
return withRunLease(cwd, runId, async () => {
|
|
301
|
+
return withRunLease(cwd, runId, async (leaseSignal) => {
|
|
302
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
214
303
|
let run = await readRunRecord(cwd, runId);
|
|
215
304
|
run = await resolveWorkflowBackend(run).refreshRun(cwd, run);
|
|
216
|
-
if (
|
|
305
|
+
if (isTerminalWorkflowStatus(run.status))
|
|
306
|
+
return run;
|
|
307
|
+
if (run.taskSummary.blocked > 0 &&
|
|
308
|
+
run.taskSummary.pending === 0 &&
|
|
309
|
+
run.taskSummary.running === 0)
|
|
217
310
|
return run;
|
|
218
311
|
const compiledFlow = compiled ?? (await readCompiledWorkflow(cwd, run.runId));
|
|
219
312
|
if (!compiledFlow)
|
|
@@ -221,7 +314,8 @@ export async function scheduleRun(cwd, runId, compiled, options = {}) {
|
|
|
221
314
|
if (compiledFlow.type !== WORKFLOW_RUN_TYPE) {
|
|
222
315
|
throw new Error(`unsupported compiled workflow type: ${compiledFlow.type}`);
|
|
223
316
|
}
|
|
224
|
-
await scheduleDag(cwd, run, compiledFlow, options);
|
|
317
|
+
await scheduleDag(cwd, run, compiledFlow, options, leaseSignal);
|
|
318
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
225
319
|
run = await readRunRecord(cwd, run.runId);
|
|
226
320
|
return run;
|
|
227
321
|
});
|
|
@@ -233,13 +327,13 @@ export async function formatStatus(cwd) {
|
|
|
233
327
|
const refreshed = (await readIndex(cwd).catch(() => cached)) ?? cached;
|
|
234
328
|
if (refreshed.runs.length === 0)
|
|
235
329
|
return "No workflow runs found.";
|
|
236
|
-
return formatIndex(refreshed);
|
|
330
|
+
return formatIndex(cwd, refreshed);
|
|
237
331
|
}
|
|
238
332
|
await reconcileActiveRuns(cwd);
|
|
239
333
|
const rebuilt = await updateIndex(cwd).catch(() => readIndex(cwd));
|
|
240
334
|
if (!rebuilt || rebuilt.runs.length === 0)
|
|
241
335
|
return "No workflow runs found.";
|
|
242
|
-
return formatIndex(rebuilt);
|
|
336
|
+
return formatIndex(cwd, rebuilt);
|
|
243
337
|
}
|
|
244
338
|
export async function formatRunDetails(cwd, runIdOrPrefix) {
|
|
245
339
|
const run = await refreshRun(cwd, runIdOrPrefix);
|
|
@@ -270,10 +364,12 @@ export async function formatLogs(cwd, runIdOrPrefix, taskId = "task-1", lineCoun
|
|
|
270
364
|
return `${run.runId}/${task.taskId} output=${task.files.output}\n${tail || "(empty log)"}`;
|
|
271
365
|
}
|
|
272
366
|
export function formatRun(run, detail = "summary") {
|
|
367
|
+
const telemetry = summarizeWorkflowTelemetry(run);
|
|
273
368
|
const lines = [
|
|
274
369
|
`${run.runId} [${run.status}] type=${run.type} backend=${run.backend.type}/${run.backend.mode}`,
|
|
275
370
|
`created=${run.createdAt} updated=${run.updatedAt}`,
|
|
276
371
|
`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}`,
|
|
372
|
+
`completion=${telemetry.completion.health}, outputRetries=${telemetry.retryCounts.output}, launchRetries=${telemetry.retryCounts.launch}, resumeEvents=${telemetry.resumeCounts.events}, contextLimitFailures=${telemetry.completion.contextLimitFailures}`,
|
|
277
373
|
];
|
|
278
374
|
for (const task of run.tasks) {
|
|
279
375
|
lines.push(formatTask(task, detail));
|
|
@@ -283,13 +379,13 @@ export function formatRun(run, detail = "summary") {
|
|
|
283
379
|
async function reconcileActiveRuns(cwd) {
|
|
284
380
|
const runs = await listRunRecords(cwd);
|
|
285
381
|
for (const run of runs) {
|
|
286
|
-
if (run
|
|
382
|
+
if (hasActiveSchedulerWork(run))
|
|
287
383
|
await refreshRun(cwd, run.runId).catch((error) => recordSupervisorError(cwd, run.runId, error));
|
|
288
384
|
}
|
|
289
385
|
}
|
|
290
386
|
async function reconcileIndexedActiveRuns(cwd, index) {
|
|
291
387
|
for (const run of index.runs) {
|
|
292
|
-
if (run
|
|
388
|
+
if (hasActiveSchedulerWork(run))
|
|
293
389
|
await refreshRun(cwd, run.runId).catch((error) => recordSupervisorError(cwd, run.runId, error));
|
|
294
390
|
}
|
|
295
391
|
}
|
|
@@ -306,11 +402,18 @@ async function recordSupervisorError(cwd, runId, error) {
|
|
|
306
402
|
error: error instanceof Error ? error.message : String(error),
|
|
307
403
|
}).catch(() => undefined);
|
|
308
404
|
}
|
|
309
|
-
async function scheduleDag(cwd, run, compiledFlow, options = {}) {
|
|
405
|
+
async function scheduleDag(cwd, run, compiledFlow, options = {}, leaseSignal) {
|
|
406
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
310
407
|
if (compiledFlow.type === WORKFLOW_RUN_TYPE) {
|
|
311
408
|
const loopReconciled = await reconcileLoopTaskMaterialization(cwd, run, compiledFlow);
|
|
312
409
|
if (loopReconciled)
|
|
313
410
|
return;
|
|
411
|
+
const foreachReconciled = reconcileForeachGeneratedRunRecords(cwd, run, compiledFlow);
|
|
412
|
+
if (foreachReconciled) {
|
|
413
|
+
await writeJsonAtomic(compiledWorkflowPath(cwd, run.runId), compiledFlow);
|
|
414
|
+
await writeRunRecord(cwd, run);
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
314
417
|
const dynamicReconciled = reconcileDynamicGeneratedRunRecords(cwd, run, compiledFlow);
|
|
315
418
|
const staleDynamicRecovered = recoverStaleRunningDynamicControllers(run, compiledFlow);
|
|
316
419
|
if (dynamicReconciled || staleDynamicRecovered)
|
|
@@ -326,6 +429,7 @@ async function scheduleDag(cwd, run, compiledFlow, options = {}) {
|
|
|
326
429
|
let running = run.tasks.filter((task) => task.status === "running").length;
|
|
327
430
|
const bySpecId = new Map(run.tasks.map((task) => [task.specId, task]));
|
|
328
431
|
for (let index = 0; index < run.tasks.length && running < maxConcurrency; index += 1) {
|
|
432
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
329
433
|
const task = run.tasks[index];
|
|
330
434
|
const compiledTask = compiledFlow.tasks[index];
|
|
331
435
|
if (!task || !compiledTask || task.status !== "pending")
|
|
@@ -345,6 +449,8 @@ async function scheduleDag(cwd, run, compiledFlow, options = {}) {
|
|
|
345
449
|
const changed = await materializeForeachTask(cwd, run, compiledFlow, index, compiledTask);
|
|
346
450
|
if (changed)
|
|
347
451
|
return;
|
|
452
|
+
if (foreachStreamingEnabled(compiledTask))
|
|
453
|
+
continue;
|
|
348
454
|
}
|
|
349
455
|
if (compiledTask.stageMaxConcurrency !== undefined) {
|
|
350
456
|
const runningInStage = run.tasks.filter((candidate) => candidate.stageId === compiledTask.stageId &&
|
|
@@ -353,8 +459,10 @@ async function scheduleDag(cwd, run, compiledFlow, options = {}) {
|
|
|
353
459
|
Math.max(1, Math.min(MAX_CONCURRENCY, compiledTask.stageMaxConcurrency)))
|
|
354
460
|
continue;
|
|
355
461
|
}
|
|
462
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
356
463
|
const launched = await launchPendingTaskAt(cwd, run, compiledFlow, index, options);
|
|
357
|
-
|
|
464
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
465
|
+
if (launched && run.tasks[index]?.status === "running")
|
|
358
466
|
running += 1;
|
|
359
467
|
}
|
|
360
468
|
}
|
|
@@ -433,10 +541,12 @@ async function materializeForeachTask(cwd, run, compiledFlow, index, template) {
|
|
|
433
541
|
return false;
|
|
434
542
|
const sourceStageIds = sourceStageIdsForFrom(template.foreach.from);
|
|
435
543
|
const sourceTasks = run.tasks.filter((task) => sourceStageIds.includes(task.stageId ?? ""));
|
|
544
|
+
const streaming = foreachStreamingEnabled(template);
|
|
436
545
|
const extracted = await extractArtifactGraphForeachItems(cwd, {
|
|
437
546
|
from: template.foreach.from,
|
|
438
547
|
sourcePolicy: stageSourcePolicy(compiledFlow, template.stageId),
|
|
439
548
|
maxItems: template.foreach.maxItems,
|
|
549
|
+
streaming,
|
|
440
550
|
}, sourceTasks);
|
|
441
551
|
if (extracted.error) {
|
|
442
552
|
setTaskTerminal(templateRunTask, "blocked", "foreach_expansion_blocked", {
|
|
@@ -455,7 +565,30 @@ async function materializeForeachTask(cwd, run, compiledFlow, index, template) {
|
|
|
455
565
|
return true;
|
|
456
566
|
}
|
|
457
567
|
const placeholderSpecId = template.id;
|
|
568
|
+
if (streaming) {
|
|
569
|
+
return await materializeStreamingForeachTask({
|
|
570
|
+
cwd,
|
|
571
|
+
run,
|
|
572
|
+
compiledFlow,
|
|
573
|
+
index,
|
|
574
|
+
templateRunTask,
|
|
575
|
+
placeholderSpecId,
|
|
576
|
+
sourceTaskSpecIds: sourceTasks.map((task) => task.specId),
|
|
577
|
+
itemMetas: extracted.itemMetas ?? [],
|
|
578
|
+
generatedTasks: generated.tasks,
|
|
579
|
+
waitingForSources: extracted.waitingForSources ?? false,
|
|
580
|
+
minChunk: foreachStreamingMinChunk(template),
|
|
581
|
+
});
|
|
582
|
+
}
|
|
458
583
|
const generatedSpecIds = generated.tasks.map((task) => task.id);
|
|
584
|
+
const hasDownstreamDependents = compiledFlow.tasks.some((task, taskIndex) => taskIndex !== index && (task.dependsOn ?? []).includes(placeholderSpecId));
|
|
585
|
+
if (generatedSpecIds.length === 0 && !hasDownstreamDependents) {
|
|
586
|
+
setTaskTerminal(templateRunTask, "completed", "foreach_empty", {
|
|
587
|
+
lastMessage: "foreach produced 0 item(s)",
|
|
588
|
+
});
|
|
589
|
+
await writeRunRecord(cwd, run);
|
|
590
|
+
return true;
|
|
591
|
+
}
|
|
459
592
|
compiledFlow.tasks.splice(index, 1, ...generated.tasks);
|
|
460
593
|
updateDownstreamDependencies(compiledFlow, placeholderSpecId, generatedSpecIds);
|
|
461
594
|
const nextIndex = nextTaskRecordIndex(run);
|
|
@@ -470,16 +603,183 @@ async function materializeForeachTask(cwd, run, compiledFlow, index, template) {
|
|
|
470
603
|
await writeRunRecord(cwd, run);
|
|
471
604
|
return true;
|
|
472
605
|
}
|
|
606
|
+
async function materializeStreamingForeachTask(input) {
|
|
607
|
+
const sourceTaskSpecIdSet = new Set(input.sourceTaskSpecIds);
|
|
608
|
+
const generatedTasksWithItemDeps = input.generatedTasks.map((task, index) => {
|
|
609
|
+
const itemMeta = input.itemMetas[index];
|
|
610
|
+
if (!itemMeta)
|
|
611
|
+
return task;
|
|
612
|
+
return {
|
|
613
|
+
...task,
|
|
614
|
+
dependsOn: replaceSourceDependenciesWithItemSource(task.dependsOn ?? [], sourceTaskSpecIdSet, itemMeta, {
|
|
615
|
+
keepPartialSourceDependency: partialGeneratedTaskNeedsCompletedSourceContext(task),
|
|
616
|
+
}),
|
|
617
|
+
foreachGenerated: {
|
|
618
|
+
...(task.foreachGenerated ?? {
|
|
619
|
+
placeholderSpecId: input.placeholderSpecId,
|
|
620
|
+
}),
|
|
621
|
+
itemHash: itemMeta.itemHash,
|
|
622
|
+
itemSourceSpecId: itemMeta.sourceSpecId,
|
|
623
|
+
itemSourceKind: itemMeta.sourceKind,
|
|
624
|
+
itemRef: itemMeta.itemRef,
|
|
625
|
+
},
|
|
626
|
+
};
|
|
627
|
+
});
|
|
628
|
+
const existingGeneratedTasks = input.compiledFlow.tasks.filter((task) => task.foreachGenerated?.placeholderSpecId === input.placeholderSpecId);
|
|
629
|
+
const existingGeneratedSpecIds = existingGeneratedTasks.map((task) => task.id);
|
|
630
|
+
const existingGeneratedTaskBySpecId = new Map(existingGeneratedTasks.map((task) => [task.id, task]));
|
|
631
|
+
for (const task of generatedTasksWithItemDeps) {
|
|
632
|
+
const existing = existingGeneratedTaskBySpecId.get(task.id);
|
|
633
|
+
const existingHash = existing?.foreachGenerated?.itemHash;
|
|
634
|
+
const nextHash = task.foreachGenerated?.itemHash;
|
|
635
|
+
if (existing && existingHash && nextHash && existingHash !== nextHash) {
|
|
636
|
+
setTaskTerminal(input.templateRunTask, "blocked", "foreach_expansion_blocked", {
|
|
637
|
+
lastMessage: `foreach streaming item ${task.id} changed after materialization`,
|
|
638
|
+
});
|
|
639
|
+
await writeRunRecord(input.cwd, input.run);
|
|
640
|
+
return true;
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const existingGeneratedSpecIdSet = new Set(existingGeneratedSpecIds);
|
|
644
|
+
const finalGeneratedSpecIdSet = new Set(generatedTasksWithItemDeps.map((task) => task.id));
|
|
645
|
+
if (!input.waitingForSources) {
|
|
646
|
+
const withdrawn = existingGeneratedTasks.find((task) => task.foreachGenerated?.itemSourceKind === "partial" &&
|
|
647
|
+
!finalGeneratedSpecIdSet.has(task.id));
|
|
648
|
+
if (withdrawn) {
|
|
649
|
+
setTaskTerminal(input.templateRunTask, "blocked", "foreach_expansion_blocked", {
|
|
650
|
+
lastMessage: `foreach streaming item ${withdrawn.id} was published as partial output but is missing from final control`,
|
|
651
|
+
});
|
|
652
|
+
await writeRunRecord(input.cwd, input.run);
|
|
653
|
+
return true;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const newGeneratedTasks = generatedTasksWithItemDeps.filter((task) => !existingGeneratedSpecIdSet.has(task.id));
|
|
657
|
+
const allGeneratedSpecIds = [
|
|
658
|
+
...existingGeneratedSpecIds,
|
|
659
|
+
...newGeneratedTasks.map((task) => task.id),
|
|
660
|
+
];
|
|
661
|
+
const shouldHoldForMinChunk = input.waitingForSources &&
|
|
662
|
+
newGeneratedTasks.length > 0 &&
|
|
663
|
+
newGeneratedTasks.length < input.minChunk;
|
|
664
|
+
if (shouldHoldForMinChunk)
|
|
665
|
+
return false;
|
|
666
|
+
let changed = false;
|
|
667
|
+
if (newGeneratedTasks.length > 0) {
|
|
668
|
+
let compiledInsertIndex = input.index + 1;
|
|
669
|
+
while (input.compiledFlow.tasks[compiledInsertIndex]?.foreachGenerated
|
|
670
|
+
?.placeholderSpecId === input.placeholderSpecId) {
|
|
671
|
+
compiledInsertIndex += 1;
|
|
672
|
+
}
|
|
673
|
+
input.compiledFlow.tasks.splice(compiledInsertIndex, 0, ...newGeneratedTasks);
|
|
674
|
+
let runInsertIndex = input.index + 1;
|
|
675
|
+
while (input.run.tasks[runInsertIndex]?.foreachGenerated?.placeholderSpecId ===
|
|
676
|
+
input.placeholderSpecId) {
|
|
677
|
+
runInsertIndex += 1;
|
|
678
|
+
}
|
|
679
|
+
const nextIndex = nextTaskRecordIndex(input.run);
|
|
680
|
+
const generatedRunTasks = newGeneratedTasks.map((task, offset) => createTaskRunRecord(input.cwd, input.run.runId, task, nextIndex + offset));
|
|
681
|
+
input.run.tasks.splice(runInsertIndex, 0, ...generatedRunTasks);
|
|
682
|
+
changed = true;
|
|
683
|
+
}
|
|
684
|
+
const dependencyTargets = [input.placeholderSpecId, ...allGeneratedSpecIds];
|
|
685
|
+
for (const task of input.compiledFlow.tasks) {
|
|
686
|
+
if (!task.dependsOn)
|
|
687
|
+
continue;
|
|
688
|
+
const replaced = replaceDependencyList(task.dependsOn, input.placeholderSpecId, dependencyTargets);
|
|
689
|
+
if (JSON.stringify(task.dependsOn) !== JSON.stringify(replaced)) {
|
|
690
|
+
task.dependsOn = replaced;
|
|
691
|
+
changed = true;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
for (const task of input.run.tasks) {
|
|
695
|
+
if (!task.dependsOn)
|
|
696
|
+
continue;
|
|
697
|
+
const replaced = replaceDependencyList(task.dependsOn, input.placeholderSpecId, dependencyTargets);
|
|
698
|
+
if (JSON.stringify(task.dependsOn) !== JSON.stringify(replaced)) {
|
|
699
|
+
task.dependsOn = replaced;
|
|
700
|
+
changed = true;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
if (!input.waitingForSources) {
|
|
704
|
+
const statusDetail = allGeneratedSpecIds.length === 0
|
|
705
|
+
? "foreach_empty"
|
|
706
|
+
: "foreach_streaming_complete";
|
|
707
|
+
const lastMessage = allGeneratedSpecIds.length === 0
|
|
708
|
+
? "foreach produced 0 item(s)"
|
|
709
|
+
: `foreach streaming materialized ${allGeneratedSpecIds.length} item(s)`;
|
|
710
|
+
setTaskTerminal(input.templateRunTask, "completed", statusDetail, {
|
|
711
|
+
lastMessage,
|
|
712
|
+
});
|
|
713
|
+
changed = true;
|
|
714
|
+
}
|
|
715
|
+
else if (newGeneratedTasks.length > 0) {
|
|
716
|
+
input.templateRunTask.statusDetail = "foreach_streaming_waiting";
|
|
717
|
+
input.templateRunTask.lastMessage = `foreach streaming materialized ${allGeneratedSpecIds.length} item(s); waiting for more source tasks`;
|
|
718
|
+
changed = true;
|
|
719
|
+
}
|
|
720
|
+
if (!changed)
|
|
721
|
+
return false;
|
|
722
|
+
await writeJsonAtomic(compiledWorkflowPath(input.cwd, input.run.runId), input.compiledFlow);
|
|
723
|
+
await writeRunRecord(input.cwd, input.run);
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
function replaceSourceDependenciesWithItemSource(dependsOn, sourceTaskSpecIds, itemMeta, options = {}) {
|
|
727
|
+
const replaced = [];
|
|
728
|
+
let inserted = false;
|
|
729
|
+
const shouldReplaceWithSource = itemMeta.sourceKind !== "partial" ||
|
|
730
|
+
options.keepPartialSourceDependency === true;
|
|
731
|
+
for (const dep of dependsOn) {
|
|
732
|
+
if (!sourceTaskSpecIds.has(dep)) {
|
|
733
|
+
replaced.push(dep);
|
|
734
|
+
continue;
|
|
735
|
+
}
|
|
736
|
+
if (!shouldReplaceWithSource)
|
|
737
|
+
continue;
|
|
738
|
+
if (!inserted) {
|
|
739
|
+
replaced.push(itemMeta.sourceSpecId);
|
|
740
|
+
inserted = true;
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (!inserted && shouldReplaceWithSource) {
|
|
744
|
+
replaced.push(itemMeta.sourceSpecId);
|
|
745
|
+
}
|
|
746
|
+
return [...new Set(replaced)];
|
|
747
|
+
}
|
|
748
|
+
function partialGeneratedTaskNeedsCompletedSourceContext(task) {
|
|
749
|
+
const artifactGraph = task.artifactGraph;
|
|
750
|
+
if (artifactGraph?.artifactAccess === "none")
|
|
751
|
+
return false;
|
|
752
|
+
return Boolean(artifactGraph?.sourceProjection !== undefined ||
|
|
753
|
+
(artifactGraph?.requiredReads?.length ?? 0) > 0);
|
|
754
|
+
}
|
|
473
755
|
async function extractArtifactGraphForeachItems(cwd, stage, sourceTasks) {
|
|
474
756
|
const items = [];
|
|
757
|
+
const itemMetas = [];
|
|
475
758
|
const path = stage.from?.path;
|
|
476
759
|
if (typeof path !== "string" || !path.startsWith("$.")) {
|
|
477
760
|
return {
|
|
478
761
|
error: "foreach.from.path must be a control JSONPath like $.items",
|
|
479
762
|
};
|
|
480
763
|
}
|
|
764
|
+
let waitingForSources = false;
|
|
481
765
|
for (const task of sourceTasks) {
|
|
482
766
|
if (task.status !== "completed") {
|
|
767
|
+
if (stage.streaming && !isTerminalTaskStatus(task.status)) {
|
|
768
|
+
const partial = await extractPartialForeachItems(cwd, task, path);
|
|
769
|
+
if (partial.error)
|
|
770
|
+
return { error: partial.error };
|
|
771
|
+
for (const item of partial.items) {
|
|
772
|
+
items.push(item.item);
|
|
773
|
+
itemMetas.push({
|
|
774
|
+
sourceSpecId: task.specId,
|
|
775
|
+
sourceKind: "partial",
|
|
776
|
+
itemHash: item.itemHash,
|
|
777
|
+
itemRef: `${task.specId}:${item.itemRef}`,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
waitingForSources = true;
|
|
781
|
+
continue;
|
|
782
|
+
}
|
|
483
783
|
if (stage.sourcePolicy !== "partial")
|
|
484
784
|
return { error: `${task.taskId} did not complete` };
|
|
485
785
|
continue;
|
|
@@ -495,7 +795,15 @@ async function extractArtifactGraphForeachItems(cwd, stage, sourceTasks) {
|
|
|
495
795
|
}
|
|
496
796
|
continue;
|
|
497
797
|
}
|
|
498
|
-
|
|
798
|
+
for (const [index, item] of value.entries()) {
|
|
799
|
+
items.push(item);
|
|
800
|
+
itemMetas.push({
|
|
801
|
+
sourceSpecId: task.specId,
|
|
802
|
+
sourceKind: "control",
|
|
803
|
+
itemHash: hashDynamicRequest(item),
|
|
804
|
+
itemRef: `${task.specId}:control:${path}[${index}]`,
|
|
805
|
+
});
|
|
806
|
+
}
|
|
499
807
|
}
|
|
500
808
|
catch (error) {
|
|
501
809
|
if (stage.sourcePolicy !== "partial") {
|
|
@@ -510,7 +818,27 @@ async function extractArtifactGraphForeachItems(cwd, stage, sourceTasks) {
|
|
|
510
818
|
error: `foreach extracted ${items.length} items, exceeding maxItems=${stage.maxItems}`,
|
|
511
819
|
};
|
|
512
820
|
}
|
|
513
|
-
return { items };
|
|
821
|
+
return { items, itemMetas, waitingForSources };
|
|
822
|
+
}
|
|
823
|
+
async function extractPartialForeachItems(cwd, task, path) {
|
|
824
|
+
const partialPaths = task.artifactGraph?.output.partial?.paths ?? [];
|
|
825
|
+
if (!partialPaths.includes(path))
|
|
826
|
+
return { items: [] };
|
|
827
|
+
const taskDir = dirname(fromProjectPath(cwd, task.files.result));
|
|
828
|
+
let ledger = await readWorkflowPartialOutputLedger(taskDir).catch(() => undefined);
|
|
829
|
+
if (!ledger) {
|
|
830
|
+
ledger = await writeWorkflowPartialOutputLedgerFromFile({
|
|
831
|
+
taskDir,
|
|
832
|
+
outputFile: fromProjectPath(cwd, task.files.output),
|
|
833
|
+
allowedPaths: partialPaths,
|
|
834
|
+
}).catch(() => undefined);
|
|
835
|
+
}
|
|
836
|
+
if (!ledger)
|
|
837
|
+
return { items: [] };
|
|
838
|
+
const fatal = hasFatalPartialOutputIssue(ledger);
|
|
839
|
+
if (fatal)
|
|
840
|
+
return { items: [], error: fatal.message };
|
|
841
|
+
return { items: ledger.items.filter((item) => item.path === path) };
|
|
514
842
|
}
|
|
515
843
|
async function launchPendingTaskAt(cwd, run, compiledFlow, index, options = {}) {
|
|
516
844
|
const task = run.tasks[index];
|
|
@@ -526,11 +854,14 @@ async function launchPendingTaskAt(cwd, run, compiledFlow, index, options = {})
|
|
|
526
854
|
await writeRunRecord(cwd, run);
|
|
527
855
|
return false;
|
|
528
856
|
}
|
|
529
|
-
let launchTask
|
|
530
|
-
|
|
531
|
-
launchTask = await prepareArtifactGraphRetryTask(cwd, task, launchTask);
|
|
532
|
-
}
|
|
857
|
+
let launchTask;
|
|
858
|
+
let prepareComplete = false;
|
|
533
859
|
try {
|
|
860
|
+
launchTask = await prepareDagTask(cwd, run, compiledFlow, index);
|
|
861
|
+
if (task.outputRetry) {
|
|
862
|
+
launchTask = await prepareArtifactGraphRetryTask(cwd, task, launchTask);
|
|
863
|
+
}
|
|
864
|
+
prepareComplete = true;
|
|
534
865
|
if (launchTask.kind === "support") {
|
|
535
866
|
return await executeSupportTask(cwd, run, task, launchTask);
|
|
536
867
|
}
|
|
@@ -547,11 +878,13 @@ async function launchPendingTaskAt(cwd, run, compiledFlow, index, options = {})
|
|
|
547
878
|
return launch.kind === "launched";
|
|
548
879
|
}
|
|
549
880
|
catch (error) {
|
|
550
|
-
const statusDetail =
|
|
551
|
-
? "
|
|
552
|
-
: launchTask
|
|
553
|
-
? "
|
|
554
|
-
:
|
|
881
|
+
const statusDetail = !prepareComplete
|
|
882
|
+
? "prepare_failed"
|
|
883
|
+
: launchTask?.kind === "support"
|
|
884
|
+
? "support_failed"
|
|
885
|
+
: launchTask?.safety.requiresWorktree
|
|
886
|
+
? "worktree_failed"
|
|
887
|
+
: "launch_failed";
|
|
555
888
|
setTaskTerminal(task, "failed", statusDetail, {
|
|
556
889
|
lastMessage: error instanceof Error ? error.message : String(error),
|
|
557
890
|
});
|
|
@@ -1815,21 +2148,36 @@ function uniqueStrings(values) {
|
|
|
1815
2148
|
async function readCompiledWorkflow(cwd, runId) {
|
|
1816
2149
|
return readJson(compiledWorkflowPath(cwd, runId));
|
|
1817
2150
|
}
|
|
1818
|
-
function formatIndex(index) {
|
|
1819
|
-
|
|
1820
|
-
.map((run) => {
|
|
2151
|
+
async function formatIndex(cwd, index) {
|
|
2152
|
+
const blocks = await Promise.all(index.runs.map(async (run) => {
|
|
1821
2153
|
const lines = [
|
|
1822
2154
|
`${run.runId} [${run.status}] type=${run.type} updated=${run.updatedAt}`,
|
|
1823
2155
|
`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}`,
|
|
1824
2156
|
];
|
|
1825
|
-
for (const task of run
|
|
2157
|
+
for (const task of await indexTasksForStatus(cwd, run)) {
|
|
1826
2158
|
const message = task.lastMessage ? ` — ${task.lastMessage}` : "";
|
|
1827
2159
|
const kind = task.kind && task.kind !== "main" ? ` ${task.kind}` : "";
|
|
1828
2160
|
lines.push(`- ${task.taskId}${kind} ${task.agent} [${task.status}/${task.statusDetail}]${message}`);
|
|
1829
2161
|
}
|
|
1830
2162
|
return lines.join("\n");
|
|
1831
|
-
})
|
|
1832
|
-
|
|
2163
|
+
}));
|
|
2164
|
+
return blocks.join("\n\n");
|
|
2165
|
+
}
|
|
2166
|
+
async function indexTasksForStatus(cwd, run) {
|
|
2167
|
+
if (Array.isArray(run.tasks))
|
|
2168
|
+
return run.tasks;
|
|
2169
|
+
const fullRun = await readRunRecord(cwd, run.runId).catch(() => undefined);
|
|
2170
|
+
return (fullRun?.tasks.map((task) => ({
|
|
2171
|
+
taskId: task.taskId,
|
|
2172
|
+
displayName: task.displayName,
|
|
2173
|
+
agent: task.agent,
|
|
2174
|
+
kind: task.kind,
|
|
2175
|
+
stageId: task.stageId,
|
|
2176
|
+
backendHandle: task.backendHandle,
|
|
2177
|
+
status: task.status,
|
|
2178
|
+
statusDetail: task.statusDetail,
|
|
2179
|
+
lastMessage: task.lastMessage,
|
|
2180
|
+
})) ?? []);
|
|
1833
2181
|
}
|
|
1834
2182
|
function formatTask(task, detail) {
|
|
1835
2183
|
const elapsed = task.elapsedMs !== undefined
|