@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.
Files changed (90) hide show
  1. package/README.md +3 -1
  2. package/dist/artifact-graph-runtime.d.ts +1 -1
  3. package/dist/artifact-graph-runtime.js +10 -5
  4. package/dist/artifact-graph-schema.js +127 -5
  5. package/dist/compiler.js +46 -11
  6. package/dist/dynamic-decision.d.ts +1 -0
  7. package/dist/dynamic-decision.js +7 -0
  8. package/dist/dynamic-generated-task-runtime.js +3 -1
  9. package/dist/dynamic-profiles.d.ts +1 -0
  10. package/dist/dynamic-profiles.js +3 -0
  11. package/dist/engine-run-graph.d.ts +2 -0
  12. package/dist/engine-run-graph.js +55 -5
  13. package/dist/engine.js +278 -15
  14. package/dist/extension.js +3 -2
  15. package/dist/index.d.ts +8 -0
  16. package/dist/index.js +4 -0
  17. package/dist/prompt-json.d.ts +7 -0
  18. package/dist/prompt-json.js +13 -0
  19. package/dist/roles.d.ts +1 -1
  20. package/dist/roles.js +5 -8
  21. package/dist/store.d.ts +20 -1
  22. package/dist/store.js +89 -29
  23. package/dist/strings.d.ts +11 -0
  24. package/dist/strings.js +24 -0
  25. package/dist/subagent-backend.js +557 -13
  26. package/dist/types.d.ts +101 -1
  27. package/dist/verification-ontology.d.ts +31 -0
  28. package/dist/verification-ontology.js +66 -0
  29. package/dist/workflow-artifact-tool.js +5 -6
  30. package/dist/workflow-artifacts.d.ts +7 -0
  31. package/dist/workflow-artifacts.js +55 -4
  32. package/dist/workflow-fetch-cache-extension.d.ts +1 -0
  33. package/dist/workflow-fetch-cache-extension.js +57 -9
  34. package/dist/workflow-metrics.d.ts +113 -0
  35. package/dist/workflow-metrics.js +272 -0
  36. package/dist/workflow-output-artifacts.js +5 -3
  37. package/dist/workflow-partial-output.d.ts +45 -0
  38. package/dist/workflow-partial-output.js +205 -0
  39. package/dist/workflow-progress-health.js +42 -10
  40. package/dist/workflow-web-source-extension.js +27 -4
  41. package/dist/workflow-web-source.js +26 -12
  42. package/docs/usage.md +76 -29
  43. package/node_modules/@agwab/pi-subagent/package.json +1 -1
  44. package/node_modules/@agwab/pi-subagent/src/index.ts +53 -5
  45. package/node_modules/@agwab/pi-subagent/src/panel.ts +7 -3
  46. package/package.json +2 -2
  47. package/skills/workflow-guide/SKILL.md +1 -0
  48. package/src/artifact-graph-runtime.ts +19 -13
  49. package/src/artifact-graph-schema.ts +143 -3
  50. package/src/cli.mjs +52 -0
  51. package/src/compiler.ts +49 -9
  52. package/src/dynamic-decision.ts +11 -0
  53. package/src/dynamic-generated-task-runtime.ts +3 -1
  54. package/src/dynamic-profiles.ts +4 -0
  55. package/src/engine-run-graph.ts +63 -4
  56. package/src/engine.ts +400 -14
  57. package/src/extension.ts +3 -2
  58. package/src/index.ts +49 -0
  59. package/src/prompt-json.ts +13 -0
  60. package/src/roles.ts +6 -9
  61. package/src/store.ts +123 -34
  62. package/src/strings.ts +38 -0
  63. package/src/subagent-backend.ts +727 -41
  64. package/src/types.ts +110 -2
  65. package/src/verification-ontology.ts +88 -0
  66. package/src/workflow-artifact-tool.ts +5 -7
  67. package/src/workflow-artifacts.ts +83 -3
  68. package/src/workflow-fetch-cache-extension.ts +78 -13
  69. package/src/workflow-metrics.ts +478 -0
  70. package/src/workflow-output-artifacts.ts +5 -3
  71. package/src/workflow-partial-output.ts +299 -0
  72. package/src/workflow-progress-health.ts +47 -15
  73. package/src/workflow-web-source-extension.ts +33 -4
  74. package/src/workflow-web-source.ts +36 -12
  75. package/workflows/README.md +7 -25
  76. package/workflows/deep-research/batched-verification.spec.json +253 -0
  77. package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
  78. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +173 -20
  79. package/workflows/deep-research/helpers/normalize-input-packet.mjs +80 -1
  80. package/workflows/deep-research/helpers/render-executive.mjs +32 -5
  81. package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
  82. package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
  83. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -2
  84. package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
  85. package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
  86. package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
  87. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +10 -3
  88. package/workflows/deep-research/spec.json +32 -12
  89. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
  90. 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: { from: unknown; sourcePolicy?: string; maxItems?: number },
1260
+ stage: {
1261
+ from: unknown;
1262
+ sourcePolicy?: string;
1263
+ maxItems?: number;
1264
+ streaming?: boolean;
1265
+ },
963
1266
  sourceTasks: WorkflowTaskRunRecord[],
964
- ): Promise<{ items?: unknown[]; error?: string }> {
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
- items.push(...value);
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(index: WorkflowIndexRecord): string {
2817
- return index.runs
2818
- .map((run) => {
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.tasks) {
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
- .join("\n\n");
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 (_event, ctx) => {
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
- await deliverMissedWorkflowFeedback(ctx, pi).catch(() => undefined);
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
+ }