@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.
Files changed (119) 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 +52 -19
  6. package/dist/dynamic-generated-task-runtime.js +3 -1
  7. package/dist/dynamic-profiles.d.ts +1 -1
  8. package/dist/engine-run-graph.d.ts +3 -0
  9. package/dist/engine-run-graph.js +194 -4
  10. package/dist/engine.d.ts +5 -0
  11. package/dist/engine.js +389 -41
  12. package/dist/extension.d.ts +2 -1
  13. package/dist/extension.js +30 -8
  14. package/dist/index.d.ts +11 -3
  15. package/dist/index.js +6 -1
  16. package/dist/prompt-json.d.ts +7 -0
  17. package/dist/prompt-json.js +13 -0
  18. package/dist/roles.d.ts +1 -1
  19. package/dist/roles.js +5 -8
  20. package/dist/store.d.ts +20 -1
  21. package/dist/store.js +139 -35
  22. package/dist/strings.d.ts +11 -0
  23. package/dist/strings.js +24 -0
  24. package/dist/subagent-backend.js +710 -40
  25. package/dist/types.d.ts +107 -1
  26. package/dist/verification-ontology.d.ts +31 -0
  27. package/dist/verification-ontology.js +66 -0
  28. package/dist/workflow-artifact-tool.js +5 -6
  29. package/dist/workflow-artifacts.d.ts +7 -0
  30. package/dist/workflow-artifacts.js +55 -4
  31. package/dist/workflow-fetch-cache-extension.d.ts +1 -0
  32. package/dist/workflow-fetch-cache-extension.js +57 -9
  33. package/dist/workflow-metrics.d.ts +113 -0
  34. package/dist/workflow-metrics.js +272 -0
  35. package/dist/workflow-output-artifacts.js +5 -3
  36. package/dist/workflow-partial-output.d.ts +45 -0
  37. package/dist/workflow-partial-output.js +205 -0
  38. package/dist/workflow-progress-health.js +42 -10
  39. package/dist/workflow-runtime.js +10 -1
  40. package/dist/workflow-view.js +3 -1
  41. package/dist/workflow-web-source-extension.js +194 -52
  42. package/dist/workflow-web-source.d.ts +2 -1
  43. package/dist/workflow-web-source.js +109 -30
  44. package/docs/usage.md +76 -29
  45. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  46. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  47. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  48. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  49. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  50. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  51. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  52. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  53. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  54. package/node_modules/@agwab/pi-subagent/src/index.ts +1046 -576
  55. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  56. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  57. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  58. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  59. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  60. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  61. package/node_modules/@agwab/pi-subagent/src/panel.ts +1356 -560
  62. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  63. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  64. package/package.json +2 -2
  65. package/skills/workflow-guide/SKILL.md +1 -0
  66. package/src/artifact-graph-runtime.ts +19 -13
  67. package/src/artifact-graph-schema.ts +143 -3
  68. package/src/cli.mjs +52 -0
  69. package/src/compiler.ts +63 -18
  70. package/src/dynamic-generated-task-runtime.ts +3 -1
  71. package/src/dynamic-profiles.ts +1 -1
  72. package/src/engine-run-graph.ts +246 -4
  73. package/src/engine.ts +545 -38
  74. package/src/extension.ts +36 -6
  75. package/src/index.ts +52 -1
  76. package/src/prompt-json.ts +13 -0
  77. package/src/roles.ts +6 -9
  78. package/src/store.ts +194 -42
  79. package/src/strings.ts +38 -0
  80. package/src/subagent-backend.ts +921 -62
  81. package/src/types.ts +116 -2
  82. package/src/verification-ontology.ts +88 -0
  83. package/src/workflow-artifact-tool.ts +5 -7
  84. package/src/workflow-artifacts.ts +83 -3
  85. package/src/workflow-fetch-cache-extension.ts +78 -13
  86. package/src/workflow-metrics.ts +478 -0
  87. package/src/workflow-output-artifacts.ts +5 -3
  88. package/src/workflow-partial-output.ts +299 -0
  89. package/src/workflow-progress-health.ts +47 -15
  90. package/src/workflow-runtime.ts +18 -2
  91. package/src/workflow-view.ts +2 -1
  92. package/src/workflow-web-source-extension.ts +654 -232
  93. package/src/workflow-web-source.ts +153 -39
  94. package/workflows/README.md +7 -25
  95. package/workflows/deep-research/batched-verification.spec.json +253 -0
  96. package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
  97. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +229 -36
  98. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  99. package/workflows/deep-research/helpers/normalize-input-packet.mjs +81 -2
  100. package/workflows/deep-research/helpers/render-executive.mjs +40 -26
  101. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  102. package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
  103. package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
  104. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -3
  105. package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
  106. package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
  107. package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
  108. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +13 -3
  109. package/workflows/deep-research/spec.json +32 -12
  110. package/workflows/impact-review/spec.json +3 -3
  111. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  112. package/dist/dynamic-loader.d.ts +0 -25
  113. package/dist/dynamic-loader.js +0 -13
  114. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
  115. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
  116. package/src/dynamic-loader.ts +0 -49
  117. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  118. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  119. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
package/src/compiler.ts CHANGED
@@ -3,6 +3,8 @@ import { dirname, resolve } from "node:path";
3
3
 
4
4
  import { loadAgentByName } from "./agents.js";
5
5
  import { DYNAMIC_OUTPUT_PROFILES } from "./dynamic-profiles.js";
6
+ import { stringifyPromptJson } from "./prompt-json.js";
7
+ import { compileRole } from "./roles.js";
6
8
  import {
7
9
  classifyToolCapability,
8
10
  effectiveToolClassification,
@@ -178,7 +180,13 @@ function lowerArtifactGraphFrom(from: ArtifactGraphStageSpec["from"]): unknown {
178
180
  !Array.isArray(from) &&
179
181
  typeof from.source === "string"
180
182
  ) {
181
- return { stage: from.source, path: from.path };
183
+ return {
184
+ stage: from.source,
185
+ path: from.path,
186
+ ...((from as { streaming?: unknown }).streaming !== undefined
187
+ ? { streaming: (from as { streaming?: unknown }).streaming }
188
+ : {}),
189
+ };
182
190
  }
183
191
  return from;
184
192
  }
@@ -209,11 +217,26 @@ function appendWorkflowOutputInstructions(
209
217
  : "Use schema `stage-control-v1` unless the workflow asks for a more specific control schema.",
210
218
  "Put detailed prose, reasoning, and evidence discussion in <analysis> only.",
211
219
  "Put structured evidence pointers in <refs> as a JSON array; use [] if none.",
220
+ ...partialOutputInstructions(stage.output?.partial?.paths),
212
221
  ]
213
222
  .filter(Boolean)
214
223
  .join("\n\n");
215
224
  }
216
225
 
226
+ function partialOutputInstructions(
227
+ paths: readonly string[] | undefined,
228
+ ): string[] {
229
+ if (!paths || paths.length === 0) return [];
230
+ return [
231
+ "# Workflow Partial Output Protocol (optional)",
232
+ `If a complete stable array item is ready before your final answer for one of these control paths (${paths.join(", ")}), you may emit a partial-control section before the final output:`,
233
+ '<partial-control>{"schema":"workflow-partial-output-v1","path":"$.items","items":[{"id":"stable-id","...":"..."}]}</partial-control>',
234
+ "Use the actual declared path, not the example path, and include only items that are final/stable enough to appear unchanged in your final <control> at that path.",
235
+ "Every partial item must be the exact JSON object that will appear in the final array and must include a stable non-empty string `id`; never revise or withdraw a published partial item.",
236
+ "If an item might change, do not publish it partially; wait for the final workflow output. The final answer must still include the normal <control>, <analysis>, and <refs> sections exactly once.",
237
+ ];
238
+ }
239
+
217
240
  function artifactGraphTaskMetadata(
218
241
  stage: ArtifactGraphStageSpec,
219
242
  specDir: string,
@@ -230,8 +253,12 @@ function artifactGraphTaskMetadata(
230
253
  ? resolve(specDir, controlSchema)
231
254
  : undefined,
232
255
  maxDigestChars: stage.output?.maxDigestChars,
256
+ partial: stage.output?.partial
257
+ ? { paths: [...stage.output.partial.paths] }
258
+ : undefined,
233
259
  },
234
260
  requiredReads: stage.inputPolicy?.requiredReads ?? [],
261
+ artifactAccess: stage.inputPolicy?.artifactAccess ?? "enabled",
235
262
  sourceProjection: stage.sourceProjection,
236
263
  };
237
264
  }
@@ -663,15 +690,19 @@ async function compileArtifactGraphPlan(
663
690
  return defaultAgent;
664
691
  };
665
692
  const roleEntries = Object.entries(spec.roles ?? {});
666
- const roles = roleEntries.map(([name, role]: [string, any]) => ({
667
- name,
668
- fromAgent: role.fromAgent,
669
- content: role.prompt ?? "",
670
- maxChars: role.maxChars ?? 8000,
671
- truncated: false,
672
- includedSections: [],
673
- excludedSections: [],
674
- }));
693
+ const roles = await Promise.all(
694
+ roleEntries.map(async ([name, role]: [string, any]) => {
695
+ const sourceAgent = role.fromAgent
696
+ ? await loadWorkflowAgent(
697
+ role.fromAgent,
698
+ options.cwd,
699
+ agentCache,
700
+ `$.roles.${name}.fromAgent`,
701
+ )
702
+ : undefined;
703
+ return compileRole(name, role, sourceAgent);
704
+ }),
705
+ );
675
706
  const roleText = roles.length
676
707
  ? `# Role Context\n\n${roles.map((r) => `## Role: ${r.name}\n${r.content}`).join("\n\n")}`
677
708
  : "";
@@ -681,7 +712,7 @@ async function compileArtifactGraphPlan(
681
712
  typeof workflowInput === "object" &&
682
713
  !Array.isArray(workflowInput) &&
683
714
  Object.keys(workflowInput).length > 0
684
- ? `# Workflow Input\n\n${JSON.stringify(workflowInput, null, 2)}`
715
+ ? `# Workflow Input\n\n${stringifyPromptJson(workflowInput)}`
685
716
  : "";
686
717
  const runtimeOverrides = options.runtimeOverrides;
687
718
  const runtimeDefaults = options.runtimeDefaults;
@@ -831,15 +862,29 @@ async function compileArtifactGraphPlan(
831
862
  /\$\{item\}/g,
832
863
  "the relevant item from the dependency context",
833
864
  );
834
- const compiledPrompt = [
865
+ const instructionText = `# Instructions\n\n${normalizedPrompt}`;
866
+ const stageText = `# Workflow Stage\n\nstage=${stage.id}\ntype=${runtimeStageKind}`;
867
+ const taskText =
835
868
  injectRuntimeTaskInPrompt && options.task
836
869
  ? `# Task\n\n${options.task}`
837
- : undefined,
838
- workflowInputText || undefined,
839
- `# Workflow Stage\n\nstage=${stage.id}\ntype=${runtimeStageKind}`,
840
- `# Instructions\n\n${normalizedPrompt}`,
841
- roleText || undefined,
842
- ]
870
+ : undefined;
871
+ const compiledPrompt = (
872
+ runtimeStageKind === "foreach"
873
+ ? [
874
+ taskText,
875
+ workflowInputText || undefined,
876
+ stageText,
877
+ roleText || undefined,
878
+ instructionText,
879
+ ]
880
+ : [
881
+ taskText,
882
+ workflowInputText || undefined,
883
+ stageText,
884
+ instructionText,
885
+ roleText || undefined,
886
+ ]
887
+ )
843
888
  .filter(Boolean)
844
889
  .join("\n\n");
845
890
  const toolSelection = resolveToolSelection(
@@ -7,6 +7,7 @@ import {
7
7
  } from "./dynamic-profiles.js";
8
8
  import { readOrRebuildDynamicState } from "./dynamic-state.js";
9
9
  import { sanitizeTaskId } from "./engine-run-graph.js";
10
+ import { compactStrings } from "./strings.js";
10
11
  import { fromProjectPath, isTerminalTaskStatus, readJson } from "./store.js";
11
12
  import {
12
13
  classifyToolCapability,
@@ -307,6 +308,7 @@ export async function buildDynamicGeneratedCompiledTask(input: {
307
308
  maxDigestChars: DYNAMIC_OUTPUT_MAX_DIGEST_CHARS,
308
309
  },
309
310
  requiredReads: input.request.requiredReads,
311
+ artifactAccess: "enabled",
310
312
  sourceProjection: dynamicInputSourceProjection(input.request.inputs),
311
313
  },
312
314
  dynamicGenerated: {
@@ -966,5 +968,5 @@ function dynamicOutputProfileInstructions(
966
968
  }
967
969
 
968
970
  function uniqueStrings(values: readonly string[]): string[] {
969
- return [...new Set(values.filter((value) => value.trim().length > 0))];
971
+ return compactStrings(values, { trim: false, dropWhitespaceOnly: true });
970
972
  }
@@ -45,6 +45,6 @@ export function isTerminalDynamicOutputProfile(
45
45
  return typeof value === "string" && TERMINAL_OUTPUT_PROFILE_SET.has(value);
46
46
  }
47
47
 
48
- export function dynamicOutputProfileValues(): string[] {
48
+ export function dynamicOutputProfileValues(): DynamicOutputProfile[] {
49
49
  return [...DYNAMIC_OUTPUT_PROFILES];
50
50
  }
@@ -127,6 +127,151 @@ export function reconcileDynamicGeneratedRunRecords(
127
127
  return changed;
128
128
  }
129
129
 
130
+ export function reconcileForeachGeneratedRunRecords(
131
+ cwd: string,
132
+ run: WorkflowRunRecord,
133
+ compiledFlow: CompiledWorkflow,
134
+ ): boolean {
135
+ let changed = false;
136
+ const compiledSpecIds = new Set(
137
+ compiledFlow.tasks.map((task) => compiledTaskSpecId(task)),
138
+ );
139
+ const compiledTaskBySpecId = new Map(
140
+ compiledFlow.tasks.map((task) => [compiledTaskSpecId(task), task]),
141
+ );
142
+ const placeholderToGeneratedSpecIds = new Map<string, string[]>();
143
+ const streamingPlaceholderSpecIds = new Set<string>();
144
+
145
+ for (const compiledTask of compiledFlow.tasks) {
146
+ const specId = compiledTaskSpecId(compiledTask);
147
+ if (compiledTask.foreach && foreachStreamingEnabled(compiledTask)) {
148
+ streamingPlaceholderSpecIds.add(specId);
149
+ }
150
+ const placeholderSpecId = foreachGeneratedPlaceholderSpecId(
151
+ compiledTask,
152
+ compiledFlow,
153
+ specId,
154
+ );
155
+ if (!placeholderSpecId) continue;
156
+ if (
157
+ compiledTask.foreachGenerated?.placeholderSpecId !== placeholderSpecId
158
+ ) {
159
+ compiledTask.foreachGenerated = { placeholderSpecId };
160
+ changed = true;
161
+ }
162
+ const generated =
163
+ placeholderToGeneratedSpecIds.get(placeholderSpecId) ?? [];
164
+ generated.push(specId);
165
+ placeholderToGeneratedSpecIds.set(placeholderSpecId, generated);
166
+ }
167
+
168
+ if (placeholderToGeneratedSpecIds.size === 0) return changed;
169
+
170
+ const filteredRunTasks: WorkflowTaskRunRecord[] = [];
171
+ const seenGeneratedSpecIds = new Set<string>();
172
+ for (const task of run.tasks) {
173
+ const generatedSpecIds = placeholderToGeneratedSpecIds.get(task.specId);
174
+ let placeholderSpecId = foreachGeneratedPlaceholderSpecId(
175
+ task,
176
+ compiledFlow,
177
+ task.specId,
178
+ );
179
+ if (generatedSpecIds && !placeholderSpecId) {
180
+ const compiledTask = compiledTaskBySpecId.get(task.specId);
181
+ if (
182
+ compiledTask?.foreach &&
183
+ streamingPlaceholderSpecIds.has(task.specId)
184
+ ) {
185
+ filteredRunTasks.push(task);
186
+ continue;
187
+ }
188
+ if (generatedSpecIds.includes(task.specId)) {
189
+ placeholderSpecId = task.specId;
190
+ task.foreachGenerated = { placeholderSpecId };
191
+ changed = true;
192
+ } else {
193
+ changed = true;
194
+ continue;
195
+ }
196
+ }
197
+ if (placeholderSpecId && !compiledSpecIds.has(task.specId)) {
198
+ changed = true;
199
+ continue;
200
+ }
201
+ if (placeholderSpecId && seenGeneratedSpecIds.has(task.specId)) {
202
+ changed = true;
203
+ continue;
204
+ }
205
+ if (placeholderSpecId) {
206
+ seenGeneratedSpecIds.add(task.specId);
207
+ if (task.foreachGenerated?.placeholderSpecId !== placeholderSpecId) {
208
+ task.foreachGenerated = { placeholderSpecId };
209
+ changed = true;
210
+ }
211
+ }
212
+ filteredRunTasks.push(task);
213
+ }
214
+
215
+ const runTaskBySpecId = new Map<string, WorkflowTaskRunRecord>();
216
+ for (const task of filteredRunTasks) {
217
+ if (!runTaskBySpecId.has(task.specId))
218
+ runTaskBySpecId.set(task.specId, task);
219
+ }
220
+
221
+ const reordered: WorkflowTaskRunRecord[] = [];
222
+ const usedSpecIds = new Set<string>();
223
+ let nextIndex = nextTaskRecordIndex({ ...run, tasks: filteredRunTasks });
224
+ for (const compiledTask of compiledFlow.tasks) {
225
+ const specId = compiledTaskSpecId(compiledTask);
226
+ const existing = runTaskBySpecId.get(specId);
227
+ if (existing) {
228
+ const placeholderSpecId =
229
+ compiledTask.foreachGenerated?.placeholderSpecId;
230
+ if (
231
+ placeholderSpecId &&
232
+ existing.foreachGenerated?.placeholderSpecId !== placeholderSpecId
233
+ ) {
234
+ existing.foreachGenerated = { placeholderSpecId };
235
+ changed = true;
236
+ }
237
+ reordered.push(existing);
238
+ usedSpecIds.add(specId);
239
+ continue;
240
+ }
241
+ if (!compiledTask.foreachGenerated) continue;
242
+ const created = createTaskRunRecord(
243
+ cwd,
244
+ run.runId,
245
+ compiledTask,
246
+ nextIndex,
247
+ );
248
+ nextIndex += 1;
249
+ reordered.push(created);
250
+ usedSpecIds.add(specId);
251
+ changed = true;
252
+ }
253
+
254
+ for (const task of filteredRunTasks) {
255
+ if (!usedSpecIds.has(task.specId)) reordered.push(task);
256
+ }
257
+
258
+ if (!sameTaskRecordOrder(run.tasks, reordered)) changed = true;
259
+ for (const task of reordered) {
260
+ if (!task.dependsOn) continue;
261
+ const replaced = replaceForeachGeneratedDependencies(
262
+ task.dependsOn,
263
+ placeholderToGeneratedSpecIds,
264
+ streamingPlaceholderSpecIds,
265
+ );
266
+ if (!sameStringList(task.dependsOn, replaced)) {
267
+ task.dependsOn = replaced;
268
+ changed = true;
269
+ }
270
+ }
271
+ if (changed) run.tasks = reordered;
272
+ return changed;
273
+ }
274
+
130
275
  export function assertRunTaskPositionalAlignment(
131
276
  run: WorkflowRunRecord,
132
277
  compiledFlow: CompiledWorkflow,
@@ -209,6 +354,57 @@ export function compiledTaskSpecId(task: CompiledTask): string {
209
354
  return typeof specId === "string" && specId.trim() !== "" ? specId : task.id;
210
355
  }
211
356
 
357
+ function foreachGeneratedPlaceholderSpecId(
358
+ task: CompiledTask | WorkflowTaskRunRecord,
359
+ compiledFlow: CompiledWorkflow,
360
+ specId: string,
361
+ ): string | undefined {
362
+ const explicit = task.foreachGenerated?.placeholderSpecId;
363
+ if (typeof explicit === "string" && explicit.trim() !== "") return explicit;
364
+ if ((task as CompiledTask).foreach) return undefined;
365
+ if (task.kind !== "foreach" || !task.stageId) return undefined;
366
+ const placeholderSpecId = foreachPlaceholderSpecId(
367
+ compiledFlow,
368
+ task.stageId,
369
+ );
370
+ if (!placeholderSpecId || specId === placeholderSpecId) return undefined;
371
+ return placeholderSpecId;
372
+ }
373
+
374
+ function foreachPlaceholderSpecId(
375
+ compiledFlow: CompiledWorkflow,
376
+ stageId: string,
377
+ ): string | undefined {
378
+ const stage = ((compiledFlow as any).stages ?? []).find(
379
+ (candidate: any) => candidate?.id === stageId,
380
+ );
381
+ if (stage?.type !== "foreach") return undefined;
382
+ return `${stageId}.item`;
383
+ }
384
+
385
+ function replaceForeachGeneratedDependencies(
386
+ dependsOn: string[],
387
+ placeholderToGeneratedSpecIds: Map<string, string[]>,
388
+ keepPlaceholderSpecIds = new Set<string>(),
389
+ ): string[] {
390
+ const replaced: string[] = [];
391
+ for (const dep of dependsOn) {
392
+ const generatedSpecIds = placeholderToGeneratedSpecIds.get(dep);
393
+ if (generatedSpecIds) {
394
+ if (keepPlaceholderSpecIds.has(dep)) replaced.push(dep);
395
+ replaced.push(...generatedSpecIds);
396
+ } else replaced.push(dep);
397
+ }
398
+ return [...new Set(replaced)];
399
+ }
400
+
401
+ function sameStringList(left: string[], right: string[]): boolean {
402
+ return (
403
+ left.length === right.length &&
404
+ left.every((value, index) => value === right[index])
405
+ );
406
+ }
407
+
212
408
  function isLoopGeneratedCompiledTask(
213
409
  task: CompiledTask,
214
410
  loopIds: Set<string>,
@@ -303,6 +499,29 @@ export function dependenciesReady(
303
499
  if (deps.length === 0) return true;
304
500
  const partial =
305
501
  stageSourcePolicy(compiledFlow, compiledTask.stageId ?? "") === "partial";
502
+ if (foreachStreamingEnabled(compiledTask)) {
503
+ let completedDependencyReady = false;
504
+ let runningDependencyMayHavePartialItems = false;
505
+ let allKnownDependenciesTerminal = true;
506
+ for (const dep of deps) {
507
+ const status = bySpecId.get(dep)?.status;
508
+ if (status === "completed") {
509
+ completedDependencyReady = true;
510
+ continue;
511
+ }
512
+ if (status && isTerminalTaskStatus(status)) {
513
+ if (!partial) return false;
514
+ continue;
515
+ }
516
+ if (status === "running") runningDependencyMayHavePartialItems = true;
517
+ allKnownDependenciesTerminal = false;
518
+ }
519
+ return (
520
+ completedDependencyReady ||
521
+ runningDependencyMayHavePartialItems ||
522
+ allKnownDependenciesTerminal
523
+ );
524
+ }
306
525
  return deps.every((dep) => {
307
526
  const status = bySpecId.get(dep)?.status;
308
527
  if (status === "completed") return true;
@@ -311,6 +530,22 @@ export function dependenciesReady(
311
530
  });
312
531
  }
313
532
 
533
+ export function foreachStreamingEnabled(compiledTask: CompiledTask): boolean {
534
+ const streaming = (compiledTask.foreach?.from as any)?.streaming;
535
+ return Boolean(
536
+ streaming &&
537
+ typeof streaming === "object" &&
538
+ (streaming as { enabled?: unknown }).enabled === true,
539
+ );
540
+ }
541
+
542
+ export function foreachStreamingMinChunk(compiledTask: CompiledTask): number {
543
+ const value = (compiledTask.foreach?.from as any)?.streaming?.minChunk;
544
+ return typeof value === "number" && Number.isFinite(value) && value > 0
545
+ ? Math.floor(value)
546
+ : 1;
547
+ }
548
+
314
549
  export function buildForeachGeneratedTasks(
315
550
  template: CompiledTask,
316
551
  runtimeTask: string | undefined,
@@ -330,15 +565,16 @@ export function buildForeachGeneratedTasks(
330
565
  const itemText = formatForeachItem(item);
331
566
  const instructions = template.foreach!.prompt.replace(
332
567
  /\$\{item\}/g,
333
- itemText,
568
+ escapeReplacementText(itemText),
334
569
  );
335
570
  const compiledPrompt = [
336
571
  template.foreach!.injectRuntimeTask && runtimeTask
337
572
  ? `# Task\n\n${runtimeTask}`
338
573
  : undefined,
339
- `# Workflow Stage\n\nstage=${template.stageId}\ntype=foreach\nitem=${taskId}`,
340
- `# Instructions\n\n${instructions}`,
574
+ `# Workflow Stage\n\nstage=${template.stageId}\ntype=foreach`,
341
575
  template.foreach!.roleText || undefined,
576
+ `# Workflow Item\n\nitem=${taskId}`,
577
+ `# Instructions\n\n${instructions}`,
342
578
  ]
343
579
  .filter(Boolean)
344
580
  .join("\n\n");
@@ -352,6 +588,7 @@ export function buildForeachGeneratedTasks(
352
588
  compiledPrompt,
353
589
  dependsOn: [...(template.dependsOn ?? [])],
354
590
  foreach: undefined,
591
+ foreachGenerated: { placeholderSpecId: template.id },
355
592
  } as CompiledTask);
356
593
  }
357
594
  return { tasks };
@@ -382,6 +619,10 @@ function formatForeachItem(item: unknown): string {
382
619
  return typeof item === "string" ? item : JSON.stringify(item);
383
620
  }
384
621
 
622
+ function escapeReplacementText(value: string): string {
623
+ return value.replace(/\$/g, "$$$$");
624
+ }
625
+
385
626
  export function sourceStageIdsForFrom(from: unknown): string[] {
386
627
  if (Array.isArray(from))
387
628
  return from.filter((item): item is string => typeof item === "string");
@@ -453,7 +694,8 @@ export function markDagDependentsSkipped(
453
694
  return (
454
695
  status === "failed" ||
455
696
  status === "interrupted" ||
456
- status === "skipped"
697
+ status === "skipped" ||
698
+ status === "blocked"
457
699
  );
458
700
  });
459
701
  if (!failedDep) continue;