@marcoscale98/pi-extensible-workflows 5.14.1-fork.1 → 5.17.0-fork.1
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 +67 -0
- package/dist/src/agent-execution.js +36 -51
- package/dist/src/background-widget.d.ts +56 -0
- package/dist/src/background-widget.js +22 -15
- package/dist/src/decoders.js +2 -1
- package/dist/src/host-navigator.d.ts +18 -0
- package/dist/src/host-navigator.js +71 -13
- package/dist/src/host-view.d.ts +16 -1
- package/dist/src/host-view.js +109 -42
- package/dist/src/host.js +57 -24
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +867 -545
- package/dist/src/index.js.map +3 -3
- package/dist/src/registry.js +2 -1
- package/dist/src/roles.d.ts +70 -0
- package/dist/src/roles.js +325 -0
- package/dist/src/settings.d.ts +13 -0
- package/dist/src/settings.js +253 -0
- package/dist/src/trajectory.d.ts +29 -4
- package/dist/src/trajectory.js +58 -3
- package/dist/src/types.d.ts +0 -1
- package/dist/src/utils.d.ts +2 -1
- package/dist/src/utils.js +10 -0
- package/dist/src/validation.d.ts +5 -17
- package/dist/src/validation.js +9 -458
- package/dist/subagents/index.js +1132 -694
- package/dist/subagents/index.js.map +3 -3
- package/dist/subagents/src/contracts.d.ts +4 -0
- package/dist/subagents/src/index.d.ts +1 -1
- package/dist/subagents/src/index.js +2 -1
- package/dist/subagents/src/manager.js +29 -10
- package/dist/subagents/src/navigator.js +396 -247
- package/dist/subagents/src/view.d.ts +23 -2
- package/dist/subagents/src/view.js +96 -30
- package/dist/trajectory/assets/index.html +347 -231
- package/dist/trajectory/assets/prism.min.js +13 -0
- package/dist/trajectory/index.js +377 -257
- package/dist/trajectory/index.js.map +3 -3
- package/dist/trajectory/src/assets/index.html +347 -231
- package/dist/trajectory/src/assets/prism.min.js +13 -0
- package/dist/trajectory/src/export.js +4 -2
- package/dist/trajectory/src/index.d.ts +2 -0
- package/dist/trajectory/src/index.js +143 -42
- package/dist/trajectory/src/server.js +134 -9
- package/dist/trajectory/src/server.js.map +1 -1
- package/package.json +2 -1
- package/src/agent-execution.ts +36 -42
- package/src/background-widget.ts +25 -18
- package/src/decoders.ts +2 -1
- package/src/host-navigator.ts +58 -14
- package/src/host-view.ts +93 -37
- package/src/host.ts +51 -21
- package/src/index.ts +3 -1
- package/src/registry.ts +2 -1
- package/src/roles.ts +326 -0
- package/src/settings.ts +206 -0
- package/src/trajectory.ts +76 -7
- package/src/types.ts +1 -1
- package/src/utils.ts +8 -1
- package/src/validation.ts +12 -356
- package/subagents/README.md +1 -1
- package/subagents/src/contracts.ts +4 -0
- package/subagents/src/index.ts +3 -2
- package/subagents/src/manager.ts +28 -10
- package/subagents/src/navigator.ts +336 -180
- package/subagents/src/view.ts +97 -28
- package/trajectory/src/assets/index.html +347 -231
- package/trajectory/src/assets/prism.min.js +13 -0
- package/trajectory/src/export.ts +4 -2
- package/trajectory/src/index.ts +118 -38
- package/trajectory/src/server.ts +112 -12
package/src/host-navigator.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { type WorkflowRegistryApi } from "./registry.js";
|
|
|
6
6
|
import { deepFreeze, errorText, object, resolveModelReference, validateModelAliases } from "./utils.js";
|
|
7
7
|
import { saveModelAliases, resolveWorkflowSettings, workflowProjectSettingsPath, workflowSettingsPath } from "./validation.js";
|
|
8
8
|
import { openWorkflowArtifact, workflowPromptArtifact, workflowResultArtifact, workflowScriptArtifact, type WorkflowArtifact } from "./workflow-artifacts.js";
|
|
9
|
-
import { agentActionLabels, agentBreadcrumb, formatCheckpointReview, formatNavigatorRun, formatWorkflowPhaseDashboard, navigatorAttentionSort, navigatorRunLabels, SETTLED_AGENT_STATES, themeWorkflowProgressStyles, visibleAgentAttemptActions as visibleRegisteredAgentAttemptActions } from "./host-view.js";
|
|
9
|
+
import { agentActionLabels, agentBreadcrumb, formatCheckpointReview, formatNavigatorRun, formatWorkflowPhaseDashboard, navigatorAttentionSort, navigatorRunLabels, SETTLED_AGENT_STATES, themeWorkflowProgressStyles, visibleAgentAttemptActions as visibleRegisteredAgentAttemptActions, type WorkflowProgressStyles } from "./host-view.js";
|
|
10
10
|
import { buildWorkflowPhaseModel, buildWorkflowPhaseTree, navigateWorkflowPhaseTree, preserveWorkflowPhaseTreeSelection, workflowPhaseTreeInitialExpanded } from "./host-phases.js";
|
|
11
11
|
import { type WorkflowRecoveryContext, type createWorkflowRecovery } from "./host-recovery.js";
|
|
12
12
|
import { runDependencyIds } from "./retention.js";
|
|
@@ -115,14 +115,52 @@ function keybindingKeys(keybindings: unknown, name: string): readonly string[] |
|
|
|
115
115
|
}
|
|
116
116
|
type WorkflowKeybindings = { matches(data: string, binding: string): boolean };
|
|
117
117
|
const WORKFLOW_VIM_KEYS: Readonly<Record<string, string>> = { "tui.select.up": "k", "tui.select.down": "j", "tui.editor.cursorLeft": "h", "tui.editor.cursorRight": "l" };
|
|
118
|
-
function workflowKeyMatches(keybindings: WorkflowKeybindings, data: string, binding: string): boolean { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
|
|
119
|
-
|
|
118
|
+
export function workflowKeyMatches(keybindings: WorkflowKeybindings, data: string, binding: string): boolean { return keybindings.matches(data, binding) || WORKFLOW_VIM_KEYS[binding] === data; }
|
|
119
|
+
const WORKFLOW_KEY_LABELS: Readonly<Record<string, string>> = { up: "↑", down: "↓", left: "←", right: "→", pageUp: "pgup", pageDown: "pgdn" };
|
|
120
|
+
export function workflowKeyLabel(keybindings: unknown, binding: string, fallback: string, labels: Readonly<Record<string, string>> = WORKFLOW_KEY_LABELS): string {
|
|
120
121
|
const keys = keybindingKeys(keybindings, binding);
|
|
121
122
|
const configured = keys?.length ? keys.map((key) => labels[key] ?? key) : [fallback];
|
|
122
123
|
const vim = WORKFLOW_VIM_KEYS[binding];
|
|
123
124
|
return [...new Set(vim ? [...configured, vim] : configured)].join("/");
|
|
124
125
|
}
|
|
125
126
|
|
|
127
|
+
/**
|
|
128
|
+
* A yes/no question drawn inside a navigator dashboard. Pi's confirm dialog replaces a custom component and afterwards
|
|
129
|
+
* restores the editor rather than the component, which leaves the dashboard's promise pending and the session waiting on it.
|
|
130
|
+
*/
|
|
131
|
+
export function createInlineConfirm(requestRender: () => void) {
|
|
132
|
+
let pending: { readonly title: string; readonly message: string; yes: boolean; readonly resolve: (value: boolean) => void } | undefined;
|
|
133
|
+
const answer = (value: boolean): void => {
|
|
134
|
+
const current = pending;
|
|
135
|
+
pending = undefined;
|
|
136
|
+
current?.resolve(value);
|
|
137
|
+
requestRender();
|
|
138
|
+
};
|
|
139
|
+
return {
|
|
140
|
+
ask(title: string, message: string): Promise<boolean> {
|
|
141
|
+
answer(false);
|
|
142
|
+
return new Promise<boolean>((resolve) => { pending = { title, message, yes: true, resolve }; requestRender(); });
|
|
143
|
+
},
|
|
144
|
+
active(): boolean { return pending !== undefined; },
|
|
145
|
+
rows(width: number, styles: WorkflowProgressStyles): string[] {
|
|
146
|
+
if (!pending) return [];
|
|
147
|
+
const yes = pending.yes;
|
|
148
|
+
const message = truncateToVisualLines(pending.message, Number.MAX_SAFE_INTEGER, Math.max(1, width), 0).visualLines.map((line) => line.trimEnd());
|
|
149
|
+
return [styles.bold(pending.title), ...message, ...["Yes", "No"].map((option) => (option === "Yes") === yes ? `→ ${styles.accent(option)}` : ` ${option}`)];
|
|
150
|
+
},
|
|
151
|
+
hint(keybindings: unknown): string {
|
|
152
|
+
return `${workflowKeyLabel(keybindings, "tui.select.up", "↑")}/${workflowKeyLabel(keybindings, "tui.select.down", "↓")} select · ${workflowKeyLabel(keybindings, "tui.select.confirm", "enter")} confirm · ${workflowKeyLabel(keybindings, "tui.select.cancel", "esc")} cancel`;
|
|
153
|
+
},
|
|
154
|
+
handleInput(keybindings: WorkflowKeybindings, data: string): void {
|
|
155
|
+
if (!pending) return;
|
|
156
|
+
if (workflowKeyMatches(keybindings, data, "tui.select.up") || workflowKeyMatches(keybindings, data, "tui.select.down")) { pending.yes = !pending.yes; requestRender(); }
|
|
157
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.confirm")) answer(pending.yes);
|
|
158
|
+
else if (workflowKeyMatches(keybindings, data, "tui.select.cancel")) answer(false);
|
|
159
|
+
},
|
|
160
|
+
cancel(): void { answer(false); },
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
126
164
|
export type WorkflowNavigatorDependencies = {
|
|
127
165
|
pi: Pick<ExtensionAPI, "registerCommand">;
|
|
128
166
|
home: string | undefined;
|
|
@@ -189,7 +227,7 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
189
227
|
const setStatus = uiHostCapabilities(ctx.ui)?.setStatus;
|
|
190
228
|
setStatus?.call(ctx.ui, "workflow-stop", text);
|
|
191
229
|
};
|
|
192
|
-
const runAction = async (actionCommand: string, status: (text: string | undefined) => void = setWorkflowStatus): Promise<"dashboard" | "picker" | "stopped"> => {
|
|
230
|
+
const runAction = async (actionCommand: string, status: (text: string | undefined) => void = setWorkflowStatus, confirm: (title: string, message: string) => Promise<boolean> = (title, message) => confirmWithBlocked(ctx.ui, reportBlocked, title, message)): Promise<"dashboard" | "picker" | "stopped"> => {
|
|
193
231
|
const [action, runId, ...rest] = actionCommand.split(/\s+/);
|
|
194
232
|
try {
|
|
195
233
|
const run = runId ? runs.get(runId) : undefined;
|
|
@@ -263,7 +301,7 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
263
301
|
}
|
|
264
302
|
if (action === "stop" && run) {
|
|
265
303
|
const workflowName = stored?.loaded.run.workflowName ?? run.metadata.name;
|
|
266
|
-
if (!await
|
|
304
|
+
if (!await confirm("Stop workflow?", `Stop workflow ${workflowName} (${run.store.runId})? This cannot be undone.`)) return "dashboard";
|
|
267
305
|
status(`Stopping workflow ${workflowName}...`);
|
|
268
306
|
await stopWorkflowRun(run.store.runId);
|
|
269
307
|
status(`Workflow ${run.store.runId} stopped.`);
|
|
@@ -554,8 +592,13 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
554
592
|
let selectedNodeId = tree.nodes[0]?.id;
|
|
555
593
|
let expandedNodeIds = new Set(workflowPhaseTreeInitialExpanded(tree));
|
|
556
594
|
const terminalRows = () => Math.max(1, tuiRows(tui) - WORKFLOW_PANEL_FOOTER_ROWS);
|
|
557
|
-
const
|
|
558
|
-
const
|
|
595
|
+
const keyLabel = (binding: string, fallback: string) => workflowKeyLabel(keybindings, binding, fallback);
|
|
596
|
+
const inlineConfirm = createInlineConfirm(() => { if (!disposed) tui.requestRender(); });
|
|
597
|
+
const confirmInDashboard = async (title: string, message: string): Promise<boolean> => {
|
|
598
|
+
reportBlocked?.(true, title);
|
|
599
|
+
try { return await inlineConfirm.ask(title, message); }
|
|
600
|
+
finally { reportBlocked?.(false); }
|
|
601
|
+
};
|
|
559
602
|
const progressNow = () => {
|
|
560
603
|
const now = Date.now();
|
|
561
604
|
if (hardTerminalRunStates.has(view.run.state)) {
|
|
@@ -629,7 +672,7 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
629
672
|
stopStatus = status;
|
|
630
673
|
setWorkflowStatus(status);
|
|
631
674
|
if (!disposed) tui.requestRender();
|
|
632
|
-
}).then(async (outcome) => {
|
|
675
|
+
}, confirmInDashboard).then(async (outcome) => {
|
|
633
676
|
if (outcome === "stopped") { done("__stopped__"); return; }
|
|
634
677
|
await updateDashboard();
|
|
635
678
|
}).catch((error: unknown) => {
|
|
@@ -662,7 +705,8 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
662
705
|
const content = [...statusLines, ...phaseLines];
|
|
663
706
|
const rows = terminalRows();
|
|
664
707
|
const hintRows = rows >= 3 ? 1 : 0;
|
|
665
|
-
const
|
|
708
|
+
const confirmRows = inlineConfirm.rows(width, styles);
|
|
709
|
+
const viewport = Math.max(1, rows - hintRows - confirmRows.length);
|
|
666
710
|
const maxOffset = Math.max(0, content.length - viewport);
|
|
667
711
|
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
668
712
|
if (actionMode) {
|
|
@@ -683,11 +727,12 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
683
727
|
dashboardOffset = Math.max(0, Math.min(maxOffset, dashboardOffset));
|
|
684
728
|
const selectedNode = selectedNodeId ? tree.byId.get(selectedNodeId) : undefined;
|
|
685
729
|
const enterAction = selectedNode?.kind === "workflow" ? "run actions" : selectedNode?.kind === "agent" ? "agent actions" : selectedNode?.children.length ? "expand/collapse" : narrow ? "inspect" : "focus details";
|
|
686
|
-
const hint = truncateToVisualLines(theme.fg("dim", actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.editor.cursorLeft", "←")} tree · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} ${enterAction} · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "back"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
687
|
-
return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...(hintRows ? [hint] : [])];
|
|
730
|
+
const hint = truncateToVisualLines(theme.fg("dim", inlineConfirm.active() ? inlineConfirm.hint(keybindings) : actionMode ? `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} actions · ${keyLabel("tui.select.confirm", "enter")} run · ${keyLabel("tui.editor.cursorLeft", "←")} tree · ${keyLabel("tui.select.cancel", "esc")} tree` : `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")} tree · ${keyLabel("tui.editor.cursorLeft", "←")}/${keyLabel("tui.editor.cursorRight", "→")} collapse/expand · ${keyLabel("tui.select.confirm", "enter")} ${enterAction} · a actions · ${keyLabel("tui.select.cancel", "esc")} ${narrow && detailsMode ? "tree" : "back"}${content.length > viewport ? ` · ${keyLabel("tui.select.pageUp", "pgup")}/${keyLabel("tui.select.pageDown", "pgdn")} scroll` : ""} · auto-refresh 1s`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
731
|
+
return [...content.slice(dashboardOffset, dashboardOffset + viewport), ...confirmRows, ...(hintRows ? [hint] : [])];
|
|
688
732
|
},
|
|
689
733
|
invalidate() {},
|
|
690
734
|
handleInput(data: string) {
|
|
735
|
+
if (inlineConfirm.active()) { inlineConfirm.handleInput(keybindings, data); return; }
|
|
691
736
|
if (stopRequested || editorRunning) return;
|
|
692
737
|
const narrow = renderedWidth < 80;
|
|
693
738
|
if (!actionMode && (data === "a" || data === "A")) { actionMode = true; actionIndex = 0; dashboardOffset = 0; tui.requestRender(); return; }
|
|
@@ -776,7 +821,7 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
776
821
|
}
|
|
777
822
|
tui.requestRender();
|
|
778
823
|
},
|
|
779
|
-
dispose() { disposed = true; stopTimer(); setWorkflowStatus(undefined); },
|
|
824
|
+
dispose() { disposed = true; stopTimer(); setWorkflowStatus(undefined); inlineConfirm.cancel(); },
|
|
780
825
|
};
|
|
781
826
|
})
|
|
782
827
|
: await ctx.ui.select(view.dashboard, [...view.actions.keys(), "Back"]);
|
|
@@ -816,8 +861,7 @@ export function registerWorkflowNavigator(deps: WorkflowNavigatorDependencies):
|
|
|
816
861
|
const currentLayout = layout();
|
|
817
862
|
const maxOffset = Math.max(0, renderedLines.length - currentLayout.contentViewport);
|
|
818
863
|
offset = Math.min(offset, maxOffset);
|
|
819
|
-
const
|
|
820
|
-
const keyLabel = (binding: string, fallback: string) => workflowKeyLabel(keybindings, binding, fallback, keyLabels);
|
|
864
|
+
const keyLabel = (binding: string, fallback: string) => workflowKeyLabel(keybindings, binding, fallback);
|
|
821
865
|
const hint = truncateToVisualLines(theme.fg("dim", `${keyLabel("tui.select.up", "↑")}/${keyLabel("tui.select.down", "↓")}/pgup/pgdn scroll · enter select · esc cancel`), Number.MAX_SAFE_INTEGER, width, 1).visualLines[0] ?? "";
|
|
822
866
|
const controls = currentLayout.compactControls
|
|
823
867
|
? [options.map((option, index) => `${index === selectedIndex ? "[" : " "}${option}${index === selectedIndex ? "]" : " "}`).join(" ")]
|
package/src/host-view.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { keyHint, truncateToVisualLines, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
3
3
|
import { type AwaitingCheckpoint, type PersistedRun, type RunStore, type WorktreeReference } from "./persistence.js";
|
|
4
4
|
import { budgetUsage } from "./budget.js";
|
|
5
5
|
import { formatCost, formatTokens } from "./background-widget.js";
|
|
6
|
-
import { BUDGET_DIMENSIONS, HARD_TERMINAL_RUN_STATES, SETTLED_AGENT_STATES, WORKFLOW_AGENT_STALL_THRESHOLD_MS, sumAccounting, type AgentAccounting, type AgentAttemptAction, type AgentAttemptActionContext, type AgentRecord, type LaunchSnapshot, type StandaloneAgentAttemptActionContext, type WorkflowCatalogFunction, type WorkflowCatalogIndex } from "./types.js";
|
|
6
|
+
import { BUDGET_DIMENSIONS, HARD_TERMINAL_RUN_STATES, SETTLED_AGENT_STATES, WORKFLOW_AGENT_STALL_THRESHOLD_MS, sumAccounting, zeroAccounting, type AgentAccounting, type AgentAttemptAction, type AgentAttemptActionContext, type AgentRecord, type LaunchSnapshot, type StandaloneAgentAttemptActionContext, type WorkflowCatalogFunction, type WorkflowCatalogIndex } from "./types.js";
|
|
7
7
|
import { object, sanitizeDisplayText } from "./utils.js";
|
|
8
8
|
import {
|
|
9
9
|
WORKFLOW_PHASE_STATES,
|
|
@@ -28,7 +28,7 @@ export interface WorkflowProgressStyles {
|
|
|
28
28
|
dim(text: string): string;
|
|
29
29
|
bold(text: string): string;
|
|
30
30
|
}
|
|
31
|
-
const PLAIN_WORKFLOW_PROGRESS_STYLES: WorkflowProgressStyles = { accent: (text) => text, success: (text) => text, error: (text) => text, warning: (text) => text, muted: (text) => text, dim: (text) => text, bold: (text) => text };
|
|
31
|
+
export const PLAIN_WORKFLOW_PROGRESS_STYLES: WorkflowProgressStyles = { accent: (text) => text, success: (text) => text, error: (text) => text, warning: (text) => text, muted: (text) => text, dim: (text) => text, bold: (text) => text };
|
|
32
32
|
type AgentGroup = { label: string; entries: readonly { agent: AgentRecord; index: number; depth: number }[] };
|
|
33
33
|
function agentGroupKey(agent: AgentRecord): string { return JSON.stringify([agent.structuralPath ?? [], agent.parentBreadcrumb ?? null]); }
|
|
34
34
|
function agentGroupLabel(agents: readonly AgentRecord[]): string {
|
|
@@ -60,7 +60,7 @@ function renderGroupedAgents(agents: readonly AgentRecord[], render: (entry: { a
|
|
|
60
60
|
}
|
|
61
61
|
const RUN_STATE_GLYPH: Record<string, string> = { "not started": "○", queued: "○", pausing: "⏸", paused: "⏸", completed: "✓", failed: "✗", stopped: "✗", interrupted: "↯", budget_exhausted: "!", awaiting_input: "?" };
|
|
62
62
|
const AGENT_STATE_GLYPH: Record<string, string> = { queued: "○", waiting_for_child: "…", paused: "⏸", retrying: "↻", completed: "✓", failed: "✗", cancelled: "✗" };
|
|
63
|
-
function runStateGlyph(state: string, running: string): string { return state === "running" ? running : RUN_STATE_GLYPH[state] ?? "◆"; }
|
|
63
|
+
export function runStateGlyph(state: string, running: string): string { return state === "running" ? running : RUN_STATE_GLYPH[state] ?? "◆"; }
|
|
64
64
|
function agentStateGlyph(state: string, running: string): string { return state === "running" ? running : AGENT_STATE_GLYPH[state] ?? "○"; }
|
|
65
65
|
type ProgressStyleKey = "success" | "error" | "warning" | "accent" | "muted";
|
|
66
66
|
const PROGRESS_STATE_STYLE: Record<string, ProgressStyleKey> = { completed: "success", failed: "error", cancelled: "error", running: "accent", paused: "warning", pausing: "warning", interrupted: "warning", retrying: "accent", budget_exhausted: "warning", awaiting_input: "warning" };
|
|
@@ -70,10 +70,10 @@ function styleForState(map: Record<string, ProgressStyleKey>, state: string, sty
|
|
|
70
70
|
const key = map[state] ?? "muted";
|
|
71
71
|
return (text) => styles[key](text);
|
|
72
72
|
}
|
|
73
|
-
function progressStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
|
|
73
|
+
export function progressStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(PROGRESS_STATE_STYLE, state, styles); }
|
|
74
74
|
function workflowIconStyle(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(WORKFLOW_ICON_STYLE, state, styles); }
|
|
75
75
|
function phaseStyleForState(state: string, styles: WorkflowProgressStyles): (text: string) => string { return styleForState(PHASE_STATE_STYLE, state, styles); }
|
|
76
|
-
function formatWorkflowRuntime(durationMs: number): string {
|
|
76
|
+
export function formatWorkflowRuntime(durationMs: number): string {
|
|
77
77
|
const seconds = Math.max(0, Math.floor(durationMs / 1000));
|
|
78
78
|
if (seconds < 60) return `${String(seconds)}s`;
|
|
79
79
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -96,7 +96,7 @@ function formatLogTimestamp(timestamp: number | undefined): string {
|
|
|
96
96
|
if (Number.isNaN(date.getTime())) return "--:--:--";
|
|
97
97
|
return [date.getHours(), date.getMinutes(), date.getSeconds()].map((part) => String(part).padStart(2, "0")).join(":");
|
|
98
98
|
}
|
|
99
|
-
function workflowLogLines(run: PersistedRun, styles: WorkflowProgressStyles, expanded: boolean, width?: number, showHint = false): string[] {
|
|
99
|
+
function workflowLogLines(run: PersistedRun, styles: WorkflowProgressStyles, expanded: boolean, width?: number, showHint = false, tail = 5): string[] {
|
|
100
100
|
const events = (run.events ?? []).filter((event) => event.type === "log");
|
|
101
101
|
if (!events.length) return [];
|
|
102
102
|
const indent = " ".repeat(11);
|
|
@@ -106,12 +106,48 @@ function workflowLogLines(run: PersistedRun, styles: WorkflowProgressStyles, exp
|
|
|
106
106
|
const chunks = truncateToVisualLines(message, Number.MAX_SAFE_INTEGER, Math.max(1, width - 11), 0).visualLines;
|
|
107
107
|
return chunks.map((chunk, chunkIndex) => `${chunkIndex === 0 ? prefix : indent}${chunk}`);
|
|
108
108
|
}));
|
|
109
|
-
const visible = expanded ? visualLines : visualLines.slice(-
|
|
109
|
+
const visible = expanded ? visualLines : visualLines.slice(-tail);
|
|
110
110
|
let hint = "";
|
|
111
111
|
if (!expanded && showHint) { try { hint = ` (${keyHint("app.tools.expand", "to expand")})`; } catch { /* Theme is unavailable in non-interactive render tests. */ } }
|
|
112
112
|
return [expanded ? ` ${styles.muted("Logs")}` : ` ${styles.muted(`Logs${hint}`)}`, ...visible];
|
|
113
113
|
}
|
|
114
|
-
|
|
114
|
+
const ACTIVE_AGENT_STATES: ReadonlySet<string> = new Set(["running", "waiting_for_child", "retrying", "paused"]);
|
|
115
|
+
const COMPACT_RECENT_SETTLED = 3;
|
|
116
|
+
const COMPACT_LOG_LINES = 3;
|
|
117
|
+
function expandHint(): string { try { return ` (${keyHint("app.tools.expand", "to expand")})`; } catch { return ""; } }
|
|
118
|
+
function agentEnd(agent: AgentRecord): number { return (agent.startedAt ?? 0) + (agent.durationMs ?? 0); }
|
|
119
|
+
/** One line for a finished phase: outcome counts, wall time across its agents, tokens, and cost. */
|
|
120
|
+
function phaseSummary(agents: readonly AgentRecord[], styles: WorkflowProgressStyles): string {
|
|
121
|
+
const count = (state: string) => agents.filter((agent) => agent.state === state).length;
|
|
122
|
+
const starts = agents.map((agent) => agent.startedAt).filter((value): value is number => value !== undefined);
|
|
123
|
+
const span = starts.length ? Math.max(...agents.map(agentEnd)) - Math.min(...starts) : 0;
|
|
124
|
+
const accounting = sumAccounting(agents.map((agent) => agent.accounting ?? zeroAccounting()));
|
|
125
|
+
const failed = count("failed");
|
|
126
|
+
const parts = [`${String(count("completed"))} done`, ...(failed ? [styles.error(`${String(failed)} failed`)] : []), ...(count("cancelled") ? [`${String(count("cancelled"))} cancelled`] : []), ...(span > 0 ? [formatWorkflowRuntime(span)] : []), formatTokens(accounting.input + accounting.output), formatCost(accounting.cost)].filter(Boolean);
|
|
127
|
+
return styles.muted(parts.join(" · "));
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Renders the run; with `maxLines`, a view taller than that limit switches to a compact one so the live block stays on screen.
|
|
131
|
+
* A block taller than the terminal pushes its changing header above the viewport, and pi-tui then clears and redraws the whole
|
|
132
|
+
* scrollback on every update, which snaps the reader back to the bottom.
|
|
133
|
+
*/
|
|
134
|
+
export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES, now = Date.now(), expanded = false, width?: number, showHint = false, maxLines?: number): string {
|
|
135
|
+
const full = workflowProgressLines(run, spinner, styles, now, expanded, width, showHint);
|
|
136
|
+
if (expanded || maxLines === undefined || full.length <= maxLines) return full.join("\n");
|
|
137
|
+
// Finished phases collapse to a summary line; the current one keeps failed, active, and the latest settled agents, in that priority.
|
|
138
|
+
const phases = run.phaseHistory?.length ? run.phaseHistory : run.phase ? [{ phase: run.phase, afterAgent: 0 }] : [];
|
|
139
|
+
const currentStart = Math.min(run.agents.length, phases.at(-1)?.afterAgent ?? 0);
|
|
140
|
+
const current = run.agents.slice(currentStart);
|
|
141
|
+
const recent = current.filter((agent) => SETTLED_AGENT_STATES.has(agent.state) && agent.state !== "failed").sort((left, right) => agentEnd(right) - agentEnd(left)).slice(0, COMPACT_RECENT_SETTLED);
|
|
142
|
+
const candidates = [...current.filter((agent) => agent.state === "failed"), ...current.filter((agent) => ACTIVE_AGENT_STATES.has(agent.state)), ...recent];
|
|
143
|
+
const render = (count: number) => workflowProgressLines(run, spinner, styles, now, false, width, showHint, new Set(candidates.slice(0, count)));
|
|
144
|
+
let low = 0;
|
|
145
|
+
let high = candidates.length;
|
|
146
|
+
while (low < high) { const middle = Math.ceil((low + high) / 2); if (render(middle).length <= maxLines) low = middle; else high = middle - 1; }
|
|
147
|
+
// NOTE: failures in finished phases always stay listed, so a run with more of them than maxLines still overflows.
|
|
148
|
+
return render(low).join("\n");
|
|
149
|
+
}
|
|
150
|
+
function workflowProgressLines(run: PersistedRun, spinner: string, styles: WorkflowProgressStyles, now: number, expanded: boolean, width: number | undefined, showHint: boolean, shown?: ReadonlySet<AgentRecord>): string[] {
|
|
115
151
|
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
116
152
|
const workflowIcon = runStateGlyph(run.state, spinner);
|
|
117
153
|
const iconStyle = workflowIconStyle(run.state, styles);
|
|
@@ -126,14 +162,15 @@ export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", style
|
|
|
126
162
|
const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, spinner, styles, now);
|
|
127
163
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
128
164
|
const byId = new Map(run.agents.map((agent) => [agent.id, agent]));
|
|
129
|
-
const
|
|
165
|
+
const position = new Map(run.agents.map((agent, index) => [agent, index]));
|
|
166
|
+
const renderAgents = (agents: readonly AgentRecord[], nested: boolean) => renderGroupedAgents(agents, ({ agent, depth }, grouped) => {
|
|
130
167
|
const icon = agentStateGlyph(agent.state, spinner);
|
|
131
168
|
const indent = " ".repeat((grouped ? 2 : 1) + depth);
|
|
132
169
|
const activity = SETTLED_AGENT_STATES.has(agent.state) ? "" : formatAgentActivity(agent, spinner, styles, now);
|
|
133
170
|
const name = grouped ? agent.label ?? agent.name : styledAgentBreadcrumb(agent, byId, styles);
|
|
134
171
|
const state = progressStyleForState(agent.state, styles);
|
|
135
172
|
const detail = expanded ? formatWorkflowAgentDetail(agent, now) : "";
|
|
136
|
-
return `${indent}#${String(
|
|
173
|
+
return `${indent}#${String((position.get(agent) ?? 0) + 1)} ${state(icon)} ${name} ${state(`[${agent.state}]`)}${activity ? ` ${activity}` : ""}${detail ? ` ${detail}` : ""}`;
|
|
137
174
|
}, run.agents, (label) => styles.muted(label)).map((line) => nested ? ` ${line}` : line);
|
|
138
175
|
const phases = run.phaseHistory?.length ? run.phaseHistory : run.phase ? [{ phase: run.phase, afterAgent: 0 }] : [];
|
|
139
176
|
if (scopedShells) {
|
|
@@ -146,10 +183,23 @@ export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", style
|
|
|
146
183
|
}
|
|
147
184
|
let renderedAgents = 0;
|
|
148
185
|
let nested = false;
|
|
186
|
+
// In compact mode (`shown` set) earlier segments list only their failures, and the current one only the shown agents plus a count of the rest.
|
|
187
|
+
const segment = (agents: readonly AgentRecord[], closed: boolean) => {
|
|
188
|
+
if (!shown) return renderAgents(agents, nested);
|
|
189
|
+
const listed = agents.filter((agent) => closed ? agent.state === "failed" : shown.has(agent));
|
|
190
|
+
const hidden = closed ? [] : agents.filter((agent) => !shown.has(agent));
|
|
191
|
+
const counts = [["queued", hidden.filter((agent) => !SETTLED_AGENT_STATES.has(agent.state) && !ACTIVE_AGENT_STATES.has(agent.state)).length], ["running", hidden.filter((agent) => ACTIVE_AGENT_STATES.has(agent.state)).length], ["done", hidden.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length]] as const;
|
|
192
|
+
const summary = counts.filter(([, count]) => count > 0).map(([label, count]) => `+${String(count)} ${label}`).join(" · ");
|
|
193
|
+
return [...renderAgents(listed, nested), ...(summary ? [`${nested ? " " : " "}${styles.muted(`… ${summary}${showHint ? expandHint() : ""}`)}`] : [])];
|
|
194
|
+
};
|
|
149
195
|
for (const [phaseIndex, phase] of phases.entries()) {
|
|
150
196
|
const boundary = Math.max(renderedAgents, Math.min(run.agents.length, phase.afterAgent));
|
|
151
|
-
|
|
152
|
-
lines.push(` ${
|
|
197
|
+
const before = run.agents.slice(renderedAgents, boundary);
|
|
198
|
+
if (shown && !nested && before.length) lines.push(` ${phaseSummary(before, styles)}`);
|
|
199
|
+
lines.push(...segment(before, true));
|
|
200
|
+
const next = phases[phaseIndex + 1];
|
|
201
|
+
const phaseAgents = next ? run.agents.slice(boundary, Math.max(boundary, Math.min(run.agents.length, next.afterAgent))) : [];
|
|
202
|
+
lines.push(` ${styles.muted(`[Phase: ${phase.phase}]`)}${shown && next ? ` ${phaseSummary(phaseAgents, styles)}` : ""}`);
|
|
153
203
|
const phaseShell = scopedShells ? shellActivityFor(run, phaseIndex) : undefined;
|
|
154
204
|
if (phaseShell) {
|
|
155
205
|
const rendered = formatShellActivity(phaseShell.active, phaseShell.startedAt, spinner, styles, now);
|
|
@@ -158,9 +208,9 @@ export function formatWorkflowProgress(run: PersistedRun, spinner = "◇", style
|
|
|
158
208
|
renderedAgents = boundary;
|
|
159
209
|
nested = true;
|
|
160
210
|
}
|
|
161
|
-
lines.push(...
|
|
162
|
-
lines.push(...workflowLogLines(run, styles, expanded, width, showHint));
|
|
163
|
-
return lines
|
|
211
|
+
lines.push(...segment(run.agents.slice(renderedAgents), false));
|
|
212
|
+
lines.push(...workflowLogLines(run, styles, expanded, width, showHint, shown ? COMPACT_LOG_LINES : 5));
|
|
213
|
+
return lines;
|
|
164
214
|
}
|
|
165
215
|
|
|
166
216
|
const workflowSpinner = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -408,7 +458,11 @@ export function workflowProgressBlock(run: PersistedRun, theme: Theme, progress?
|
|
|
408
458
|
}
|
|
409
459
|
previousState = displayed.state;
|
|
410
460
|
const frame = displayed.state === "running" ? workflowSpinner[Math.floor(now / 80) % workflowSpinner.length] ?? "◇" : "◇";
|
|
411
|
-
|
|
461
|
+
// pi-tui only patches rows that are still on screen; leave room below the block for the editor and footer.
|
|
462
|
+
// A finished or frozen block no longer changes, so it keeps the full list.
|
|
463
|
+
const rows = freezeAt === undefined && !terminal ? process.stdout.rows : undefined;
|
|
464
|
+
const maxLines = rows ? Math.max(6, rows - 10) - (prefix ? prefix.split("\n").length + 1 : 0) : undefined;
|
|
465
|
+
const progressText = formatWorkflowProgress(displayed, frame, styles, now, expanded, width, true, maxLines);
|
|
412
466
|
return truncateWorkflowProgress(prefix ? `${prefix}\n\n${progressText}` : progressText, width);
|
|
413
467
|
},
|
|
414
468
|
setExpanded(value: boolean) { expanded = value; },
|
|
@@ -582,7 +636,7 @@ export interface AgentDetailPresentation {
|
|
|
582
636
|
readonly accounting?: AgentAccounting;
|
|
583
637
|
readonly error?: { readonly code: string; readonly message: string };
|
|
584
638
|
}
|
|
585
|
-
function formatAgentError(error: NonNullable<AgentDetailPresentation["error"]>, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES): string {
|
|
639
|
+
export function formatAgentError(error: NonNullable<AgentDetailPresentation["error"]>, styles: WorkflowProgressStyles = PLAIN_WORKFLOW_PROGRESS_STYLES): string {
|
|
586
640
|
return styles.error(`Error: ${error.code}: ${error.message}`);
|
|
587
641
|
}
|
|
588
642
|
|
|
@@ -731,11 +785,7 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
731
785
|
const model = buildWorkflowPhaseModel(run, snapshot);
|
|
732
786
|
const tree = buildWorkflowPhaseTree(model);
|
|
733
787
|
const expanded = selection.expandedNodeIds === undefined ? workflowPhaseTreeInitialExpanded(tree) : new Set(selection.expandedNodeIds);
|
|
734
|
-
const wrap = (text: string
|
|
735
|
-
// ponytail: ANSI-only width, good enough for the ASCII labels the tree renders
|
|
736
|
-
const ansiPattern = new RegExp(ANSI_SGR_SOURCE, "g");
|
|
737
|
-
const visibleLength = (text: string): number => text.replace(ansiPattern, "").length;
|
|
738
|
-
const padTo = (text: string, limit: number): string => `${text}${" ".repeat(Math.max(0, limit - visibleLength(text)))}`;
|
|
788
|
+
const wrap = (text: string): string[] => wrapNavigatorLine(text, safeWidth);
|
|
739
789
|
const phaseStyle = (state: string): ((text: string) => string) => phaseStyleForState(state, styles);
|
|
740
790
|
const phase = selection.phaseId ? model.phases.find((candidate) => candidate.id === selection.phaseId) : undefined;
|
|
741
791
|
const selectedByAgent = selection.agentId ? tree.nodes.find((node) => node.kind === "agent" && node.agentId === selection.agentId && (!selection.phaseId || node.phaseId === selection.phaseId)) : undefined;
|
|
@@ -799,28 +849,34 @@ export function formatWorkflowPhaseDashboard(run: PersistedRun, snapshot: Readon
|
|
|
799
849
|
const scopedShells = (run.activeShellsByPhase?.length ?? 0) > 0;
|
|
800
850
|
const shellActivity = scopedShells ? undefined : formatShellActivity(run.activeShells, run.activeShellStartedAt, "⠦", styles, now);
|
|
801
851
|
if (shellActivity) lines.push(` ${shellActivity}`);
|
|
802
|
-
const renderTree = (limit: number): string[] => [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.flatMap((node) => wrap(treeLine(node), limit)) : [styles.muted("(empty)")])];
|
|
803
852
|
const actionRows = (): string[] => {
|
|
804
853
|
const actions = selection.actions;
|
|
805
854
|
if (!actions) return [];
|
|
806
855
|
return ["", styles.bold(actions.title), ...actions.options.map((option, index) => `${index === actions.index ? "→ " : " "}${index === actions.index ? styles.accent(option) : option}`)];
|
|
807
856
|
};
|
|
808
|
-
const
|
|
857
|
+
const treeRows = [styles.bold("Tree"), ...(visibleNodes.length ? visibleNodes.map(treeLine) : [styles.muted("(empty)")])];
|
|
858
|
+
lines.push(...formatNavigatorColumns(treeRows, [...details(selectedNode), ...actionRows()], safeWidth, selection));
|
|
859
|
+
if (model.unassignedAgents?.length && !tree.nodes.some((node) => node.phaseId === "unassigned")) lines.push(...wrap(styles.muted(`Unassigned agents: ${String(model.unassignedAgents.length)}`)));
|
|
860
|
+
return lines.flatMap((line) => wrap(line));
|
|
861
|
+
}
|
|
862
|
+
function wrapNavigatorLine(text: string, width: number): string[] {
|
|
863
|
+
return truncateToVisualLines(text, Number.MAX_SAFE_INTEGER, Math.max(1, width), 0).visualLines.map((line) => line.trimEnd());
|
|
864
|
+
}
|
|
865
|
+
/** The navigator layout: a sidebar beside details at 80 columns or wider, otherwise one of them (or both stacked). */
|
|
866
|
+
export function formatNavigatorColumns(sidebar: readonly string[], detail: readonly string[], width: number, layout: Readonly<{ treeOnly?: boolean | undefined; detailsOnly?: boolean | undefined }> = {}): string[] {
|
|
867
|
+
const safeWidth = Math.max(1, width);
|
|
809
868
|
if (safeWidth >= 80) {
|
|
810
869
|
const sidebarWidth = Math.min(42, Math.max(24, Math.floor((safeWidth - 3) * 0.38)));
|
|
811
|
-
const
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
lines.push(...detailRows().flatMap((line) => wrap(line)));
|
|
818
|
-
} else {
|
|
819
|
-
lines.push(...renderTree(safeWidth));
|
|
820
|
-
if (!selection.treeOnly) lines.push("", ...detailRows().flatMap((line) => wrap(line)));
|
|
870
|
+
const left = sidebar.flatMap((line) => wrapNavigatorLine(line, sidebarWidth));
|
|
871
|
+
const right = detail.flatMap((line) => wrapNavigatorLine(line, safeWidth - sidebarWidth - 3));
|
|
872
|
+
return Array.from({ length: Math.max(left.length, right.length) }, (_, index) => {
|
|
873
|
+
const cell = left[index] ?? "";
|
|
874
|
+
return `${cell}${" ".repeat(Math.max(0, sidebarWidth - visibleWidth(cell)))} | ${right[index] ?? ""}`;
|
|
875
|
+
});
|
|
821
876
|
}
|
|
822
|
-
if (
|
|
823
|
-
|
|
877
|
+
if (layout.detailsOnly) return detail.flatMap((line) => wrapNavigatorLine(line, safeWidth));
|
|
878
|
+
const left = sidebar.flatMap((line) => wrapNavigatorLine(line, safeWidth));
|
|
879
|
+
return layout.treeOnly ? left : [...left, ...detail.flatMap((line) => wrapNavigatorLine(line, safeWidth))];
|
|
824
880
|
}
|
|
825
881
|
export function formatCheckpointReview(checkpoint: AwaitingCheckpoint): string {
|
|
826
882
|
const context = JSON.stringify(checkpoint.context, null, 2);
|
package/src/host.ts
CHANGED
|
@@ -14,7 +14,8 @@ import { retainTerminalRuns } from "./retention.js";
|
|
|
14
14
|
import type { PersistedRun, WorktreeReference } from "./persistence.js";
|
|
15
15
|
import { validateBudget, WorkflowBudgetRuntime } from "./budget.js";
|
|
16
16
|
import { SerialLane, asWorkflowError, createLaunchSnapshot, errorCode, errorText, fail, isNodeError, jsonValue, mergeWorkflowExtensionSettings, modelAliasErrorName, modelCapability, object, parseModelReference, positiveInteger, sanitizeDisplayText, validateModelAliases } from "./utils.js";
|
|
17
|
-
import {
|
|
17
|
+
import { loadSettings, preflight, resolveAgentResourcePolicy, resolveWorkflowSettings, validateCheckpoint, validateModelAliasAvailability, validateWorkflowLaunchWithRegistry, workflowProjectSettingsPath, workflowSettingsPath } from "./validation.js";
|
|
18
|
+
import { loadAgentDefinitions, loadProjectAgentDefinitions } from "./roles.js";
|
|
18
19
|
import { beginWorkflowExtensionLoading, loadingRegistry, resetWorkflowRegistryIfIdle, retainWorkflowRegistry, type WorkflowRegistryApi } from "./registry.js";
|
|
19
20
|
import { agentHandleTurnPath, agentIdentityPath, agentWorktree, encoded, executeShellCommand, persistActiveAgentAttempt, persistAgentAttempts, readShellResult, runWorkflow, shellIdentityPath } from "./execution.js";
|
|
20
21
|
import backgroundWidget, { type BackgroundWidgetAPI } from "./background-widget.js";
|
|
@@ -34,7 +35,6 @@ import {
|
|
|
34
35
|
workflowControlCall,
|
|
35
36
|
workflowControlResult,
|
|
36
37
|
workflowProgressBlock,
|
|
37
|
-
formatWorkflowProgress,
|
|
38
38
|
type WorkflowProgressRenderState,
|
|
39
39
|
} from "./host-view.js";
|
|
40
40
|
import {
|
|
@@ -176,7 +176,9 @@ export const WORKFLOW_STATUS_PARAMETERS = Type.Object({ runId: Type.String({ des
|
|
|
176
176
|
export const WORKFLOW_RETRY_PARAMETERS = Type.Object({ runId: Type.String({ description: "Explicit failed workflow run ID" }), expectedState: Type.Optional(Type.String({ description: "Persisted source state observed before recovery" })), foreground: Type.Optional(Type.Boolean({ description: "Override the source launch mode for this recovery" })) });
|
|
177
177
|
|
|
178
178
|
function workflowToolUpdate(run: PersistedRun): WorkflowToolUpdate {
|
|
179
|
-
|
|
179
|
+
//NOTE: renderers read details.run; the partial text stays O(1) so high-frequency updates never format the whole tree.
|
|
180
|
+
const done = run.agents.filter((agent) => SETTLED_AGENT_STATES.has(agent.state)).length;
|
|
181
|
+
return { content: [{ type: "text", text: `Workflow: ${run.workflowName} (${String(done)}/${String(run.agents.length)} done) [${run.state}]` }], details: { runId: run.id, run } };
|
|
180
182
|
}
|
|
181
183
|
function agentWithProgress(agent: AgentRecord, progress: AgentProgress): AgentRecord {
|
|
182
184
|
const next = { ...agent, accounting: progress.accounting, toolCalls: progress.toolCalls };
|
|
@@ -200,6 +202,7 @@ function deliver(pi: WorkflowExtensionAPI, content: string): void {
|
|
|
200
202
|
pi.sendMessage({ customType: "workflow", content, display: true }, { deliverAs: "followUp", triggerTurn: true });
|
|
201
203
|
}
|
|
202
204
|
const WORKFLOW_WARNING_ENTRY = "workflow-warning";
|
|
205
|
+
const PROGRESS_COALESCE_MS = 100;
|
|
203
206
|
interface WorkflowWarningEntry { message: string }
|
|
204
207
|
function deliverWarning(pi: WorkflowExtensionAPI, content: string): void {
|
|
205
208
|
pi.appendEntry<WorkflowWarningEntry>(WORKFLOW_WARNING_ENTRY, { message: content });
|
|
@@ -415,7 +418,6 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
415
418
|
const trusted = projectTrusted(context);
|
|
416
419
|
const settings = resolveWorkflowSettings(cwd, trusted, workflowSettingsPath(extensionAgentDir)).effective.extensionSettings?.trajectory;
|
|
417
420
|
const port = settings?.port;
|
|
418
|
-
const themes = settings?.themes ?? false;
|
|
419
421
|
const loadRuns = createTrajectoryRunLoader(cwd, sessionId, home, overlayLiveRun);
|
|
420
422
|
const loadSubagents = createTrajectorySubagentLoader(cwd, sessionId, extensionAgentDir, overlayLiveSubagent);
|
|
421
423
|
const loadRunsMetadata = createTrajectoryRunMetadataLoader(cwd, sessionId, home, overlayLiveRun);
|
|
@@ -424,7 +426,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
424
426
|
const live = liveSubagents.get(subagentId);
|
|
425
427
|
return live?.status.sessionId === sessionId ? live.status.attemptDetails?.at(-1)?.session : undefined;
|
|
426
428
|
});
|
|
427
|
-
return { cwd, sessionId, ...(port === undefined ? {} : { port }),
|
|
429
|
+
return { cwd, sessionId, ...(port === undefined ? {} : { port }), loadRuns, loadSubagents, loadMetadata: async () => ({ runs: await loadRunsMetadata(), subagents: await loadSubagentsMetadata() }), loadTranscript, handleAction: (request: Readonly<TrajectoryActionRequest>) => trajectoryAction(request, context) };
|
|
428
430
|
};
|
|
429
431
|
const withLiveActivities = (run: PersistedRun): PersistedRun => liveAgents.overlay(run);
|
|
430
432
|
/** Overlays the in-memory lifecycle state and budget usage of an active run onto its persisted record. */
|
|
@@ -586,22 +588,49 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
586
588
|
const scheduler = new FairAgentScheduler(async ({ id, runId, tuiIndex, parentId, prompt, options, signal, setSteer }) => {
|
|
587
589
|
const run = runs.get(runId);
|
|
588
590
|
if (!run) throw new WorkflowError("INTERNAL_ERROR", `Unknown production run: ${runId}`);
|
|
591
|
+
const applyProgress = async (progress: AgentProgress) => {
|
|
592
|
+
let runState: PersistedRun;
|
|
593
|
+
if (progress.persist) {
|
|
594
|
+
runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) } : current);
|
|
595
|
+
} else {
|
|
596
|
+
const loaded = await run.store.loadStatus();
|
|
597
|
+
if (!loaded.agents.some((agent) => agent.id === id)) return;
|
|
598
|
+
runState = { ...loaded, ...run.budget.snapshot(), agents: loaded.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) };
|
|
599
|
+
}
|
|
600
|
+
if (!runState.agents.some((agent) => agent.id === id)) return;
|
|
601
|
+
liveAgents.setActivity(runId, id, progress.activity);
|
|
602
|
+
liveAgents.setEventTime(runId, id, progress.lastEventAt);
|
|
603
|
+
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
604
|
+
};
|
|
605
|
+
// Display-only progress bursts coalesce to one trailing apply per window; persisted progress always applies immediately.
|
|
606
|
+
let deferred: AgentProgress | undefined;
|
|
607
|
+
let deferredTimer: NodeJS.Timeout | undefined;
|
|
608
|
+
let appliedAt = 0;
|
|
609
|
+
const cancelDeferredProgress = () => {
|
|
610
|
+
if (deferredTimer) clearTimeout(deferredTimer);
|
|
611
|
+
deferredTimer = undefined;
|
|
612
|
+
deferred = undefined;
|
|
613
|
+
};
|
|
614
|
+
const onProgress = async (progress: AgentProgress) => {
|
|
615
|
+
const now = Date.now();
|
|
616
|
+
if (progress.persist || (!deferredTimer && now - appliedAt >= PROGRESS_COALESCE_MS)) {
|
|
617
|
+
cancelDeferredProgress();
|
|
618
|
+
appliedAt = now;
|
|
619
|
+
await applyProgress(progress);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
deferred = progress;
|
|
623
|
+
deferredTimer ??= setTimeout(() => {
|
|
624
|
+
deferredTimer = undefined;
|
|
625
|
+
const next = deferred;
|
|
626
|
+
deferred = undefined;
|
|
627
|
+
if (!next) return;
|
|
628
|
+
appliedAt = Date.now();
|
|
629
|
+
void applyProgress(next).catch(() => undefined);
|
|
630
|
+
}, Math.max(0, PROGRESS_COALESCE_MS - (now - appliedAt))).unref();
|
|
631
|
+
};
|
|
589
632
|
try {
|
|
590
633
|
const budget = run.budget.forAgent(id);
|
|
591
|
-
const onProgress = async (progress: AgentProgress) => {
|
|
592
|
-
let runState: PersistedRun;
|
|
593
|
-
if (progress.persist) {
|
|
594
|
-
runState = await persistRunState(run.store, run.metadata, (current) => current.agents.some((agent) => agent.id === id) ? { ...current, ...run.budget.snapshot(), agents: current.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) } : current);
|
|
595
|
-
} else {
|
|
596
|
-
const loaded = await run.store.load();
|
|
597
|
-
if (!loaded.run.agents.some((agent) => agent.id === id)) return;
|
|
598
|
-
runState = { ...loaded.run, ...run.budget.snapshot(), agents: loaded.run.agents.map((agent) => agent.id === id ? agentWithProgress(agent, progress) : agent) };
|
|
599
|
-
}
|
|
600
|
-
if (!runState.agents.some((agent) => agent.id === id)) return;
|
|
601
|
-
liveAgents.setActivity(runId, id, progress.activity);
|
|
602
|
-
liveAgents.setEventTime(runId, id, progress.lastEventAt);
|
|
603
|
-
run.update?.(workflowToolUpdate(withLiveActivities(runState)));
|
|
604
|
-
};
|
|
605
634
|
const onAttempt = async (attempt: AgentAttempt) => {
|
|
606
635
|
liveAgents.setSession(runId, id, attempt.liveSession);
|
|
607
636
|
liveAgents.setHandoff(runId, id, attempt);
|
|
@@ -618,6 +647,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
618
647
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
619
648
|
};
|
|
620
649
|
const result = await run.executor.execute(prompt, { label: options.label, workflowName: run.metadata.name, tuiIndex, tuiLabel: options.requestedLabel ?? options.label, agentNodeId: id, ...(options.extensionSettings === undefined ? {} : { inheritedExtensionSettings: options.extensionSettings }), onProgress, onAttempt, budget, ...(run.providerErrorRecovery ? { providerErrorRecovery: run.providerErrorRecovery } : {}), ...(parentId ? { parent: parentId, cwd: options.cwd, ...(options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}) } : options.worktreeOwner ? { worktreeOwner: options.worktreeOwner } : {}), ...(options.model ? { model: options.model } : {}), ...(options.role ? { role: options.role } : {}), ...(options.contextFiles ? { contextFiles: options.contextFiles } : {}), tools: options.tools, ...(options.skills ? { skills: options.skills } : {}), ...(options.extensions ? { extensions: options.extensions } : {}), effectiveTools: options.tools, ...(options.schema ? { schema: options.schema } : {}), ...(options.retries === undefined ? {} : { retries: options.retries }), ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }), ...(options.sessionPath ? { sessionPath: options.sessionPath } : {}), ...(options.agentOptions ? { agentOptions: options.agentOptions } : {}), ...(options.agentIdentity ? { agentIdentity: options.agentIdentity } : {}) }, signal, scheduler.toolsFor(id, (role, tools, model, inheritedTools, skills, extensions) => run.executor.resolve({ label: "child", workflowName: run.metadata.name, ...(model ? { model } : {}), ...(role ? { role } : {}), ...(tools !== undefined ? { tools } : {}), ...(skills !== undefined ? { skills } : {}), ...(extensions !== undefined ? { extensions } : {}) }, inheritedTools).tools), setSteer, () => { scheduler.cancelChildren(id); scheduler.retry(id); });
|
|
650
|
+
cancelDeferredProgress();
|
|
621
651
|
const before = (await run.store.load()).run;
|
|
622
652
|
await persistAgentAttempts(run.store, id, result.attempts);
|
|
623
653
|
const completed = (await run.store.load()).run;
|
|
@@ -628,6 +658,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
628
658
|
run.update?.(workflowToolUpdate(withLiveActivities(persisted)));
|
|
629
659
|
return result.value;
|
|
630
660
|
} catch (error) {
|
|
661
|
+
cancelDeferredProgress();
|
|
631
662
|
liveAgents.setSession(runId, id);
|
|
632
663
|
const attempts = getAgentAttempts(error);
|
|
633
664
|
if (attempts?.length) {
|
|
@@ -1365,8 +1396,7 @@ export default function workflowExtension(pi: WorkflowExtensionAPI, home?: strin
|
|
|
1365
1396
|
state.workflowProgressComponent = workflowProgressBlock(currentProgress?.run ?? incoming, theme, currentProgress, async () => {
|
|
1366
1397
|
const active = runs.get(incoming.id);
|
|
1367
1398
|
const store = active?.store ?? new RunStore(incoming.cwd, incoming.sessionId, incoming.id, home);
|
|
1368
|
-
|
|
1369
|
-
return withLiveActivities(loaded.run);
|
|
1399
|
+
return withLiveActivities(await store.loadStatus());
|
|
1370
1400
|
}, () => { if (state.workflowProgress === currentProgress) requestRender(); }, undefined, state.workflowProgressFrozenAt);
|
|
1371
1401
|
}
|
|
1372
1402
|
state.workflowProgressComponent.setExpanded(expanded);
|
package/src/index.ts
CHANGED
|
@@ -6,12 +6,14 @@ export * from "./utils.js";
|
|
|
6
6
|
export { canonicalPath, sameFilesystemPath } from "./paths.js";
|
|
7
7
|
export * from "./budget.js";
|
|
8
8
|
export * from "./validation.js";
|
|
9
|
+
export * from "./roles.js";
|
|
9
10
|
export * from "./registry.js";
|
|
10
11
|
export * from "./execution.js";
|
|
11
12
|
export * from "./host.js";
|
|
12
13
|
export * from "./workflow-artifacts.js";
|
|
13
14
|
export { formatCost } from "./background-widget.js";
|
|
14
|
-
export { agentActionLabels, formatAgentDetail, navigatorAttentionSortByState, themeWorkflowProgressStyles, visibleStandaloneAgentAttemptActions } from "./host-view.js";
|
|
15
|
+
export { agentActionLabels, formatAgentDetail, formatAgentError, formatNavigatorColumns, formatWorkflowRuntime, navigatorAttentionSortByState, PLAIN_WORKFLOW_PROGRESS_STYLES, progressStyleForState, runStateGlyph, themeWorkflowProgressStyles, visibleStandaloneAgentAttemptActions } from "./host-view.js";
|
|
16
|
+
export { createInlineConfirm, workflowKeyLabel, workflowKeyMatches } from "./host-navigator.js";
|
|
15
17
|
export type { AgentDetailPresentation, WorkflowProgressStyles } from "./host-view.js";
|
|
16
18
|
export { default } from "./host.js";
|
|
17
19
|
export { acquireSessionLease, hasLiveSessionLease, listPersistedSessionIds, projectSessionsDirectory, projectStorageKey, RunStore, runsDirectory, SessionLease, structuralPath } from "./persistence.js";
|
package/src/registry.ts
CHANGED
|
@@ -5,7 +5,8 @@ import { Value } from "typebox/value";
|
|
|
5
5
|
import type { AgentAttemptAction, JsonSchema, JsonValue, RegisteredAgentSetupHook, WorkflowCatalog, WorkflowCatalogContext, WorkflowCatalogError, WorkflowCatalogFunction, WorkflowCatalogIndex, WorkflowCatalogModelAlias, WorkflowExtension, WorkflowExtensionSettings, WorkflowExtensionSettingsValidatorContext, WorkflowFunction, WorkflowFunctionContext, WorkflowFunctionSource, WorkflowJournal, WorkflowModelAlias, WorkflowModelAliasResolverContext, WorkflowRoleDirectoryRegistration } from "./types.js";
|
|
6
6
|
import type { SubagentRunRequest, SubagentStatus } from "../subagents/src/contracts.js";
|
|
7
7
|
import { byPriorityThenName, deepFreeze, errorCode, errorText, fail, jsonValue, MODEL_ALIAS_NAME, object, validWorkflowExtensionNamespace } from "./utils.js";
|
|
8
|
-
import { loadSettings, resolveWorkflowSettings
|
|
8
|
+
import { loadSettings, resolveWorkflowSettings } from "./settings.js";
|
|
9
|
+
import { validateSchema } from "./utils.js";
|
|
9
10
|
import { canonicalPath } from "./paths.js";
|
|
10
11
|
import { isCorePackageName } from "./package-identity.js";
|
|
11
12
|
|