@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/README.md CHANGED
@@ -12,7 +12,7 @@
12
12
 
13
13
  `pi-workflow` lets Pi run named, repeatable multi-step workflows: research, code review, spec conformance checks, impact review, and project-specific team routines.
14
14
 
15
- Built on [`@agwab/pi-subagent`](https://github.com/AgwaB/pi-subagent), it coordinates Pi subagent workers across workflow steps, passes results between them, and records the run so it can be inspected or resumed.
15
+ Built on [`@agwab/pi-subagent`](https://github.com/AgwaB/pi-subagent), it coordinates Pi subagent workers across workflow steps, passes results between them, and records the run so it can be inspected, stopped, or resumed.
16
16
 
17
17
  You choose a workflow and describe the task in natural language.
18
18
 
@@ -74,6 +74,8 @@ For a one-off adaptive workflow that should plan, fan out, and synthesize withou
74
74
 
75
75
  `/workflow dynamic` uses pi-workflow's built-in trusted dynamic controller and records a normal workflow run under `.pi/workflows/`. Use it when you explicitly want adaptive orchestration rather than a named reusable workflow.
76
76
 
77
+ To interrupt a non-terminal run and stop its local supervisor watch, use `/workflow stop <run-id>`. Resume later with `/workflow resume <run-id>` if unfinished tasks should be retried.
78
+
77
79
  ## Usage: choose an execution mode
78
80
 
79
81
  Use the bundled `execution-router` skill when you are not sure whether a task should be handled directly, by a targeted verifier/subagent, by an existing workflow, or by a new workflow:
@@ -1,5 +1,5 @@
1
1
  import { type WorkflowSourceManifestSource } from "./workflow-artifact-tool.js";
2
- import { type CompiledTask, type CompiledWorkflow, type WorkflowRunRecord, type WorkflowTaskRunRecord } from "./types.js";
2
+ import type { CompiledTask, CompiledWorkflow, WorkflowRunRecord, WorkflowTaskRunRecord } from "./types.js";
3
3
  export declare function executeSupportTask(cwd: string, run: WorkflowRunRecord, task: WorkflowTaskRunRecord, compiledTask: CompiledWorkflow["tasks"][number]): Promise<boolean>;
4
4
  export declare function readSupportSources(cwd: string, run: WorkflowRunRecord, dependsOn: string[]): Promise<Record<string, unknown>>;
5
5
  export declare function readArtifactGraphSupportSources(cwd: string, run: WorkflowRunRecord, dependsOn: string[]): Promise<Record<string, unknown>>;
@@ -1,6 +1,8 @@
1
1
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
2
2
  import { dirname, extname, join } from "node:path";
3
3
  import { fileURLToPath } from "node:url";
4
+ import { stringifyPromptJson } from "./prompt-json.js";
5
+ import { compactStrings } from "./strings.js";
4
6
  import { loadWorkflowHelper } from "./workflow-helpers.js";
5
7
  import { WORKFLOW_ARTIFACT_TOOL_NAME, writeWorkflowArtifactExtensionWrapper, } from "./workflow-artifact-extension.js";
6
8
  import { WORKFLOW_SOURCE_MANIFEST_SCHEMA, } from "./workflow-artifact-tool.js";
@@ -337,11 +339,14 @@ export async function prepareDagTask(cwd, run, compiledFlow, index) {
337
339
  compiledTask.compiledPrompt,
338
340
  "# Source Stage Context",
339
341
  "Use this deterministic source context packet. Prefer structuredOutput over outputPreview. Do not assume dependencies beyond this explicit packet.",
340
- JSON.stringify({ ...context, missingDependencies: missing }, null, 2),
342
+ stringifyPromptJson({ ...context, missingDependencies: missing }),
341
343
  ].join("\n\n"),
342
344
  };
343
345
  }
344
346
  async function prepareArtifactGraphTask(cwd, run, compiledTask, task, contextDependsOn) {
347
+ if (compiledTask.artifactGraph?.artifactAccess === "none") {
348
+ return { ...compiledTask, cwd: task.cwd };
349
+ }
345
350
  const taskDir = dirname(fromProjectPath(cwd, task.files.result));
346
351
  const manifestPath = join(taskDir, "source-manifest.json");
347
352
  const ledgerPath = join(taskDir, "read-ledger.jsonl");
@@ -665,7 +670,7 @@ export function formatArtifactGraphSourceContext(sources, requiredReads) {
665
670
  return [
666
671
  "# Workflow Artifact Inputs",
667
672
  "Use workflow_artifact to list/read upstream workflow artifacts. Inline controlProjection fields are authoritative for the projected data they contain; use artifact reads for declared requiredReads, missing fields, or debug detail.",
668
- "Projected reads must include a JSON path when using maxItems or maxChars, for example {\"action\":\"read\",\"source\":\"plan\",\"artifact\":\"control\",\"path\":\"$.factSlots\",\"maxItems\":8,\"maxChars\":2000}. For a whole artifact read, omit maxItems/maxChars.",
673
+ 'Projected reads must include a JSON path when using maxItems or maxChars, for example {"action":"read","source":"plan","artifact":"control","path":"$.factSlots","maxItems":8,"maxChars":2000}. For a whole artifact read, omit maxItems/maxChars.',
669
674
  requiredReads.length > 0
670
675
  ? [
671
676
  "Required reads before final output:",
@@ -673,7 +678,7 @@ export function formatArtifactGraphSourceContext(sources, requiredReads) {
673
678
  ].join("\n")
674
679
  : "No hard requiredReads are declared for this stage.",
675
680
  "Available sources:",
676
- JSON.stringify(sources.map((source) => ({
681
+ stringifyPromptJson(sources.map((source) => ({
677
682
  source: source.source,
678
683
  taskId: source.taskId,
679
684
  specId: source.specId,
@@ -687,11 +692,11 @@ export function formatArtifactGraphSourceContext(sources, requiredReads) {
687
692
  projectionMissingPaths: source.projectionMissingPaths,
688
693
  projectionTruncated: source.projectionTruncated,
689
694
  availableArtifacts: Object.keys(source.artifacts),
690
- })), null, 2),
695
+ }))),
691
696
  ].join("\n\n");
692
697
  }
693
698
  function uniqueStrings(values) {
694
- return [...new Set(values.filter((value) => value.trim().length > 0))];
699
+ return compactStrings(values, { trim: false, dropWhitespaceOnly: true });
695
700
  }
696
701
  export async function readArtifactGraphControl(cwd, task) {
697
702
  const taskDir = dirname(fromProjectPath(cwd, task.files.result));
@@ -1,5 +1,6 @@
1
1
  import { isAbsolute } from "node:path";
2
2
  import { DYNAMIC_OUTPUT_PROFILES } from "./dynamic-profiles.js";
3
+ import { compactStrings } from "./strings.js";
3
4
  import { APPROVAL_MODES, FAST_MODES, THINKING_LEVELS, TOOL_CLASSIFICATIONS, WORKTREE_POLICIES, WorkflowValidationError, } from "./types.js";
4
5
  const TOP_LEVEL_KEYS = new Set([
5
6
  "schemaVersion",
@@ -58,10 +59,16 @@ const OUTPUT_KEYS = new Set([
58
59
  "analysis",
59
60
  "refs",
60
61
  "maxDigestChars",
62
+ "partial",
61
63
  ]);
64
+ const OUTPUT_PARTIAL_KEYS = new Set(["paths"]);
62
65
  const REQUIRED_FLAG_KEYS = new Set(["required"]);
63
66
  const REFS_OUTPUT_KEYS = new Set(["required", "minItems"]);
64
- const INPUT_POLICY_KEYS = new Set(["requiredReads", "enforcement"]);
67
+ const INPUT_POLICY_KEYS = new Set([
68
+ "requiredReads",
69
+ "enforcement",
70
+ "artifactAccess",
71
+ ]);
65
72
  const SOURCE_PROJECTION_KEYS = new Set(["include", "maxChars"]);
66
73
  const SUPPORT_KEYS = new Set(["uses", "options"]);
67
74
  const DYNAMIC_STAGE_FORBIDDEN_KEYS = new Set([
@@ -172,7 +179,8 @@ const UNTIL_KEYS = new Set([
172
179
  "all",
173
180
  "any",
174
181
  ]);
175
- const FOREACH_FROM_KEYS = new Set(["source", "path"]);
182
+ const FOREACH_FROM_KEYS = new Set(["source", "path", "streaming"]);
183
+ const FOREACH_STREAMING_KEYS = new Set(["enabled", "minChunk"]);
176
184
  const NORMAL_ARTIFACT_KINDS = new Set([
177
185
  "control",
178
186
  "analysis",
@@ -262,8 +270,48 @@ function validateStageArray(value, path, issues) {
262
270
  for (const [index, item] of value.entries()) {
263
271
  validateStage(item, `${path}[${index}]`, ids, sourceIds, issues);
264
272
  }
273
+ validateStreamingProducerDeclarations(value, path, issues);
265
274
  validateStageDependencyGraph(value, path, issues);
266
275
  }
276
+ function validateStreamingProducerDeclarations(stages, path, issues) {
277
+ const byId = new Map();
278
+ for (const stage of stages) {
279
+ if (isRecord(stage) && typeof stage.id === "string") {
280
+ byId.set(stage.id, stage);
281
+ }
282
+ }
283
+ for (const [index, stage] of stages.entries()) {
284
+ if (!isRecord(stage) || !isRecord(stage.from))
285
+ continue;
286
+ const from = stage.from;
287
+ if (!isRecord(from.streaming) || from.streaming.enabled !== true)
288
+ continue;
289
+ const source = typeof from.source === "string" ? from.source : undefined;
290
+ const controlPath = typeof from.path === "string" ? from.path : undefined;
291
+ if (!source || !controlPath)
292
+ continue;
293
+ const sourceStage = byId.get(source);
294
+ const partialPaths = outputPartialPaths(sourceStage);
295
+ if (!partialPaths.includes(controlPath)) {
296
+ issues.push({
297
+ path: `${path}[${index}].from.streaming`,
298
+ message: `source stage "${source}" must declare output.partial.paths including "${controlPath}" to use streaming`,
299
+ });
300
+ }
301
+ }
302
+ }
303
+ function outputPartialPaths(stage) {
304
+ const output = isRecord(stage?.output) ? stage.output : undefined;
305
+ const partial = isRecord(output?.partial) ? output.partial : undefined;
306
+ return Array.isArray(partial?.paths)
307
+ ? compactStrings(partial.paths, {
308
+ trim: false,
309
+ unique: false,
310
+ dropEmpty: false,
311
+ dropWhitespaceOnly: false,
312
+ })
313
+ : [];
314
+ }
267
315
  function validateStageDependencyGraph(stages, path, issues) {
268
316
  const ids = new Set(stages
269
317
  .filter(isRecord)
@@ -334,7 +382,12 @@ function extractDependencyRefs(value) {
334
382
  if (typeof value === "string")
335
383
  return [value];
336
384
  if (Array.isArray(value))
337
- return value.filter((item) => typeof item === "string");
385
+ return compactStrings(value, {
386
+ trim: false,
387
+ unique: false,
388
+ dropEmpty: false,
389
+ dropWhitespaceOnly: false,
390
+ });
338
391
  if (isRecord(value) && typeof value.source === "string")
339
392
  return [value.source];
340
393
  return [];
@@ -445,7 +498,7 @@ function validateStage(value, path, siblingIds, sourceIds, issues) {
445
498
  allowControlPath: false,
446
499
  });
447
500
  validateSourceProjection(stage.sourceProjection, `${path}.sourceProjection`, issues);
448
- validateInputPolicy(stage.inputPolicy, `${path}.inputPolicy`, sourceIds, issues);
501
+ validateInputPolicy(stage.inputPolicy, `${path}.inputPolicy`, sourceIds, issues, stage.sourceProjection);
449
502
  validateOutput(stage.output, `${path}.output`, issues);
450
503
  validateSupportStage(stage, type, path, issues);
451
504
  validateDynamicStage(stage, type, path, issues);
@@ -518,6 +571,19 @@ function validateControlPathRef(value, path, siblingIds, issues) {
518
571
  message: "must be a control JSONPath starting with $.",
519
572
  });
520
573
  }
574
+ if (value.streaming !== undefined) {
575
+ validateForeachStreaming(value.streaming, `${path}.streaming`, issues);
576
+ }
577
+ }
578
+ function validateForeachStreaming(value, path, issues) {
579
+ const streaming = recordAt(value, path, issues);
580
+ if (!streaming)
581
+ return;
582
+ rejectUnknownKeys(streaming, FOREACH_STREAMING_KEYS, path, issues);
583
+ if (streaming.enabled !== true) {
584
+ issues.push({ path: `${path}.enabled`, message: "must be true" });
585
+ }
586
+ optionalPositiveInteger(streaming.minChunk, `${path}.minChunk`, issues);
521
587
  }
522
588
  function validateKnownStageRef(stageId, path, siblingIds, issues) {
523
589
  if (stageId.trim() === "") {
@@ -539,6 +605,39 @@ function validateOutput(value, path, issues) {
539
605
  optionalPositiveInteger(output.maxDigestChars, `${path}.maxDigestChars`, issues);
540
606
  validateRequiredFlagObject(output.analysis, `${path}.analysis`, issues);
541
607
  validateRefsOutputObject(output.refs, `${path}.refs`, issues);
608
+ validatePartialOutput(output.partial, `${path}.partial`, issues);
609
+ }
610
+ function validatePartialOutput(value, path, issues) {
611
+ if (value === undefined)
612
+ return;
613
+ const partial = recordAt(value, path, issues);
614
+ if (!partial)
615
+ return;
616
+ rejectUnknownKeys(partial, OUTPUT_PARTIAL_KEYS, path, issues);
617
+ if (!Array.isArray(partial.paths)) {
618
+ issues.push({ path: `${path}.paths`, message: "must be an array" });
619
+ return;
620
+ }
621
+ if (partial.paths.length === 0) {
622
+ issues.push({ path: `${path}.paths`, message: "must not be empty" });
623
+ }
624
+ const seen = new Set();
625
+ for (const [index, item] of partial.paths.entries()) {
626
+ const itemPath = `${path}.paths[${index}]`;
627
+ if (typeof item !== "string" || item.trim() === "") {
628
+ issues.push({ path: itemPath, message: "must be a non-empty string" });
629
+ continue;
630
+ }
631
+ if (!item.startsWith("$.")) {
632
+ issues.push({
633
+ path: itemPath,
634
+ message: "must be a control JSONPath starting with $.",
635
+ });
636
+ }
637
+ if (seen.has(item))
638
+ issues.push({ path: itemPath, message: `duplicate value "${item}"` });
639
+ seen.add(item);
640
+ }
542
641
  }
543
642
  function validateControlSchemaRef(value, path, issues) {
544
643
  if (value === undefined)
@@ -568,7 +667,7 @@ function validateRefsOutputObject(value, path, issues) {
568
667
  optionalBoolean(object.required, `${path}.required`, issues);
569
668
  optionalPositiveInteger(object.minItems, `${path}.minItems`, issues);
570
669
  }
571
- function validateInputPolicy(value, path, siblingIds, issues) {
670
+ function validateInputPolicy(value, path, siblingIds, issues, sourceProjection) {
572
671
  if (value === undefined)
573
672
  return;
574
673
  const policy = recordAt(value, path, issues);
@@ -579,6 +678,29 @@ function validateInputPolicy(value, path, siblingIds, issues) {
579
678
  if (policy.enforcement !== undefined && policy.enforcement !== "fail") {
580
679
  issues.push({ path: `${path}.enforcement`, message: 'must be "fail"' });
581
680
  }
681
+ if (policy.artifactAccess !== undefined &&
682
+ policy.artifactAccess !== "enabled" &&
683
+ policy.artifactAccess !== "none") {
684
+ issues.push({
685
+ path: `${path}.artifactAccess`,
686
+ message: 'must be "enabled" or "none"',
687
+ });
688
+ }
689
+ if (policy.artifactAccess === "none") {
690
+ if (Array.isArray(policy.requiredReads) &&
691
+ policy.requiredReads.length > 0) {
692
+ issues.push({
693
+ path: `${path}.requiredReads`,
694
+ message: 'must be empty when artifactAccess is "none"',
695
+ });
696
+ }
697
+ if (sourceProjection !== undefined) {
698
+ issues.push({
699
+ path: `${path}.artifactAccess`,
700
+ message: 'cannot be "none" when sourceProjection is declared',
701
+ });
702
+ }
703
+ }
582
704
  }
583
705
  function validateRequiredReads(value, path, sourceIds, issues) {
584
706
  if (value === undefined)
package/dist/compiler.js CHANGED
@@ -2,6 +2,7 @@ import { readFile } from "node:fs/promises";
2
2
  import { dirname, resolve } from "node:path";
3
3
  import { loadAgentByName } from "./agents.js";
4
4
  import { DYNAMIC_OUTPUT_PROFILES } from "./dynamic-profiles.js";
5
+ import { stringifyPromptJson } from "./prompt-json.js";
5
6
  import { compileRole } from "./roles.js";
6
7
  import { classifyToolCapability, effectiveToolClassification, providersForSelectedTools, resolveToolSelection, TOOL_NAME_PATTERN, toolAllowedByAuthorityCeiling, toolNameForSpec, } from "./tool-metadata.js";
7
8
  import { WorkflowValidationError, WORKFLOW_RUN_TYPE, } from "./types.js";
@@ -102,7 +103,13 @@ function lowerArtifactGraphFrom(from) {
102
103
  typeof from === "object" &&
103
104
  !Array.isArray(from) &&
104
105
  typeof from.source === "string") {
105
- return { stage: from.source, path: from.path };
106
+ return {
107
+ stage: from.source,
108
+ path: from.path,
109
+ ...(from.streaming !== undefined
110
+ ? { streaming: from.streaming }
111
+ : {}),
112
+ };
106
113
  }
107
114
  return from;
108
115
  }
@@ -127,10 +134,23 @@ function appendWorkflowOutputInstructions(prompt, stage) {
127
134
  : "Use schema `stage-control-v1` unless the workflow asks for a more specific control schema.",
128
135
  "Put detailed prose, reasoning, and evidence discussion in <analysis> only.",
129
136
  "Put structured evidence pointers in <refs> as a JSON array; use [] if none.",
137
+ ...partialOutputInstructions(stage.output?.partial?.paths),
130
138
  ]
131
139
  .filter(Boolean)
132
140
  .join("\n\n");
133
141
  }
142
+ function partialOutputInstructions(paths) {
143
+ if (!paths || paths.length === 0)
144
+ return [];
145
+ return [
146
+ "# Workflow Partial Output Protocol (optional)",
147
+ `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:`,
148
+ '<partial-control>{"schema":"workflow-partial-output-v1","path":"$.items","items":[{"id":"stable-id","...":"..."}]}</partial-control>',
149
+ "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.",
150
+ "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.",
151
+ "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.",
152
+ ];
153
+ }
134
154
  function artifactGraphTaskMetadata(stage, specDir) {
135
155
  const controlSchema = stage.output?.controlSchema;
136
156
  return {
@@ -144,8 +164,12 @@ function artifactGraphTaskMetadata(stage, specDir) {
144
164
  ? resolve(specDir, controlSchema)
145
165
  : undefined,
146
166
  maxDigestChars: stage.output?.maxDigestChars,
167
+ partial: stage.output?.partial
168
+ ? { paths: [...stage.output.partial.paths] }
169
+ : undefined,
147
170
  },
148
171
  requiredReads: stage.inputPolicy?.requiredReads ?? [],
172
+ artifactAccess: stage.inputPolicy?.artifactAccess ?? "enabled",
149
173
  sourceProjection: stage.sourceProjection,
150
174
  };
151
175
  }
@@ -457,7 +481,7 @@ async function compileArtifactGraphPlan(spec, options) {
457
481
  typeof workflowInput === "object" &&
458
482
  !Array.isArray(workflowInput) &&
459
483
  Object.keys(workflowInput).length > 0
460
- ? `# Workflow Input\n\n${JSON.stringify(workflowInput, null, 2)}`
484
+ ? `# Workflow Input\n\n${stringifyPromptJson(workflowInput)}`
461
485
  : "";
462
486
  const runtimeOverrides = options.runtimeOverrides;
463
487
  const runtimeDefaults = options.runtimeDefaults;
@@ -538,15 +562,26 @@ async function compileArtifactGraphPlan(spec, options) {
538
562
  const injectRuntimeTaskInPrompt = (runtimeStageKind !== "foreach" && injectTask) ||
539
563
  (runtimeStageKind === "foreach" && optInInjectRuntimeTask);
540
564
  const normalizedPrompt = String(prompt ?? "").replace(/\$\{item\}/g, "the relevant item from the dependency context");
541
- const compiledPrompt = [
542
- injectRuntimeTaskInPrompt && options.task
543
- ? `# Task\n\n${options.task}`
544
- : undefined,
545
- workflowInputText || undefined,
546
- `# Workflow Stage\n\nstage=${stage.id}\ntype=${runtimeStageKind}`,
547
- `# Instructions\n\n${normalizedPrompt}`,
548
- roleText || undefined,
549
- ]
565
+ const instructionText = `# Instructions\n\n${normalizedPrompt}`;
566
+ const stageText = `# Workflow Stage\n\nstage=${stage.id}\ntype=${runtimeStageKind}`;
567
+ const taskText = injectRuntimeTaskInPrompt && options.task
568
+ ? `# Task\n\n${options.task}`
569
+ : undefined;
570
+ const compiledPrompt = (runtimeStageKind === "foreach"
571
+ ? [
572
+ taskText,
573
+ workflowInputText || undefined,
574
+ stageText,
575
+ roleText || undefined,
576
+ instructionText,
577
+ ]
578
+ : [
579
+ taskText,
580
+ workflowInputText || undefined,
581
+ stageText,
582
+ instructionText,
583
+ roleText || undefined,
584
+ ])
550
585
  .filter(Boolean)
551
586
  .join("\n\n");
552
587
  const toolSelection = resolveToolSelection([spec.defaults?.tools, stage.tools], stageAgent.tools);
@@ -97,6 +97,7 @@ export interface DynamicDecisionArtifactWriteResult {
97
97
  hash?: string;
98
98
  }
99
99
  export declare function validateDynamicDecision(value: unknown, context?: DynamicDecisionValidationContext): DynamicDecisionValidationResult;
100
+ export declare function assertValidDynamicDecision(value: unknown, context?: DynamicDecisionValidationContext): NormalizedDynamicDecision;
100
101
  export declare function hashDynamicDecision(value: unknown): string;
101
102
  export declare function dynamicLoopSignature(decision: NormalizedDynamicDecision): string;
102
103
  export declare function writeDynamicDecisionArtifacts(input: DynamicDecisionArtifactWriteInput): Promise<DynamicDecisionArtifactWriteResult>;
@@ -121,6 +121,13 @@ export function validateDynamicDecision(value, context = {}) {
121
121
  hash: hashDynamicDecision(decision),
122
122
  };
123
123
  }
124
+ export function assertValidDynamicDecision(value, context = {}) {
125
+ const result = validateDynamicDecision(value, context);
126
+ if (!result.ok || !result.decision) {
127
+ throw new Error(`invalid dynamic decision: ${result.errors.join("; ")}`);
128
+ }
129
+ return result.decision;
130
+ }
124
131
  export function hashDynamicDecision(value) {
125
132
  return createHash("sha256")
126
133
  .update(stableStringify(toJsonNormalizedValue(value)))
@@ -4,6 +4,7 @@ import { DynamicControllerBudgetBlocked } from "./dynamic-controller-errors.js";
4
4
  import { isDynamicOutputProfile, } from "./dynamic-profiles.js";
5
5
  import { readOrRebuildDynamicState } from "./dynamic-state.js";
6
6
  import { sanitizeTaskId } from "./engine-run-graph.js";
7
+ import { compactStrings } from "./strings.js";
7
8
  import { fromProjectPath, isTerminalTaskStatus, readJson } from "./store.js";
8
9
  import { classifyToolCapability, effectiveToolClassification, providersForSelectedTools, toolAllowedByAuthorityCeiling, } from "./tool-metadata.js";
9
10
  import { resolveWorkflowRuntime, selectWorkflowRuntime, } from "./workflow-runtime.js";
@@ -173,6 +174,7 @@ export async function buildDynamicGeneratedCompiledTask(input) {
173
174
  maxDigestChars: DYNAMIC_OUTPUT_MAX_DIGEST_CHARS,
174
175
  },
175
176
  requiredReads: input.request.requiredReads,
177
+ artifactAccess: "enabled",
176
178
  sourceProjection: dynamicInputSourceProjection(input.request.inputs),
177
179
  },
178
180
  dynamicGenerated: {
@@ -630,5 +632,5 @@ function dynamicOutputProfileInstructions(outputProfile) {
630
632
  return `# Dynamic Output Profile: ${outputProfile}\nEmit control JSON suitable for this output profile and surface gaps/blockers explicitly.`;
631
633
  }
632
634
  function uniqueStrings(values) {
633
- return [...new Set(values.filter((value) => value.trim().length > 0))];
635
+ return compactStrings(values, { trim: false, dropWhitespaceOnly: true });
634
636
  }
@@ -5,3 +5,4 @@ export declare const DYNAMIC_TERMINAL_OUTPUT_PROFILES: readonly ["synthesis_v1"]
5
5
  export declare function isDynamicOutputProfile(value: unknown): value is DynamicOutputProfile;
6
6
  export declare function isExtractableDynamicOutputProfile(value: unknown): value is (typeof DYNAMIC_EXTRACTABLE_OUTPUT_PROFILES)[number];
7
7
  export declare function isTerminalDynamicOutputProfile(value: unknown): value is (typeof DYNAMIC_TERMINAL_OUTPUT_PROFILES)[number];
8
+ export declare function dynamicOutputProfileValues(): DynamicOutputProfile[];
@@ -26,3 +26,6 @@ export function isExtractableDynamicOutputProfile(value) {
26
26
  export function isTerminalDynamicOutputProfile(value) {
27
27
  return typeof value === "string" && TERMINAL_OUTPUT_PROFILE_SET.has(value);
28
28
  }
29
+ export function dynamicOutputProfileValues() {
30
+ return [...DYNAMIC_OUTPUT_PROFILES];
31
+ }
@@ -10,6 +10,8 @@ export declare function compiledTaskSpecId(task: CompiledTask): string;
10
10
  export declare function loopStageIdSet(compiledFlow: CompiledWorkflow): Set<string>;
11
11
  export declare function nextTaskRecordIndex(run: WorkflowRunRecord): number;
12
12
  export declare function dependenciesReady(compiledTask: CompiledTask, bySpecId: Map<string, WorkflowTaskRunRecord>, compiledFlow: CompiledWorkflow): boolean;
13
+ export declare function foreachStreamingEnabled(compiledTask: CompiledTask): boolean;
14
+ export declare function foreachStreamingMinChunk(compiledTask: CompiledTask): number;
13
15
  export declare function buildForeachGeneratedTasks(template: CompiledTask, runtimeTask: string | undefined, items: unknown[]): {
14
16
  tasks: CompiledTask[];
15
17
  error?: string;
@@ -95,9 +95,14 @@ export function reconcileDynamicGeneratedRunRecords(cwd, run, compiledFlow) {
95
95
  export function reconcileForeachGeneratedRunRecords(cwd, run, compiledFlow) {
96
96
  let changed = false;
97
97
  const compiledSpecIds = new Set(compiledFlow.tasks.map((task) => compiledTaskSpecId(task)));
98
+ const compiledTaskBySpecId = new Map(compiledFlow.tasks.map((task) => [compiledTaskSpecId(task), task]));
98
99
  const placeholderToGeneratedSpecIds = new Map();
100
+ const streamingPlaceholderSpecIds = new Set();
99
101
  for (const compiledTask of compiledFlow.tasks) {
100
102
  const specId = compiledTaskSpecId(compiledTask);
103
+ if (compiledTask.foreach && foreachStreamingEnabled(compiledTask)) {
104
+ streamingPlaceholderSpecIds.add(specId);
105
+ }
101
106
  const placeholderSpecId = foreachGeneratedPlaceholderSpecId(compiledTask, compiledFlow, specId);
102
107
  if (!placeholderSpecId)
103
108
  continue;
@@ -117,6 +122,12 @@ export function reconcileForeachGeneratedRunRecords(cwd, run, compiledFlow) {
117
122
  const generatedSpecIds = placeholderToGeneratedSpecIds.get(task.specId);
118
123
  let placeholderSpecId = foreachGeneratedPlaceholderSpecId(task, compiledFlow, task.specId);
119
124
  if (generatedSpecIds && !placeholderSpecId) {
125
+ const compiledTask = compiledTaskBySpecId.get(task.specId);
126
+ if (compiledTask?.foreach &&
127
+ streamingPlaceholderSpecIds.has(task.specId)) {
128
+ filteredRunTasks.push(task);
129
+ continue;
130
+ }
120
131
  if (generatedSpecIds.includes(task.specId)) {
121
132
  placeholderSpecId = task.specId;
122
133
  task.foreachGenerated = { placeholderSpecId };
@@ -183,7 +194,7 @@ export function reconcileForeachGeneratedRunRecords(cwd, run, compiledFlow) {
183
194
  for (const task of reordered) {
184
195
  if (!task.dependsOn)
185
196
  continue;
186
- const replaced = replaceForeachGeneratedDependencies(task.dependsOn, placeholderToGeneratedSpecIds);
197
+ const replaced = replaceForeachGeneratedDependencies(task.dependsOn, placeholderToGeneratedSpecIds, streamingPlaceholderSpecIds);
187
198
  if (!sameStringList(task.dependsOn, replaced)) {
188
199
  task.dependsOn = replaced;
189
200
  changed = true;
@@ -261,12 +272,15 @@ function foreachPlaceholderSpecId(compiledFlow, stageId) {
261
272
  return undefined;
262
273
  return `${stageId}.item`;
263
274
  }
264
- function replaceForeachGeneratedDependencies(dependsOn, placeholderToGeneratedSpecIds) {
275
+ function replaceForeachGeneratedDependencies(dependsOn, placeholderToGeneratedSpecIds, keepPlaceholderSpecIds = new Set()) {
265
276
  const replaced = [];
266
277
  for (const dep of dependsOn) {
267
278
  const generatedSpecIds = placeholderToGeneratedSpecIds.get(dep);
268
- if (generatedSpecIds)
279
+ if (generatedSpecIds) {
280
+ if (keepPlaceholderSpecIds.has(dep))
281
+ replaced.push(dep);
269
282
  replaced.push(...generatedSpecIds);
283
+ }
270
284
  else
271
285
  replaced.push(dep);
272
286
  }
@@ -341,6 +355,29 @@ export function dependenciesReady(compiledTask, bySpecId, compiledFlow) {
341
355
  if (deps.length === 0)
342
356
  return true;
343
357
  const partial = stageSourcePolicy(compiledFlow, compiledTask.stageId ?? "") === "partial";
358
+ if (foreachStreamingEnabled(compiledTask)) {
359
+ let completedDependencyReady = false;
360
+ let runningDependencyMayHavePartialItems = false;
361
+ let allKnownDependenciesTerminal = true;
362
+ for (const dep of deps) {
363
+ const status = bySpecId.get(dep)?.status;
364
+ if (status === "completed") {
365
+ completedDependencyReady = true;
366
+ continue;
367
+ }
368
+ if (status && isTerminalTaskStatus(status)) {
369
+ if (!partial)
370
+ return false;
371
+ continue;
372
+ }
373
+ if (status === "running")
374
+ runningDependencyMayHavePartialItems = true;
375
+ allKnownDependenciesTerminal = false;
376
+ }
377
+ return (completedDependencyReady ||
378
+ runningDependencyMayHavePartialItems ||
379
+ allKnownDependenciesTerminal);
380
+ }
344
381
  return deps.every((dep) => {
345
382
  const status = bySpecId.get(dep)?.status;
346
383
  if (status === "completed")
@@ -350,6 +387,18 @@ export function dependenciesReady(compiledTask, bySpecId, compiledFlow) {
350
387
  return false;
351
388
  });
352
389
  }
390
+ export function foreachStreamingEnabled(compiledTask) {
391
+ const streaming = compiledTask.foreach?.from?.streaming;
392
+ return Boolean(streaming &&
393
+ typeof streaming === "object" &&
394
+ streaming.enabled === true);
395
+ }
396
+ export function foreachStreamingMinChunk(compiledTask) {
397
+ const value = compiledTask.foreach?.from?.streaming?.minChunk;
398
+ return typeof value === "number" && Number.isFinite(value) && value > 0
399
+ ? Math.floor(value)
400
+ : 1;
401
+ }
353
402
  export function buildForeachGeneratedTasks(template, runtimeTask, items) {
354
403
  const seen = new Set();
355
404
  const tasks = [];
@@ -368,9 +417,10 @@ export function buildForeachGeneratedTasks(template, runtimeTask, items) {
368
417
  template.foreach.injectRuntimeTask && runtimeTask
369
418
  ? `# Task\n\n${runtimeTask}`
370
419
  : undefined,
371
- `# Workflow Stage\n\nstage=${template.stageId}\ntype=foreach\nitem=${taskId}`,
372
- `# Instructions\n\n${instructions}`,
420
+ `# Workflow Stage\n\nstage=${template.stageId}\ntype=foreach`,
373
421
  template.foreach.roleText || undefined,
422
+ `# Workflow Item\n\nitem=${taskId}`,
423
+ `# Instructions\n\n${instructions}`,
374
424
  ]
375
425
  .filter(Boolean)
376
426
  .join("\n\n");