@agwab/pi-workflow 0.3.0 → 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 +46 -11
- package/dist/dynamic-decision.d.ts +1 -0
- package/dist/dynamic-decision.js +7 -0
- package/dist/dynamic-generated-task-runtime.js +3 -1
- package/dist/dynamic-profiles.d.ts +1 -0
- package/dist/dynamic-profiles.js +3 -0
- package/dist/engine-run-graph.d.ts +2 -0
- package/dist/engine-run-graph.js +55 -5
- package/dist/engine.js +278 -15
- package/dist/extension.js +3 -2
- package/dist/index.d.ts +8 -0
- package/dist/index.js +4 -0
- 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 +89 -29
- package/dist/strings.d.ts +11 -0
- package/dist/strings.js +24 -0
- package/dist/subagent-backend.js +557 -13
- package/dist/types.d.ts +101 -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-web-source-extension.js +27 -4
- package/dist/workflow-web-source.js +26 -12
- package/docs/usage.md +76 -29
- package/node_modules/@agwab/pi-subagent/package.json +1 -1
- package/node_modules/@agwab/pi-subagent/src/index.ts +53 -5
- package/node_modules/@agwab/pi-subagent/src/panel.ts +7 -3
- 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 +49 -9
- package/src/dynamic-decision.ts +11 -0
- package/src/dynamic-generated-task-runtime.ts +3 -1
- package/src/dynamic-profiles.ts +4 -0
- package/src/engine-run-graph.ts +63 -4
- package/src/engine.ts +400 -14
- package/src/extension.ts +3 -2
- package/src/index.ts +49 -0
- package/src/prompt-json.ts +13 -0
- package/src/roles.ts +6 -9
- package/src/store.ts +123 -34
- package/src/strings.ts +38 -0
- package/src/subagent-backend.ts +727 -41
- package/src/types.ts +110 -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-web-source-extension.ts +33 -4
- package/src/workflow-web-source.ts +36 -12
- 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 +173 -20
- package/workflows/deep-research/helpers/normalize-input-packet.mjs +80 -1
- package/workflows/deep-research/helpers/render-executive.mjs +32 -5
- 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 -2
- 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 +10 -3
- package/workflows/deep-research/spec.json +32 -12
- 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/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,6 +95,8 @@ import {
|
|
|
93
95
|
assertRunTaskPositionalAlignment,
|
|
94
96
|
buildForeachGeneratedTasks,
|
|
95
97
|
dependenciesReady,
|
|
98
|
+
foreachStreamingEnabled,
|
|
99
|
+
foreachStreamingMinChunk,
|
|
96
100
|
markDagDependentsSkipped,
|
|
97
101
|
nextTaskRecordIndex,
|
|
98
102
|
reconcileDynamicGeneratedRunRecords,
|
|
@@ -121,6 +125,12 @@ import {
|
|
|
121
125
|
DIRECT_DYNAMIC_RUNTIME_VERSION,
|
|
122
126
|
ensureDirectDynamicRuntimeBundle,
|
|
123
127
|
} from "./dynamic-runtime-bundle.js";
|
|
128
|
+
import {
|
|
129
|
+
hasFatalPartialOutputIssue,
|
|
130
|
+
readWorkflowPartialOutputLedger,
|
|
131
|
+
writeWorkflowPartialOutputLedgerFromFile,
|
|
132
|
+
type WorkflowPartialOutputItem,
|
|
133
|
+
} from "./workflow-partial-output.js";
|
|
124
134
|
import {
|
|
125
135
|
type CompiledDynamicWorkflowTask,
|
|
126
136
|
type CompiledTask,
|
|
@@ -306,6 +316,22 @@ export interface StopRunSummary {
|
|
|
306
316
|
interruptedTaskIds: string[];
|
|
307
317
|
}
|
|
308
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
|
+
|
|
309
335
|
export async function stopRun(
|
|
310
336
|
cwd: string,
|
|
311
337
|
runIdOrPrefix: string,
|
|
@@ -358,6 +384,7 @@ export async function resumeRun(
|
|
|
358
384
|
`resume requires a failed, interrupted, or resumable blocked run; ${current.runId} is ${current.status}`,
|
|
359
385
|
);
|
|
360
386
|
}
|
|
387
|
+
assertBlockedRunResumable(current);
|
|
361
388
|
const compiledFlow = await readCompiledWorkflow(cwd, current.runId);
|
|
362
389
|
const hasLoopTasks =
|
|
363
390
|
compiledFlow?.tasks.some(
|
|
@@ -375,6 +402,7 @@ export async function resumeRun(
|
|
|
375
402
|
const resetTaskIds: string[] = [];
|
|
376
403
|
const updated = await withRunLease(cwd, current.runId, async () => {
|
|
377
404
|
const run = await readRunRecord(cwd, current.runId);
|
|
405
|
+
assertBlockedRunResumable(run);
|
|
378
406
|
await resolveWorkflowBackend(run)
|
|
379
407
|
.cleanupRun(cwd, run)
|
|
380
408
|
.catch(() => undefined);
|
|
@@ -501,13 +529,25 @@ function isMissingRunError(error: unknown): boolean {
|
|
|
501
529
|
);
|
|
502
530
|
}
|
|
503
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
|
+
|
|
504
543
|
export async function scheduleRun(
|
|
505
544
|
cwd: string,
|
|
506
545
|
runId: string,
|
|
507
546
|
compiled?: CompiledWorkflow,
|
|
508
547
|
options: WorkflowScheduleOptions = {},
|
|
509
548
|
): Promise<WorkflowRunRecord | undefined> {
|
|
510
|
-
return withRunLease(cwd, runId, async () => {
|
|
549
|
+
return withRunLease(cwd, runId, async (leaseSignal) => {
|
|
550
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
511
551
|
let run = await readRunRecord(cwd, runId);
|
|
512
552
|
run = await resolveWorkflowBackend(run).refreshRun(cwd, run);
|
|
513
553
|
if (isTerminalWorkflowStatus(run.status)) return run;
|
|
@@ -527,7 +567,8 @@ export async function scheduleRun(
|
|
|
527
567
|
`unsupported compiled workflow type: ${compiledFlow.type}`,
|
|
528
568
|
);
|
|
529
569
|
}
|
|
530
|
-
await scheduleDag(cwd, run, compiledFlow, options);
|
|
570
|
+
await scheduleDag(cwd, run, compiledFlow, options, leaseSignal);
|
|
571
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
531
572
|
|
|
532
573
|
run = await readRunRecord(cwd, run.runId);
|
|
533
574
|
return run;
|
|
@@ -540,13 +581,13 @@ export async function formatStatus(cwd: string): Promise<string> {
|
|
|
540
581
|
await reconcileIndexedActiveRuns(cwd, cached);
|
|
541
582
|
const refreshed = (await readIndex(cwd).catch(() => cached)) ?? cached;
|
|
542
583
|
if (refreshed.runs.length === 0) return "No workflow runs found.";
|
|
543
|
-
return formatIndex(refreshed);
|
|
584
|
+
return formatIndex(cwd, refreshed);
|
|
544
585
|
}
|
|
545
586
|
|
|
546
587
|
await reconcileActiveRuns(cwd);
|
|
547
588
|
const rebuilt = await updateIndex(cwd).catch(() => readIndex(cwd));
|
|
548
589
|
if (!rebuilt || rebuilt.runs.length === 0) return "No workflow runs found.";
|
|
549
|
-
return formatIndex(rebuilt);
|
|
590
|
+
return formatIndex(cwd, rebuilt);
|
|
550
591
|
}
|
|
551
592
|
|
|
552
593
|
export async function formatRunDetails(
|
|
@@ -598,10 +639,12 @@ export function formatRun(
|
|
|
598
639
|
run: WorkflowRunRecord,
|
|
599
640
|
detail: "summary" | "full" = "summary",
|
|
600
641
|
): string {
|
|
642
|
+
const telemetry = summarizeWorkflowTelemetry(run);
|
|
601
643
|
const lines = [
|
|
602
644
|
`${run.runId} [${run.status}] type=${run.type} backend=${run.backend.type}/${run.backend.mode}`,
|
|
603
645
|
`created=${run.createdAt} updated=${run.updatedAt}`,
|
|
604
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}`,
|
|
605
648
|
];
|
|
606
649
|
|
|
607
650
|
for (const task of run.tasks) {
|
|
@@ -657,7 +700,9 @@ async function scheduleDag(
|
|
|
657
700
|
run: WorkflowRunRecord,
|
|
658
701
|
compiledFlow: CompiledWorkflow,
|
|
659
702
|
options: WorkflowScheduleOptions = {},
|
|
703
|
+
leaseSignal?: AbortSignal,
|
|
660
704
|
): Promise<void> {
|
|
705
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
661
706
|
if (compiledFlow.type === WORKFLOW_RUN_TYPE) {
|
|
662
707
|
const loopReconciled = await reconcileLoopTaskMaterialization(
|
|
663
708
|
cwd,
|
|
@@ -707,6 +752,7 @@ async function scheduleDag(
|
|
|
707
752
|
index < run.tasks.length && running < maxConcurrency;
|
|
708
753
|
index += 1
|
|
709
754
|
) {
|
|
755
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
710
756
|
const task = run.tasks[index];
|
|
711
757
|
const compiledTask = compiledFlow.tasks[index];
|
|
712
758
|
if (!task || !compiledTask || task.status !== "pending") continue;
|
|
@@ -738,6 +784,7 @@ async function scheduleDag(
|
|
|
738
784
|
compiledTask,
|
|
739
785
|
);
|
|
740
786
|
if (changed) return;
|
|
787
|
+
if (foreachStreamingEnabled(compiledTask)) continue;
|
|
741
788
|
}
|
|
742
789
|
|
|
743
790
|
if (compiledTask.stageMaxConcurrency !== undefined) {
|
|
@@ -753,6 +800,7 @@ async function scheduleDag(
|
|
|
753
800
|
continue;
|
|
754
801
|
}
|
|
755
802
|
|
|
803
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
756
804
|
const launched = await launchPendingTaskAt(
|
|
757
805
|
cwd,
|
|
758
806
|
run,
|
|
@@ -760,6 +808,7 @@ async function scheduleDag(
|
|
|
760
808
|
index,
|
|
761
809
|
options,
|
|
762
810
|
);
|
|
811
|
+
assertScheduleLeaseActive(leaseSignal);
|
|
763
812
|
if (launched && run.tasks[index]?.status === "running") running += 1;
|
|
764
813
|
}
|
|
765
814
|
}
|
|
@@ -886,12 +935,14 @@ async function materializeForeachTask(
|
|
|
886
935
|
const sourceTasks = run.tasks.filter((task) =>
|
|
887
936
|
sourceStageIds.includes(task.stageId ?? ""),
|
|
888
937
|
);
|
|
938
|
+
const streaming = foreachStreamingEnabled(template);
|
|
889
939
|
const extracted = await extractArtifactGraphForeachItems(
|
|
890
940
|
cwd,
|
|
891
941
|
{
|
|
892
942
|
from: template.foreach.from,
|
|
893
943
|
sourcePolicy: stageSourcePolicy(compiledFlow, template.stageId),
|
|
894
944
|
maxItems: template.foreach.maxItems,
|
|
945
|
+
streaming,
|
|
895
946
|
},
|
|
896
947
|
sourceTasks,
|
|
897
948
|
);
|
|
@@ -919,6 +970,21 @@ async function materializeForeachTask(
|
|
|
919
970
|
}
|
|
920
971
|
|
|
921
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
|
+
}
|
|
922
988
|
const generatedSpecIds = generated.tasks.map((task) => task.id);
|
|
923
989
|
const hasDownstreamDependents = compiledFlow.tasks.some(
|
|
924
990
|
(task, taskIndex) =>
|
|
@@ -957,20 +1023,279 @@ async function materializeForeachTask(
|
|
|
957
1023
|
return true;
|
|
958
1024
|
}
|
|
959
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
|
+
|
|
960
1258
|
async function extractArtifactGraphForeachItems(
|
|
961
1259
|
cwd: string,
|
|
962
|
-
stage: {
|
|
1260
|
+
stage: {
|
|
1261
|
+
from: unknown;
|
|
1262
|
+
sourcePolicy?: string;
|
|
1263
|
+
maxItems?: number;
|
|
1264
|
+
streaming?: boolean;
|
|
1265
|
+
},
|
|
963
1266
|
sourceTasks: WorkflowTaskRunRecord[],
|
|
964
|
-
): Promise<{
|
|
1267
|
+
): Promise<{
|
|
1268
|
+
items?: unknown[];
|
|
1269
|
+
itemMetas?: ForeachExtractedItemMeta[];
|
|
1270
|
+
error?: string;
|
|
1271
|
+
waitingForSources?: boolean;
|
|
1272
|
+
}> {
|
|
965
1273
|
const items: unknown[] = [];
|
|
1274
|
+
const itemMetas: ForeachExtractedItemMeta[] = [];
|
|
966
1275
|
const path = (stage.from as any)?.path;
|
|
967
1276
|
if (typeof path !== "string" || !path.startsWith("$.")) {
|
|
968
1277
|
return {
|
|
969
1278
|
error: "foreach.from.path must be a control JSONPath like $.items",
|
|
970
1279
|
};
|
|
971
1280
|
}
|
|
1281
|
+
let waitingForSources = false;
|
|
972
1282
|
for (const task of sourceTasks) {
|
|
973
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
|
+
}
|
|
974
1299
|
if (stage.sourcePolicy !== "partial")
|
|
975
1300
|
return { error: `${task.taskId} did not complete` };
|
|
976
1301
|
continue;
|
|
@@ -986,7 +1311,15 @@ async function extractArtifactGraphForeachItems(
|
|
|
986
1311
|
}
|
|
987
1312
|
continue;
|
|
988
1313
|
}
|
|
989
|
-
|
|
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
|
+
}
|
|
990
1323
|
} catch (error) {
|
|
991
1324
|
if (stage.sourcePolicy !== "partial") {
|
|
992
1325
|
return {
|
|
@@ -1000,7 +1333,31 @@ async function extractArtifactGraphForeachItems(
|
|
|
1000
1333
|
error: `foreach extracted ${items.length} items, exceeding maxItems=${stage.maxItems}`,
|
|
1001
1334
|
};
|
|
1002
1335
|
}
|
|
1003
|
-
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) };
|
|
1004
1361
|
}
|
|
1005
1362
|
|
|
1006
1363
|
async function launchPendingTaskAt(
|
|
@@ -2813,14 +3170,17 @@ async function readCompiledWorkflow(
|
|
|
2813
3170
|
return readJson<CompiledWorkflow>(compiledWorkflowPath(cwd, runId));
|
|
2814
3171
|
}
|
|
2815
3172
|
|
|
2816
|
-
function formatIndex(
|
|
2817
|
-
|
|
2818
|
-
|
|
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) => {
|
|
2819
3179
|
const lines = [
|
|
2820
3180
|
`${run.runId} [${run.status}] type=${run.type} updated=${run.updatedAt}`,
|
|
2821
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}`,
|
|
2822
3182
|
];
|
|
2823
|
-
for (const task of run
|
|
3183
|
+
for (const task of await indexTasksForStatus(cwd, run)) {
|
|
2824
3184
|
const message = task.lastMessage ? ` — ${task.lastMessage}` : "";
|
|
2825
3185
|
const kind = task.kind && task.kind !== "main" ? ` ${task.kind}` : "";
|
|
2826
3186
|
lines.push(
|
|
@@ -2828,8 +3188,34 @@ function formatIndex(index: WorkflowIndexRecord): string {
|
|
|
2828
3188
|
);
|
|
2829
3189
|
}
|
|
2830
3190
|
return lines.join("\n");
|
|
2831
|
-
})
|
|
2832
|
-
|
|
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
|
+
);
|
|
2833
3219
|
}
|
|
2834
3220
|
|
|
2835
3221
|
function formatTask(
|
package/src/extension.ts
CHANGED
|
@@ -114,7 +114,7 @@ const WORKFLOW_DYNAMIC_TOOL_PARAMETERS = {
|
|
|
114
114
|
|
|
115
115
|
export default function workflowExtension(pi: ExtensionAPI): void {
|
|
116
116
|
let workflowCompletionCache: Array<{ name: string }> = [];
|
|
117
|
-
pi.on("session_start", async (
|
|
117
|
+
pi.on("session_start", async (event, ctx) => {
|
|
118
118
|
if (!isWorkflowSupervisorEnabled()) return;
|
|
119
119
|
workflowCompletionCache = await listWorkflows(ctx.cwd).catch(
|
|
120
120
|
() => workflowCompletionCache,
|
|
@@ -125,7 +125,8 @@ export default function workflowExtension(pi: ExtensionAPI): void {
|
|
|
125
125
|
await notifyUnfinishedRuns(ctx.cwd, (message, type) =>
|
|
126
126
|
ctx.ui.notify(message, type),
|
|
127
127
|
).catch(() => undefined);
|
|
128
|
-
|
|
128
|
+
if (event.reason !== "reload")
|
|
129
|
+
await deliverMissedWorkflowFeedback(ctx, pi).catch(() => undefined);
|
|
129
130
|
});
|
|
130
131
|
|
|
131
132
|
registerWorkflowNaturalLanguageTools(pi);
|
package/src/index.ts
CHANGED
|
@@ -53,6 +53,55 @@ export type {
|
|
|
53
53
|
DynamicDecisionLoopRunResult,
|
|
54
54
|
RunDynamicDecisionLoopOptions,
|
|
55
55
|
} from "./dynamic-decision-loop.js";
|
|
56
|
+
export {
|
|
57
|
+
assertValidDynamicDecision,
|
|
58
|
+
validateDynamicDecision,
|
|
59
|
+
} from "./dynamic-decision.js";
|
|
60
|
+
export type {
|
|
61
|
+
DynamicDecisionAction,
|
|
62
|
+
DynamicDecisionPhase,
|
|
63
|
+
DynamicDecisionStatus,
|
|
64
|
+
DynamicDecisionValidationContext,
|
|
65
|
+
DynamicDecisionValidationResult,
|
|
66
|
+
NormalizedDynamicDecision,
|
|
67
|
+
} from "./dynamic-decision.js";
|
|
68
|
+
export { dynamicOutputProfileValues } from "./dynamic-profiles.js";
|
|
69
|
+
export type { DynamicOutputProfile } from "./dynamic-profiles.js";
|
|
70
|
+
export {
|
|
71
|
+
buildWorkflowRunMetrics,
|
|
72
|
+
WORKFLOW_METRICS_PRICING_MODEL_VERSION,
|
|
73
|
+
WORKFLOW_METRICS_SCHEMA_VERSION,
|
|
74
|
+
} from "./workflow-metrics.js";
|
|
75
|
+
export {
|
|
76
|
+
VERIFICATION_STATUS,
|
|
77
|
+
VERIFICATION_STATUS_BUCKETS,
|
|
78
|
+
VERIFICATION_STATUS_LABELS,
|
|
79
|
+
VERIFICATION_STATUS_VALUES,
|
|
80
|
+
canonicalVerificationStatus,
|
|
81
|
+
isNonVerifiedTerminalStatus,
|
|
82
|
+
isVerificationBlockedStatus,
|
|
83
|
+
isVerifiedStatus,
|
|
84
|
+
verificationStatusBucket,
|
|
85
|
+
} from "./verification-ontology.js";
|
|
86
|
+
export type {
|
|
87
|
+
TerminalVerificationStatus,
|
|
88
|
+
VerificationStatus,
|
|
89
|
+
} from "./verification-ontology.js";
|
|
90
|
+
export type {
|
|
91
|
+
WorkflowLaunchTimingMetrics,
|
|
92
|
+
WorkflowMetricValue,
|
|
93
|
+
WorkflowMetricsPricingModelVersion,
|
|
94
|
+
WorkflowMetricsPricingSource,
|
|
95
|
+
WorkflowMetricsSchemaVersion,
|
|
96
|
+
WorkflowRetryMetrics,
|
|
97
|
+
WorkflowRunMetrics,
|
|
98
|
+
WorkflowRunMetricsMetadata,
|
|
99
|
+
WorkflowRunMetricsRollup,
|
|
100
|
+
WorkflowStageMetrics,
|
|
101
|
+
WorkflowTaskMetrics,
|
|
102
|
+
WorkflowTaskStatusCounts,
|
|
103
|
+
WorkflowUsageMetrics,
|
|
104
|
+
} from "./workflow-metrics.js";
|
|
56
105
|
|
|
57
106
|
export const WORKFLOW_COMMAND = "workflow";
|
|
58
107
|
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize JSON embedded directly in prompts/model context.
|
|
3
|
+
*
|
|
4
|
+
* Persisted artifacts can stay pretty-printed for humans, but prompt context
|
|
5
|
+
* should avoid indentation bytes when the JSON data is otherwise identical.
|
|
6
|
+
*/
|
|
7
|
+
export function stringifyPromptJson(value: unknown): string {
|
|
8
|
+
const serialized = JSON.stringify(value);
|
|
9
|
+
if (serialized === undefined) {
|
|
10
|
+
throw new TypeError("prompt JSON value must be JSON-serializable");
|
|
11
|
+
}
|
|
12
|
+
return serialized;
|
|
13
|
+
}
|