pi-subagents 0.62.0 → 0.63.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 +28 -0
- package/docs/agents.md +5 -4
- package/docs/configuration.md +28 -2
- package/docs/models.md +5 -5
- package/docs/observability.md +4 -1
- package/docs/tool-reference.md +2 -2
- package/package.json +1 -1
- package/skills/pi-subagents/references/management-authoring-rpc.md +1 -1
- package/skills/pi-subagents/references/prompting-and-roles.md +3 -3
- package/src/agents/agent-management.ts +22 -4
- package/src/agents/agents.ts +107 -125
- package/src/api/shared-types.ts +3 -0
- package/src/extension/config.ts +20 -0
- package/src/inspectors/herdr/inspector-runner.ts +19 -13
- package/src/runs/background/active-async-capacity.ts +0 -1
- package/src/runs/background/async-execution.ts +53 -7
- package/src/runs/background/async-resume.ts +3 -0
- package/src/runs/background/async-status.ts +18 -2
- package/src/runs/background/notify.ts +13 -1
- package/src/runs/background/run-status.ts +22 -2
- package/src/runs/background/subagent-runner.ts +30 -2
- package/src/runs/background/wait-completions.ts +13 -0
- package/src/runs/foreground/subagent-executor.ts +35 -3
- package/src/runs/shared/acceptance.ts +15 -9
- package/src/runs/shared/lane-metadata.ts +24 -3
- package/src/runs/shared/parallel-handoff.ts +4 -0
- package/src/runs/shared/pi-args.ts +8 -2
- package/src/runs/shared/task-intent.ts +5 -2
- package/src/runs/shared/worktree.ts +467 -63
- package/src/shared/types.ts +29 -0
- package/src/shared/utils.ts +18 -7
- package/src/slash/subagents-admin.ts +24 -12
- package/src/tui/fleet-status.ts +61 -2
- package/src/tui/fleet.ts +12 -7
- package/src/tui/render.ts +222 -14
- package/src/workflows/workflow-checklist.ts +441 -0
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
import type { AsyncJobStep, HostStepNodeV1, WorkflowGraphNode, WorkflowGraphSnapshot, WorkflowPreflightLaneV1, WorkflowPreflightV1 } from "../shared/types.ts";
|
|
2
|
+
import { sanitizeDisplayText } from "../shared/display-text.ts";
|
|
3
|
+
|
|
4
|
+
export type WorkflowChecklistState = "complete" | "running" | "queued" | "blocked" | "failed" | "paused" | "stopped";
|
|
5
|
+
|
|
6
|
+
export interface WorkflowChecklistStep {
|
|
7
|
+
key?: string;
|
|
8
|
+
workflowKey?: string;
|
|
9
|
+
runId?: string;
|
|
10
|
+
label?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
phase?: string;
|
|
13
|
+
agent?: string;
|
|
14
|
+
status: string;
|
|
15
|
+
context?: "fresh" | "fork";
|
|
16
|
+
activityState?: string;
|
|
17
|
+
startedAt?: number;
|
|
18
|
+
endedAt?: number;
|
|
19
|
+
durationMs?: number;
|
|
20
|
+
currentTool?: string;
|
|
21
|
+
currentToolStartedAt?: number;
|
|
22
|
+
currentPath?: string;
|
|
23
|
+
turnCount?: number;
|
|
24
|
+
toolCount?: number;
|
|
25
|
+
outputName?: string;
|
|
26
|
+
error?: string;
|
|
27
|
+
toolBudgetBlocked?: boolean;
|
|
28
|
+
turnBudgetExceeded?: boolean;
|
|
29
|
+
timedOut?: boolean;
|
|
30
|
+
stopped?: boolean;
|
|
31
|
+
acceptance?: { status?: string; reviewResult?: { status?: string } };
|
|
32
|
+
review?: { status?: string };
|
|
33
|
+
watchdog?: { phase?: string };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface WorkflowChecklistTraceEntry {
|
|
37
|
+
operation?: string;
|
|
38
|
+
key: string;
|
|
39
|
+
state: string;
|
|
40
|
+
agent?: string;
|
|
41
|
+
runId?: string;
|
|
42
|
+
phase?: string;
|
|
43
|
+
label?: string;
|
|
44
|
+
generatedLaneKey?: string;
|
|
45
|
+
durationMs?: number;
|
|
46
|
+
error?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface WorkflowChecklistItem {
|
|
50
|
+
key: string;
|
|
51
|
+
label: string;
|
|
52
|
+
phase: string;
|
|
53
|
+
state: WorkflowChecklistState;
|
|
54
|
+
agent?: string;
|
|
55
|
+
context?: "fresh" | "fork";
|
|
56
|
+
startedAt?: number;
|
|
57
|
+
durationMs?: number;
|
|
58
|
+
currentTool?: string;
|
|
59
|
+
currentToolStartedAt?: number;
|
|
60
|
+
currentPath?: string;
|
|
61
|
+
toolCount?: number;
|
|
62
|
+
outputName?: string;
|
|
63
|
+
error?: string;
|
|
64
|
+
preflight?: WorkflowPreflightLaneV1;
|
|
65
|
+
kind?: "child" | "host";
|
|
66
|
+
monitorKind?: HostStepNodeV1["monitorKind"];
|
|
67
|
+
provider?: string;
|
|
68
|
+
role?: string;
|
|
69
|
+
verdict?: HostStepNodeV1["verdict"];
|
|
70
|
+
target?: string;
|
|
71
|
+
reasonCode?: string;
|
|
72
|
+
stale?: boolean;
|
|
73
|
+
reportPath?: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface WorkflowChecklistPhase {
|
|
77
|
+
key: string;
|
|
78
|
+
label: string;
|
|
79
|
+
state: WorkflowChecklistState;
|
|
80
|
+
items: WorkflowChecklistItem[];
|
|
81
|
+
total: number;
|
|
82
|
+
done: number;
|
|
83
|
+
running: number;
|
|
84
|
+
queued: number;
|
|
85
|
+
blocked: number;
|
|
86
|
+
failed: number;
|
|
87
|
+
paused: number;
|
|
88
|
+
stopped: number;
|
|
89
|
+
parallel: boolean;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface WorkflowChecklistProjection {
|
|
93
|
+
phases: WorkflowChecklistPhase[];
|
|
94
|
+
total: number;
|
|
95
|
+
done: number;
|
|
96
|
+
running: number;
|
|
97
|
+
queued: number;
|
|
98
|
+
blocked: number;
|
|
99
|
+
failed: number;
|
|
100
|
+
paused: number;
|
|
101
|
+
stopped: number;
|
|
102
|
+
bottleneck?: WorkflowChecklistItem;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface WorkflowChecklistInput {
|
|
106
|
+
graph?: WorkflowGraphSnapshot;
|
|
107
|
+
steps?: readonly WorkflowChecklistStep[] | readonly AsyncJobStep[];
|
|
108
|
+
hostSteps?: readonly HostStepNodeV1[];
|
|
109
|
+
preflight?: WorkflowPreflightV1;
|
|
110
|
+
trace?: readonly WorkflowChecklistTraceEntry[];
|
|
111
|
+
now?: number;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const MAX_TEXT = 160;
|
|
115
|
+
const TERMINAL_STATES = new Set<WorkflowChecklistState>(["complete", "blocked", "failed", "paused", "stopped"]);
|
|
116
|
+
|
|
117
|
+
function text(value: unknown, fallback?: string): string | undefined {
|
|
118
|
+
if (typeof value !== "string") return fallback;
|
|
119
|
+
const clean = sanitizeDisplayText(value).replace(/\s+/g, " ").trim();
|
|
120
|
+
return clean ? clean.slice(0, MAX_TEXT) : fallback;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function keyText(value: unknown, fallback: string): string {
|
|
124
|
+
return text(value, fallback) ?? fallback;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function normalizedStatus(value: unknown): string {
|
|
128
|
+
return typeof value === "string" ? value.toLowerCase() : "queued";
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function finite(value: unknown): number | undefined {
|
|
132
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function count(value: unknown): number | undefined {
|
|
136
|
+
const number = finite(value);
|
|
137
|
+
return number !== undefined && number >= 0 ? Math.round(number) : undefined;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function explicitBlocked(source: Pick<WorkflowChecklistStep, "activityState" | "toolBudgetBlocked" | "turnBudgetExceeded" | "timedOut" | "acceptance" | "review" | "watchdog"> & { verdict?: unknown; stale?: unknown }): boolean {
|
|
141
|
+
const acceptance = normalizedStatus(source.acceptance?.status);
|
|
142
|
+
const review = normalizedStatus(source.review?.status ?? source.acceptance?.reviewResult?.status);
|
|
143
|
+
return source.toolBudgetBlocked === true
|
|
144
|
+
|| source.turnBudgetExceeded === true
|
|
145
|
+
|| source.activityState === "needs_attention"
|
|
146
|
+
|| source.timedOut === true
|
|
147
|
+
|| source.watchdog?.phase === "stale"
|
|
148
|
+
|| source.stale === true
|
|
149
|
+
|| source.verdict === "inconclusive"
|
|
150
|
+
|| acceptance === "rejected"
|
|
151
|
+
|| acceptance === "blockers"
|
|
152
|
+
|| review === "blockers"
|
|
153
|
+
|| review === "review-required";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function checklistState(source: Pick<WorkflowChecklistStep, "status" | "activityState" | "toolBudgetBlocked" | "turnBudgetExceeded" | "timedOut" | "acceptance" | "review" | "watchdog"> & { verdict?: unknown; stale?: unknown }): WorkflowChecklistState {
|
|
157
|
+
if (explicitBlocked(source)) return "blocked";
|
|
158
|
+
switch (normalizedStatus(source.status)) {
|
|
159
|
+
case "complete":
|
|
160
|
+
case "completed":
|
|
161
|
+
case "done":
|
|
162
|
+
case "pass":
|
|
163
|
+
case "accepted": return "complete";
|
|
164
|
+
case "running":
|
|
165
|
+
case "started":
|
|
166
|
+
case "active": return "running";
|
|
167
|
+
case "failed":
|
|
168
|
+
case "error":
|
|
169
|
+
case "fail": return "failed";
|
|
170
|
+
case "blocked":
|
|
171
|
+
case "rejected":
|
|
172
|
+
case "partial":
|
|
173
|
+
case "needs_attention": return "blocked";
|
|
174
|
+
case "paused":
|
|
175
|
+
case "detached": return "paused";
|
|
176
|
+
case "stopped":
|
|
177
|
+
case "cancelled":
|
|
178
|
+
case "canceled": return "stopped";
|
|
179
|
+
default: return "queued";
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function duration(step: Pick<WorkflowChecklistStep, "durationMs" | "startedAt" | "endedAt">, now: number | undefined, state: WorkflowChecklistState): number | undefined {
|
|
184
|
+
const explicit = finite(step.durationMs);
|
|
185
|
+
const startedAt = finite(step.startedAt);
|
|
186
|
+
if (explicit !== undefined) return Math.max(0, explicit);
|
|
187
|
+
if (startedAt === undefined) return undefined;
|
|
188
|
+
const end = state === "running" ? now : finite(step.endedAt) ?? now;
|
|
189
|
+
return end === undefined ? undefined : Math.max(0, end - startedAt);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function laneFor(key: string, phase: string, lanes: ReadonlyMap<string, WorkflowPreflightLaneV1>): WorkflowPreflightLaneV1 | undefined {
|
|
193
|
+
return lanes.get(key) ?? lanes.get(phase) ?? [...lanes.values()].find((lane) => key.startsWith(`${lane.key}.`));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function stepKey(step: WorkflowChecklistStep): string | undefined {
|
|
197
|
+
return step.key ?? step.workflowKey ?? step.runId;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function stepItem(step: WorkflowChecklistStep, index: number, phase: string, key = stepKey(step) ?? `step-${index + 1}`, label = step.label ?? step.description ?? stepKey(step) ?? step.agent ?? key, preflight?: WorkflowPreflightLaneV1): WorkflowChecklistItem {
|
|
201
|
+
const state = checklistState(step);
|
|
202
|
+
return {
|
|
203
|
+
key: keyText(key, `step-${index + 1}`),
|
|
204
|
+
label: keyText(label, key),
|
|
205
|
+
phase,
|
|
206
|
+
state,
|
|
207
|
+
...(text(step.agent) ? { agent: text(step.agent) } : {}),
|
|
208
|
+
...(step.context ? { context: step.context } : {}),
|
|
209
|
+
...(finite(step.startedAt) !== undefined ? { startedAt: finite(step.startedAt) } : {}),
|
|
210
|
+
...(duration(step, undefined, state) !== undefined ? { durationMs: duration(step, undefined, state) } : {}),
|
|
211
|
+
...(text(step.currentTool) ? { currentTool: text(step.currentTool) } : {}),
|
|
212
|
+
...(finite(step.currentToolStartedAt) !== undefined ? { currentToolStartedAt: finite(step.currentToolStartedAt) } : {}),
|
|
213
|
+
...(text(step.currentPath) ? { currentPath: text(step.currentPath) } : {}),
|
|
214
|
+
...(count(step.toolCount) !== undefined ? { toolCount: count(step.toolCount) } : {}),
|
|
215
|
+
...(text(step.outputName) ? { outputName: text(step.outputName) } : {}),
|
|
216
|
+
...(text(step.error) ? { error: text(step.error) } : {}),
|
|
217
|
+
...(preflight ? { preflight } : {}),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function hostItem(host: HostStepNodeV1, phase: string, key = host.id): WorkflowChecklistItem {
|
|
222
|
+
const state = checklistState({ status: host.state, verdict: host.verdict, stale: host.freshness?.stale });
|
|
223
|
+
return {
|
|
224
|
+
key: keyText(key, "host-step"),
|
|
225
|
+
label: keyText(host.label, "host step"),
|
|
226
|
+
phase,
|
|
227
|
+
state,
|
|
228
|
+
kind: "host",
|
|
229
|
+
monitorKind: host.monitorKind,
|
|
230
|
+
...(text(host.provider) ? { provider: text(host.provider) } : {}),
|
|
231
|
+
...(text(host.role) ? { role: text(host.role) } : {}),
|
|
232
|
+
...(host.verdict ? { verdict: host.verdict } : {}),
|
|
233
|
+
...(text(host.target) ? { target: text(host.target) } : {}),
|
|
234
|
+
...(text(host.reasonCode) ? { reasonCode: text(host.reasonCode) } : {}),
|
|
235
|
+
...(text(host.detail) ? { error: text(host.detail) } : {}),
|
|
236
|
+
...(host.freshness?.stale !== undefined ? { stale: host.freshness.stale } : {}),
|
|
237
|
+
...(text(host.reportPath) ? { reportPath: text(host.reportPath) } : {}),
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function graphNodes(graph: WorkflowGraphSnapshot | undefined): WorkflowGraphNode[] {
|
|
242
|
+
const out: WorkflowGraphNode[] = [];
|
|
243
|
+
const visit = (node: WorkflowGraphNode): void => {
|
|
244
|
+
if (node.kind === "parallel-group" || node.kind === "dynamic-parallel-group") {
|
|
245
|
+
for (const child of node.children ?? []) visit(child);
|
|
246
|
+
if (!node.children?.length) out.push(node);
|
|
247
|
+
} else {
|
|
248
|
+
out.push(node);
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
for (const node of graph?.nodes ?? []) visit(node);
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function traceSources(trace: readonly WorkflowChecklistTraceEntry[] | undefined): WorkflowChecklistTraceEntry[] {
|
|
256
|
+
const latest = new Map<string, WorkflowChecklistTraceEntry>();
|
|
257
|
+
for (const entry of trace ?? []) {
|
|
258
|
+
const state = normalizedStatus(entry.state);
|
|
259
|
+
if ((entry.operation !== undefined && entry.operation !== "run" && entry.operation !== "host") || !entry.key || state === "delivered" || state === "missed") continue;
|
|
260
|
+
const existing = latest.get(entry.key);
|
|
261
|
+
latest.set(entry.key, state === "reused" && existing ? { ...existing, ...entry, state: existing.state } : { ...existing, ...entry });
|
|
262
|
+
}
|
|
263
|
+
return [...latest.values()];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function traceItem(entry: WorkflowChecklistTraceEntry, index: number, preflight: WorkflowPreflightLaneV1 | undefined): WorkflowChecklistItem {
|
|
267
|
+
const phase = keyText(entry.generatedLaneKey ?? entry.phase ?? preflight?.key, "Workflow");
|
|
268
|
+
const item = stepItem({ key: entry.key, label: entry.label, phase, agent: entry.agent, status: entry.state === "started" ? "running" : entry.state, durationMs: entry.durationMs, error: entry.error }, index, phase, entry.key, entry.label ?? entry.key, preflight);
|
|
269
|
+
if (entry.operation === "host") item.kind = "host";
|
|
270
|
+
return item;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function phaseFor(phases: Map<string, WorkflowChecklistPhase>, label: string): WorkflowChecklistPhase {
|
|
274
|
+
let phase = phases.get(label);
|
|
275
|
+
if (!phase) {
|
|
276
|
+
phase = { key: label, label, state: "queued", items: [], total: 0, done: 0, running: 0, queued: 0, blocked: 0, failed: 0, paused: 0, stopped: 0, parallel: false };
|
|
277
|
+
phases.set(label, phase);
|
|
278
|
+
}
|
|
279
|
+
return phase;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function add(phases: Map<string, WorkflowChecklistPhase>, phase: string, item: WorkflowChecklistItem): void {
|
|
283
|
+
phaseFor(phases, phase).items.push(item);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function mergeNodeStep(node: WorkflowGraphNode, step: WorkflowChecklistStep, phase: string, trace: WorkflowChecklistTraceEntry | undefined, preflight: WorkflowPreflightLaneV1 | undefined): WorkflowChecklistItem {
|
|
287
|
+
const state = checklistState(step);
|
|
288
|
+
const nodeState = checklistState({ status: node.status, acceptance: node.acceptanceStatus ? { status: node.acceptanceStatus } : undefined });
|
|
289
|
+
const status = TERMINAL_STATES.has(nodeState) && !TERMINAL_STATES.has(state)
|
|
290
|
+
? nodeState
|
|
291
|
+
: trace && !TERMINAL_STATES.has(state)
|
|
292
|
+
? (trace.state === "started" ? "running" : trace.state)
|
|
293
|
+
: normalizedStatus(step.status) === "pending" && normalizedStatus(node.status) !== "pending" ? node.status : step.status;
|
|
294
|
+
return stepItem({ ...step, status, key: node.id, label: step.label ?? node.label, phase: step.phase ?? phase, agent: step.agent ?? node.agent, outputName: step.outputName ?? node.outputName, error: step.error ?? trace?.error ?? node.error, acceptance: step.acceptance ?? (node.acceptanceStatus ? { status: node.acceptanceStatus } : undefined), durationMs: step.durationMs ?? trace?.durationMs }, node.flatIndex ?? 0, phase, node.id, step.label ?? node.label, preflight);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function priority(item: WorkflowChecklistItem): number {
|
|
298
|
+
return item.state === "blocked" ? 0 : item.state === "failed" ? 1 : item.state === "running" ? 2 : item.state === "paused" ? 3 : item.state === "stopped" ? 4 : item.state === "queued" ? 5 : 6;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function applyNow(item: WorkflowChecklistItem, now: number | undefined): WorkflowChecklistItem {
|
|
302
|
+
return item.durationMs === undefined && item.startedAt !== undefined && now !== undefined ? { ...item, durationMs: Math.max(0, now - item.startedAt) } : item;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function finalize(phase: WorkflowChecklistPhase): void {
|
|
306
|
+
phase.total = phase.items.length;
|
|
307
|
+
phase.done = phase.items.filter((item) => item.state === "complete").length;
|
|
308
|
+
phase.running = phase.items.filter((item) => item.state === "running").length;
|
|
309
|
+
phase.queued = phase.items.filter((item) => item.state === "queued").length;
|
|
310
|
+
phase.blocked = phase.items.filter((item) => item.state === "blocked").length;
|
|
311
|
+
phase.failed = phase.items.filter((item) => item.state === "failed").length;
|
|
312
|
+
phase.paused = phase.items.filter((item) => item.state === "paused").length;
|
|
313
|
+
phase.stopped = phase.items.filter((item) => item.state === "stopped").length;
|
|
314
|
+
phase.parallel = phase.total > 1;
|
|
315
|
+
phase.state = phase.blocked ? "blocked" : phase.failed ? "failed" : phase.running ? "running" : phase.paused ? "paused" : phase.stopped ? "stopped" : phase.queued ? "queued" : "complete";
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function projectWorkflowChecklist(input: WorkflowChecklistInput): WorkflowChecklistProjection {
|
|
319
|
+
const phases = new Map<string, WorkflowChecklistPhase>();
|
|
320
|
+
const lanes = new Map((input.preflight?.lanes ?? []).map((lane) => [lane.key, lane]));
|
|
321
|
+
const steps = (input.steps ?? []) as readonly WorkflowChecklistStep[];
|
|
322
|
+
const nodes = graphNodes(input.graph);
|
|
323
|
+
const trace = traceSources(input.trace);
|
|
324
|
+
const traceByKey = new Map(trace.map((entry) => [entry.key, entry]));
|
|
325
|
+
const phaseByNode = new Map<string, string>();
|
|
326
|
+
const graphPhaseLabels = new Set<string>();
|
|
327
|
+
for (const phase of input.graph?.phases ?? []) {
|
|
328
|
+
const title = keyText(phase.title, "Workflow");
|
|
329
|
+
graphPhaseLabels.add(title);
|
|
330
|
+
phaseFor(phases, title);
|
|
331
|
+
for (const nodeId of phase.nodeIds) if (!phaseByNode.has(nodeId)) phaseByNode.set(nodeId, title);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const stepsByKey = new Map<string, Array<{ step: WorkflowChecklistStep; index: number }>>();
|
|
335
|
+
for (const [index, step] of steps.entries()) {
|
|
336
|
+
const key = stepKey(step);
|
|
337
|
+
if (!key) continue;
|
|
338
|
+
stepsByKey.set(key, [...(stepsByKey.get(key) ?? []), { step, index }]);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const usedSteps = new Set<number>();
|
|
342
|
+
const hostById = new Map((input.hostSteps ?? []).map((host) => [host.id, host]));
|
|
343
|
+
const graphKeys = new Set(nodes.map((node) => node.id));
|
|
344
|
+
const stepKeys = new Set([...stepsByKey.keys()]);
|
|
345
|
+
const hostKeys = new Set(hostById.keys());
|
|
346
|
+
|
|
347
|
+
for (const node of nodes) {
|
|
348
|
+
const host = node.hostStep ?? hostById.get(node.id);
|
|
349
|
+
const phase = keyText(phaseByNode.get(node.id) ?? node.phase ?? (host ? host.label : node.label), "Workflow");
|
|
350
|
+
if (host) {
|
|
351
|
+
add(phases, phase, hostItem(host, phase, node.id));
|
|
352
|
+
hostById.delete(node.id);
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
const matches = (stepsByKey.get(node.id) ?? []).filter(({ index }) => !usedSteps.has(index));
|
|
356
|
+
if (matches.length) {
|
|
357
|
+
for (const match of matches) {
|
|
358
|
+
usedSteps.add(match.index);
|
|
359
|
+
add(phases, phase, applyNow(mergeNodeStep(node, match.step, phase, traceByKey.get(stepKey(match.step) ?? node.id), laneFor(node.id, phase, lanes)), input.now));
|
|
360
|
+
}
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
add(phases, phase, applyNow(mergeNodeStep(node, { key: node.id, label: node.label, phase, agent: node.agent, status: node.status, outputName: node.outputName, error: node.error, acceptance: node.acceptanceStatus ? { status: node.acceptanceStatus } : undefined }, phase, traceByKey.get(node.id), laneFor(node.id, phase, lanes)), input.now));
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
for (const [index, step] of steps.entries()) {
|
|
367
|
+
if (usedSteps.has(index)) continue;
|
|
368
|
+
const key = keyText(stepKey(step), `step-${index + 1}`);
|
|
369
|
+
const traceEntry = traceByKey.get(key);
|
|
370
|
+
const phase = keyText(step.phase ?? laneFor(key, "", lanes)?.key ?? traceEntry?.generatedLaneKey ?? traceEntry?.phase, "Workflow");
|
|
371
|
+
add(phases, phase, applyNow(stepItem(step, index, phase, key, step.label ?? step.key ?? step.agent, laneFor(key, phase, lanes)), input.now));
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
for (const host of hostById.values()) add(phases, keyText(host.label, "Host"), hostItem(host, keyText(host.label, "Host")));
|
|
375
|
+
for (const entry of trace) {
|
|
376
|
+
if (graphKeys.has(entry.key) || stepKeys.has(entry.key) || hostKeys.has(entry.key)) continue;
|
|
377
|
+
const item = traceItem(entry, phases.size, laneFor(entry.key, entry.generatedLaneKey ?? entry.phase ?? "", lanes));
|
|
378
|
+
add(phases, item.phase, item);
|
|
379
|
+
}
|
|
380
|
+
for (const lane of input.preflight?.lanes ?? []) if (!graphKeys.has(lane.key) && !graphPhaseLabels.has(lane.key) && !phases.get(lane.key)?.items.length) add(phases, lane.key, { key: lane.key, label: lane.key, phase: lane.key, state: "queued", preflight: lane });
|
|
381
|
+
|
|
382
|
+
const finalized = [...phases.values()].filter((phase) => phase.items.length > 0);
|
|
383
|
+
for (const phase of finalized) {
|
|
384
|
+
phase.items = phase.items.map((item) => applyNow(item, input.now));
|
|
385
|
+
finalize(phase);
|
|
386
|
+
}
|
|
387
|
+
const all = finalized.flatMap((phase) => phase.items);
|
|
388
|
+
const counts = (state: WorkflowChecklistState): number => all.filter((item) => item.state === state).length;
|
|
389
|
+
const bottleneck = [...all].sort((left, right) => priority(left) - priority(right))[0];
|
|
390
|
+
return { phases: finalized, total: all.length, done: counts("complete"), running: counts("running"), queued: counts("queued"), blocked: counts("blocked"), failed: counts("failed"), paused: counts("paused"), stopped: counts("stopped"), ...(bottleneck && bottleneck.state !== "complete" ? { bottleneck } : {}) };
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
function stateLabel(state: WorkflowChecklistState): string {
|
|
394
|
+
return state === "running" ? "active" : state;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
export function formatWorkflowChecklistSummary(projection: WorkflowChecklistProjection): string {
|
|
398
|
+
if (!projection.total) return "";
|
|
399
|
+
return [`${projection.done}/${projection.total} done`, projection.running ? `${projection.running} active` : undefined, projection.queued ? `${projection.queued} queued` : undefined, projection.blocked ? `${projection.blocked} blocked` : undefined, projection.failed ? `${projection.failed} failed` : undefined, projection.paused ? `${projection.paused} paused` : undefined, projection.stopped ? `${projection.stopped} stopped` : undefined].filter((value): value is string => Boolean(value)).join(" · ");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function formatWorkflowChecklistPhase(phase: WorkflowChecklistPhase): string {
|
|
403
|
+
const counts = [phase.total > 1 && phase.done ? `${phase.done} done` : undefined, phase.running ? `${phase.running} active` : undefined, phase.queued ? `${phase.queued} queued` : undefined, phase.blocked ? `${phase.blocked} blocked` : undefined, phase.failed ? `${phase.failed} failed` : undefined, phase.paused ? `${phase.paused} paused` : undefined, phase.stopped ? `${phase.stopped} stopped` : undefined].filter((value): value is string => Boolean(value));
|
|
404
|
+
return counts.length ? `${phase.label} ${counts.join(" · ")}` : phase.label;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function formatWorkflowChecklistBottleneck(item: WorkflowChecklistItem | undefined, options: { includeOutput?: boolean } = {}): string | undefined {
|
|
408
|
+
if (!item) return undefined;
|
|
409
|
+
const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : undefined].filter((value): value is string => Boolean(value)).join(" · ") || item.key;
|
|
410
|
+
const includeOutput = options.includeOutput ?? true;
|
|
411
|
+
const details = [item.context ? `(${item.context})` : undefined, item.currentTool ? `${item.currentTool}${item.durationMs !== undefined ? ` ${formatDurationText(item.durationMs)}` : ""}` : undefined, !item.currentTool && item.currentPath ? item.currentPath : undefined, !item.currentTool && item.durationMs !== undefined ? formatDurationText(item.durationMs) : undefined, item.toolCount !== undefined ? `${item.toolCount} tools` : undefined, includeOutput && item.outputName ? `out:${item.outputName}` : undefined, item.error ? `error:${item.error.replace(/\bOutput:/g, "output:")}` : undefined].filter((value): value is string => Boolean(value));
|
|
412
|
+
return [identity, ...details].join(" · ");
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
function formatWorkflowChecklistItem(item: WorkflowChecklistItem): string {
|
|
416
|
+
const identity = [item.label, item.agent && item.agent !== item.label ? item.agent : undefined].filter((value): value is string => Boolean(value)).join(" · ") || item.key;
|
|
417
|
+
const details = [item.kind && item.monitorKind ? item.monitorKind : undefined, item.provider ? `provider:${item.provider}` : undefined, item.role ? `role:${item.role}` : undefined, item.target, item.currentTool, !item.currentTool && item.currentPath ? item.currentPath : undefined, item.durationMs !== undefined ? formatDurationText(item.durationMs) : undefined, item.toolCount !== undefined ? `${item.toolCount} tools` : undefined, item.outputName ? `out:${item.outputName}` : undefined, item.reportPath ? `out:${item.reportPath}` : undefined, item.stale ? "stale" : undefined, item.reasonCode ? `reason:${item.reasonCode}` : undefined, item.error ? `error:${item.error.replace(/\bOutput:/g, "output:")}` : undefined].filter((value): value is string => Boolean(value));
|
|
418
|
+
return `${identity}${item.context ? ` (${item.context})` : ""}${item.state === "complete" ? "" : ` · ${stateLabel(item.state)}`}${details.length ? ` · ${details.join(" · ")}` : ""}`;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function formatDurationText(ms: number): string {
|
|
422
|
+
const seconds = Math.max(0, Math.round(ms / 1000));
|
|
423
|
+
return seconds < 60 ? `${seconds}s` : `${Math.floor(seconds / 60)}m ${seconds % 60}s`;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
export function formatWorkflowChecklistText(projection: WorkflowChecklistProjection, indent = "", options: { includeItems?: boolean } = {}): string[] {
|
|
427
|
+
if (!projection.total) return [];
|
|
428
|
+
const lines = [`${indent}Workflow checklist: ${formatWorkflowChecklistSummary(projection)}`];
|
|
429
|
+
for (const phase of projection.phases) {
|
|
430
|
+
const phaseMarker = phase.state === "complete" ? "✓" : phase.state === "running" ? "⠼" : phase.state === "blocked" ? "!" : phase.state === "failed" ? "✗" : phase.state === "paused" || phase.state === "stopped" ? "■" : "◦";
|
|
431
|
+
lines.push(`${indent} ${phaseMarker} ${formatWorkflowChecklistPhase(phase)}`);
|
|
432
|
+
if (options.includeItems === false) continue;
|
|
433
|
+
for (const item of phase.items) {
|
|
434
|
+
const marker = item.state === "complete" ? "✓" : item.state === "running" ? "⠼" : item.state === "blocked" ? "!" : item.state === "failed" ? "✗" : item.state === "paused" || item.state === "stopped" ? "■" : "◦";
|
|
435
|
+
lines.push(`${indent} ${marker} ${formatWorkflowChecklistItem(item)}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
const bottleneck = formatWorkflowChecklistBottleneck(projection.bottleneck);
|
|
439
|
+
if (bottleneck) lines.push(`${indent} bottleneck · ${bottleneck}`);
|
|
440
|
+
return lines;
|
|
441
|
+
}
|