pi-subagents 0.25.0 → 0.27.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 (38) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +129 -17
  3. package/package.json +1 -1
  4. package/prompts/parallel-context-build.md +3 -1
  5. package/prompts/parallel-handoff-plan.md +3 -1
  6. package/skills/pi-subagents/SKILL.md +32 -17
  7. package/src/agents/agent-management.ts +57 -15
  8. package/src/agents/agent-serializer.ts +3 -2
  9. package/src/agents/agents.ts +47 -16
  10. package/src/agents/chain-serializer.ts +120 -0
  11. package/src/extension/fanout-child.ts +1 -0
  12. package/src/extension/index.ts +1 -0
  13. package/src/extension/schemas.ts +138 -5
  14. package/src/runs/background/async-execution.ts +84 -6
  15. package/src/runs/background/async-status.ts +11 -1
  16. package/src/runs/background/run-status.ts +10 -1
  17. package/src/runs/background/subagent-runner.ts +600 -31
  18. package/src/runs/foreground/chain-execution.ts +325 -118
  19. package/src/runs/foreground/execution.ts +222 -10
  20. package/src/runs/foreground/subagent-executor.ts +67 -0
  21. package/src/runs/shared/acceptance-contract.ts +291 -0
  22. package/src/runs/shared/acceptance-evaluation.ts +221 -0
  23. package/src/runs/shared/acceptance-finalization.ts +161 -0
  24. package/src/runs/shared/acceptance-reports.ts +127 -0
  25. package/src/runs/shared/acceptance.ts +22 -0
  26. package/src/runs/shared/chain-outputs.ts +101 -0
  27. package/src/runs/shared/completion-guard.ts +26 -3
  28. package/src/runs/shared/dynamic-fanout.ts +293 -0
  29. package/src/runs/shared/parallel-utils.ts +31 -1
  30. package/src/runs/shared/pi-args.ts +11 -0
  31. package/src/runs/shared/structured-output.ts +77 -0
  32. package/src/runs/shared/subagent-prompt-runtime.ts +53 -3
  33. package/src/runs/shared/workflow-graph.ts +206 -0
  34. package/src/shared/formatters.ts +2 -2
  35. package/src/shared/settings.ts +53 -4
  36. package/src/shared/types.ts +250 -0
  37. package/src/slash/slash-commands.ts +41 -3
  38. package/src/tui/render.ts +162 -34
package/src/tui/render.ts CHANGED
@@ -14,6 +14,7 @@ import {
14
14
  type Details,
15
15
  type NestedRunSummary,
16
16
  type NestedStepSummary,
17
+ type WorkflowNodeStatus,
17
18
  MAX_WIDGET_JOBS,
18
19
  WIDGET_KEY,
19
20
  } from "../shared/types.ts";
@@ -221,10 +222,21 @@ function firstOutputLine(text: string): string {
221
222
  return text.split("\n").find((line) => line.trim())?.trim() ?? "";
222
223
  }
223
224
 
225
+ function formatAcceptanceStatus(result: Details["results"][number]): string | undefined {
226
+ const acceptance = result.acceptance;
227
+ if (!acceptance?.status || acceptance.status === "not-required") return undefined;
228
+ const finalization = acceptance.finalization
229
+ ? ` · finalization: ${acceptance.finalization.status} after ${acceptance.finalization.turns.length}/${acceptance.finalization.maxTurns} turns`
230
+ : "";
231
+ return `acceptance: ${acceptance.status}${finalization}`;
232
+ }
233
+
224
234
  function resultStatusLine(result: Details["results"][number], output: string): string {
225
235
  if (result.detached) return result.detachedReason ? `Detached: ${result.detachedReason}` : "Detached";
226
236
  if (result.interrupted) return "Paused";
227
237
  if (result.exitCode !== 0) return `Error: ${result.error ?? (firstOutputLine(output) || `exit ${result.exitCode}`)}`;
238
+ const acceptance = formatAcceptanceStatus(result);
239
+ if (acceptance) return `Done · ${acceptance}`;
228
240
  if (hasEmptyTextOutputWithoutOutputTarget(result.task, output)) return "Done (no text output)";
229
241
  return "Done";
230
242
  }
@@ -427,26 +439,44 @@ function parseParallelGroupAgentCount(label: string | undefined): number | undef
427
439
  return inner.split("+").map((part) => part.trim()).filter(Boolean).length;
428
440
  }
429
441
 
430
- function isChainParallelGroupActive(details: Pick<Details, "mode" | "chainAgents" | "currentStepIndex">): boolean {
431
- if (details.mode !== "chain") return false;
432
- if (details.currentStepIndex === undefined) return false;
433
- const currentLabel = details.chainAgents?.[details.currentStepIndex];
434
- return parseParallelGroupAgentCount(currentLabel) !== undefined;
435
- }
436
-
437
442
  interface ChainStepSpan {
438
443
  stepIndex: number;
439
444
  start: number;
440
445
  count: number;
441
446
  isParallel: boolean;
442
- }
447
+ status?: WorkflowNodeStatus;
448
+ label?: string;
449
+ error?: string;
450
+ }
451
+
452
+ function buildChainStepSpans(details: Pick<Details, "chainAgents" | "workflowGraph">): ChainStepSpan[] {
453
+ if (details.workflowGraph?.nodes?.length) {
454
+ const spans: ChainStepSpan[] = [];
455
+ let flatCursor = 0;
456
+ for (const node of details.workflowGraph.nodes) {
457
+ if (node.stepIndex === undefined) continue;
458
+ if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
459
+ const childFlatIndexes = (node.children ?? [])
460
+ .map((child) => child.flatIndex)
461
+ .filter((value): value is number => typeof value === "number");
462
+ const start = childFlatIndexes.length ? Math.min(...childFlatIndexes) : flatCursor;
463
+ const count = node.children?.length ?? 0;
464
+ spans.push({ stepIndex: node.stepIndex, start, count, isParallel: true, status: node.status, label: node.label, error: node.error });
465
+ flatCursor = Math.max(flatCursor, start + count);
466
+ continue;
467
+ }
468
+ const start = node.flatIndex ?? flatCursor;
469
+ spans.push({ stepIndex: node.stepIndex, start, count: 1, isParallel: false, status: node.status, label: node.label, error: node.error });
470
+ flatCursor = Math.max(flatCursor, start + 1);
471
+ }
472
+ if (spans.length) return spans.sort((left, right) => left.stepIndex - right.stepIndex);
473
+ }
443
474
 
444
- function buildChainStepSpans(chainAgents: string[] | undefined): ChainStepSpan[] {
445
- if (!chainAgents?.length) return [];
475
+ if (!details.chainAgents?.length) return [];
446
476
  const spans: ChainStepSpan[] = [];
447
477
  let start = 0;
448
- for (let stepIndex = 0; stepIndex < chainAgents.length; stepIndex++) {
449
- const label = chainAgents[stepIndex]!;
478
+ for (let stepIndex = 0; stepIndex < details.chainAgents.length; stepIndex++) {
479
+ const label = details.chainAgents[stepIndex]!;
450
480
  const parsedCount = parseParallelGroupAgentCount(label);
451
481
  const count = parsedCount ?? 1;
452
482
  spans.push({ stepIndex, start, count, isParallel: parsedCount !== undefined });
@@ -455,6 +485,12 @@ function buildChainStepSpans(chainAgents: string[] | undefined): ChainStepSpan[]
455
485
  return spans;
456
486
  }
457
487
 
488
+ function isChainParallelGroupActive(details: Pick<Details, "mode" | "chainAgents" | "currentStepIndex" | "workflowGraph">): boolean {
489
+ if (details.mode !== "chain") return false;
490
+ if (details.currentStepIndex === undefined) return false;
491
+ return buildChainStepSpans(details).some((span) => span.stepIndex === details.currentStepIndex && span.isParallel);
492
+ }
493
+
458
494
  function buildAsyncChainStepSpans(total: number, stepCount: number, parallelGroups: AsyncParallelGroupStatus[] = []): ChainStepSpan[] {
459
495
  const spans: ChainStepSpan[] = [];
460
496
  let flatIndex = 0;
@@ -479,6 +515,55 @@ function isDoneResult(result: Details["results"][number]): boolean {
479
515
  return result.exitCode === 0;
480
516
  }
481
517
 
518
+ function workflowGraphHasStatus(details: Pick<Details, "workflowGraph">, statuses: WorkflowNodeStatus[]): boolean {
519
+ return details.workflowGraph?.nodes.some((node) => statuses.includes(node.status)) ?? false;
520
+ }
521
+
522
+ interface ChainRenderResultEntry {
523
+ kind: "result";
524
+ resultIndex: number;
525
+ rowNumber: number;
526
+ agentName: string;
527
+ }
528
+
529
+ interface ChainRenderPlaceholderEntry {
530
+ kind: "placeholder";
531
+ rowNumber: number;
532
+ stepLabel: string;
533
+ agentName: string;
534
+ status: WorkflowNodeStatus;
535
+ error?: string;
536
+ }
537
+
538
+ type ChainRenderEntry = ChainRenderResultEntry | ChainRenderPlaceholderEntry;
539
+
540
+ function buildChainRenderEntries(details: Details, label: MultiProgressLabel): ChainRenderEntry[] | undefined {
541
+ if (details.mode !== "chain" || !label.hasParallelInChain || label.showActiveGroupOnly) return undefined;
542
+ const entries: ChainRenderEntry[] = [];
543
+ for (const span of buildChainStepSpans(details)) {
544
+ if (span.isParallel && span.count === 0) {
545
+ entries.push({
546
+ kind: "placeholder",
547
+ rowNumber: span.stepIndex + 1,
548
+ stepLabel: `Step ${span.stepIndex + 1}`,
549
+ agentName: span.label ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
550
+ status: span.status ?? "pending",
551
+ error: span.error,
552
+ });
553
+ continue;
554
+ }
555
+ for (let index = span.start; index < span.start + span.count; index++) {
556
+ entries.push({
557
+ kind: "result",
558
+ resultIndex: index,
559
+ rowNumber: index + 1,
560
+ agentName: details.results[index]?.agent ?? details.chainAgents?.[span.stepIndex] ?? `step-${span.stepIndex + 1}`,
561
+ });
562
+ }
563
+ }
564
+ return entries;
565
+ }
566
+
482
567
  interface MultiProgressLabel {
483
568
  headerLabel: string;
484
569
  itemTitle: "Step" | "Agent";
@@ -490,8 +575,8 @@ interface MultiProgressLabel {
490
575
  showActiveGroupOnly: boolean;
491
576
  }
492
577
 
493
- function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents">, hasRunning: boolean): MultiProgressLabel {
494
- const stepSpans = buildChainStepSpans(details.chainAgents);
578
+ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "progress" | "totalSteps" | "currentStepIndex" | "chainAgents" | "workflowGraph">, hasRunning: boolean): MultiProgressLabel {
579
+ const stepSpans = buildChainStepSpans(details);
495
580
  const hasParallelInChain = details.mode === "chain" && stepSpans.some((span) => span.isParallel);
496
581
  const activeParallelGroup = isChainParallelGroupActive(details);
497
582
  const itemTitle: "Step" | "Agent" = details.mode === "parallel" || activeParallelGroup ? "Agent" : "Step";
@@ -555,11 +640,13 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
555
640
  if (details.mode === "chain" && details.chainAgents?.length) {
556
641
  const totalCount = details.totalSteps ?? details.chainAgents.length;
557
642
  const doneLogical = stepSpans.filter((span) => {
643
+ if (span.status && span.status !== "completed") return false;
644
+ if (span.count === 0) return span.status === "completed";
558
645
  for (let index = span.start; index < span.start + span.count; index++) {
559
646
  const progressEntry = details.progress?.find((progress) => progress.index === index);
560
647
  const resultEntry = details.results.find((result) => result.progress?.index === index) ?? details.results[index];
561
- if (progressEntry?.status === "running" || progressEntry?.status === "pending") return false;
562
- if (resultEntry && !isDoneResult(resultEntry)) return false;
648
+ if (progressEntry?.status === "running" || progressEntry?.status === "pending" || progressEntry?.status === "failed") return false;
649
+ if (!resultEntry || !isDoneResult(resultEntry)) return false;
563
650
  }
564
651
  return true;
565
652
  }).length;
@@ -575,9 +662,9 @@ function buildMultiProgressLabel(details: Pick<Details, "mode" | "results" | "pr
575
662
  return { headerLabel, itemTitle, totalCount, hasParallelInChain, activeParallelGroup, groupStartIndex: 0, groupEndIndex: details.results.length, showActiveGroupOnly: false };
576
663
  }
577
664
 
578
- function resultRowLabel(details: Pick<Details, "mode" | "chainAgents">, label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
665
+ function resultRowLabel(details: Pick<Details, "mode" | "chainAgents" | "workflowGraph">, label: MultiProgressLabel, resultIndex: number, stepNumber: number): string {
579
666
  if (details.mode === "chain" && label.hasParallelInChain) {
580
- const span = buildChainStepSpans(details.chainAgents).find((candidate) => resultIndex >= candidate.start && resultIndex < candidate.start + candidate.count);
667
+ const span = buildChainStepSpans(details).find((candidate) => resultIndex >= candidate.start && resultIndex < candidate.start + candidate.count);
581
668
  if (span?.isParallel) return `Agent ${resultIndex - span.start + 1}/${span.count}`;
582
669
  if (span) return `Step ${span.stepIndex + 1}`;
583
670
  }
@@ -970,9 +1057,12 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
970
1057
 
971
1058
  function renderMultiCompact(d: Details, theme: Theme): Component {
972
1059
  const hasRunning = d.progress?.some((p) => p.status === "running")
973
- || d.results.some((r) => r.progress?.status === "running");
974
- const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running");
975
- const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running");
1060
+ || d.results.some((r) => r.progress?.status === "running")
1061
+ || workflowGraphHasStatus(d, ["running"]);
1062
+ const failed = d.results.some((r) => r.exitCode !== 0 && r.progress?.status !== "running")
1063
+ || workflowGraphHasStatus(d, ["failed"]);
1064
+ const paused = d.results.some((r) => (r.interrupted || r.detached) && r.progress?.status !== "running")
1065
+ || workflowGraphHasStatus(d, ["paused", "detached"]);
976
1066
  let totalSummary = d.progressSummary;
977
1067
  if (!totalSummary) {
978
1068
  let sawProgress = false;
@@ -1005,13 +1095,29 @@ function renderMultiCompact(d: Details, theme: Theme): Component {
1005
1095
  const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
1006
1096
  const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
1007
1097
  const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
1008
- for (let i = displayStart; i < displayEnd; i++) {
1098
+ const chainEntries = buildChainRenderEntries(d, multiLabel);
1099
+ const renderEntries = chainEntries ?? Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderEntry => {
1100
+ const i = displayStart + offset;
1009
1101
  const r = d.results[i];
1010
1102
  const fallbackLabel = itemTitle.toLowerCase();
1011
1103
  const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
1012
- const agentName = useResultsDirectly ? (r?.agent || `${fallbackLabel}-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `${fallbackLabel}-${rowNumber}`);
1104
+ return { kind: "result", resultIndex: i, rowNumber, agentName: useResultsDirectly ? (r?.agent || `${fallbackLabel}-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `${fallbackLabel}-${rowNumber}`) };
1105
+ });
1106
+ for (const entry of renderEntries) {
1107
+ if (entry.kind === "placeholder") {
1108
+ const glyph = widgetStepGlyph(entry.status as AsyncJobStep["status"], theme);
1109
+ const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
1110
+ c.addChild(new Text(truncLine(` ${glyph} ${entry.stepLabel}: ${themeBold(theme, entry.agentName)} ${theme.fg("dim", "·")} ${statusLabel}`, width), 0, 0));
1111
+ if (entry.error) c.addChild(new Text(truncLine(theme.fg("error", ` ⎿ Error: ${entry.error}`), width), 0, 0));
1112
+ continue;
1113
+ }
1114
+ const i = entry.resultIndex;
1115
+ const r = d.results[i];
1116
+ const rowNumber = entry.rowNumber;
1117
+ const agentName = entry.agentName;
1013
1118
  if (!r) {
1014
- c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${itemTitle} ${rowNumber}: ${agentName} · pending`), width), 0, 0));
1119
+ const pendingLabel = chainEntries ? resultRowLabel(d, multiLabel, i, rowNumber) : `${itemTitle} ${rowNumber}`;
1120
+ c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${pendingLabel}: ${agentName} · pending`), width), 0, 0));
1015
1121
  continue;
1016
1122
  }
1017
1123
  const output = getSingleResultOutput(r);
@@ -1159,20 +1265,27 @@ export function renderSubagentResult(
1159
1265
  if (!expanded) return renderMultiCompact(d, theme);
1160
1266
 
1161
1267
  const hasRunning = d.progress?.some((p) => p.status === "running")
1162
- || d.results.some((r) => r.progress?.status === "running");
1268
+ || d.results.some((r) => r.progress?.status === "running")
1269
+ || workflowGraphHasStatus(d, ["running"]);
1163
1270
  const ok = d.results.filter((r) => r.progress?.status === "completed" || (r.exitCode === 0 && r.progress?.status !== "running")).length;
1164
1271
  const hasEmptyWithoutTarget = d.results.some((r) =>
1165
1272
  r.exitCode === 0
1166
1273
  && r.progress?.status !== "running"
1167
1274
  && hasEmptyTextOutputWithoutOutputTarget(r.task, getSingleResultOutput(r)),
1168
1275
  );
1276
+ const hasWorkflowFailure = workflowGraphHasStatus(d, ["failed"]);
1277
+ const hasWorkflowPause = workflowGraphHasStatus(d, ["paused", "detached"]);
1169
1278
  const icon = hasRunning
1170
1279
  ? theme.fg("warning", "running")
1171
1280
  : hasEmptyWithoutTarget
1172
1281
  ? theme.fg("warning", "warning")
1173
- : ok === d.results.length
1174
- ? theme.fg("success", "ok")
1175
- : theme.fg("error", "failed");
1282
+ : hasWorkflowFailure
1283
+ ? theme.fg("error", "failed")
1284
+ : hasWorkflowPause
1285
+ ? theme.fg("warning", "paused")
1286
+ : ok === d.results.length
1287
+ ? theme.fg("success", "ok")
1288
+ : theme.fg("error", "failed");
1176
1289
 
1177
1290
  const totalSummary =
1178
1291
  d.progressSummary ||
@@ -1243,18 +1356,33 @@ export function renderSubagentResult(
1243
1356
  const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
1244
1357
  const displayStart = multiLabel.showActiveGroupOnly ? multiLabel.groupStartIndex : 0;
1245
1358
  const displayEnd = multiLabel.showActiveGroupOnly ? multiLabel.groupEndIndex : (useResultsDirectly ? d.results.length : d.chainAgents!.length);
1359
+ const chainEntries = buildChainRenderEntries(d, multiLabel);
1360
+ const renderEntries = chainEntries ?? Array.from({ length: displayEnd - displayStart }, (_, offset): ChainRenderEntry => {
1361
+ const i = displayStart + offset;
1362
+ const r = d.results[i];
1363
+ const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
1364
+ return { kind: "result", resultIndex: i, rowNumber, agentName: useResultsDirectly ? (r?.agent || `step-${rowNumber}`) : (d.chainAgents![i] || r?.agent || `step-${rowNumber}`) };
1365
+ });
1246
1366
 
1247
1367
  c.addChild(new Spacer(1));
1248
1368
 
1249
- for (let i = displayStart; i < displayEnd; i++) {
1369
+ for (const entry of renderEntries) {
1370
+ if (entry.kind === "placeholder") {
1371
+ const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
1372
+ c.addChild(new Text(fit(` ${statusLabel} ${entry.stepLabel}: ${theme.bold(entry.agentName)}`), 0, 0));
1373
+ c.addChild(new Text(theme.fg(entry.status === "failed" ? "error" : "dim", ` status: ${entry.status}`), 0, 0));
1374
+ if (entry.error) c.addChild(new Text(theme.fg("error", ` error: ${entry.error}`), 0, 0));
1375
+ c.addChild(new Spacer(1));
1376
+ continue;
1377
+ }
1378
+ const i = entry.resultIndex;
1250
1379
  const r = d.results[i];
1251
- const rowNumber = multiLabel.showActiveGroupOnly ? (i - multiLabel.groupStartIndex + 1) : (i + 1);
1252
- const agentName = useResultsDirectly
1253
- ? (r?.agent || `step-${rowNumber}`)
1254
- : (d.chainAgents![i] || r?.agent || `step-${rowNumber}`);
1380
+ const rowNumber = entry.rowNumber;
1381
+ const agentName = entry.agentName;
1255
1382
 
1256
1383
  if (!r) {
1257
- c.addChild(new Text(fit(theme.fg("dim", ` ${itemTitle} ${rowNumber}: ${agentName}`)), 0, 0));
1384
+ const pendingLabel = chainEntries ? resultRowLabel(d, multiLabel, i, rowNumber) : `${itemTitle} ${rowNumber}`;
1385
+ c.addChild(new Text(fit(theme.fg("dim", ` ${pendingLabel}: ${agentName}`)), 0, 0));
1258
1386
  c.addChild(new Text(theme.fg("dim", ` status: pending`), 0, 0));
1259
1387
  c.addChild(new Spacer(1));
1260
1388
  continue;