@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/dist/engine.js CHANGED
@@ -4,11 +4,12 @@ import { fileURLToPath, pathToFileURL } from "node:url";
4
4
  import { Worker } from "node:worker_threads";
5
5
  import { compileWorkflow } from "./compiler.js";
6
6
  import { loadWorkflowSpec } from "./schema.js";
7
- import { createRunRecord, createTaskRunRecord, compiledWorkflowPath, fromProjectPath, indexSupervisorErrorPath, isTerminalWorkflowStatus, isTerminalTaskStatus, listRunRecords, readIndex, readJson, readRunRecord, resetTaskForResume, setTaskTerminal, supervisorPath, toProjectPath, updateIndex, withRunLease, workflowRunPath, writeJsonAtomic, writeRunRecord, writeCompiledRunArtifact, writeStaticRunArtifacts, } from "./store.js";
7
+ import { createRunRecord, createTaskRunRecord, compiledWorkflowPath, fromProjectPath, indexSupervisorErrorPath, isBlockedTaskResumableForResume, isTerminalWorkflowStatus, isTerminalTaskStatus, listRunRecords, readIndex, readJson, readRunRecord, resetTaskForResume, setTaskTerminal, supervisorPath, toProjectPath, updateIndex, withRunLease, workflowRunPath, writeJsonAtomic, writeRunRecord, writeCompiledRunArtifact, writeStaticRunArtifacts, } from "./store.js";
8
8
  import { resolveWorkflowBackend } from "./backend.js";
9
9
  import { ensureManagedWorktree } from "./worktree.js";
10
10
  import { resolveWorkflowHelperRef } from "./workflow-helpers.js";
11
11
  import { buildAvailableToolView } from "./tool-metadata.js";
12
+ import { summarizeWorkflowTelemetry } from "./workflow-artifacts.js";
12
13
  import { workflowBundleFingerprint, workflowBundleSpecPath, } from "./workflow-source-context-runtime.js";
13
14
  import { readSimpleJsonPath, } from "./workflow-runtime.js";
14
15
  import { dynamicRunDir, hashDynamicRequest, readDynamicEvents, } from "./dynamic-events.js";
@@ -18,10 +19,11 @@ import { assertDynamicRuntimeBudgetAvailable, dynamicRuntimeBudgetExceededMessag
18
19
  import { assertDynamicGeneratedMetadataMatches, assertDynamicGenerationBudgetAvailable, buildDynamicGeneratedCompiledTask, dynamicGeneratedInsertIndex, isDynamicCompiledTaskPayload, normalizeDynamicAgentRequest, readDynamicGeneratedTaskResult, } from "./dynamic-generated-task-runtime.js";
19
20
  import { optionalEventString, runDynamicHelperCall, runDynamicNestedWorkflowCall, } from "./dynamic-controller-calls.js";
20
21
  import { normalizeDynamicFanoutPlanRequest, runDynamicDecisionLoopStatusPersistCall, runDynamicDecisionPersistCall, runDynamicFanoutPlanPersistCall, runDynamicResultReadCall, runDynamicStateIndexPersistCall, } from "./dynamic-control-ops.js";
21
- import { assertRunTaskPositionalAlignment, buildForeachGeneratedTasks, dependenciesReady, markDagDependentsSkipped, nextTaskRecordIndex, reconcileDynamicGeneratedRunRecords, reconcileForeachGeneratedRunRecords, recoverStaleRunningDynamicControllers, replaceDependencyList, sourceStageIdsForFrom, stageSourcePolicy, updateDownstreamDependencies, } from "./engine-run-graph.js";
22
+ import { assertRunTaskPositionalAlignment, buildForeachGeneratedTasks, dependenciesReady, foreachStreamingEnabled, foreachStreamingMinChunk, markDagDependentsSkipped, nextTaskRecordIndex, reconcileDynamicGeneratedRunRecords, reconcileForeachGeneratedRunRecords, recoverStaleRunningDynamicControllers, replaceDependencyList, sourceStageIdsForFrom, stageSourcePolicy, updateDownstreamDependencies, } from "./engine-run-graph.js";
22
23
  import { reconcileLoopTaskMaterialization, scheduleLoop, } from "./loop-runtime.js";
23
24
  import { executeSupportTask, normalizeDynamicControllerOutput, prepareArtifactGraphRetryTask, prepareDagTask, readArtifactGraphControl, readArtifactGraphSupportSources, readSupportSources, writeArtifactGraphDynamicResult, } from "./artifact-graph-runtime.js";
24
25
  import { DIRECT_DYNAMIC_RUNTIME_VERSION, ensureDirectDynamicRuntimeBundle, } from "./dynamic-runtime-bundle.js";
26
+ import { hasFatalPartialOutputIssue, readWorkflowPartialOutputLedger, writeWorkflowPartialOutputLedgerFromFile, } from "./workflow-partial-output.js";
25
27
  import { WORKFLOW_RUN_TYPE, } from "./types.js";
26
28
  export { buildRunSourceContext } from "./workflow-source-context-runtime.js";
27
29
  export { evaluateLoopUntilCondition } from "./loop-runtime.js";
@@ -126,6 +128,18 @@ export async function waitForRun(cwd, runIdOrPrefix, timeoutMs, options = {}) {
126
128
  }
127
129
  return run;
128
130
  }
131
+ function assertBlockedRunResumable(run) {
132
+ if (run.status !== "blocked")
133
+ return;
134
+ const blockers = run.tasks.filter((task) => task.status === "blocked" && !isBlockedTaskResumableForResume(task));
135
+ if (blockers.length === 0)
136
+ return;
137
+ const details = blockers
138
+ .slice(0, 3)
139
+ .map((task) => `${task.specId} statusDetail=${task.statusDetail}`)
140
+ .join(", ");
141
+ throw new Error(`Cannot resume blocked run ${run.runId}: non-resumable blocked task(s): ${details}. Resolve the attention/approval blocker before resuming.`);
142
+ }
129
143
  export async function stopRun(cwd, runIdOrPrefix) {
130
144
  const current = await readRunRecord(cwd, runIdOrPrefix);
131
145
  const stopped = await withRunLease(cwd, current.runId, async () => {
@@ -160,6 +174,7 @@ export async function resumeRun(cwd, runIdOrPrefix, options = {}) {
160
174
  current.status !== "blocked") {
161
175
  throw new Error(`resume requires a failed, interrupted, or resumable blocked run; ${current.runId} is ${current.status}`);
162
176
  }
177
+ assertBlockedRunResumable(current);
163
178
  const compiledFlow = await readCompiledWorkflow(cwd, current.runId);
164
179
  const hasLoopTasks = compiledFlow?.tasks.some((task) => task.kind === "loop" ||
165
180
  task.loopPlaceholder !== undefined ||
@@ -170,6 +185,7 @@ export async function resumeRun(cwd, runIdOrPrefix, options = {}) {
170
185
  const resetTaskIds = [];
171
186
  const updated = await withRunLease(cwd, current.runId, async () => {
172
187
  const run = await readRunRecord(cwd, current.runId);
188
+ assertBlockedRunResumable(run);
173
189
  await resolveWorkflowBackend(run)
174
190
  .cleanupRun(cwd, run)
175
191
  .catch(() => undefined);
@@ -271,8 +287,19 @@ function isMissingRunError(error) {
271
287
  return (isEnoentError(error) ||
272
288
  (error instanceof Error && /^Flow run not found: /.test(error.message)));
273
289
  }
290
+ function assertScheduleLeaseActive(signal) {
291
+ if (!signal?.aborted)
292
+ return;
293
+ const reason = signal.reason;
294
+ if (reason instanceof Error)
295
+ throw reason;
296
+ throw new Error(reason === undefined
297
+ ? "Lost supervisor lease"
298
+ : `Lost supervisor lease: ${String(reason)}`);
299
+ }
274
300
  export async function scheduleRun(cwd, runId, compiled, options = {}) {
275
- return withRunLease(cwd, runId, async () => {
301
+ return withRunLease(cwd, runId, async (leaseSignal) => {
302
+ assertScheduleLeaseActive(leaseSignal);
276
303
  let run = await readRunRecord(cwd, runId);
277
304
  run = await resolveWorkflowBackend(run).refreshRun(cwd, run);
278
305
  if (isTerminalWorkflowStatus(run.status))
@@ -287,7 +314,8 @@ export async function scheduleRun(cwd, runId, compiled, options = {}) {
287
314
  if (compiledFlow.type !== WORKFLOW_RUN_TYPE) {
288
315
  throw new Error(`unsupported compiled workflow type: ${compiledFlow.type}`);
289
316
  }
290
- await scheduleDag(cwd, run, compiledFlow, options);
317
+ await scheduleDag(cwd, run, compiledFlow, options, leaseSignal);
318
+ assertScheduleLeaseActive(leaseSignal);
291
319
  run = await readRunRecord(cwd, run.runId);
292
320
  return run;
293
321
  });
@@ -299,13 +327,13 @@ export async function formatStatus(cwd) {
299
327
  const refreshed = (await readIndex(cwd).catch(() => cached)) ?? cached;
300
328
  if (refreshed.runs.length === 0)
301
329
  return "No workflow runs found.";
302
- return formatIndex(refreshed);
330
+ return formatIndex(cwd, refreshed);
303
331
  }
304
332
  await reconcileActiveRuns(cwd);
305
333
  const rebuilt = await updateIndex(cwd).catch(() => readIndex(cwd));
306
334
  if (!rebuilt || rebuilt.runs.length === 0)
307
335
  return "No workflow runs found.";
308
- return formatIndex(rebuilt);
336
+ return formatIndex(cwd, rebuilt);
309
337
  }
310
338
  export async function formatRunDetails(cwd, runIdOrPrefix) {
311
339
  const run = await refreshRun(cwd, runIdOrPrefix);
@@ -336,10 +364,12 @@ export async function formatLogs(cwd, runIdOrPrefix, taskId = "task-1", lineCoun
336
364
  return `${run.runId}/${task.taskId} output=${task.files.output}\n${tail || "(empty log)"}`;
337
365
  }
338
366
  export function formatRun(run, detail = "summary") {
367
+ const telemetry = summarizeWorkflowTelemetry(run);
339
368
  const lines = [
340
369
  `${run.runId} [${run.status}] type=${run.type} backend=${run.backend.type}/${run.backend.mode}`,
341
370
  `created=${run.createdAt} updated=${run.updatedAt}`,
342
371
  `tasks=${run.taskSummary.completed}/${run.taskSummary.total} completed, running=${run.taskSummary.running}, pending=${run.taskSummary.pending}, blocked=${run.taskSummary.blocked}, failed=${run.taskSummary.failed}, interrupted=${run.taskSummary.interrupted}`,
372
+ `completion=${telemetry.completion.health}, outputRetries=${telemetry.retryCounts.output}, launchRetries=${telemetry.retryCounts.launch}, resumeEvents=${telemetry.resumeCounts.events}, contextLimitFailures=${telemetry.completion.contextLimitFailures}`,
343
373
  ];
344
374
  for (const task of run.tasks) {
345
375
  lines.push(formatTask(task, detail));
@@ -372,7 +402,8 @@ async function recordSupervisorError(cwd, runId, error) {
372
402
  error: error instanceof Error ? error.message : String(error),
373
403
  }).catch(() => undefined);
374
404
  }
375
- async function scheduleDag(cwd, run, compiledFlow, options = {}) {
405
+ async function scheduleDag(cwd, run, compiledFlow, options = {}, leaseSignal) {
406
+ assertScheduleLeaseActive(leaseSignal);
376
407
  if (compiledFlow.type === WORKFLOW_RUN_TYPE) {
377
408
  const loopReconciled = await reconcileLoopTaskMaterialization(cwd, run, compiledFlow);
378
409
  if (loopReconciled)
@@ -398,6 +429,7 @@ async function scheduleDag(cwd, run, compiledFlow, options = {}) {
398
429
  let running = run.tasks.filter((task) => task.status === "running").length;
399
430
  const bySpecId = new Map(run.tasks.map((task) => [task.specId, task]));
400
431
  for (let index = 0; index < run.tasks.length && running < maxConcurrency; index += 1) {
432
+ assertScheduleLeaseActive(leaseSignal);
401
433
  const task = run.tasks[index];
402
434
  const compiledTask = compiledFlow.tasks[index];
403
435
  if (!task || !compiledTask || task.status !== "pending")
@@ -417,6 +449,8 @@ async function scheduleDag(cwd, run, compiledFlow, options = {}) {
417
449
  const changed = await materializeForeachTask(cwd, run, compiledFlow, index, compiledTask);
418
450
  if (changed)
419
451
  return;
452
+ if (foreachStreamingEnabled(compiledTask))
453
+ continue;
420
454
  }
421
455
  if (compiledTask.stageMaxConcurrency !== undefined) {
422
456
  const runningInStage = run.tasks.filter((candidate) => candidate.stageId === compiledTask.stageId &&
@@ -425,7 +459,9 @@ async function scheduleDag(cwd, run, compiledFlow, options = {}) {
425
459
  Math.max(1, Math.min(MAX_CONCURRENCY, compiledTask.stageMaxConcurrency)))
426
460
  continue;
427
461
  }
462
+ assertScheduleLeaseActive(leaseSignal);
428
463
  const launched = await launchPendingTaskAt(cwd, run, compiledFlow, index, options);
464
+ assertScheduleLeaseActive(leaseSignal);
429
465
  if (launched && run.tasks[index]?.status === "running")
430
466
  running += 1;
431
467
  }
@@ -505,10 +541,12 @@ async function materializeForeachTask(cwd, run, compiledFlow, index, template) {
505
541
  return false;
506
542
  const sourceStageIds = sourceStageIdsForFrom(template.foreach.from);
507
543
  const sourceTasks = run.tasks.filter((task) => sourceStageIds.includes(task.stageId ?? ""));
544
+ const streaming = foreachStreamingEnabled(template);
508
545
  const extracted = await extractArtifactGraphForeachItems(cwd, {
509
546
  from: template.foreach.from,
510
547
  sourcePolicy: stageSourcePolicy(compiledFlow, template.stageId),
511
548
  maxItems: template.foreach.maxItems,
549
+ streaming,
512
550
  }, sourceTasks);
513
551
  if (extracted.error) {
514
552
  setTaskTerminal(templateRunTask, "blocked", "foreach_expansion_blocked", {
@@ -527,6 +565,21 @@ async function materializeForeachTask(cwd, run, compiledFlow, index, template) {
527
565
  return true;
528
566
  }
529
567
  const placeholderSpecId = template.id;
568
+ if (streaming) {
569
+ return await materializeStreamingForeachTask({
570
+ cwd,
571
+ run,
572
+ compiledFlow,
573
+ index,
574
+ templateRunTask,
575
+ placeholderSpecId,
576
+ sourceTaskSpecIds: sourceTasks.map((task) => task.specId),
577
+ itemMetas: extracted.itemMetas ?? [],
578
+ generatedTasks: generated.tasks,
579
+ waitingForSources: extracted.waitingForSources ?? false,
580
+ minChunk: foreachStreamingMinChunk(template),
581
+ });
582
+ }
530
583
  const generatedSpecIds = generated.tasks.map((task) => task.id);
531
584
  const hasDownstreamDependents = compiledFlow.tasks.some((task, taskIndex) => taskIndex !== index && (task.dependsOn ?? []).includes(placeholderSpecId));
532
585
  if (generatedSpecIds.length === 0 && !hasDownstreamDependents) {
@@ -550,16 +603,183 @@ async function materializeForeachTask(cwd, run, compiledFlow, index, template) {
550
603
  await writeRunRecord(cwd, run);
551
604
  return true;
552
605
  }
606
+ async function materializeStreamingForeachTask(input) {
607
+ const sourceTaskSpecIdSet = new Set(input.sourceTaskSpecIds);
608
+ const generatedTasksWithItemDeps = input.generatedTasks.map((task, index) => {
609
+ const itemMeta = input.itemMetas[index];
610
+ if (!itemMeta)
611
+ return task;
612
+ return {
613
+ ...task,
614
+ dependsOn: replaceSourceDependenciesWithItemSource(task.dependsOn ?? [], sourceTaskSpecIdSet, itemMeta, {
615
+ keepPartialSourceDependency: partialGeneratedTaskNeedsCompletedSourceContext(task),
616
+ }),
617
+ foreachGenerated: {
618
+ ...(task.foreachGenerated ?? {
619
+ placeholderSpecId: input.placeholderSpecId,
620
+ }),
621
+ itemHash: itemMeta.itemHash,
622
+ itemSourceSpecId: itemMeta.sourceSpecId,
623
+ itemSourceKind: itemMeta.sourceKind,
624
+ itemRef: itemMeta.itemRef,
625
+ },
626
+ };
627
+ });
628
+ const existingGeneratedTasks = input.compiledFlow.tasks.filter((task) => task.foreachGenerated?.placeholderSpecId === input.placeholderSpecId);
629
+ const existingGeneratedSpecIds = existingGeneratedTasks.map((task) => task.id);
630
+ const existingGeneratedTaskBySpecId = new Map(existingGeneratedTasks.map((task) => [task.id, task]));
631
+ for (const task of generatedTasksWithItemDeps) {
632
+ const existing = existingGeneratedTaskBySpecId.get(task.id);
633
+ const existingHash = existing?.foreachGenerated?.itemHash;
634
+ const nextHash = task.foreachGenerated?.itemHash;
635
+ if (existing && existingHash && nextHash && existingHash !== nextHash) {
636
+ setTaskTerminal(input.templateRunTask, "blocked", "foreach_expansion_blocked", {
637
+ lastMessage: `foreach streaming item ${task.id} changed after materialization`,
638
+ });
639
+ await writeRunRecord(input.cwd, input.run);
640
+ return true;
641
+ }
642
+ }
643
+ const existingGeneratedSpecIdSet = new Set(existingGeneratedSpecIds);
644
+ const finalGeneratedSpecIdSet = new Set(generatedTasksWithItemDeps.map((task) => task.id));
645
+ if (!input.waitingForSources) {
646
+ const withdrawn = existingGeneratedTasks.find((task) => task.foreachGenerated?.itemSourceKind === "partial" &&
647
+ !finalGeneratedSpecIdSet.has(task.id));
648
+ if (withdrawn) {
649
+ setTaskTerminal(input.templateRunTask, "blocked", "foreach_expansion_blocked", {
650
+ lastMessage: `foreach streaming item ${withdrawn.id} was published as partial output but is missing from final control`,
651
+ });
652
+ await writeRunRecord(input.cwd, input.run);
653
+ return true;
654
+ }
655
+ }
656
+ const newGeneratedTasks = generatedTasksWithItemDeps.filter((task) => !existingGeneratedSpecIdSet.has(task.id));
657
+ const allGeneratedSpecIds = [
658
+ ...existingGeneratedSpecIds,
659
+ ...newGeneratedTasks.map((task) => task.id),
660
+ ];
661
+ const shouldHoldForMinChunk = input.waitingForSources &&
662
+ newGeneratedTasks.length > 0 &&
663
+ newGeneratedTasks.length < input.minChunk;
664
+ if (shouldHoldForMinChunk)
665
+ return false;
666
+ let changed = false;
667
+ if (newGeneratedTasks.length > 0) {
668
+ let compiledInsertIndex = input.index + 1;
669
+ while (input.compiledFlow.tasks[compiledInsertIndex]?.foreachGenerated
670
+ ?.placeholderSpecId === input.placeholderSpecId) {
671
+ compiledInsertIndex += 1;
672
+ }
673
+ input.compiledFlow.tasks.splice(compiledInsertIndex, 0, ...newGeneratedTasks);
674
+ let runInsertIndex = input.index + 1;
675
+ while (input.run.tasks[runInsertIndex]?.foreachGenerated?.placeholderSpecId ===
676
+ input.placeholderSpecId) {
677
+ runInsertIndex += 1;
678
+ }
679
+ const nextIndex = nextTaskRecordIndex(input.run);
680
+ const generatedRunTasks = newGeneratedTasks.map((task, offset) => createTaskRunRecord(input.cwd, input.run.runId, task, nextIndex + offset));
681
+ input.run.tasks.splice(runInsertIndex, 0, ...generatedRunTasks);
682
+ changed = true;
683
+ }
684
+ const dependencyTargets = [input.placeholderSpecId, ...allGeneratedSpecIds];
685
+ for (const task of input.compiledFlow.tasks) {
686
+ if (!task.dependsOn)
687
+ continue;
688
+ const replaced = replaceDependencyList(task.dependsOn, input.placeholderSpecId, dependencyTargets);
689
+ if (JSON.stringify(task.dependsOn) !== JSON.stringify(replaced)) {
690
+ task.dependsOn = replaced;
691
+ changed = true;
692
+ }
693
+ }
694
+ for (const task of input.run.tasks) {
695
+ if (!task.dependsOn)
696
+ continue;
697
+ const replaced = replaceDependencyList(task.dependsOn, input.placeholderSpecId, dependencyTargets);
698
+ if (JSON.stringify(task.dependsOn) !== JSON.stringify(replaced)) {
699
+ task.dependsOn = replaced;
700
+ changed = true;
701
+ }
702
+ }
703
+ if (!input.waitingForSources) {
704
+ const statusDetail = allGeneratedSpecIds.length === 0
705
+ ? "foreach_empty"
706
+ : "foreach_streaming_complete";
707
+ const lastMessage = allGeneratedSpecIds.length === 0
708
+ ? "foreach produced 0 item(s)"
709
+ : `foreach streaming materialized ${allGeneratedSpecIds.length} item(s)`;
710
+ setTaskTerminal(input.templateRunTask, "completed", statusDetail, {
711
+ lastMessage,
712
+ });
713
+ changed = true;
714
+ }
715
+ else if (newGeneratedTasks.length > 0) {
716
+ input.templateRunTask.statusDetail = "foreach_streaming_waiting";
717
+ input.templateRunTask.lastMessage = `foreach streaming materialized ${allGeneratedSpecIds.length} item(s); waiting for more source tasks`;
718
+ changed = true;
719
+ }
720
+ if (!changed)
721
+ return false;
722
+ await writeJsonAtomic(compiledWorkflowPath(input.cwd, input.run.runId), input.compiledFlow);
723
+ await writeRunRecord(input.cwd, input.run);
724
+ return true;
725
+ }
726
+ function replaceSourceDependenciesWithItemSource(dependsOn, sourceTaskSpecIds, itemMeta, options = {}) {
727
+ const replaced = [];
728
+ let inserted = false;
729
+ const shouldReplaceWithSource = itemMeta.sourceKind !== "partial" ||
730
+ options.keepPartialSourceDependency === true;
731
+ for (const dep of dependsOn) {
732
+ if (!sourceTaskSpecIds.has(dep)) {
733
+ replaced.push(dep);
734
+ continue;
735
+ }
736
+ if (!shouldReplaceWithSource)
737
+ continue;
738
+ if (!inserted) {
739
+ replaced.push(itemMeta.sourceSpecId);
740
+ inserted = true;
741
+ }
742
+ }
743
+ if (!inserted && shouldReplaceWithSource) {
744
+ replaced.push(itemMeta.sourceSpecId);
745
+ }
746
+ return [...new Set(replaced)];
747
+ }
748
+ function partialGeneratedTaskNeedsCompletedSourceContext(task) {
749
+ const artifactGraph = task.artifactGraph;
750
+ if (artifactGraph?.artifactAccess === "none")
751
+ return false;
752
+ return Boolean(artifactGraph?.sourceProjection !== undefined ||
753
+ (artifactGraph?.requiredReads?.length ?? 0) > 0);
754
+ }
553
755
  async function extractArtifactGraphForeachItems(cwd, stage, sourceTasks) {
554
756
  const items = [];
757
+ const itemMetas = [];
555
758
  const path = stage.from?.path;
556
759
  if (typeof path !== "string" || !path.startsWith("$.")) {
557
760
  return {
558
761
  error: "foreach.from.path must be a control JSONPath like $.items",
559
762
  };
560
763
  }
764
+ let waitingForSources = false;
561
765
  for (const task of sourceTasks) {
562
766
  if (task.status !== "completed") {
767
+ if (stage.streaming && !isTerminalTaskStatus(task.status)) {
768
+ const partial = await extractPartialForeachItems(cwd, task, path);
769
+ if (partial.error)
770
+ return { error: partial.error };
771
+ for (const item of partial.items) {
772
+ items.push(item.item);
773
+ itemMetas.push({
774
+ sourceSpecId: task.specId,
775
+ sourceKind: "partial",
776
+ itemHash: item.itemHash,
777
+ itemRef: `${task.specId}:${item.itemRef}`,
778
+ });
779
+ }
780
+ waitingForSources = true;
781
+ continue;
782
+ }
563
783
  if (stage.sourcePolicy !== "partial")
564
784
  return { error: `${task.taskId} did not complete` };
565
785
  continue;
@@ -575,7 +795,15 @@ async function extractArtifactGraphForeachItems(cwd, stage, sourceTasks) {
575
795
  }
576
796
  continue;
577
797
  }
578
- items.push(...value);
798
+ for (const [index, item] of value.entries()) {
799
+ items.push(item);
800
+ itemMetas.push({
801
+ sourceSpecId: task.specId,
802
+ sourceKind: "control",
803
+ itemHash: hashDynamicRequest(item),
804
+ itemRef: `${task.specId}:control:${path}[${index}]`,
805
+ });
806
+ }
579
807
  }
580
808
  catch (error) {
581
809
  if (stage.sourcePolicy !== "partial") {
@@ -590,7 +818,27 @@ async function extractArtifactGraphForeachItems(cwd, stage, sourceTasks) {
590
818
  error: `foreach extracted ${items.length} items, exceeding maxItems=${stage.maxItems}`,
591
819
  };
592
820
  }
593
- return { items };
821
+ return { items, itemMetas, waitingForSources };
822
+ }
823
+ async function extractPartialForeachItems(cwd, task, path) {
824
+ const partialPaths = task.artifactGraph?.output.partial?.paths ?? [];
825
+ if (!partialPaths.includes(path))
826
+ return { items: [] };
827
+ const taskDir = dirname(fromProjectPath(cwd, task.files.result));
828
+ let ledger = await readWorkflowPartialOutputLedger(taskDir).catch(() => undefined);
829
+ if (!ledger) {
830
+ ledger = await writeWorkflowPartialOutputLedgerFromFile({
831
+ taskDir,
832
+ outputFile: fromProjectPath(cwd, task.files.output),
833
+ allowedPaths: partialPaths,
834
+ }).catch(() => undefined);
835
+ }
836
+ if (!ledger)
837
+ return { items: [] };
838
+ const fatal = hasFatalPartialOutputIssue(ledger);
839
+ if (fatal)
840
+ return { items: [], error: fatal.message };
841
+ return { items: ledger.items.filter((item) => item.path === path) };
594
842
  }
595
843
  async function launchPendingTaskAt(cwd, run, compiledFlow, index, options = {}) {
596
844
  const task = run.tasks[index];
@@ -1900,21 +2148,36 @@ function uniqueStrings(values) {
1900
2148
  async function readCompiledWorkflow(cwd, runId) {
1901
2149
  return readJson(compiledWorkflowPath(cwd, runId));
1902
2150
  }
1903
- function formatIndex(index) {
1904
- return index.runs
1905
- .map((run) => {
2151
+ async function formatIndex(cwd, index) {
2152
+ const blocks = await Promise.all(index.runs.map(async (run) => {
1906
2153
  const lines = [
1907
2154
  `${run.runId} [${run.status}] type=${run.type} updated=${run.updatedAt}`,
1908
2155
  `tasks=${run.taskSummary.completed}/${run.taskSummary.total} completed, running=${run.taskSummary.running}, pending=${run.taskSummary.pending}, blocked=${run.taskSummary.blocked}, failed=${run.taskSummary.failed}, skipped=${run.taskSummary.skipped}, interrupted=${run.taskSummary.interrupted}`,
1909
2156
  ];
1910
- for (const task of run.tasks) {
2157
+ for (const task of await indexTasksForStatus(cwd, run)) {
1911
2158
  const message = task.lastMessage ? ` — ${task.lastMessage}` : "";
1912
2159
  const kind = task.kind && task.kind !== "main" ? ` ${task.kind}` : "";
1913
2160
  lines.push(`- ${task.taskId}${kind} ${task.agent} [${task.status}/${task.statusDetail}]${message}`);
1914
2161
  }
1915
2162
  return lines.join("\n");
1916
- })
1917
- .join("\n\n");
2163
+ }));
2164
+ return blocks.join("\n\n");
2165
+ }
2166
+ async function indexTasksForStatus(cwd, run) {
2167
+ if (Array.isArray(run.tasks))
2168
+ return run.tasks;
2169
+ const fullRun = await readRunRecord(cwd, run.runId).catch(() => undefined);
2170
+ return (fullRun?.tasks.map((task) => ({
2171
+ taskId: task.taskId,
2172
+ displayName: task.displayName,
2173
+ agent: task.agent,
2174
+ kind: task.kind,
2175
+ stageId: task.stageId,
2176
+ backendHandle: task.backendHandle,
2177
+ status: task.status,
2178
+ statusDetail: task.statusDetail,
2179
+ lastMessage: task.lastMessage,
2180
+ })) ?? []);
1918
2181
  }
1919
2182
  function formatTask(task, detail) {
1920
2183
  const elapsed = task.elapsedMs !== undefined
package/dist/extension.js CHANGED
@@ -73,7 +73,7 @@ const WORKFLOW_DYNAMIC_TOOL_PARAMETERS = {
73
73
  };
74
74
  export default function workflowExtension(pi) {
75
75
  let workflowCompletionCache = [];
76
- pi.on("session_start", async (_event, ctx) => {
76
+ pi.on("session_start", async (event, ctx) => {
77
77
  if (!isWorkflowSupervisorEnabled())
78
78
  return;
79
79
  workflowCompletionCache = await listWorkflows(ctx.cwd).catch(() => workflowCompletionCache);
@@ -81,7 +81,8 @@ export default function workflowExtension(pi) {
81
81
  dynamicUi: dynamicUiFromContext(ctx),
82
82
  }).catch(() => undefined);
83
83
  await notifyUnfinishedRuns(ctx.cwd, (message, type) => ctx.ui.notify(message, type)).catch(() => undefined);
84
- await deliverMissedWorkflowFeedback(ctx, pi).catch(() => undefined);
84
+ if (event.reason !== "reload")
85
+ await deliverMissedWorkflowFeedback(ctx, pi).catch(() => undefined);
85
86
  });
86
87
  registerWorkflowNaturalLanguageTools(pi);
87
88
  pi.registerCommand(WORKFLOW_COMMAND, {
package/dist/index.d.ts CHANGED
@@ -10,5 +10,13 @@ export type { AgentDefinition, ApprovalMode, BackendOptions, CompiledWorkflow, C
10
10
  export { WorkflowValidationError } from "./types.js";
11
11
  export { runDynamicDecisionLoop } from "./dynamic-decision-loop.js";
12
12
  export type { DynamicDecisionLoopControllerContext, DynamicDecisionLoopResult, DynamicDecisionLoopRunResult, RunDynamicDecisionLoopOptions, } from "./dynamic-decision-loop.js";
13
+ export { assertValidDynamicDecision, validateDynamicDecision, } from "./dynamic-decision.js";
14
+ export type { DynamicDecisionAction, DynamicDecisionPhase, DynamicDecisionStatus, DynamicDecisionValidationContext, DynamicDecisionValidationResult, NormalizedDynamicDecision, } from "./dynamic-decision.js";
15
+ export { dynamicOutputProfileValues } from "./dynamic-profiles.js";
16
+ export type { DynamicOutputProfile } from "./dynamic-profiles.js";
17
+ export { buildWorkflowRunMetrics, WORKFLOW_METRICS_PRICING_MODEL_VERSION, WORKFLOW_METRICS_SCHEMA_VERSION, } from "./workflow-metrics.js";
18
+ export { VERIFICATION_STATUS, VERIFICATION_STATUS_BUCKETS, VERIFICATION_STATUS_LABELS, VERIFICATION_STATUS_VALUES, canonicalVerificationStatus, isNonVerifiedTerminalStatus, isVerificationBlockedStatus, isVerifiedStatus, verificationStatusBucket, } from "./verification-ontology.js";
19
+ export type { TerminalVerificationStatus, VerificationStatus, } from "./verification-ontology.js";
20
+ export type { WorkflowLaunchTimingMetrics, WorkflowMetricValue, WorkflowMetricsPricingModelVersion, WorkflowMetricsPricingSource, WorkflowMetricsSchemaVersion, WorkflowRetryMetrics, WorkflowRunMetrics, WorkflowRunMetricsMetadata, WorkflowRunMetricsRollup, WorkflowStageMetrics, WorkflowTaskMetrics, WorkflowTaskStatusCounts, WorkflowUsageMetrics, } from "./workflow-metrics.js";
13
21
  export declare const WORKFLOW_COMMAND = "workflow";
14
22
  export declare const WORKFLOW_HELP = "pi-workflow\n\nUsage:\n /workflow [run-id]\n /workflow help\n /workflow validate <workflow-name-or-path>\n /workflow roles <workflow-name-or-path>\n /workflow agents\n /workflow list\n /workflow run [--model MODEL] [--thinking LEVEL] <workflow-name-or-path> \"<task>\" [--detach]\n /workflow dynamic [--model MODEL] [--thinking LEVEL] \"<task>\" [--detach]\n /workflow status [run-id]\n /workflow show <run-id-or-workflow-name>\n /workflow logs <run-id> [task-id] [lines]\n /workflow wait <run-id> [timeout-ms]\n /workflow resume <run-id>\n /workflow stop <run-id>\n\n/workflow opens the read-only workflow board TUI.\n/workflow <run-id> opens the board focused on that run.\n/workflow dynamic starts a spec-less direct dynamic run: no workflow name,\nuser-selected spec, or generated workflow spec is required.\n\nWith --detach, a standalone supervisor process (pi-workflow supervise) keeps\nthe run progressing after this session exits.\n";
package/dist/index.js CHANGED
@@ -6,6 +6,10 @@ export { loadWorkflow, loadWorkflowSpec, parseWorkflow } from "./schema.js";
6
6
  export { parseArtifactGraphWorkflowSpec } from "./artifact-graph-schema.js";
7
7
  export { WorkflowValidationError } from "./types.js";
8
8
  export { runDynamicDecisionLoop } from "./dynamic-decision-loop.js";
9
+ export { assertValidDynamicDecision, validateDynamicDecision, } from "./dynamic-decision.js";
10
+ export { dynamicOutputProfileValues } from "./dynamic-profiles.js";
11
+ export { buildWorkflowRunMetrics, WORKFLOW_METRICS_PRICING_MODEL_VERSION, WORKFLOW_METRICS_SCHEMA_VERSION, } from "./workflow-metrics.js";
12
+ export { VERIFICATION_STATUS, VERIFICATION_STATUS_BUCKETS, VERIFICATION_STATUS_LABELS, VERIFICATION_STATUS_VALUES, canonicalVerificationStatus, isNonVerifiedTerminalStatus, isVerificationBlockedStatus, isVerifiedStatus, verificationStatusBucket, } from "./verification-ontology.js";
9
13
  export const WORKFLOW_COMMAND = "workflow";
10
14
  export const WORKFLOW_HELP = `pi-workflow
11
15
 
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Serialize JSON embedded directly in prompts/model context.
3
+ *
4
+ * Persisted artifacts can stay pretty-printed for humans, but prompt context
5
+ * should avoid indentation bytes when the JSON data is otherwise identical.
6
+ */
7
+ export declare function stringifyPromptJson(value: unknown): string;
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Serialize JSON embedded directly in prompts/model context.
3
+ *
4
+ * Persisted artifacts can stay pretty-printed for humans, but prompt context
5
+ * should avoid indentation bytes when the JSON data is otherwise identical.
6
+ */
7
+ export function stringifyPromptJson(value) {
8
+ const serialized = JSON.stringify(value);
9
+ if (serialized === undefined) {
10
+ throw new TypeError("prompt JSON value must be JSON-serializable");
11
+ }
12
+ return serialized;
13
+ }
package/dist/roles.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { AgentDefinition, CompiledRole, RoleSpec } from "./types.js";
1
+ import type { AgentDefinition, CompiledRole, RoleSpec } from "./types.js";
2
2
  export declare const DEFAULT_SAFE_SECTIONS: readonly ["Core Principles", "Domain Expertise", "Safety Review", "Rules", "Research Manifest"];
3
3
  export declare function compileRole(name: string, spec: RoleSpec, sourceAgent?: AgentDefinition): CompiledRole;
4
4
  export declare function extractMarkdownSections(markdown: string, includeSections: readonly string[], excludeSections: readonly string[]): string;
package/dist/roles.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { compactStrings } from "./strings.js";
1
2
  export const DEFAULT_SAFE_SECTIONS = [
2
3
  "Core Principles",
3
4
  "Domain Expertise",
@@ -19,14 +20,10 @@ export function compileRole(name, spec, sourceAgent) {
19
20
  const maxChars = spec.maxChars ?? DEFAULT_MAX_ROLE_CHARS;
20
21
  const includeSections = spec.includeSections ?? [...DEFAULT_SAFE_SECTIONS];
21
22
  const excludedSections = [...ALWAYS_EXCLUDED_SECTIONS, ...(spec.excludeSections ?? [])];
22
- const parts = [];
23
- if (sourceAgent) {
24
- const extracted = extractMarkdownSections(sourceAgent.body, includeSections, excludedSections);
25
- if (extracted.trim() !== "")
26
- parts.push(extracted.trim());
27
- }
28
- if (spec.prompt?.trim())
29
- parts.push(spec.prompt.trim());
23
+ const parts = compactStrings([
24
+ sourceAgent ? extractMarkdownSections(sourceAgent.body, includeSections, excludedSections) : undefined,
25
+ spec.prompt,
26
+ ], { unique: false });
30
27
  const fullContent = parts.join("\n\n");
31
28
  const truncated = fullContent.length > maxChars;
32
29
  return {
package/dist/store.d.ts CHANGED
@@ -1,4 +1,20 @@
1
1
  import { type CompiledWorkflow, type CompiledTask, type WorkflowIndexRecord, type WorkflowRunRecord, type WorkflowRunStatus, type WorkflowTaskRunRecord, type TaskRunStatus, type TaskSummary } from "./types.js";
2
+ type RunLeaseTestHooks = {
3
+ heartbeatIntervalMs?: number;
4
+ onAfterReclaimRename?: (context: {
5
+ lockFile: string;
6
+ reclaimFile: string;
7
+ }) => void | Promise<void>;
8
+ onBeforeRestoreReclaimFile?: (context: {
9
+ lockFile: string;
10
+ reclaimFile: string;
11
+ }) => void | Promise<void>;
12
+ onBeforeReleaseLockRename?: (context: {
13
+ lockFile: string;
14
+ releaseFile: string;
15
+ ownerId: string;
16
+ }) => void | Promise<void>;
17
+ };
2
18
  export declare function nowIso(): string;
3
19
  export declare function makeRunId(): string;
4
20
  export declare function workflowsRoot(cwd: string): string;
@@ -15,7 +31,8 @@ export declare function fromProjectPath(cwd: string, filePath: string): string;
15
31
  export declare function ensureDir(dir: string): Promise<void>;
16
32
  export declare function readJson<T>(file: string): Promise<T | undefined>;
17
33
  export declare function writeJsonAtomic(file: string, value: unknown): Promise<void>;
18
- export declare function withRunLease<T>(cwd: string, runId: string, action: () => Promise<T>): Promise<T | undefined>;
34
+ export declare function setRunLeaseTestHooksForTests(hooks?: RunLeaseTestHooks): void;
35
+ export declare function withRunLease<T>(cwd: string, runId: string, action: (abortSignal: AbortSignal) => Promise<T>): Promise<T | undefined>;
19
36
  export declare function createRunRecord(cwd: string, compiled: CompiledWorkflow, specPath: string, options?: {
20
37
  runId?: string;
21
38
  parentRunId?: string;
@@ -44,6 +61,7 @@ export declare function setTaskTerminal(task: WorkflowTaskRunRecord, status: Tas
44
61
  exitCode?: number;
45
62
  lastMessage?: string;
46
63
  }): boolean;
64
+ export declare function isBlockedTaskResumableForResume(task: Pick<WorkflowTaskRunRecord, "status" | "statusDetail">): boolean;
47
65
  export declare function resetTaskForResume(task: WorkflowTaskRunRecord): boolean;
48
66
  export declare function createTaskRunRecord(cwd: string, runId: string, task: CompiledTask, index: number): WorkflowTaskRunRecord;
49
67
  export declare function resolveFlowsCwd(cwd: string): Promise<string>;
@@ -56,3 +74,4 @@ export declare function workflowSupervisorOwnerIdForTests(): string;
56
74
  export declare function workflowProcessRoleForTests(): string;
57
75
  export declare function acquireSupervisorLease(cwd: string, runId: string): Promise<boolean>;
58
76
  export declare function heartbeatSupervisorLease(cwd: string, runId: string): Promise<boolean>;
77
+ export {};