@alexeiled/pi-fusion 0.1.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/AGENTS.md +7 -0
- package/LICENSE +21 -0
- package/README.md +94 -0
- package/agents/fusion-judge.md +38 -0
- package/agents/fusion-panelist.md +30 -0
- package/docs/user-guide.md +252 -0
- package/package.json +74 -0
- package/src/commands.ts +273 -0
- package/src/config.ts +256 -0
- package/src/errors.ts +13 -0
- package/src/index.ts +34 -0
- package/src/orchestrator.ts +708 -0
- package/src/report.ts +478 -0
- package/src/result-extract.ts +311 -0
- package/src/run-builder.ts +273 -0
- package/src/run-store.ts +372 -0
- package/src/status.ts +136 -0
- package/src/subagents-rpc.ts +407 -0
- package/src/types.ts +54 -0
- package/src/utils.ts +0 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,708 @@
|
|
|
1
|
+
import {
|
|
2
|
+
loadFusionConfig,
|
|
3
|
+
resolveProfile as resolveFusionProfile,
|
|
4
|
+
type ResolvedFusionProfile,
|
|
5
|
+
} from "./config.js";
|
|
6
|
+
import { FusionArgsError } from "./errors.js";
|
|
7
|
+
import {
|
|
8
|
+
renderCancelledReport,
|
|
9
|
+
renderFailureReport,
|
|
10
|
+
renderJudgeReport,
|
|
11
|
+
renderPanelFailureReport,
|
|
12
|
+
renderSinglePanelReport,
|
|
13
|
+
} from "./report.js";
|
|
14
|
+
import { extractPanelResults } from "./result-extract.js";
|
|
15
|
+
import {
|
|
16
|
+
buildJudgeSpawnParams,
|
|
17
|
+
buildPanelSpawnParams,
|
|
18
|
+
type FailedPanelSummary,
|
|
19
|
+
type PanelOutput,
|
|
20
|
+
} from "./run-builder.js";
|
|
21
|
+
import { FusionRunStore, FusionRunStoreError } from "./run-store.js";
|
|
22
|
+
import {
|
|
23
|
+
clearFusionUi,
|
|
24
|
+
extractFusionProgressCounts,
|
|
25
|
+
formatProgressCounts,
|
|
26
|
+
publishFusionStatus,
|
|
27
|
+
type FusionProgressCounts,
|
|
28
|
+
type FusionUi,
|
|
29
|
+
} from "./status.js";
|
|
30
|
+
import type { FusionProfile, FusionRun } from "./types.js";
|
|
31
|
+
import { parseFusionArgs, type ParsedFusionArgs } from "./commands.js";
|
|
32
|
+
import type { SubagentsTargetParams } from "./subagents-rpc.js";
|
|
33
|
+
|
|
34
|
+
export const SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
|
|
35
|
+
|
|
36
|
+
export type FusionNotifyType = "info" | "warning" | "error";
|
|
37
|
+
|
|
38
|
+
export interface FusionCommandUi extends FusionUi {
|
|
39
|
+
notify(message: string, type?: FusionNotifyType): void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface FusionCommandContext {
|
|
43
|
+
cwd: string;
|
|
44
|
+
hasUI: boolean;
|
|
45
|
+
isProjectTrusted(): boolean;
|
|
46
|
+
sessionManager: {
|
|
47
|
+
getEntries(): readonly unknown[];
|
|
48
|
+
};
|
|
49
|
+
ui: FusionCommandUi;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FusionRpcClientLike {
|
|
53
|
+
ping(): Promise<unknown>;
|
|
54
|
+
spawn(params: object): Promise<unknown>;
|
|
55
|
+
status(params?: SubagentsTargetParams): Promise<unknown>;
|
|
56
|
+
stop(params: SubagentsTargetParams): Promise<unknown>;
|
|
57
|
+
interrupt(params: SubagentsTargetParams): Promise<unknown>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface FusionMessageSink {
|
|
61
|
+
sendMessage(message: {
|
|
62
|
+
customType: string;
|
|
63
|
+
content: string;
|
|
64
|
+
display: boolean;
|
|
65
|
+
details?: unknown;
|
|
66
|
+
}): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface FusionOrchestratorDeps {
|
|
70
|
+
rpc: FusionRpcClientLike;
|
|
71
|
+
runStore?: FusionRunStore;
|
|
72
|
+
sendMessage?: FusionMessageSink["sendMessage"];
|
|
73
|
+
loadConfig?: typeof loadFusionConfig;
|
|
74
|
+
resolveProfile?: typeof resolveFusionProfile;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type FusionCommandResult =
|
|
78
|
+
| { status: "started"; run: FusionRun }
|
|
79
|
+
| { status: "done"; run: FusionRun; report: string }
|
|
80
|
+
| { status: "failed"; error: string; report?: string }
|
|
81
|
+
| { status: "conflict"; activeRunId: string }
|
|
82
|
+
| { status: "cancelled"; run: FusionRun; report: string }
|
|
83
|
+
| { status: "ignored" };
|
|
84
|
+
|
|
85
|
+
export class FusionOrchestrator {
|
|
86
|
+
private readonly rpc: FusionRpcClientLike;
|
|
87
|
+
private readonly runStore: FusionRunStore;
|
|
88
|
+
private readonly sendMessage: FusionMessageSink["sendMessage"] | undefined;
|
|
89
|
+
private readonly loadConfig: typeof loadFusionConfig;
|
|
90
|
+
private readonly resolveProfile: typeof resolveFusionProfile;
|
|
91
|
+
private context: FusionCommandContext | undefined;
|
|
92
|
+
private activeProfile: FusionProfile | undefined;
|
|
93
|
+
private activePanelOutputs: PanelOutput[] = [];
|
|
94
|
+
private activePanelFailures: FailedPanelSummary[] = [];
|
|
95
|
+
private installWarning: string | undefined;
|
|
96
|
+
private configWarning: string | undefined;
|
|
97
|
+
|
|
98
|
+
constructor(deps: FusionOrchestratorDeps) {
|
|
99
|
+
this.rpc = deps.rpc;
|
|
100
|
+
this.runStore = deps.runStore ?? new FusionRunStore();
|
|
101
|
+
this.sendMessage = deps.sendMessage;
|
|
102
|
+
this.loadConfig = deps.loadConfig ?? loadFusionConfig;
|
|
103
|
+
this.resolveProfile = deps.resolveProfile ?? resolveFusionProfile;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async startRun(
|
|
107
|
+
input: string | ParsedFusionArgs,
|
|
108
|
+
ctx: FusionCommandContext,
|
|
109
|
+
): Promise<FusionCommandResult> {
|
|
110
|
+
this.context = ctx;
|
|
111
|
+
|
|
112
|
+
let args: ParsedFusionArgs;
|
|
113
|
+
try {
|
|
114
|
+
args = typeof input === "string" ? parseFusionArgs(input) : input;
|
|
115
|
+
} catch (error: unknown) {
|
|
116
|
+
const message = errorMessage(error);
|
|
117
|
+
this.notify(ctx, message, "error");
|
|
118
|
+
return { status: "failed", error: message };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const existing = this.runStore.getActiveRun();
|
|
122
|
+
if (existing) {
|
|
123
|
+
const message = `Fusion run ${existing.id} is already active.`;
|
|
124
|
+
this.notify(ctx, message, "warning");
|
|
125
|
+
return { status: "conflict", activeRunId: existing.id };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
try {
|
|
129
|
+
await this.rpc.ping();
|
|
130
|
+
this.installWarning = undefined;
|
|
131
|
+
} catch (error: unknown) {
|
|
132
|
+
const message = `pi-subagents RPC is unavailable: ${errorMessage(error)}`;
|
|
133
|
+
this.installWarning = message;
|
|
134
|
+
this.notify(ctx, message, "error");
|
|
135
|
+
return { status: "failed", error: message };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let resolved: ResolvedFusionProfile;
|
|
139
|
+
try {
|
|
140
|
+
const config = await this.loadConfig(ctx);
|
|
141
|
+
resolved = this.resolveProfile(config, args.profile);
|
|
142
|
+
this.configWarning = undefined;
|
|
143
|
+
} catch (error: unknown) {
|
|
144
|
+
const message = errorMessage(error);
|
|
145
|
+
this.configWarning = message;
|
|
146
|
+
this.notify(ctx, message, "error");
|
|
147
|
+
return { status: "failed", error: message };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const run = this.runStore.startRun({
|
|
151
|
+
prompt: args.prompt,
|
|
152
|
+
profileName: resolved.name,
|
|
153
|
+
});
|
|
154
|
+
this.activeProfile = resolved.profile;
|
|
155
|
+
this.activePanelOutputs = [];
|
|
156
|
+
this.activePanelFailures = [];
|
|
157
|
+
publishFusionStatus(ctx, run);
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const spawnResult = await this.rpc.spawn(
|
|
161
|
+
buildPanelSpawnParams(resolved.profile, args.prompt),
|
|
162
|
+
);
|
|
163
|
+
const panelRunId = extractSubagentRunId(spawnResult);
|
|
164
|
+
if (!panelRunId) {
|
|
165
|
+
throw new FusionArgsError(
|
|
166
|
+
"pi-subagents spawn did not return a panel run ID.",
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
const updated = this.runStore.updateRun(run.id, { panelRunId });
|
|
170
|
+
publishFusionStatus(ctx, updated);
|
|
171
|
+
this.notify(ctx, `Fusion panel started: ${panelRunId}`, "info");
|
|
172
|
+
return { status: "started", run: updated };
|
|
173
|
+
} catch (error: unknown) {
|
|
174
|
+
return this.failActiveRun(errorMessage(error));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async handleSubagentComplete(payload: unknown): Promise<FusionCommandResult> {
|
|
179
|
+
const active = this.runStore.getActiveRun();
|
|
180
|
+
if (!active) return { status: "ignored" };
|
|
181
|
+
|
|
182
|
+
const completedRunId = extractSubagentRunId(payload);
|
|
183
|
+
if (active.phase === "panel") {
|
|
184
|
+
if (!active.panelRunId || completedRunId !== active.panelRunId) {
|
|
185
|
+
return { status: "ignored" };
|
|
186
|
+
}
|
|
187
|
+
return this.handlePanelComplete(active, payload);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (active.phase === "judge") {
|
|
191
|
+
if (!active.judgeRunId || completedRunId !== active.judgeRunId) {
|
|
192
|
+
return { status: "ignored" };
|
|
193
|
+
}
|
|
194
|
+
return this.handleJudgeComplete(active, payload);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return { status: "ignored" };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async refreshStatus(targetRunId?: string): Promise<unknown> {
|
|
201
|
+
const active = this.runStore.getActiveRun();
|
|
202
|
+
const runId = targetRunId ?? activeRunId(active);
|
|
203
|
+
if (!runId) return undefined;
|
|
204
|
+
|
|
205
|
+
const payload = await this.rpc.status({ id: runId });
|
|
206
|
+
const progress = extractFusionProgressCounts(payload);
|
|
207
|
+
if (active) publishFusionStatus(this.context, active, progress);
|
|
208
|
+
return payload;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async cancelActiveRun(
|
|
212
|
+
ctx: FusionCommandContext,
|
|
213
|
+
): Promise<FusionCommandResult> {
|
|
214
|
+
this.context = ctx;
|
|
215
|
+
const active = this.runStore.getActiveRun();
|
|
216
|
+
if (!active) {
|
|
217
|
+
this.notify(ctx, "No active fusion run.", "info");
|
|
218
|
+
return { status: "ignored" };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const targetRunId = activeRunId(active);
|
|
222
|
+
let method: "stop" | "interrupt" | "local" = "local";
|
|
223
|
+
if (targetRunId) {
|
|
224
|
+
try {
|
|
225
|
+
await this.rpc.stop({ id: targetRunId });
|
|
226
|
+
method = "stop";
|
|
227
|
+
} catch (stopError: unknown) {
|
|
228
|
+
this.installWarning = `Subagent stop failed for ${targetRunId}: ${errorMessage(stopError)}`;
|
|
229
|
+
try {
|
|
230
|
+
await this.rpc.interrupt({ id: targetRunId });
|
|
231
|
+
method = "interrupt";
|
|
232
|
+
} catch (interruptError: unknown) {
|
|
233
|
+
const message = `Could not cancel subagent run ${targetRunId}: ${errorMessage(interruptError)}`;
|
|
234
|
+
this.notify(ctx, message, "error");
|
|
235
|
+
return { status: "failed", error: message };
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const report = renderCancelledReport({
|
|
241
|
+
run: active,
|
|
242
|
+
method,
|
|
243
|
+
...(targetRunId ? { targetRunId } : {}),
|
|
244
|
+
panelOutputs: this.activePanelOutputs,
|
|
245
|
+
failures: this.activePanelFailures,
|
|
246
|
+
});
|
|
247
|
+
const cancelled = this.runStore.cancelRun(active.id, {
|
|
248
|
+
report,
|
|
249
|
+
error: `Cancellation requested with ${method}.`,
|
|
250
|
+
});
|
|
251
|
+
this.postMessage("fusion-report", report, { runId: cancelled.id });
|
|
252
|
+
this.clearActiveRuntime();
|
|
253
|
+
this.clearUi();
|
|
254
|
+
this.notify(ctx, `Fusion run ${cancelled.id} cancelled.`, "info");
|
|
255
|
+
return { status: "cancelled", run: cancelled, report };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async restore(
|
|
259
|
+
ctx: FusionCommandContext,
|
|
260
|
+
): Promise<ReturnType<FusionRunStore["restoreFromSession"]>> {
|
|
261
|
+
this.context = ctx;
|
|
262
|
+
const summary = this.runStore.restoreFromSession(ctx);
|
|
263
|
+
this.clearActiveRuntime();
|
|
264
|
+
|
|
265
|
+
const active = this.runStore.getActiveRun();
|
|
266
|
+
if (!active) {
|
|
267
|
+
clearFusionUi(ctx);
|
|
268
|
+
return summary;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
try {
|
|
272
|
+
const config = await this.loadConfig(ctx);
|
|
273
|
+
this.activeProfile = this.resolveProfile(
|
|
274
|
+
config,
|
|
275
|
+
active.profileName,
|
|
276
|
+
).profile;
|
|
277
|
+
this.configWarning = undefined;
|
|
278
|
+
} catch (error: unknown) {
|
|
279
|
+
const message = `Could not restore fusion profile "${active.profileName}": ${errorMessage(error)}`;
|
|
280
|
+
this.configWarning = message;
|
|
281
|
+
this.notify(ctx, message, "warning");
|
|
282
|
+
}
|
|
283
|
+
publishFusionStatus(ctx, active);
|
|
284
|
+
return summary;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
clearUi(ctx: FusionCommandContext | undefined = this.context): void {
|
|
288
|
+
clearFusionUi(ctx);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async showStatus(ctx: FusionCommandContext): Promise<string> {
|
|
292
|
+
this.context = ctx;
|
|
293
|
+
const report = await this.getStatusReport();
|
|
294
|
+
this.postMessage("fusion-status", report);
|
|
295
|
+
return report;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async getStatusReport(): Promise<string> {
|
|
299
|
+
const active = this.runStore.getActiveRun();
|
|
300
|
+
let progress: FusionProgressCounts | undefined;
|
|
301
|
+
let statusWarning: string | undefined;
|
|
302
|
+
|
|
303
|
+
if (active) {
|
|
304
|
+
const targetRunId = activeRunId(active);
|
|
305
|
+
if (targetRunId) {
|
|
306
|
+
try {
|
|
307
|
+
const payload = await this.refreshStatus(targetRunId);
|
|
308
|
+
progress = extractFusionProgressCounts(payload);
|
|
309
|
+
} catch (error: unknown) {
|
|
310
|
+
statusWarning = `Could not refresh ${targetRunId}: ${errorMessage(error)}`;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return formatFusionStatusReport({
|
|
314
|
+
active,
|
|
315
|
+
...(progress ? { progress } : {}),
|
|
316
|
+
warnings: this.warnings(statusWarning),
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return formatFusionStatusReport({
|
|
321
|
+
last: this.runStore.getLastRunSummary(),
|
|
322
|
+
warnings: this.warnings(),
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
getActiveRun(): FusionRun | undefined {
|
|
327
|
+
return this.runStore.getActiveRun();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
private async handlePanelComplete(
|
|
331
|
+
active: FusionRun,
|
|
332
|
+
payload: unknown,
|
|
333
|
+
): Promise<FusionCommandResult> {
|
|
334
|
+
const profile = this.activeProfile;
|
|
335
|
+
if (!profile) {
|
|
336
|
+
return this.failActiveRun(
|
|
337
|
+
"Fusion panel completed, but the active profile was not available.",
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const statusPayload = await this.refreshStatusOrPayload(
|
|
342
|
+
active.panelRunId,
|
|
343
|
+
payload,
|
|
344
|
+
);
|
|
345
|
+
const extracted = extractPanelResults(statusPayload, {
|
|
346
|
+
panel: profile.panel,
|
|
347
|
+
});
|
|
348
|
+
if (!extracted.ok) {
|
|
349
|
+
return this.failActiveRun(
|
|
350
|
+
`${extracted.error.message} (${extracted.error.path})`,
|
|
351
|
+
);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
this.activePanelOutputs = extracted.outputs;
|
|
355
|
+
this.activePanelFailures = extracted.failures;
|
|
356
|
+
|
|
357
|
+
if (extracted.outputs.length === 0) {
|
|
358
|
+
const report = renderPanelFailureReport({
|
|
359
|
+
run: active,
|
|
360
|
+
failures: extracted.failures,
|
|
361
|
+
});
|
|
362
|
+
return this.failActiveRun(
|
|
363
|
+
"No fusion panelists completed successfully.",
|
|
364
|
+
report,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (extracted.outputs.length === 1) {
|
|
369
|
+
const report = renderSinglePanelReport({
|
|
370
|
+
run: active,
|
|
371
|
+
output: extracted.outputs[0]!,
|
|
372
|
+
failures: extracted.failures,
|
|
373
|
+
});
|
|
374
|
+
return this.completeActiveRun(report);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
try {
|
|
378
|
+
const spawnResult = await this.rpc.spawn(
|
|
379
|
+
buildJudgeSpawnParams({
|
|
380
|
+
profile,
|
|
381
|
+
prompt: active.prompt,
|
|
382
|
+
panelOutputs: extracted.outputs,
|
|
383
|
+
failedPanelists: extracted.failures,
|
|
384
|
+
}),
|
|
385
|
+
);
|
|
386
|
+
const judgeRunId = extractSubagentRunId(spawnResult);
|
|
387
|
+
if (!judgeRunId) {
|
|
388
|
+
throw new FusionArgsError(
|
|
389
|
+
"pi-subagents spawn did not return a judge run ID.",
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
const updated = this.runStore.updateRun(active.id, {
|
|
393
|
+
phase: "judge",
|
|
394
|
+
judgeRunId,
|
|
395
|
+
});
|
|
396
|
+
publishFusionStatus(this.context, updated);
|
|
397
|
+
this.notify(this.context, `Fusion judge started: ${judgeRunId}`, "info");
|
|
398
|
+
return { status: "started", run: updated };
|
|
399
|
+
} catch (error: unknown) {
|
|
400
|
+
return this.failActiveRun(errorMessage(error));
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
private async handleJudgeComplete(
|
|
405
|
+
active: FusionRun,
|
|
406
|
+
payload: unknown,
|
|
407
|
+
): Promise<FusionCommandResult> {
|
|
408
|
+
const statusPayload = await this.refreshStatusOrPayload(
|
|
409
|
+
active.judgeRunId,
|
|
410
|
+
payload,
|
|
411
|
+
);
|
|
412
|
+
const output = extractJudgeOutput(statusPayload);
|
|
413
|
+
if (!output.ok) return this.failActiveRun(output.error);
|
|
414
|
+
|
|
415
|
+
const report = renderJudgeReport({
|
|
416
|
+
run: active,
|
|
417
|
+
judgeOutput: output.output,
|
|
418
|
+
panelOutputs: this.activePanelOutputs,
|
|
419
|
+
failures: this.activePanelFailures,
|
|
420
|
+
});
|
|
421
|
+
return this.completeActiveRun(report);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
private async refreshStatusOrPayload(
|
|
425
|
+
runId: string | undefined,
|
|
426
|
+
payload: unknown,
|
|
427
|
+
): Promise<unknown> {
|
|
428
|
+
if (!runId) return payload;
|
|
429
|
+
const payloadResults = findResultsArray(payload);
|
|
430
|
+
try {
|
|
431
|
+
const statusPayload = await this.refreshStatus(runId);
|
|
432
|
+
const statusResults = findResultsArray(statusPayload);
|
|
433
|
+
if (payloadResults && payloadResults.length > 0) return payload;
|
|
434
|
+
if (statusResults && statusResults.length > 0) return statusPayload;
|
|
435
|
+
return payloadResults ? payload : (statusPayload ?? payload);
|
|
436
|
+
} catch (error: unknown) {
|
|
437
|
+
this.installWarning = `Could not refresh subagent run ${runId}: ${errorMessage(error)}`;
|
|
438
|
+
return payload;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private completeActiveRun(report: string): FusionCommandResult {
|
|
443
|
+
const active = this.runStore.getActiveRun();
|
|
444
|
+
if (!active) return { status: "failed", error: "No active fusion run." };
|
|
445
|
+
const done = this.runStore.completeRun(active.id, { report });
|
|
446
|
+
this.postMessage("fusion-report", report, { runId: done.id });
|
|
447
|
+
this.clearActiveRuntime();
|
|
448
|
+
this.clearUi();
|
|
449
|
+
return { status: "done", run: done, report };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private failActiveRun(
|
|
453
|
+
error: string,
|
|
454
|
+
report = this.defaultFailureReport(error),
|
|
455
|
+
): FusionCommandResult {
|
|
456
|
+
const active = this.runStore.getActiveRun();
|
|
457
|
+
if (!active) return { status: "failed", error };
|
|
458
|
+
let failed: FusionRun;
|
|
459
|
+
try {
|
|
460
|
+
failed = this.runStore.failRun(active.id, { error, report });
|
|
461
|
+
} catch (storeError: unknown) {
|
|
462
|
+
if (!(storeError instanceof FusionRunStoreError)) throw storeError;
|
|
463
|
+
return { status: "failed", error: errorMessage(storeError), report };
|
|
464
|
+
}
|
|
465
|
+
this.postMessage("fusion-report", report, { runId: failed.id });
|
|
466
|
+
this.clearActiveRuntime();
|
|
467
|
+
this.clearUi();
|
|
468
|
+
this.notify(this.context, `Fusion run ${failed.id} failed.`, "error");
|
|
469
|
+
return { status: "failed", error, report };
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
private defaultFailureReport(error: string): string {
|
|
473
|
+
const active = this.runStore.getActiveRun();
|
|
474
|
+
if (!active) return error;
|
|
475
|
+
return renderFailureReport({
|
|
476
|
+
run: active,
|
|
477
|
+
error,
|
|
478
|
+
panelOutputs: this.activePanelOutputs,
|
|
479
|
+
failures: this.activePanelFailures,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
private clearActiveRuntime(): void {
|
|
484
|
+
this.activeProfile = undefined;
|
|
485
|
+
this.activePanelOutputs = [];
|
|
486
|
+
this.activePanelFailures = [];
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private warnings(extra?: string): string[] {
|
|
490
|
+
return [this.installWarning, this.configWarning, extra].filter(
|
|
491
|
+
(warning): warning is string => Boolean(warning),
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private postMessage(
|
|
496
|
+
customType: "fusion-report" | "fusion-status",
|
|
497
|
+
content: string,
|
|
498
|
+
details?: unknown,
|
|
499
|
+
): void {
|
|
500
|
+
this.sendMessage?.({
|
|
501
|
+
customType,
|
|
502
|
+
content,
|
|
503
|
+
display: true,
|
|
504
|
+
...(details !== undefined ? { details } : {}),
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
private notify(
|
|
509
|
+
ctx: FusionCommandContext | undefined,
|
|
510
|
+
message: string,
|
|
511
|
+
type: FusionNotifyType,
|
|
512
|
+
): void {
|
|
513
|
+
ctx?.ui.notify(message, type);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function extractSubagentRunId(payload: unknown): string | undefined {
|
|
518
|
+
if (!isRecord(payload)) return undefined;
|
|
519
|
+
const direct = firstNonBlankString(
|
|
520
|
+
payload.runId,
|
|
521
|
+
payload.id,
|
|
522
|
+
payload.asyncId,
|
|
523
|
+
);
|
|
524
|
+
if (direct) return direct;
|
|
525
|
+
if (isRecord(payload.details)) {
|
|
526
|
+
const details = firstNonBlankString(
|
|
527
|
+
payload.details.runId,
|
|
528
|
+
payload.details.id,
|
|
529
|
+
payload.details.asyncId,
|
|
530
|
+
);
|
|
531
|
+
if (details) return details;
|
|
532
|
+
}
|
|
533
|
+
if (isRecord(payload.data)) return extractSubagentRunId(payload.data);
|
|
534
|
+
return undefined;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
export function extractJudgeOutput(
|
|
538
|
+
payload: unknown,
|
|
539
|
+
): { ok: true; output: string } | { ok: false; error: string } {
|
|
540
|
+
const result = findFirstResult(payload);
|
|
541
|
+
if (result) {
|
|
542
|
+
const failed = resultFailed(result);
|
|
543
|
+
const output = firstNonBlankString(
|
|
544
|
+
result.output,
|
|
545
|
+
result.finalOutput,
|
|
546
|
+
result.summary,
|
|
547
|
+
result.text,
|
|
548
|
+
);
|
|
549
|
+
if (failed) {
|
|
550
|
+
return {
|
|
551
|
+
ok: false,
|
|
552
|
+
error:
|
|
553
|
+
firstNonBlankString(result.error, output) ??
|
|
554
|
+
"Fusion judge failed without output.",
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (output) return { ok: true, output };
|
|
558
|
+
const artifactPath = extractArtifactPath(result);
|
|
559
|
+
if (artifactPath)
|
|
560
|
+
return { ok: true, output: `Output artifact: ${artifactPath}` };
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const output = firstNonBlankStringFromPayload(payload);
|
|
564
|
+
if (output) return { ok: true, output };
|
|
565
|
+
return { ok: false, error: "Fusion judge completed without output." };
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
function formatFusionStatusReport(input: {
|
|
569
|
+
active?: FusionRun;
|
|
570
|
+
last?: ReturnType<FusionRunStore["getLastRunSummary"]>;
|
|
571
|
+
progress?: FusionProgressCounts;
|
|
572
|
+
warnings: readonly string[];
|
|
573
|
+
}): string {
|
|
574
|
+
const lines = ["Fusion status"];
|
|
575
|
+
if (input.active) {
|
|
576
|
+
lines.push("State: active");
|
|
577
|
+
lines.push(`Run: ${input.active.id}`);
|
|
578
|
+
lines.push(`Profile: ${input.active.profileName}`);
|
|
579
|
+
lines.push(`Phase: ${input.active.phase}`);
|
|
580
|
+
if (input.active.panelRunId)
|
|
581
|
+
lines.push(`Panel run: ${input.active.panelRunId}`);
|
|
582
|
+
if (input.active.judgeRunId)
|
|
583
|
+
lines.push(`Judge run: ${input.active.judgeRunId}`);
|
|
584
|
+
lines.push(
|
|
585
|
+
`Progress: ${input.progress ? formatProgressCounts(input.progress) : "unknown"}`,
|
|
586
|
+
);
|
|
587
|
+
} else if (input.last) {
|
|
588
|
+
lines.push("State: idle");
|
|
589
|
+
lines.push(`Last run: ${input.last.id}`);
|
|
590
|
+
lines.push(`Profile: ${input.last.profileName}`);
|
|
591
|
+
lines.push(`Phase: ${input.last.phase}`);
|
|
592
|
+
if (input.last.panelRunId)
|
|
593
|
+
lines.push(`Panel run: ${input.last.panelRunId}`);
|
|
594
|
+
if (input.last.judgeRunId)
|
|
595
|
+
lines.push(`Judge run: ${input.last.judgeRunId}`);
|
|
596
|
+
} else {
|
|
597
|
+
lines.push("State: idle");
|
|
598
|
+
lines.push("Last run: none");
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (input.warnings.length === 0) lines.push("Warnings: none");
|
|
602
|
+
else
|
|
603
|
+
lines.push("Warnings:", ...input.warnings.map((warning) => `- ${warning}`));
|
|
604
|
+
return lines.join("\n");
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function activeRunId(run: FusionRun | undefined): string | undefined {
|
|
608
|
+
if (!run) return undefined;
|
|
609
|
+
return run.phase === "judge" ? run.judgeRunId : run.panelRunId;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function findResultsArray(payload: unknown): readonly unknown[] | undefined {
|
|
613
|
+
if (!isRecord(payload)) return undefined;
|
|
614
|
+
const directResults = unknownArray(payload.results);
|
|
615
|
+
if (directResults && directResults.length > 0) return directResults;
|
|
616
|
+
if (isRecord(payload.details)) {
|
|
617
|
+
const detailsResults = unknownArray(payload.details.results);
|
|
618
|
+
if (detailsResults && detailsResults.length > 0) return detailsResults;
|
|
619
|
+
}
|
|
620
|
+
if (isRecord(payload.data)) {
|
|
621
|
+
const dataResults = findResultsArray(payload.data);
|
|
622
|
+
if (dataResults) return dataResults;
|
|
623
|
+
}
|
|
624
|
+
if (directResults) return directResults;
|
|
625
|
+
if (isRecord(payload.details)) return unknownArray(payload.details.results);
|
|
626
|
+
return undefined;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function findFirstResult(
|
|
630
|
+
payload: unknown,
|
|
631
|
+
): Record<string, unknown> | undefined {
|
|
632
|
+
const first = findResultsArray(payload)?.[0];
|
|
633
|
+
return isRecord(first) ? first : undefined;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function resultFailed(result: Record<string, unknown>): boolean {
|
|
637
|
+
if (result.success === false) return true;
|
|
638
|
+
if (result.timedOut === true || result.interrupted === true) return true;
|
|
639
|
+
if (firstNonBlankString(result.error)) return true;
|
|
640
|
+
const status = firstString(result.status, result.state);
|
|
641
|
+
if (!status) return false;
|
|
642
|
+
return status === "failed" || status === "paused" || status === "detached";
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function firstNonBlankStringFromPayload(payload: unknown): string | undefined {
|
|
646
|
+
if (!isRecord(payload)) return undefined;
|
|
647
|
+
const direct = firstNonBlankString(
|
|
648
|
+
payload.output,
|
|
649
|
+
payload.finalOutput,
|
|
650
|
+
payload.summary,
|
|
651
|
+
payload.text,
|
|
652
|
+
);
|
|
653
|
+
if (direct) return direct;
|
|
654
|
+
if (isRecord(payload.details)) {
|
|
655
|
+
const fromDetails = firstNonBlankString(
|
|
656
|
+
payload.details.output,
|
|
657
|
+
payload.details.finalOutput,
|
|
658
|
+
payload.details.summary,
|
|
659
|
+
payload.details.text,
|
|
660
|
+
);
|
|
661
|
+
if (fromDetails) return fromDetails;
|
|
662
|
+
}
|
|
663
|
+
if (isRecord(payload.data))
|
|
664
|
+
return firstNonBlankStringFromPayload(payload.data);
|
|
665
|
+
return undefined;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
function extractArtifactPath(
|
|
669
|
+
result: Record<string, unknown>,
|
|
670
|
+
): string | undefined {
|
|
671
|
+
const direct = firstString(result.artifactPath, result.savedOutputPath);
|
|
672
|
+
if (direct) return direct;
|
|
673
|
+
if (isRecord(result.artifactPaths))
|
|
674
|
+
return firstString(result.artifactPaths.outputPath);
|
|
675
|
+
if (isRecord(result.outputReference))
|
|
676
|
+
return firstString(result.outputReference.path);
|
|
677
|
+
return undefined;
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function firstString(...values: readonly unknown[]): string | undefined {
|
|
681
|
+
for (const value of values) {
|
|
682
|
+
if (typeof value === "string") return value;
|
|
683
|
+
}
|
|
684
|
+
return undefined;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
function firstNonBlankString(
|
|
688
|
+
...values: readonly unknown[]
|
|
689
|
+
): string | undefined {
|
|
690
|
+
for (const value of values) {
|
|
691
|
+
if (typeof value !== "string") continue;
|
|
692
|
+
const trimmed = value.trim();
|
|
693
|
+
if (trimmed) return trimmed;
|
|
694
|
+
}
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
function errorMessage(error: unknown): string {
|
|
699
|
+
return error instanceof Error ? error.message : String(error);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function unknownArray(value: unknown): readonly unknown[] | undefined {
|
|
703
|
+
return Array.isArray(value) ? (value as readonly unknown[]) : undefined;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
707
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
708
|
+
}
|