@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
@@ -1,6 +1,7 @@
1
1
  import { isAbsolute } from "node:path";
2
2
 
3
3
  import { DYNAMIC_OUTPUT_PROFILES } from "./dynamic-profiles.js";
4
+ import { compactStrings } from "./strings.js";
4
5
  import {
5
6
  APPROVAL_MODES,
6
7
  FAST_MODES,
@@ -71,10 +72,16 @@ const OUTPUT_KEYS = new Set([
71
72
  "analysis",
72
73
  "refs",
73
74
  "maxDigestChars",
75
+ "partial",
74
76
  ]);
77
+ const OUTPUT_PARTIAL_KEYS = new Set(["paths"]);
75
78
  const REQUIRED_FLAG_KEYS = new Set(["required"]);
76
79
  const REFS_OUTPUT_KEYS = new Set(["required", "minItems"]);
77
- const INPUT_POLICY_KEYS = new Set(["requiredReads", "enforcement"]);
80
+ const INPUT_POLICY_KEYS = new Set([
81
+ "requiredReads",
82
+ "enforcement",
83
+ "artifactAccess",
84
+ ]);
78
85
  const SOURCE_PROJECTION_KEYS = new Set(["include", "maxChars"]);
79
86
  const SUPPORT_KEYS = new Set(["uses", "options"]);
80
87
  const DYNAMIC_STAGE_FORBIDDEN_KEYS = new Set([
@@ -185,7 +192,8 @@ const UNTIL_KEYS = new Set([
185
192
  "all",
186
193
  "any",
187
194
  ]);
188
- const FOREACH_FROM_KEYS = new Set(["source", "path"]);
195
+ const FOREACH_FROM_KEYS = new Set(["source", "path", "streaming"]);
196
+ const FOREACH_STREAMING_KEYS = new Set(["enabled", "minChunk"]);
189
197
  const NORMAL_ARTIFACT_KINDS = new Set<WorkflowArtifactKind>([
190
198
  "control",
191
199
  "analysis",
@@ -296,9 +304,54 @@ function validateStageArray(
296
304
  for (const [index, item] of value.entries()) {
297
305
  validateStage(item, `${path}[${index}]`, ids, sourceIds, issues);
298
306
  }
307
+ validateStreamingProducerDeclarations(value, path, issues);
299
308
  validateStageDependencyGraph(value, path, issues);
300
309
  }
301
310
 
311
+ function validateStreamingProducerDeclarations(
312
+ stages: readonly unknown[],
313
+ path: string,
314
+ issues: ValidationIssue[],
315
+ ): void {
316
+ const byId = new Map<string, Record<string, unknown>>();
317
+ for (const stage of stages) {
318
+ if (isRecord(stage) && typeof stage.id === "string") {
319
+ byId.set(stage.id, stage);
320
+ }
321
+ }
322
+ for (const [index, stage] of stages.entries()) {
323
+ if (!isRecord(stage) || !isRecord(stage.from)) continue;
324
+ const from = stage.from;
325
+ if (!isRecord(from.streaming) || from.streaming.enabled !== true) continue;
326
+ const source = typeof from.source === "string" ? from.source : undefined;
327
+ const controlPath = typeof from.path === "string" ? from.path : undefined;
328
+ if (!source || !controlPath) continue;
329
+ const sourceStage = byId.get(source);
330
+ const partialPaths = outputPartialPaths(sourceStage);
331
+ if (!partialPaths.includes(controlPath)) {
332
+ issues.push({
333
+ path: `${path}[${index}].from.streaming`,
334
+ message: `source stage "${source}" must declare output.partial.paths including "${controlPath}" to use streaming`,
335
+ });
336
+ }
337
+ }
338
+ }
339
+
340
+ function outputPartialPaths(
341
+ stage: Record<string, unknown> | undefined,
342
+ ): string[] {
343
+ const output = isRecord(stage?.output) ? stage.output : undefined;
344
+ const partial = isRecord(output?.partial) ? output.partial : undefined;
345
+ return Array.isArray(partial?.paths)
346
+ ? compactStrings(partial.paths, {
347
+ trim: false,
348
+ unique: false,
349
+ dropEmpty: false,
350
+ dropWhitespaceOnly: false,
351
+ })
352
+ : [];
353
+ }
354
+
302
355
  function validateStageDependencyGraph(
303
356
  stages: readonly unknown[],
304
357
  path: string,
@@ -372,7 +425,12 @@ function extractDependencyRefs(value: unknown): string[] {
372
425
  if (value === undefined) return [];
373
426
  if (typeof value === "string") return [value];
374
427
  if (Array.isArray(value))
375
- return value.filter((item): item is string => typeof item === "string");
428
+ return compactStrings(value, {
429
+ trim: false,
430
+ unique: false,
431
+ dropEmpty: false,
432
+ dropWhitespaceOnly: false,
433
+ });
376
434
  if (isRecord(value) && typeof value.source === "string")
377
435
  return [value.source];
378
436
  return [];
@@ -517,6 +575,7 @@ function validateStage(
517
575
  `${path}.inputPolicy`,
518
576
  sourceIds,
519
577
  issues,
578
+ stage.sourceProjection,
520
579
  );
521
580
  validateOutput(stage.output, `${path}.output`, issues);
522
581
  validateSupportStage(stage, type, path, issues);
@@ -611,6 +670,23 @@ function validateControlPathRef(
611
670
  message: "must be a control JSONPath starting with $.",
612
671
  });
613
672
  }
673
+ if (value.streaming !== undefined) {
674
+ validateForeachStreaming(value.streaming, `${path}.streaming`, issues);
675
+ }
676
+ }
677
+
678
+ function validateForeachStreaming(
679
+ value: unknown,
680
+ path: string,
681
+ issues: ValidationIssue[],
682
+ ): void {
683
+ const streaming = recordAt(value, path, issues);
684
+ if (!streaming) return;
685
+ rejectUnknownKeys(streaming, FOREACH_STREAMING_KEYS, path, issues);
686
+ if (streaming.enabled !== true) {
687
+ issues.push({ path: `${path}.enabled`, message: "must be true" });
688
+ }
689
+ optionalPositiveInteger(streaming.minChunk, `${path}.minChunk`, issues);
614
690
  }
615
691
 
616
692
  function validateKnownStageRef(
@@ -649,6 +725,42 @@ function validateOutput(
649
725
  );
650
726
  validateRequiredFlagObject(output.analysis, `${path}.analysis`, issues);
651
727
  validateRefsOutputObject(output.refs, `${path}.refs`, issues);
728
+ validatePartialOutput(output.partial, `${path}.partial`, issues);
729
+ }
730
+
731
+ function validatePartialOutput(
732
+ value: unknown,
733
+ path: string,
734
+ issues: ValidationIssue[],
735
+ ): void {
736
+ if (value === undefined) return;
737
+ const partial = recordAt(value, path, issues);
738
+ if (!partial) return;
739
+ rejectUnknownKeys(partial, OUTPUT_PARTIAL_KEYS, path, issues);
740
+ if (!Array.isArray(partial.paths)) {
741
+ issues.push({ path: `${path}.paths`, message: "must be an array" });
742
+ return;
743
+ }
744
+ if (partial.paths.length === 0) {
745
+ issues.push({ path: `${path}.paths`, message: "must not be empty" });
746
+ }
747
+ const seen = new Set<string>();
748
+ for (const [index, item] of partial.paths.entries()) {
749
+ const itemPath = `${path}.paths[${index}]`;
750
+ if (typeof item !== "string" || item.trim() === "") {
751
+ issues.push({ path: itemPath, message: "must be a non-empty string" });
752
+ continue;
753
+ }
754
+ if (!item.startsWith("$.")) {
755
+ issues.push({
756
+ path: itemPath,
757
+ message: "must be a control JSONPath starting with $.",
758
+ });
759
+ }
760
+ if (seen.has(item))
761
+ issues.push({ path: itemPath, message: `duplicate value "${item}"` });
762
+ seen.add(item);
763
+ }
652
764
  }
653
765
 
654
766
  function validateControlSchemaRef(
@@ -694,6 +806,7 @@ function validateInputPolicy(
694
806
  path: string,
695
807
  siblingIds: ReadonlySet<string>,
696
808
  issues: ValidationIssue[],
809
+ sourceProjection: unknown,
697
810
  ): void {
698
811
  if (value === undefined) return;
699
812
  const policy = recordAt(value, path, issues);
@@ -708,6 +821,33 @@ function validateInputPolicy(
708
821
  if (policy.enforcement !== undefined && policy.enforcement !== "fail") {
709
822
  issues.push({ path: `${path}.enforcement`, message: 'must be "fail"' });
710
823
  }
824
+ if (
825
+ policy.artifactAccess !== undefined &&
826
+ policy.artifactAccess !== "enabled" &&
827
+ policy.artifactAccess !== "none"
828
+ ) {
829
+ issues.push({
830
+ path: `${path}.artifactAccess`,
831
+ message: 'must be "enabled" or "none"',
832
+ });
833
+ }
834
+ if (policy.artifactAccess === "none") {
835
+ if (
836
+ Array.isArray(policy.requiredReads) &&
837
+ policy.requiredReads.length > 0
838
+ ) {
839
+ issues.push({
840
+ path: `${path}.requiredReads`,
841
+ message: 'must be empty when artifactAccess is "none"',
842
+ });
843
+ }
844
+ if (sourceProjection !== undefined) {
845
+ issues.push({
846
+ path: `${path}.artifactAccess`,
847
+ message: 'cannot be "none" when sourceProjection is declared',
848
+ });
849
+ }
850
+ }
711
851
  }
712
852
 
713
853
  function validateRequiredReads(
package/src/cli.mjs CHANGED
@@ -53,6 +53,7 @@ const tasks = Array.isArray(run.tasks) ? run.tasks : [];
53
53
  const selected = options.has("--failures")
54
54
  ? tasks.filter((task) => ["failed", "blocked", "interrupted"].includes(task.status))
55
55
  : tasks;
56
+ const reliability = summarizeReliability(tasks);
56
57
 
57
58
  const lines = [
58
59
  `runId: ${run.runId}`,
@@ -60,6 +61,8 @@ const lines = [
60
61
  `type: ${run.type}`,
61
62
  `status: ${run.status}`,
62
63
  `tasks: ${tasks.length}`,
64
+ `completion: ${reliability.health}`,
65
+ `retries: output=${reliability.outputRetries}, launch=${reliability.launchRetries}, resumes=${reliability.resumeEvents}, contextLimitFailures=${reliability.contextLimitFailures}`,
63
66
  ];
64
67
 
65
68
  for (const task of selected) {
@@ -184,3 +187,52 @@ async function readJson(path) {
184
187
  function indent(text, prefix) {
185
188
  return text.split(/\r?\n/).map((line) => `${prefix}${line}`).join("\n");
186
189
  }
190
+
191
+ function summarizeReliability(tasks) {
192
+ let outputRetries = 0;
193
+ let launchRetries = 0;
194
+ let resumeEvents = 0;
195
+ let contextLimitFailures = 0;
196
+ for (const task of tasks) {
197
+ outputRetries += positiveCount(task.outputRetry?.attempts);
198
+ launchRetries += positiveCount(task.launchRetry?.attempts);
199
+ if (hasContextLimitFailure(task)) contextLimitFailures += 1;
200
+ for (const event of Array.isArray(task.resumeEvents) ? task.resumeEvents : []) {
201
+ resumeEvents += 1;
202
+ outputRetries += positiveCount(event.outputRetryAttempts);
203
+ launchRetries += positiveCount(event.launchRetryAttempts);
204
+ if (hasContextLimitFailure(event)) contextLimitFailures += 1;
205
+ }
206
+ }
207
+ const allCompleted = tasks.length > 0 && tasks.every((task) => task.status === "completed");
208
+ const repairEvents = outputRetries + launchRetries + resumeEvents;
209
+ const health = !allCompleted
210
+ ? "incomplete"
211
+ : repairEvents === 0 && contextLimitFailures === 0
212
+ ? "clean"
213
+ : "repaired";
214
+ return { health, outputRetries, launchRetries, resumeEvents, contextLimitFailures };
215
+ }
216
+
217
+ function positiveCount(value) {
218
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
219
+ }
220
+
221
+ function hasContextLimitFailure(value) {
222
+ return [
223
+ value?.statusDetail,
224
+ value?.fromStatusDetail,
225
+ value?.lastMessage,
226
+ value?.outputRetry?.reason,
227
+ value?.outputRetry?.message,
228
+ value?.launchRetry?.reason,
229
+ value?.launchRetry?.message,
230
+ value?.outputRetryReason,
231
+ value?.launchRetryReason,
232
+ ].some(isContextLimitText);
233
+ }
234
+
235
+ function isContextLimitText(value) {
236
+ const text = String(value ?? "").toLowerCase();
237
+ return text.includes("context_or_request_too_large") || /context (window|length)|maximum context|request too large|token limit/.test(text);
238
+ }
package/src/compiler.ts CHANGED
@@ -3,6 +3,7 @@ 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";
6
7
  import { compileRole } from "./roles.js";
7
8
  import {
8
9
  classifyToolCapability,
@@ -179,7 +180,13 @@ function lowerArtifactGraphFrom(from: ArtifactGraphStageSpec["from"]): unknown {
179
180
  !Array.isArray(from) &&
180
181
  typeof from.source === "string"
181
182
  ) {
182
- 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
+ };
183
190
  }
184
191
  return from;
185
192
  }
@@ -210,11 +217,26 @@ function appendWorkflowOutputInstructions(
210
217
  : "Use schema `stage-control-v1` unless the workflow asks for a more specific control schema.",
211
218
  "Put detailed prose, reasoning, and evidence discussion in <analysis> only.",
212
219
  "Put structured evidence pointers in <refs> as a JSON array; use [] if none.",
220
+ ...partialOutputInstructions(stage.output?.partial?.paths),
213
221
  ]
214
222
  .filter(Boolean)
215
223
  .join("\n\n");
216
224
  }
217
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
+
218
240
  function artifactGraphTaskMetadata(
219
241
  stage: ArtifactGraphStageSpec,
220
242
  specDir: string,
@@ -231,8 +253,12 @@ function artifactGraphTaskMetadata(
231
253
  ? resolve(specDir, controlSchema)
232
254
  : undefined,
233
255
  maxDigestChars: stage.output?.maxDigestChars,
256
+ partial: stage.output?.partial
257
+ ? { paths: [...stage.output.partial.paths] }
258
+ : undefined,
234
259
  },
235
260
  requiredReads: stage.inputPolicy?.requiredReads ?? [],
261
+ artifactAccess: stage.inputPolicy?.artifactAccess ?? "enabled",
236
262
  sourceProjection: stage.sourceProjection,
237
263
  };
238
264
  }
@@ -686,7 +712,7 @@ async function compileArtifactGraphPlan(
686
712
  typeof workflowInput === "object" &&
687
713
  !Array.isArray(workflowInput) &&
688
714
  Object.keys(workflowInput).length > 0
689
- ? `# Workflow Input\n\n${JSON.stringify(workflowInput, null, 2)}`
715
+ ? `# Workflow Input\n\n${stringifyPromptJson(workflowInput)}`
690
716
  : "";
691
717
  const runtimeOverrides = options.runtimeOverrides;
692
718
  const runtimeDefaults = options.runtimeDefaults;
@@ -836,15 +862,29 @@ async function compileArtifactGraphPlan(
836
862
  /\$\{item\}/g,
837
863
  "the relevant item from the dependency context",
838
864
  );
839
- const compiledPrompt = [
865
+ const instructionText = `# Instructions\n\n${normalizedPrompt}`;
866
+ const stageText = `# Workflow Stage\n\nstage=${stage.id}\ntype=${runtimeStageKind}`;
867
+ const taskText =
840
868
  injectRuntimeTaskInPrompt && options.task
841
869
  ? `# Task\n\n${options.task}`
842
- : undefined,
843
- workflowInputText || undefined,
844
- `# Workflow Stage\n\nstage=${stage.id}\ntype=${runtimeStageKind}`,
845
- `# Instructions\n\n${normalizedPrompt}`,
846
- roleText || undefined,
847
- ]
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
+ )
848
888
  .filter(Boolean)
849
889
  .join("\n\n");
850
890
  const toolSelection = resolveToolSelection(
@@ -283,6 +283,17 @@ export function validateDynamicDecision(
283
283
  };
284
284
  }
285
285
 
286
+ export function assertValidDynamicDecision(
287
+ value: unknown,
288
+ context: DynamicDecisionValidationContext = {},
289
+ ): NormalizedDynamicDecision {
290
+ const result = validateDynamicDecision(value, context);
291
+ if (!result.ok || !result.decision) {
292
+ throw new Error(`invalid dynamic decision: ${result.errors.join("; ")}`);
293
+ }
294
+ return result.decision;
295
+ }
296
+
286
297
  export function hashDynamicDecision(value: unknown): string {
287
298
  return createHash("sha256")
288
299
  .update(stableStringify(toJsonNormalizedValue(value)))
@@ -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
  }
@@ -44,3 +44,7 @@ export function isTerminalDynamicOutputProfile(
44
44
  ): value is (typeof DYNAMIC_TERMINAL_OUTPUT_PROFILES)[number] {
45
45
  return typeof value === "string" && TERMINAL_OUTPUT_PROFILE_SET.has(value);
46
46
  }
47
+
48
+ export function dynamicOutputProfileValues(): DynamicOutputProfile[] {
49
+ return [...DYNAMIC_OUTPUT_PROFILES];
50
+ }
@@ -136,10 +136,17 @@ export function reconcileForeachGeneratedRunRecords(
136
136
  const compiledSpecIds = new Set(
137
137
  compiledFlow.tasks.map((task) => compiledTaskSpecId(task)),
138
138
  );
139
+ const compiledTaskBySpecId = new Map(
140
+ compiledFlow.tasks.map((task) => [compiledTaskSpecId(task), task]),
141
+ );
139
142
  const placeholderToGeneratedSpecIds = new Map<string, string[]>();
143
+ const streamingPlaceholderSpecIds = new Set<string>();
140
144
 
141
145
  for (const compiledTask of compiledFlow.tasks) {
142
146
  const specId = compiledTaskSpecId(compiledTask);
147
+ if (compiledTask.foreach && foreachStreamingEnabled(compiledTask)) {
148
+ streamingPlaceholderSpecIds.add(specId);
149
+ }
143
150
  const placeholderSpecId = foreachGeneratedPlaceholderSpecId(
144
151
  compiledTask,
145
152
  compiledFlow,
@@ -170,6 +177,14 @@ export function reconcileForeachGeneratedRunRecords(
170
177
  task.specId,
171
178
  );
172
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
+ }
173
188
  if (generatedSpecIds.includes(task.specId)) {
174
189
  placeholderSpecId = task.specId;
175
190
  task.foreachGenerated = { placeholderSpecId };
@@ -246,6 +261,7 @@ export function reconcileForeachGeneratedRunRecords(
246
261
  const replaced = replaceForeachGeneratedDependencies(
247
262
  task.dependsOn,
248
263
  placeholderToGeneratedSpecIds,
264
+ streamingPlaceholderSpecIds,
249
265
  );
250
266
  if (!sameStringList(task.dependsOn, replaced)) {
251
267
  task.dependsOn = replaced;
@@ -369,12 +385,15 @@ function foreachPlaceholderSpecId(
369
385
  function replaceForeachGeneratedDependencies(
370
386
  dependsOn: string[],
371
387
  placeholderToGeneratedSpecIds: Map<string, string[]>,
388
+ keepPlaceholderSpecIds = new Set<string>(),
372
389
  ): string[] {
373
390
  const replaced: string[] = [];
374
391
  for (const dep of dependsOn) {
375
392
  const generatedSpecIds = placeholderToGeneratedSpecIds.get(dep);
376
- if (generatedSpecIds) replaced.push(...generatedSpecIds);
377
- else replaced.push(dep);
393
+ if (generatedSpecIds) {
394
+ if (keepPlaceholderSpecIds.has(dep)) replaced.push(dep);
395
+ replaced.push(...generatedSpecIds);
396
+ } else replaced.push(dep);
378
397
  }
379
398
  return [...new Set(replaced)];
380
399
  }
@@ -480,6 +499,29 @@ export function dependenciesReady(
480
499
  if (deps.length === 0) return true;
481
500
  const partial =
482
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
+ }
483
525
  return deps.every((dep) => {
484
526
  const status = bySpecId.get(dep)?.status;
485
527
  if (status === "completed") return true;
@@ -488,6 +530,22 @@ export function dependenciesReady(
488
530
  });
489
531
  }
490
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
+
491
549
  export function buildForeachGeneratedTasks(
492
550
  template: CompiledTask,
493
551
  runtimeTask: string | undefined,
@@ -513,9 +571,10 @@ export function buildForeachGeneratedTasks(
513
571
  template.foreach!.injectRuntimeTask && runtimeTask
514
572
  ? `# Task\n\n${runtimeTask}`
515
573
  : undefined,
516
- `# Workflow Stage\n\nstage=${template.stageId}\ntype=foreach\nitem=${taskId}`,
517
- `# Instructions\n\n${instructions}`,
574
+ `# Workflow Stage\n\nstage=${template.stageId}\ntype=foreach`,
518
575
  template.foreach!.roleText || undefined,
576
+ `# Workflow Item\n\nitem=${taskId}`,
577
+ `# Instructions\n\n${instructions}`,
519
578
  ]
520
579
  .filter(Boolean)
521
580
  .join("\n\n");