@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/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,8 @@ 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";
6
+ import { compileRole } from "./roles.js";
5
7
  import { classifyToolCapability, effectiveToolClassification, providersForSelectedTools, resolveToolSelection, TOOL_NAME_PATTERN, toolAllowedByAuthorityCeiling, toolNameForSpec, } from "./tool-metadata.js";
6
8
  import { WorkflowValidationError, WORKFLOW_RUN_TYPE, } from "./types.js";
7
9
  import { resolveWorkflowRuntime, selectWorkflowRuntime, } from "./workflow-runtime.js";
@@ -101,7 +103,13 @@ function lowerArtifactGraphFrom(from) {
101
103
  typeof from === "object" &&
102
104
  !Array.isArray(from) &&
103
105
  typeof from.source === "string") {
104
- 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
+ };
105
113
  }
106
114
  return from;
107
115
  }
@@ -126,10 +134,23 @@ function appendWorkflowOutputInstructions(prompt, stage) {
126
134
  : "Use schema `stage-control-v1` unless the workflow asks for a more specific control schema.",
127
135
  "Put detailed prose, reasoning, and evidence discussion in <analysis> only.",
128
136
  "Put structured evidence pointers in <refs> as a JSON array; use [] if none.",
137
+ ...partialOutputInstructions(stage.output?.partial?.paths),
129
138
  ]
130
139
  .filter(Boolean)
131
140
  .join("\n\n");
132
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
+ }
133
154
  function artifactGraphTaskMetadata(stage, specDir) {
134
155
  const controlSchema = stage.output?.controlSchema;
135
156
  return {
@@ -143,8 +164,12 @@ function artifactGraphTaskMetadata(stage, specDir) {
143
164
  ? resolve(specDir, controlSchema)
144
165
  : undefined,
145
166
  maxDigestChars: stage.output?.maxDigestChars,
167
+ partial: stage.output?.partial
168
+ ? { paths: [...stage.output.partial.paths] }
169
+ : undefined,
146
170
  },
147
171
  requiredReads: stage.inputPolicy?.requiredReads ?? [],
172
+ artifactAccess: stage.inputPolicy?.artifactAccess ?? "enabled",
148
173
  sourceProjection: stage.sourceProjection,
149
174
  };
150
175
  }
@@ -442,14 +467,11 @@ async function compileArtifactGraphPlan(spec, options) {
442
467
  return defaultAgent;
443
468
  };
444
469
  const roleEntries = Object.entries(spec.roles ?? {});
445
- const roles = roleEntries.map(([name, role]) => ({
446
- name,
447
- fromAgent: role.fromAgent,
448
- content: role.prompt ?? "",
449
- maxChars: role.maxChars ?? 8000,
450
- truncated: false,
451
- includedSections: [],
452
- excludedSections: [],
470
+ const roles = await Promise.all(roleEntries.map(async ([name, role]) => {
471
+ const sourceAgent = role.fromAgent
472
+ ? await loadWorkflowAgent(role.fromAgent, options.cwd, agentCache, `$.roles.${name}.fromAgent`)
473
+ : undefined;
474
+ return compileRole(name, role, sourceAgent);
453
475
  }));
454
476
  const roleText = roles.length
455
477
  ? `# Role Context\n\n${roles.map((r) => `## Role: ${r.name}\n${r.content}`).join("\n\n")}`
@@ -459,7 +481,7 @@ async function compileArtifactGraphPlan(spec, options) {
459
481
  typeof workflowInput === "object" &&
460
482
  !Array.isArray(workflowInput) &&
461
483
  Object.keys(workflowInput).length > 0
462
- ? `# Workflow Input\n\n${JSON.stringify(workflowInput, null, 2)}`
484
+ ? `# Workflow Input\n\n${stringifyPromptJson(workflowInput)}`
463
485
  : "";
464
486
  const runtimeOverrides = options.runtimeOverrides;
465
487
  const runtimeDefaults = options.runtimeDefaults;
@@ -540,15 +562,26 @@ async function compileArtifactGraphPlan(spec, options) {
540
562
  const injectRuntimeTaskInPrompt = (runtimeStageKind !== "foreach" && injectTask) ||
541
563
  (runtimeStageKind === "foreach" && optInInjectRuntimeTask);
542
564
  const normalizedPrompt = String(prompt ?? "").replace(/\$\{item\}/g, "the relevant item from the dependency context");
543
- const compiledPrompt = [
544
- injectRuntimeTaskInPrompt && options.task
545
- ? `# Task\n\n${options.task}`
546
- : undefined,
547
- workflowInputText || undefined,
548
- `# Workflow Stage\n\nstage=${stage.id}\ntype=${runtimeStageKind}`,
549
- `# Instructions\n\n${normalizedPrompt}`,
550
- roleText || undefined,
551
- ]
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
+ ])
552
585
  .filter(Boolean)
553
586
  .join("\n\n");
554
587
  const toolSelection = resolveToolSelection([spec.defaults?.tools, stage.tools], stageAgent.tools);
@@ -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,4 +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(): string[];
8
+ export declare function dynamicOutputProfileValues(): DynamicOutputProfile[];
@@ -2,6 +2,7 @@ import type { CompiledTask, CompiledWorkflow, WorkflowRunRecord, WorkflowTaskRun
2
2
  export declare function reconcileLoopTaskRecordsInMemory(cwd: string, run: WorkflowRunRecord, compiledFlow: CompiledWorkflow, loopIds: Set<string>): boolean;
3
3
  export declare function recoverStaleRunningDynamicControllers(run: WorkflowRunRecord, compiledFlow: CompiledWorkflow): boolean;
4
4
  export declare function reconcileDynamicGeneratedRunRecords(cwd: string, run: WorkflowRunRecord, compiledFlow: CompiledWorkflow): boolean;
5
+ export declare function reconcileForeachGeneratedRunRecords(cwd: string, run: WorkflowRunRecord, compiledFlow: CompiledWorkflow): boolean;
5
6
  export declare function assertRunTaskPositionalAlignment(run: WorkflowRunRecord, compiledFlow: CompiledWorkflow): void;
6
7
  export declare function assertLoopTaskPositionalAlignment(run: WorkflowRunRecord, compiledFlow: CompiledWorkflow, loopIds?: Set<string>): void;
7
8
  export declare function upsertCompiledLoopTasksAtInsertion(compiledFlow: CompiledWorkflow, loopId: string, placeholderIndex: number, tasks: CompiledTask[]): void;
@@ -9,6 +10,8 @@ export declare function compiledTaskSpecId(task: CompiledTask): string;
9
10
  export declare function loopStageIdSet(compiledFlow: CompiledWorkflow): Set<string>;
10
11
  export declare function nextTaskRecordIndex(run: WorkflowRunRecord): number;
11
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;
12
15
  export declare function buildForeachGeneratedTasks(template: CompiledTask, runtimeTask: string | undefined, items: unknown[]): {
13
16
  tasks: CompiledTask[];
14
17
  error?: string;