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.
- package/CHANGELOG.md +51 -0
- package/README.md +2 -0
- package/docs/agents.md +1 -0
- package/docs/configuration.md +74 -7
- package/docs/missions.md +4 -2
- package/docs/observability.md +27 -3
- package/docs/tool-reference.md +1 -1
- package/package.json +1 -1
- package/src/agents/agents.ts +36 -15
- package/src/api/preflight.ts +1 -1
- package/src/extension/config.ts +26 -0
- package/src/extension/doctor.ts +40 -0
- package/src/extension/index.ts +39 -8
- package/src/extension/public-execution.ts +35 -4
- package/src/extension/rpc.ts +3 -9
- package/src/extension/schemas.ts +10 -9
- package/src/extension/tool-description.ts +10 -8
- package/src/inspectors/herdr/actions.ts +11 -2
- package/src/inspectors/herdr/inspector-runner.ts +16 -3
- package/src/intercom/intercom-bridge.ts +7 -2
- package/src/missions/lifecycle.ts +4 -7
- package/src/missions/store.ts +12 -7
- package/src/missions/workflow-state.ts +6 -2
- package/src/runs/background/active-async-capacity.ts +431 -0
- package/src/runs/background/active-run-index.ts +9 -5
- package/src/runs/background/async-execution.ts +141 -65
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +18 -2
- package/src/runs/background/async-status.ts +11 -5
- package/src/runs/background/chain-append.ts +33 -15
- package/src/runs/background/fleet-view.ts +18 -5
- package/src/runs/background/owned-process-tree.ts +104 -0
- package/src/runs/background/process-terminal.ts +17 -3
- package/src/runs/background/resume-guidance.ts +27 -7
- package/src/runs/background/retained-children.ts +14 -6
- package/src/runs/background/run-status.ts +101 -5
- package/src/runs/background/stale-run-reconciler.ts +3 -3
- package/src/runs/background/subagent-runner.ts +60 -32
- package/src/runs/foreground/chain-execution.ts +37 -2
- package/src/runs/foreground/execution.ts +90 -19
- package/src/runs/foreground/foreground-control.ts +12 -0
- package/src/runs/foreground/prompt-audit.ts +172 -0
- package/src/runs/foreground/subagent-executor.ts +829 -203
- package/src/runs/shared/acceptance.ts +13 -4
- package/src/runs/shared/completion-guard.ts +17 -1
- package/src/runs/shared/llm-intent-arbiter.ts +302 -0
- package/src/runs/shared/parallel-utils.ts +2 -0
- package/src/runs/shared/pi-args.ts +44 -1
- package/src/runs/shared/run-fanout-budget.ts +280 -0
- package/src/runs/shared/single-output.ts +4 -2
- package/src/runs/shared/task-intent.ts +19 -3
- package/src/runs/shared/worktree.ts +17 -5
- package/src/shared/agent-stream-options.ts +5 -0
- package/src/shared/artifacts.ts +2 -6
- package/src/shared/display-text.ts +50 -0
- package/src/shared/node-executable.ts +21 -0
- package/src/shared/types.ts +115 -4
- package/src/shared/utils.ts +3 -1
- package/src/tui/fleet-status.ts +7 -5
- package/src/tui/fleet-transcript.ts +1 -48
- package/src/tui/fleet.ts +228 -13
- package/src/tui/render.ts +86 -39
- package/src/watchdog/permission-arbiter.ts +2 -1
- package/src/watchdog/review.ts +4 -3
- package/src/workflows/chat-progress.ts +8 -2
- 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",
|
|
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",
|
|
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(
|
|
1614
|
+
c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
|
|
1580
1615
|
}
|
|
1581
|
-
c.addChild(new Text(truncLine(theme.fg("accent",
|
|
1582
|
-
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim",
|
|
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(
|
|
1622
|
+
c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
|
|
1588
1623
|
}
|
|
1589
|
-
c.addChild(new Text(truncLine(theme.fg("dim",
|
|
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",
|
|
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",
|
|
1595
|
-
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim",
|
|
1596
|
-
if (r.truncation?.artifactPath) c.addChild(new Text(truncLine(theme.fg("dim",
|
|
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",
|
|
1631
|
-
if (phase) c.addChild(new Text(truncLine(theme.fg("dim",
|
|
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",
|
|
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",
|
|
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(
|
|
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",
|
|
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(
|
|
1708
|
-
if (entry.error) c.addChild(new Text(truncLine(theme.fg("error",
|
|
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",
|
|
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(
|
|
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",
|
|
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(
|
|
1780
|
+
c.addChild(new Text(truncLine(`${detailIndent}${nestedLine}`, width), 0, 0));
|
|
1740
1781
|
}
|
|
1741
|
-
c.addChild(new Text(truncLine(theme.fg("accent",
|
|
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",
|
|
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(
|
|
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",
|
|
1752
|
-
if (r.artifactPaths) c.addChild(new Text(truncLine(theme.fg("dim",
|
|
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",
|
|
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)
|
|
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",
|
|
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
|
-
|
|
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",
|
package/src/watchdog/review.ts
CHANGED
|
@@ -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?:
|
|
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
|
-
|
|
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"
|
|
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
|
|
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
|
-
|
|
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 = () =>
|
|
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
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
|