pi-subagents 0.47.1 → 0.49.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 (66) hide show
  1. package/CHANGELOG.md +51 -0
  2. package/README.md +2 -0
  3. package/docs/agents.md +1 -0
  4. package/docs/configuration.md +74 -7
  5. package/docs/missions.md +4 -2
  6. package/docs/observability.md +27 -3
  7. package/docs/tool-reference.md +1 -1
  8. package/package.json +1 -1
  9. package/src/agents/agents.ts +36 -15
  10. package/src/api/preflight.ts +1 -1
  11. package/src/extension/config.ts +26 -0
  12. package/src/extension/doctor.ts +40 -0
  13. package/src/extension/index.ts +39 -8
  14. package/src/extension/public-execution.ts +35 -4
  15. package/src/extension/rpc.ts +3 -9
  16. package/src/extension/schemas.ts +10 -9
  17. package/src/extension/tool-description.ts +10 -8
  18. package/src/inspectors/herdr/actions.ts +11 -2
  19. package/src/inspectors/herdr/inspector-runner.ts +16 -3
  20. package/src/intercom/intercom-bridge.ts +7 -2
  21. package/src/missions/lifecycle.ts +4 -7
  22. package/src/missions/store.ts +12 -7
  23. package/src/missions/workflow-state.ts +6 -2
  24. package/src/runs/background/active-async-capacity.ts +431 -0
  25. package/src/runs/background/active-run-index.ts +9 -5
  26. package/src/runs/background/async-execution.ts +141 -65
  27. package/src/runs/background/async-job-tracker.ts +4 -0
  28. package/src/runs/background/async-resume.ts +18 -2
  29. package/src/runs/background/async-status.ts +11 -5
  30. package/src/runs/background/chain-append.ts +33 -15
  31. package/src/runs/background/fleet-view.ts +18 -5
  32. package/src/runs/background/owned-process-tree.ts +104 -0
  33. package/src/runs/background/process-terminal.ts +17 -3
  34. package/src/runs/background/resume-guidance.ts +27 -7
  35. package/src/runs/background/retained-children.ts +14 -6
  36. package/src/runs/background/run-status.ts +101 -5
  37. package/src/runs/background/stale-run-reconciler.ts +3 -3
  38. package/src/runs/background/subagent-runner.ts +60 -32
  39. package/src/runs/foreground/chain-execution.ts +37 -2
  40. package/src/runs/foreground/execution.ts +90 -19
  41. package/src/runs/foreground/foreground-control.ts +12 -0
  42. package/src/runs/foreground/prompt-audit.ts +172 -0
  43. package/src/runs/foreground/subagent-executor.ts +829 -203
  44. package/src/runs/shared/acceptance.ts +13 -4
  45. package/src/runs/shared/completion-guard.ts +17 -1
  46. package/src/runs/shared/llm-intent-arbiter.ts +302 -0
  47. package/src/runs/shared/parallel-utils.ts +2 -0
  48. package/src/runs/shared/pi-args.ts +44 -1
  49. package/src/runs/shared/run-fanout-budget.ts +280 -0
  50. package/src/runs/shared/single-output.ts +4 -2
  51. package/src/runs/shared/task-intent.ts +19 -3
  52. package/src/runs/shared/worktree.ts +17 -5
  53. package/src/shared/agent-stream-options.ts +5 -0
  54. package/src/shared/artifacts.ts +2 -6
  55. package/src/shared/display-text.ts +50 -0
  56. package/src/shared/node-executable.ts +21 -0
  57. package/src/shared/types.ts +115 -4
  58. package/src/shared/utils.ts +3 -1
  59. package/src/tui/fleet-status.ts +7 -5
  60. package/src/tui/fleet-transcript.ts +1 -48
  61. package/src/tui/fleet.ts +228 -13
  62. package/src/tui/render.ts +86 -39
  63. package/src/watchdog/permission-arbiter.ts +2 -1
  64. package/src/watchdog/review.ts +4 -3
  65. package/src/workflows/chat-progress.ts +8 -2
  66. package/src/workflows/scripted-workflow.ts +66 -10
package/src/tui/render.ts CHANGED
@@ -15,6 +15,7 @@ import {
15
15
  type NestedRunSummary,
16
16
  type NestedStepSummary,
17
17
  type WorkflowNodeStatus,
18
+ type MainWindowRendererConfig,
18
19
  MAX_WIDGET_JOBS,
19
20
  POLL_INTERVAL_MS,
20
21
  WIDGET_KEY,
@@ -30,6 +31,38 @@ import { buildWorkflowChatProgressRows, type WorkflowChatProgressRow } from "../
30
31
 
31
32
  type Theme = ExtensionContext["ui"]["theme"];
32
33
 
34
+ interface MainWindowRenderLayout {
35
+ horizontalSpacing: number;
36
+ compactResultMaxLines?: number;
37
+ }
38
+
39
+ function resolveMainWindowRenderLayout(config?: MainWindowRendererConfig): MainWindowRenderLayout {
40
+ return {
41
+ horizontalSpacing: config?.horizontalSpacing ?? 2,
42
+ ...(config?.compactResultMaxLines !== undefined ? { compactResultMaxLines: config.compactResultMaxLines } : {}),
43
+ };
44
+ }
45
+
46
+ function mainWindowIndent(layout: MainWindowRenderLayout, level: number): string {
47
+ return " ".repeat(Math.max(0, layout.horizontalSpacing * level));
48
+ }
49
+
50
+ function capCompactMainWindowResult(component: Component, layout: MainWindowRenderLayout, theme: Theme, enabled: boolean): Component {
51
+ const maxLines = layout.compactResultMaxLines;
52
+ if (!enabled || maxLines === undefined) return component;
53
+ const capped = new Container();
54
+ capped.render = (width: number): string[] => {
55
+ const lines = component.render(width);
56
+ if (lines.length <= maxLines) return lines;
57
+ const visibleRows = maxLines === 1 ? 1 : maxLines - 1;
58
+ const hiddenCount = lines.length - visibleRows;
59
+ const hint = theme.fg("accent", `… ${hiddenCount} rows hidden · ${liveDetailKeyText()} expands`);
60
+ if (maxLines === 1) return [truncLine(`${lines[0] ?? ""} ${hint}`, width)];
61
+ return [...lines.slice(0, visibleRows), truncLine(hint, width)];
62
+ };
63
+ return capped;
64
+ }
65
+
33
66
  function liveDetailKeyText(): string {
34
67
  return keyText("app.tools.expand");
35
68
  }
@@ -1555,7 +1588,7 @@ export function renderWidget(ctx: ExtensionContext, jobs: AsyncJobState[]): void
1555
1588
  ctx.ui.setWidget(WIDGET_KEY, buildWidgetComponent(jobs, ctx.ui.getToolsExpanded?.() ?? false));
1556
1589
  }
1557
1590
 
1558
- function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, frame?: number): Component {
1591
+ function renderSingleCompact(d: Details, r: Details["results"][number], theme: Theme, layout: MainWindowRenderLayout, frame?: number): Component {
1559
1592
  const output = r.truncation?.text || getSingleResultOutput(r);
1560
1593
  const progress = r.progress || r.progressSummary;
1561
1594
  const isRunning = isResultRunning(r);
@@ -1566,40 +1599,43 @@ function renderSingleCompact(d: Details, r: Details["results"][number], theme: T
1566
1599
  ]);
1567
1600
  const c = new Container();
1568
1601
  const width = getTermWidth() - 4;
1602
+ const detailIndent = mainWindowIndent(layout, 1);
1603
+ const continuationIndent = mainWindowIndent(layout, 2) + (layout.horizontalSpacing > 0 ? " " : "");
1569
1604
  const modelDisplay = modelThinkingBadge(theme, r.model ?? r.progress?.model, r.thinking ?? r.progress?.thinking);
1570
1605
  c.addChild(new Text(truncLine(`${resultGlyph(r, output, theme, isRunning, undefined, frame)} ${theme.fg("toolTitle", theme.bold(r.agent))}${modelDisplay}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1571
1606
 
1572
1607
  if (isRunning && r.progress) {
1573
1608
  const progressSnapshotNow = snapshotNowForProgress(r.progress);
1574
1609
  const activity = compactCurrentActivity(r.progress);
1575
- c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
1610
+ c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}⎿ ${activity}`), width), 0, 0));
1576
1611
  const liveStatus = buildLiveStatusLine(r.progress, progressSnapshotNow);
1577
- if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", ` ${liveStatus}`), width), 0, 0));
1612
+ if (liveStatus && liveStatus !== activity) c.addChild(new Text(truncLine(theme.fg("dim", `${continuationIndent}${liveStatus}`), width), 0, 0));
1578
1613
  for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, progressSnapshotNow)) {
1579
- c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
1614
+ c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
1580
1615
  }
1581
- c.addChild(new Text(truncLine(theme.fg("accent", ` ${liveDetailHintText()}`), width), 0, 0));
1582
- if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1616
+ c.addChild(new Text(truncLine(theme.fg("accent", `${detailIndent}${liveDetailHintText()}`), width), 0, 0));
1617
+ if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1583
1618
  return c;
1584
1619
  }
1585
1620
 
1586
1621
  for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, r.progress?.lastActivityAt)) {
1587
- c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
1622
+ c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
1588
1623
  }
1589
- c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
1624
+ c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
1590
1625
  const preview = firstOutputLine(output);
1591
1626
  if (preview && r.exitCode === 0 && !hasEmptyTextOutputWithoutOutputTarget(r.task, output)) {
1592
- c.addChild(new Text(truncLine(theme.fg("dim", ` ${preview}`), width), 0, 0));
1627
+ c.addChild(new Text(truncLine(theme.fg("dim", `${continuationIndent}${preview}`), width), 0, 0));
1593
1628
  }
1594
- if (r.sessionFile) c.addChild(new Text(truncLine(theme.fg("dim", ` session: ${shortenPath(r.sessionFile)}`), width), 0, 0));
1595
- if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1596
- if (r.truncation?.artifactPath) c.addChild(new Text(truncLine(theme.fg("dim", ` full output: ${shortenPath(r.truncation.artifactPath)}`), width), 0, 0));
1629
+ if (r.sessionFile) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}session: ${shortenPath(r.sessionFile)}`), width), 0, 0));
1630
+ if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1631
+ if (r.truncation?.artifactPath) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}full output: ${shortenPath(r.truncation.artifactPath)}`), width), 0, 0));
1597
1632
  return c;
1598
1633
  }
1599
1634
 
1600
1635
  function workflowRowGlyph(row: WorkflowChatProgressRow, theme: Theme, frame?: number): string {
1601
1636
  if (row.state === "running") return theme.fg("accent", runningGlyph(frame));
1602
1637
  if (row.state === "complete") return theme.fg("success", "✓");
1638
+ if (row.state === "stopped") return theme.fg("warning", "■");
1603
1639
  return theme.fg("error", "✗");
1604
1640
  }
1605
1641
 
@@ -1607,6 +1643,7 @@ function workflowRowStateLabel(row: WorkflowChatProgressRow, theme: Theme): stri
1607
1643
  const label = (row.state === "complete" ? "complete" : row.state).padEnd(8);
1608
1644
  if (row.state === "running") return theme.fg("accent", label);
1609
1645
  if (row.state === "complete") return theme.fg("success", label);
1646
+ if (row.state === "stopped") return theme.fg("warning", label);
1610
1647
  return theme.fg("error", label);
1611
1648
  }
1612
1649
 
@@ -1616,7 +1653,7 @@ function workflowOverallState(rows: WorkflowChatProgressRow[], hasTerminalValue:
1616
1653
  return "running";
1617
1654
  }
1618
1655
 
1619
- function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>, theme: Theme, frame?: number): Component {
1656
+ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>, theme: Theme, layout: MainWindowRenderLayout, frame?: number): Component {
1620
1657
  const workflow = d.workflow;
1621
1658
  const rows = workflow ? buildWorkflowChatProgressRows(workflow.trace) : [];
1622
1659
  const state = workflowOverallState(rows, workflow?.value !== undefined, result.isError);
@@ -1626,28 +1663,29 @@ function renderWorkflowChatProgress(d: Details, result: AgentToolResult<Details>
1626
1663
  const repoLabel = d.chatProgress?.repoLabel ?? (d.chatProgress?.repoRelation === "same" ? "same repo" : "other repo");
1627
1664
  const phase = rows.find((row) => row.state === "running" && row.phase)?.phase ?? [...rows].reverse().find((row) => row.phase)?.phase;
1628
1665
  const c = new Container();
1666
+ const rowIndent = mainWindowIndent(layout, 1);
1629
1667
  c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold("workflow"))} ${runId} ${theme.fg("dim", "·")} ${d.chatProgress?.repoRelation === "same" ? "same repo" : "other repo"} ${theme.fg("dim", "·")} ${state}`, width), 0, 0));
1630
- c.addChild(new Text(truncLine(theme.fg("dim", ` Repo ${repoLabel}`), width), 0, 0));
1631
- if (phase) c.addChild(new Text(truncLine(theme.fg("dim", ` Phase ${phase}`), width), 0, 0));
1668
+ c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Repo ${repoLabel}`), width), 0, 0));
1669
+ if (phase) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Phase ${phase}`), width), 0, 0));
1632
1670
  if (rows.length === 0) {
1633
- c.addChild(new Text(truncLine(theme.fg("dim", " ◦ waiting for workflow child launches"), width), 0, 0));
1671
+ c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}◦ waiting for workflow child launches`), width), 0, 0));
1634
1672
  return c;
1635
1673
  }
1636
1674
  const visible = visibleWorkflowRows(rows);
1637
- if (visible.hiddenRows > 0) c.addChild(new Text(truncLine(theme.fg("dim", ` … ${visible.hiddenRows} older workflow rows hidden`), width), 0, 0));
1675
+ if (visible.hiddenRows > 0) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}… ${visible.hiddenRows} older workflow rows hidden`), width), 0, 0));
1638
1676
  for (const row of visible.rows) {
1639
1677
  const status = workflowRowStateLabel(row, theme);
1640
1678
  const label = row.label && row.label !== row.key ? ` ${oneLine(row.label)}` : "";
1641
1679
  const duration = row.durationMs !== undefined ? ` ${theme.fg("dim", `· ${formatDuration(row.durationMs)}`)}` : "";
1642
1680
  const run = row.runId ? ` ${theme.fg("dim", `[${row.runId.slice(0, 8)}]`)}` : "";
1643
1681
  const error = row.error ? ` ${theme.fg("error", `· ${compactWorkflowError(row.error)}`)}` : "";
1644
- c.addChild(new Text(truncLine(` ${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}`, width), 0, 0));
1682
+ c.addChild(new Text(truncLine(`${rowIndent}${workflowRowGlyph(row, theme, frame)} ${status} ${theme.bold(row.key)}${label}${run}${duration}${error}`, width), 0, 0));
1645
1683
  }
1646
- if (workflow?.emits.length) c.addChild(new Text(truncLine(theme.fg("dim", ` Emits ${workflow.emits.length}`), width), 0, 0));
1684
+ if (workflow?.emits.length) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}Emits ${workflow.emits.length}`), width), 0, 0));
1647
1685
  return c;
1648
1686
  }
1649
1687
 
1650
- function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component {
1688
+ function renderMultiCompact(d: Details, theme: Theme, layout: MainWindowRenderLayout, frame?: number): Component {
1651
1689
  const hasRunning = detailsHaveRunningResult(d);
1652
1690
  const detached = d.results.some((r) => r.detached)
1653
1691
  || workflowGraphHasStatus(d, ["detached"]);
@@ -1687,6 +1725,9 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1687
1725
  const contextBadge = contextModeBadge(theme, d.context);
1688
1726
  const c = new Container();
1689
1727
  const width = getTermWidth() - 4;
1728
+ const rowIndent = mainWindowIndent(layout, 1);
1729
+ const detailIndent = mainWindowIndent(layout, 2);
1730
+ const continuationIndent = mainWindowIndent(layout, 3) + (layout.horizontalSpacing > 0 ? " " : "");
1690
1731
  c.addChild(new Text(truncLine(`${glyph} ${theme.fg("toolTitle", theme.bold(d.mode))}${contextBadge}${stats ? ` ${theme.fg("dim", "·")} ${stats}` : ""}`, width), 0, 0));
1691
1732
 
1692
1733
  const useResultsDirectly = multiLabel.hasParallelInChain || !d.chainAgents?.length;
@@ -1704,8 +1745,8 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1704
1745
  if (entry.kind === "placeholder") {
1705
1746
  const glyph = widgetStepGlyph(entry.status as AsyncJobStep["status"], theme);
1706
1747
  const statusLabel = widgetStepStatus(entry.status as AsyncJobStep["status"], theme);
1707
- c.addChild(new Text(truncLine(` ${glyph} ${entry.stepLabel}: ${themeBold(theme, entry.agentName)} ${theme.fg("dim", "·")} ${statusLabel}`, width), 0, 0));
1708
- if (entry.error) c.addChild(new Text(truncLine(theme.fg("error", ` ⎿ Error: ${entry.error}`), width), 0, 0));
1748
+ c.addChild(new Text(truncLine(`${rowIndent}${glyph} ${entry.stepLabel}: ${themeBold(theme, entry.agentName)} ${theme.fg("dim", "·")} ${statusLabel}`, width), 0, 0));
1749
+ if (entry.error) c.addChild(new Text(truncLine(theme.fg("error", `${detailIndent}⎿ Error: ${entry.error}`), width), 0, 0));
1709
1750
  continue;
1710
1751
  }
1711
1752
  const i = entry.resultIndex;
@@ -1714,7 +1755,7 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1714
1755
  const agentName = entry.agentName;
1715
1756
  if (!r) {
1716
1757
  const pendingLabel = entry.rowLabel ?? `${itemTitle} ${rowNumber}`;
1717
- c.addChild(new Text(truncLine(theme.fg("dim", ` ◦ ${pendingLabel}: ${agentName} · pending`), width), 0, 0));
1758
+ c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}◦ ${pendingLabel}: ${agentName} · pending`), width), 0, 0));
1718
1759
  continue;
1719
1760
  }
1720
1761
  const output = getSingleResultOutput(r);
@@ -1730,28 +1771,28 @@ function renderMultiCompact(d: Details, theme: Theme, frame?: number): Component
1730
1771
  const rowProgressModel = rProg && "status" in rProg ? rProg : undefined;
1731
1772
  const rowModelDisplay = modelThinkingBadge(theme, r.model ?? rowProgressModel?.model, r.thinking ?? rowProgressModel?.thinking);
1732
1773
  const line = `${glyph} ${stepLabel}: ${themeBold(theme, agentName)}${contextModeBadge(theme, r.context)}${rowModelDisplay}${stepStats ? ` ${theme.fg("dim", "·")} ${stepStats}` : ""}${pendingLabel}`;
1733
- c.addChild(new Text(truncLine(` ${line}`, width), 0, 0));
1774
+ c.addChild(new Text(truncLine(`${rowIndent}${line}`, width), 0, 0));
1734
1775
  if (rRunning && rProg && "status" in rProg) {
1735
1776
  const liveProgress = rProg as AgentProgress;
1736
1777
  const activity = compactCurrentActivity(liveProgress);
1737
- c.addChild(new Text(truncLine(theme.fg("dim", ` ⎿ ${activity}`), width), 0, 0));
1778
+ c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}⎿ ${activity}`), width), 0, 0));
1738
1779
  for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, snapshotNowForProgress(liveProgress))) {
1739
- c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
1780
+ c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
1740
1781
  }
1741
- c.addChild(new Text(truncLine(theme.fg("accent", ` ${liveDetailHintText()}`), width), 0, 0));
1782
+ c.addChild(new Text(truncLine(theme.fg("accent", `${detailIndent}${liveDetailHintText()}`), width), 0, 0));
1742
1783
  } else if (!rPending && (r.exitCode !== 0 || r.interrupted || r.detached || hasEmptyTextOutputWithoutOutputTarget(r.task, output))) {
1743
- c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", ` ⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
1784
+ c.addChild(new Text(truncLine(theme.fg(r.exitCode !== 0 ? "error" : "dim", `${detailIndent}⎿ ${resultStatusLine(r, output)}`), width), 0, 0));
1744
1785
  }
1745
1786
  if (!rRunning && !rPending) {
1746
1787
  for (const nestedLine of formatNestedWidgetLines(r.children, theme, width, false, r.progress?.lastActivityAt)) {
1747
- c.addChild(new Text(truncLine(` ${nestedLine}`, width), 0, 0));
1788
+ c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
1748
1789
  }
1749
1790
  }
1750
1791
  const outputTarget = extractOutputTarget(r.task);
1751
- if (outputTarget) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${outputTarget}`), width), 0, 0));
1752
- if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", ` output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1792
+ if (outputTarget) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}output: ${outputTarget}`), width), 0, 0));
1793
+ if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim", `${detailIndent}output: ${shortenPath(r.artifactPaths.outputPath)}`), width), 0, 0));
1753
1794
  }
1754
- if (d.artifacts) c.addChild(new Text(truncLine(theme.fg("dim", ` artifacts: ${shortenPath(d.artifacts.dir)}`), width), 0, 0));
1795
+ if (d.artifacts) c.addChild(new Text(truncLine(theme.fg("dim", `${rowIndent}artifacts: ${shortenPath(d.artifacts.dir)}`), width), 0, 0));
1755
1796
  return c;
1756
1797
  }
1757
1798
 
@@ -1802,22 +1843,28 @@ export function renderSubagentResult(
1802
1843
  options: { expanded: boolean },
1803
1844
  theme: Theme,
1804
1845
  frame?: number,
1846
+ rendererConfig?: MainWindowRendererConfig,
1805
1847
  ): Component {
1848
+ const layout = resolveMainWindowRenderLayout(rendererConfig);
1849
+ const compact = (component: Component): Component => capCompactMainWindowResult(component, layout, theme, !options.expanded);
1806
1850
  const d = result.details;
1807
- if (d?.mode === "workflow" && d.chatProgress?.mode === "live-card" && !result.isError && d.workflow?.value === undefined) return renderWorkflowChatProgress(d, result, theme, frame);
1851
+ if (d?.mode === "workflow" && d.chatProgress?.mode === "live-card" && !result.isError && d.workflow?.value === undefined) {
1852
+ return compact(renderWorkflowChatProgress(d, result, theme, options.expanded ? resolveMainWindowRenderLayout() : layout, frame));
1853
+ }
1808
1854
  if (!d || !d.results.length) {
1809
1855
  const t = result.content[0];
1810
1856
  const text = t?.type === "text" ? t.text : "(no output)";
1811
1857
  const contextPrefix = contextModePrefix(theme, d?.context);
1812
1858
  const width = getTermWidth() - 4;
1813
- if (!text.includes("\n")) return new Text(truncLine(`${contextPrefix}${text}`, width), 0, 0);
1859
+ if (!text.includes("\n")) return compact(new Text(truncLine(`${contextPrefix}${text}`, width), 0, 0));
1814
1860
  if (d && !options.expanded && !result.isError) {
1815
1861
  const lines = text.split(/\r?\n/);
1816
1862
  const firstNonEmptyLine = lines.find((line) => line.trim())?.trim() || "(no output)";
1817
1863
  const c = new Container();
1864
+ const detailIndent = mainWindowIndent(layout, 1);
1818
1865
  c.addChild(new Text(truncLine(`${contextPrefix}${firstNonEmptyLine} · ${lines.length} lines`, width), 0, 0));
1819
- c.addChild(new Text(truncLine(theme.fg("accent", ` Press ${liveDetailKeyText()} for full output`), width), 0, 0));
1820
- return c;
1866
+ c.addChild(new Text(truncLine(theme.fg("accent", `${detailIndent}Press ${liveDetailKeyText()} for full output`), width), 0, 0));
1867
+ return compact(c);
1821
1868
  }
1822
1869
  const c = new Container();
1823
1870
  const wrapped = wrapPlainText(`${contextPrefix}${text}`, width);
@@ -1830,8 +1877,8 @@ export function renderSubagentResult(
1830
1877
 
1831
1878
  if (d.mode === "single" && d.results.length === 1) {
1832
1879
  const r = d.results[0];
1833
- if (!r) return renderMultiCompact(d, theme, frame);
1834
- if (!expanded) return renderSingleCompact(d, r, theme, frame);
1880
+ if (!r) return compact(renderMultiCompact(d, theme, layout, frame));
1881
+ if (!expanded) return compact(renderSingleCompact(d, r, theme, layout, frame));
1835
1882
  const isRunning = isResultRunning(r);
1836
1883
  const contextBadge = contextModeBadge(theme, r.context ?? d.context);
1837
1884
  const output = r.truncation?.text || getSingleResultOutput(r);
@@ -1924,7 +1971,7 @@ export function renderSubagentResult(
1924
1971
  return c;
1925
1972
  }
1926
1973
 
1927
- if (!expanded) return renderMultiCompact(d, theme, frame);
1974
+ if (!expanded) return compact(renderMultiCompact(d, theme, layout, frame));
1928
1975
 
1929
1976
  const hasRunning = detailsHaveRunningResult(d);
1930
1977
  const detached = d.results.some((r) => r.detached)
@@ -3,6 +3,7 @@ import { convertToLlm, type ExtensionContext } from "@earendil-works/pi-coding-a
3
3
  import { streamSimple } from "@earendil-works/pi-ai/compat";
4
4
  import { Type, type Static } from "typebox";
5
5
  import { appendPermissionAudit, permissionArgsPreview } from "../runs/shared/permissions.ts";
6
+ import { agentStreamOptions } from "../shared/agent-stream-options.ts";
6
7
  import { decodeChildWatchdogConfig } from "./child-status.ts";
7
8
  import { childResolvedConfig } from "./register-child.ts";
8
9
  import { resolveWatchdogReviewModel } from "./review.ts";
@@ -112,7 +113,7 @@ export function createWatchdogPermissionArbiter(options: WatchdogPermissionArbit
112
113
  tools: [tool],
113
114
  },
114
115
  convertToLlm,
115
- streamFunction: streamFn,
116
+ ...agentStreamOptions(streamFn),
116
117
  getApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,
117
118
  beforeToolCall: async ({ toolCall }) => toolCall.name === tool.name ? undefined : { block: true, reason: `Permission arbiter tool '${toolCall.name}' is not allowed.` },
118
119
  toolExecution: "sequential",
@@ -1,9 +1,10 @@
1
1
  import { Agent, type AgentTool, type StreamFn, type ThinkingLevel } from "@earendil-works/pi-agent-core";
2
2
  import { createReadOnlyTools, convertToLlm, type ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { streamSimple } from "@earendil-works/pi-ai/compat";
4
- import type { Model } from "@earendil-works/pi-ai";
4
+ import type { Model, ProviderHeaders } from "@earendil-works/pi-ai";
5
5
  import { Type, type Static } from "typebox";
6
6
  import { resolveModelCandidate } from "../runs/shared/model-fallback.ts";
7
+ import { agentStreamOptions } from "../shared/agent-stream-options.ts";
7
8
  import { resolveEffectiveThinking, splitKnownThinkingSuffix, THINKING_LEVELS, toModelInfo } from "../shared/model-info.ts";
8
9
  import type { WatchdogReviewFunction, WatchdogReviewRequest } from "./runtime.ts";
9
10
  import {
@@ -36,7 +37,7 @@ type RegistryModel = Model<any>;
36
37
 
37
38
  interface WatchdogReviewAuth {
38
39
  apiKey?: string;
39
- headers?: Record<string, string>;
40
+ headers?: ProviderHeaders;
40
41
  env?: Record<string, string>;
41
42
  }
42
43
 
@@ -280,7 +281,7 @@ export function createMainWatchdogReview(provider: WatchdogContextProvider, opti
280
281
  tools,
281
282
  },
282
283
  convertToLlm,
283
- streamFunction: streamFn,
284
+ ...agentStreamOptions(streamFn),
284
285
  getApiKey: (providerName) => providerName === selection.model.provider ? auth.apiKey : undefined,
285
286
  beforeToolCall: async ({ toolCall }) => WATCHDOG_ALLOWED_TOOL_NAMES.has(toolCall.name)
286
287
  ? undefined
@@ -96,7 +96,7 @@ export function resolveWorkflowChatProgress(input: ResolveWorkflowChatProgressIn
96
96
 
97
97
  export interface WorkflowChatProgressRow {
98
98
  key: string;
99
- state: "running" | "complete" | "failed";
99
+ state: "running" | "complete" | "failed" | "stopped";
100
100
  label?: string;
101
101
  phase?: string;
102
102
  runId?: string;
@@ -123,7 +123,13 @@ export function buildWorkflowChatProgressRows(trace: NonNullable<Details["workfl
123
123
  continue;
124
124
  }
125
125
  const next: WorkflowChatProgressRow = existing ?? { key: entry.key, state: "running" };
126
- next.state = entry.state === "completed" ? "complete" : entry.state === "failed" ? "failed" : "running";
126
+ next.state = entry.state === "completed"
127
+ ? "complete"
128
+ : entry.state === "failed"
129
+ ? "failed"
130
+ : entry.state === "stopped"
131
+ ? "stopped"
132
+ : "running";
127
133
  const label = cleanLabel(entry.label);
128
134
  const phase = cleanLabel(entry.phase);
129
135
  if (label) next.label = label;
@@ -131,11 +131,11 @@ function hostCall(method, args, observation) {
131
131
  : promise;
132
132
  }
133
133
 
134
- function runHostCall(key, params, collectFailure) {
134
+ function runHostCall(key, params, collectFailure, batch) {
135
135
  const callId = ++nextCallId;
136
136
  const promise = new Promise((resolve, reject) => {
137
137
  pending.set(callId, { resolve, reject });
138
- parentPort.postMessage({ type: "call", callId, method: "run", args: { key, params, ...(collectFailure ? { collectFailure: true } : {}) } });
138
+ parentPort.postMessage({ type: "call", callId, method: "run", args: { key, params, ...(collectFailure ? { collectFailure: true } : {}), ...(batch ? { batch } : {}) } });
139
139
  });
140
140
  return { key, callId, promise };
141
141
  }
@@ -188,7 +188,8 @@ const runs = Object.freeze({
188
188
  calls.push({ key, params });
189
189
  }
190
190
  for (const { key, params } of calls) runFingerprints.set(key, stableRunJson(params));
191
- const launched = calls.map(({ key, params }) => runHostCall(key, params, true));
191
+ const batch = { id: "batch-" + (++nextCallId), calls };
192
+ const launched = calls.map(({ key, params }) => runHostCall(key, params, true, batch));
192
193
  return trackRunObservation(launched.map(({ key, callId }) => ({ key, callId })), Promise.all(launched.map(({ promise }) => promise)));
193
194
  },
194
195
  status(keyOrRunId) { return hostCall("status", { keyOrRunId }); },
@@ -331,7 +332,7 @@ export interface WorkflowScriptChildResult {
331
332
  export interface WorkflowScriptTraceEntry {
332
333
  operation: "run" | "status";
333
334
  key: string;
334
- state: "started" | "completed" | "failed" | "reused";
335
+ state: "started" | "completed" | "failed" | "stopped" | "reused";
335
336
  /** Canonical child agent name when resolved launch or result data is available. */
336
337
  agent?: string;
337
338
  runId?: string;
@@ -363,7 +364,8 @@ export interface RunWorkflowScriptOptions {
363
364
  script: string;
364
365
  timeoutMs?: number;
365
366
  signal?: AbortSignal;
366
- launch: (key: string, params: Record<string, unknown>, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
367
+ admit?: (calls: Array<{ key: string; params: Record<string, unknown> }>) => void | Promise<void>;
368
+ launch: (key: string, params: Record<string, unknown>, signal: AbortSignal, admission: { admitted: boolean }) => Promise<WorkflowScriptChildResult>;
367
369
  status: (keyOrRunId: string, signal: AbortSignal) => Promise<WorkflowScriptChildResult>;
368
370
  state?: {
369
371
  get: (key: string) => unknown | Promise<unknown>;
@@ -490,6 +492,8 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
490
492
  const children = new Map<string, WorkflowScriptChildResult>();
491
493
  const childOrder: string[] = [];
492
494
  const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult>; observed: boolean }>();
495
+ const stoppedLaunches = new Set<string>();
496
+ const batchAdmissions = new Map<string, Promise<void>>();
493
497
  const observedRunCalls = new Set<number>();
494
498
  const childController = new AbortController();
495
499
  let settled = false;
@@ -516,7 +520,30 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
516
520
  else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
517
521
  else resolve({ value: outcome.value, ...partial() });
518
522
  };
519
- const onAbort = () => finish({ error: new Error("Workflow script aborted.") });
523
+ const onAbort = () => {
524
+ const signalReason = options.signal?.reason;
525
+ const error = signalReason instanceof Error
526
+ ? signalReason
527
+ : typeof signalReason === "string"
528
+ ? new Error(signalReason)
529
+ : new Error("Workflow script aborted.");
530
+ for (const key of launches.keys()) {
531
+ if (children.has(key)) continue;
532
+ stoppedLaunches.add(key);
533
+ const started = trace.findLast((entry) => entry.operation === "run" && entry.key === key && entry.state === "started");
534
+ trace.push({
535
+ operation: "run",
536
+ key,
537
+ state: "stopped",
538
+ ...(started?.agent ? { agent: started.agent } : {}),
539
+ ...(started?.phase ? { phase: started.phase } : {}),
540
+ ...(started?.label ? { label: started.label } : {}),
541
+ error: error.message,
542
+ });
543
+ }
544
+ traceChanged();
545
+ finish({ error });
546
+ };
520
547
  const timer = options.timeoutMs === undefined
521
548
  ? undefined
522
549
  : setTimeout(() => finish({ error: new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) }), options.timeoutMs);
@@ -528,6 +555,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
528
555
  if (!settled && code !== 0) finish({ error: new Error(`Workflow worker exited with code ${code}.`) });
529
556
  });
530
557
  worker.on("message", (message: Record<string, unknown>) => {
558
+ if (settled) return;
531
559
  if (message.type === "emit") {
532
560
  try {
533
561
  assertWorkflowJsonValue(message.value, "emit");
@@ -614,7 +642,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
614
642
  const target = known?.runId ?? keyOrRunId;
615
643
  trace.push({ operation: "status", key: keyOrRunId, state: "started", ...(known?.runId ? { runId: known.runId } : {}) });
616
644
  traceChanged();
645
+ if (settled) return;
617
646
  respond(options.status(target, childController.signal).then((result) => {
647
+ if (settled) return result;
618
648
  trace.push({ operation: "status", key: keyOrRunId, state: result.ok ? "completed" : "failed", ...(result.runId ? { runId: result.runId } : {}), ...(!result.ok ? { error: result.output } : {}) });
619
649
  traceChanged();
620
650
  if (!result.ok) throw new Error(`Status '${keyOrRunId}' failed: ${result.output}`);
@@ -677,11 +707,33 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
677
707
  }
678
708
 
679
709
  const startedAt = Date.now();
680
- childOrder.push(key);
681
- trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params) });
682
- traceChanged();
683
- const promise = Promise.resolve().then(() => options.launch(key, { ...params, async: params.async ?? false }, childController.signal)).then((result) => {
710
+ const batch = isRecord(message.args.batch) && typeof message.args.batch.id === "string" && Array.isArray(message.args.batch.calls)
711
+ ? { id: message.args.batch.id, calls: message.args.batch.calls.filter((call): call is { key: string; params: Record<string, unknown> } => isRecord(call) && typeof call.key === "string" && isRecord(call.params)) }
712
+ : undefined;
713
+ let admission = batch ? batchAdmissions.get(batch.id) : undefined;
714
+ if (!admission) {
715
+ const seenKeys = new Set<string>();
716
+ const calls = (batch?.calls ?? [{ key, params }]).filter((call) => {
717
+ if (seenKeys.has(call.key) || launches.has(call.key) || call.params.resume !== undefined) return false;
718
+ seenKeys.add(call.key);
719
+ return true;
720
+ });
721
+ admission = Promise.resolve().then(() => {
722
+ if (settled) return;
723
+ return options.admit?.(calls);
724
+ });
725
+ if (batch) batchAdmissions.set(batch.id, admission);
726
+ }
727
+ const promise = admission.then(() => {
728
+ if (settled || stoppedLaunches.has(key)) {
729
+ const reason = childController.signal.reason;
730
+ const text = reason instanceof Error ? reason.message : typeof reason === "string" ? reason : "Workflow script aborted.";
731
+ return { key, ok: false, output: text, error: text, artifactPaths: [] };
732
+ }
733
+ return options.launch(key, { ...params, async: params.async ?? false }, childController.signal, { admitted: true });
734
+ }).then((result) => {
684
735
  const normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
736
+ if (stoppedLaunches.has(key)) return normalized;
685
737
  children.set(key, normalized);
686
738
  trace.push({ operation: "run", key, state: normalized.ok ? "completed" : "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), ...(normalized.agent ? { agent: normalized.agent } : {}), ...(normalized.runId ? { runId: normalized.runId } : {}), ...(!normalized.ok ? { error: normalized.error ?? normalized.output } : {}) });
687
739
  traceChanged();
@@ -689,12 +741,16 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
689
741
  }, (error: unknown) => {
690
742
  const text = error instanceof Error ? error.message : String(error);
691
743
  const failure: WorkflowScriptChildResult = { key, ok: false, output: text, error: text, artifactPaths: [] };
744
+ if (stoppedLaunches.has(key)) return failure;
692
745
  children.set(key, failure);
693
746
  trace.push({ operation: "run", key, state: "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), error: text });
694
747
  traceChanged();
695
748
  return failure;
696
749
  });
697
750
  launches.set(key, { fingerprint, promise, observed: callObserved });
751
+ childOrder.push(key);
752
+ trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params) });
753
+ traceChanged();
698
754
  respond(deliver(promise));
699
755
  });
700
756