pi-subagents 0.48.0 → 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 +47 -19
- package/docs/agents.md +1 -0
- package/docs/configuration.md +28 -5
- package/docs/missions.md +4 -2
- package/docs/observability.md +26 -2
- package/package.json +1 -1
- package/src/agents/agents.ts +27 -12
- package/src/api/preflight.ts +1 -1
- package/src/extension/config.ts +20 -0
- package/src/extension/index.ts +18 -8
- package/src/extension/public-execution.ts +30 -4
- package/src/extension/schemas.ts +8 -7
- 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 +3 -1
- package/src/missions/store.ts +8 -3
- package/src/runs/background/active-async-capacity.ts +82 -25
- package/src/runs/background/async-execution.ts +24 -36
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +11 -1
- package/src/runs/background/fleet-view.ts +18 -5
- 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 +96 -4
- package/src/runs/background/subagent-runner.ts +4 -1
- package/src/runs/foreground/prompt-audit.ts +4 -3
- package/src/runs/foreground/subagent-executor.ts +219 -20
- package/src/runs/shared/completion-guard.ts +17 -1
- package/src/runs/shared/llm-intent-arbiter.ts +39 -23
- 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 +23 -4
- package/src/tui/fleet-transcript.ts +1 -48
- package/src/tui/fleet.ts +3 -1
- 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 +46 -7
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;
|
|
@@ -332,7 +332,7 @@ export interface WorkflowScriptChildResult {
|
|
|
332
332
|
export interface WorkflowScriptTraceEntry {
|
|
333
333
|
operation: "run" | "status";
|
|
334
334
|
key: string;
|
|
335
|
-
state: "started" | "completed" | "failed" | "reused";
|
|
335
|
+
state: "started" | "completed" | "failed" | "stopped" | "reused";
|
|
336
336
|
/** Canonical child agent name when resolved launch or result data is available. */
|
|
337
337
|
agent?: string;
|
|
338
338
|
runId?: string;
|
|
@@ -492,6 +492,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
492
492
|
const children = new Map<string, WorkflowScriptChildResult>();
|
|
493
493
|
const childOrder: string[] = [];
|
|
494
494
|
const launches = new Map<string, { fingerprint: string; promise: Promise<WorkflowScriptChildResult>; observed: boolean }>();
|
|
495
|
+
const stoppedLaunches = new Set<string>();
|
|
495
496
|
const batchAdmissions = new Map<string, Promise<void>>();
|
|
496
497
|
const observedRunCalls = new Set<number>();
|
|
497
498
|
const childController = new AbortController();
|
|
@@ -519,7 +520,30 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
519
520
|
else if (completionError) reject(new WorkflowScriptError(completionError.message, partial()));
|
|
520
521
|
else resolve({ value: outcome.value, ...partial() });
|
|
521
522
|
};
|
|
522
|
-
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
|
+
};
|
|
523
547
|
const timer = options.timeoutMs === undefined
|
|
524
548
|
? undefined
|
|
525
549
|
: setTimeout(() => finish({ error: new Error(`Workflow script timed out after ${options.timeoutMs}ms.`) }), options.timeoutMs);
|
|
@@ -531,6 +555,7 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
531
555
|
if (!settled && code !== 0) finish({ error: new Error(`Workflow worker exited with code ${code}.`) });
|
|
532
556
|
});
|
|
533
557
|
worker.on("message", (message: Record<string, unknown>) => {
|
|
558
|
+
if (settled) return;
|
|
534
559
|
if (message.type === "emit") {
|
|
535
560
|
try {
|
|
536
561
|
assertWorkflowJsonValue(message.value, "emit");
|
|
@@ -617,7 +642,9 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
617
642
|
const target = known?.runId ?? keyOrRunId;
|
|
618
643
|
trace.push({ operation: "status", key: keyOrRunId, state: "started", ...(known?.runId ? { runId: known.runId } : {}) });
|
|
619
644
|
traceChanged();
|
|
645
|
+
if (settled) return;
|
|
620
646
|
respond(options.status(target, childController.signal).then((result) => {
|
|
647
|
+
if (settled) return result;
|
|
621
648
|
trace.push({ operation: "status", key: keyOrRunId, state: result.ok ? "completed" : "failed", ...(result.runId ? { runId: result.runId } : {}), ...(!result.ok ? { error: result.output } : {}) });
|
|
622
649
|
traceChanged();
|
|
623
650
|
if (!result.ok) throw new Error(`Status '${keyOrRunId}' failed: ${result.output}`);
|
|
@@ -680,9 +707,6 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
680
707
|
}
|
|
681
708
|
|
|
682
709
|
const startedAt = Date.now();
|
|
683
|
-
childOrder.push(key);
|
|
684
|
-
trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params) });
|
|
685
|
-
traceChanged();
|
|
686
710
|
const batch = isRecord(message.args.batch) && typeof message.args.batch.id === "string" && Array.isArray(message.args.batch.calls)
|
|
687
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)) }
|
|
688
712
|
: undefined;
|
|
@@ -694,11 +718,22 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
694
718
|
seenKeys.add(call.key);
|
|
695
719
|
return true;
|
|
696
720
|
});
|
|
697
|
-
admission = Promise.resolve().then(() =>
|
|
721
|
+
admission = Promise.resolve().then(() => {
|
|
722
|
+
if (settled) return;
|
|
723
|
+
return options.admit?.(calls);
|
|
724
|
+
});
|
|
698
725
|
if (batch) batchAdmissions.set(batch.id, admission);
|
|
699
726
|
}
|
|
700
|
-
const promise = admission.then(() =>
|
|
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) => {
|
|
701
735
|
const normalized = !result.ok && !result.error ? { ...result, error: result.output } : result;
|
|
736
|
+
if (stoppedLaunches.has(key)) return normalized;
|
|
702
737
|
children.set(key, normalized);
|
|
703
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 } : {}) });
|
|
704
739
|
traceChanged();
|
|
@@ -706,12 +741,16 @@ export async function runWorkflowScript(options: RunWorkflowScriptOptions): Prom
|
|
|
706
741
|
}, (error: unknown) => {
|
|
707
742
|
const text = error instanceof Error ? error.message : String(error);
|
|
708
743
|
const failure: WorkflowScriptChildResult = { key, ok: false, output: text, error: text, artifactPaths: [] };
|
|
744
|
+
if (stoppedLaunches.has(key)) return failure;
|
|
709
745
|
children.set(key, failure);
|
|
710
746
|
trace.push({ operation: "run", key, state: "failed", durationMs: Date.now() - startedAt, ...workflowStringMetadata(params), error: text });
|
|
711
747
|
traceChanged();
|
|
712
748
|
return failure;
|
|
713
749
|
});
|
|
714
750
|
launches.set(key, { fingerprint, promise, observed: callObserved });
|
|
751
|
+
childOrder.push(key);
|
|
752
|
+
trace.push({ operation: "run", key, state: "started", ...workflowStringMetadata(params) });
|
|
753
|
+
traceChanged();
|
|
715
754
|
respond(deliver(promise));
|
|
716
755
|
});
|
|
717
756
|
|