@alexeiled/pi-fusion 0.1.2 → 0.2.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/README.md +119 -35
- package/docs/user-guide.md +16 -6
- package/package.json +5 -8
- package/src/index.ts +1 -0
- package/src/orchestrator.ts +801 -89
- package/src/report.ts +21 -3
- package/src/result-extract.ts +21 -2
- package/src/run-builder.ts +131 -19
- package/src/run-store.ts +121 -1
- package/src/status.ts +34 -14
- package/src/subagent-artifacts.ts +40 -0
- package/src/types.ts +31 -1
- package/AGENTS.md +0 -7
- package/tsconfig.json +0 -19
package/src/orchestrator.ts
CHANGED
|
@@ -13,10 +13,9 @@ import {
|
|
|
13
13
|
} from "./report.js";
|
|
14
14
|
import { extractPanelResults } from "./result-extract.js";
|
|
15
15
|
import {
|
|
16
|
+
appendThinkingSuffix,
|
|
17
|
+
buildFusionChainSpawnParams,
|
|
16
18
|
buildJudgeSpawnParams,
|
|
17
|
-
buildPanelSpawnParams,
|
|
18
|
-
type FailedPanelSummary,
|
|
19
|
-
type PanelOutput,
|
|
20
19
|
} from "./run-builder.js";
|
|
21
20
|
import { FusionRunStore, FusionRunStoreError } from "./run-store.js";
|
|
22
21
|
import {
|
|
@@ -27,12 +26,23 @@ import {
|
|
|
27
26
|
type FusionProgressCounts,
|
|
28
27
|
type FusionUi,
|
|
29
28
|
} from "./status.js";
|
|
30
|
-
import
|
|
29
|
+
import {
|
|
30
|
+
readSubagentResultArtifact,
|
|
31
|
+
readSubagentStatusArtifact,
|
|
32
|
+
} from "./subagent-artifacts.js";
|
|
33
|
+
import type {
|
|
34
|
+
FailedPanelSummary,
|
|
35
|
+
FusionProfile,
|
|
36
|
+
FusionRun,
|
|
37
|
+
PanelOutput,
|
|
38
|
+
} from "./types.js";
|
|
31
39
|
import { parseFusionArgs, type ParsedFusionArgs } from "./commands.js";
|
|
32
40
|
import type { SubagentsTargetParams } from "./subagents-rpc.js";
|
|
33
41
|
|
|
34
42
|
export const SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
|
|
35
43
|
|
|
44
|
+
const RECONCILE_INTERVAL_MS = 2_000;
|
|
45
|
+
|
|
36
46
|
export type FusionNotifyType = "info" | "warning" | "error";
|
|
37
47
|
|
|
38
48
|
export interface FusionCommandUi extends FusionUi {
|
|
@@ -82,6 +92,11 @@ export type FusionCommandResult =
|
|
|
82
92
|
| { status: "cancelled"; run: FusionRun; report: string }
|
|
83
93
|
| { status: "ignored" };
|
|
84
94
|
|
|
95
|
+
interface RunLifecycleSnapshot {
|
|
96
|
+
statusPayload?: unknown;
|
|
97
|
+
resultPayload?: unknown;
|
|
98
|
+
}
|
|
99
|
+
|
|
85
100
|
export class FusionOrchestrator {
|
|
86
101
|
private readonly rpc: FusionRpcClientLike;
|
|
87
102
|
private readonly runStore: FusionRunStore;
|
|
@@ -90,10 +105,10 @@ export class FusionOrchestrator {
|
|
|
90
105
|
private readonly resolveProfile: typeof resolveFusionProfile;
|
|
91
106
|
private context: FusionCommandContext | undefined;
|
|
92
107
|
private activeProfile: FusionProfile | undefined;
|
|
93
|
-
private activePanelOutputs: PanelOutput[] = [];
|
|
94
|
-
private activePanelFailures: FailedPanelSummary[] = [];
|
|
95
108
|
private installWarning: string | undefined;
|
|
96
109
|
private configWarning: string | undefined;
|
|
110
|
+
private reconcileTimer: NodeJS.Timeout | undefined;
|
|
111
|
+
private reconciling = false;
|
|
97
112
|
|
|
98
113
|
constructor(deps: FusionOrchestratorDeps) {
|
|
99
114
|
this.rpc = deps.rpc;
|
|
@@ -150,25 +165,33 @@ export class FusionOrchestrator {
|
|
|
150
165
|
const run = this.runStore.startRun({
|
|
151
166
|
prompt: args.prompt,
|
|
152
167
|
profileName: resolved.name,
|
|
168
|
+
phase: "chain",
|
|
153
169
|
});
|
|
154
170
|
this.activeProfile = resolved.profile;
|
|
155
|
-
this.activePanelOutputs = [];
|
|
156
|
-
this.activePanelFailures = [];
|
|
157
171
|
publishFusionStatus(ctx, run);
|
|
158
172
|
|
|
159
173
|
try {
|
|
160
174
|
const spawnResult = await this.rpc.spawn(
|
|
161
|
-
|
|
175
|
+
buildFusionChainSpawnParams(resolved.profile, args.prompt),
|
|
162
176
|
);
|
|
163
|
-
const
|
|
164
|
-
if (!
|
|
177
|
+
const chainRunId = extractSubagentRunId(spawnResult);
|
|
178
|
+
if (!chainRunId) {
|
|
165
179
|
throw new FusionArgsError(
|
|
166
|
-
"pi-subagents spawn did not return a
|
|
180
|
+
"pi-subagents spawn did not return a fusion chain run ID.",
|
|
167
181
|
);
|
|
168
182
|
}
|
|
169
|
-
const
|
|
183
|
+
const chainAsyncDir = extractSubagentAsyncDir(spawnResult);
|
|
184
|
+
const updated = this.runStore.updateRun(run.id, {
|
|
185
|
+
chainRunId,
|
|
186
|
+
...(chainAsyncDir ? { chainAsyncDir } : {}),
|
|
187
|
+
});
|
|
170
188
|
publishFusionStatus(ctx, updated);
|
|
171
|
-
this.
|
|
189
|
+
this.ensureReconcileLoop();
|
|
190
|
+
this.notify(
|
|
191
|
+
ctx,
|
|
192
|
+
`Fusion ${resolved.name} started (${resolved.profile.panel.length} panelists): "${promptPreview(args.prompt)}" — ${chainRunId}`,
|
|
193
|
+
"info",
|
|
194
|
+
);
|
|
172
195
|
return { status: "started", run: updated };
|
|
173
196
|
} catch (error: unknown) {
|
|
174
197
|
return this.failActiveRun(errorMessage(error));
|
|
@@ -180,32 +203,43 @@ export class FusionOrchestrator {
|
|
|
180
203
|
if (!active) return { status: "ignored" };
|
|
181
204
|
|
|
182
205
|
const completedRunId = extractSubagentRunId(payload);
|
|
183
|
-
if (
|
|
184
|
-
if (!active.panelRunId || completedRunId !== active.panelRunId) {
|
|
185
|
-
return { status: "ignored" };
|
|
186
|
-
}
|
|
187
|
-
return this.handlePanelComplete(active, payload);
|
|
188
|
-
}
|
|
206
|
+
if (!completedRunId) return { status: "ignored" };
|
|
189
207
|
|
|
190
208
|
if (active.phase === "judge") {
|
|
191
209
|
if (!active.judgeRunId || completedRunId !== active.judgeRunId) {
|
|
192
210
|
return { status: "ignored" };
|
|
193
211
|
}
|
|
194
|
-
return this.
|
|
212
|
+
return this.reconcileActiveRun(payload);
|
|
195
213
|
}
|
|
196
214
|
|
|
197
|
-
|
|
215
|
+
const rootRunId = active.chainRunId ?? active.panelRunId;
|
|
216
|
+
if (!rootRunId || completedRunId !== rootRunId) {
|
|
217
|
+
return { status: "ignored" };
|
|
218
|
+
}
|
|
219
|
+
return this.reconcileActiveRun(payload);
|
|
198
220
|
}
|
|
199
221
|
|
|
200
|
-
async refreshStatus(
|
|
222
|
+
async refreshStatus(
|
|
223
|
+
targetRunId?: string,
|
|
224
|
+
asyncDir?: string,
|
|
225
|
+
): Promise<unknown> {
|
|
201
226
|
const active = this.runStore.getActiveRun();
|
|
202
227
|
const runId = targetRunId ?? activeRunId(active);
|
|
203
|
-
|
|
228
|
+
const resolvedAsyncDir = asyncDir ?? activeAsyncDir(active);
|
|
204
229
|
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
230
|
+
const statusPayload =
|
|
231
|
+
readSubagentStatusArtifact(resolvedAsyncDir) ??
|
|
232
|
+
(runId ? await this.rpc.status({ id: runId }) : undefined);
|
|
233
|
+
const progress = extractFusionProgressCounts(statusPayload);
|
|
234
|
+
if (active) {
|
|
235
|
+
publishFusionStatus(
|
|
236
|
+
this.context,
|
|
237
|
+
active,
|
|
238
|
+
progress,
|
|
239
|
+
deriveFusionStatusPhase(active, statusPayload),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
return statusPayload;
|
|
209
243
|
}
|
|
210
244
|
|
|
211
245
|
async cancelActiveRun(
|
|
@@ -241,10 +275,18 @@ export class FusionOrchestrator {
|
|
|
241
275
|
run: active,
|
|
242
276
|
method,
|
|
243
277
|
...(targetRunId ? { targetRunId } : {}),
|
|
244
|
-
panelOutputs:
|
|
245
|
-
failures:
|
|
278
|
+
panelOutputs: storedPanelOutputs(active),
|
|
279
|
+
failures: storedPanelFailures(active),
|
|
280
|
+
...withJudgeModel(
|
|
281
|
+
this.activeProfile
|
|
282
|
+
? configuredJudgeModel(this.activeProfile)
|
|
283
|
+
: undefined,
|
|
284
|
+
),
|
|
246
285
|
});
|
|
247
286
|
const cancelled = this.runStore.cancelRun(active.id, {
|
|
287
|
+
...(active.chainRunId ? { chainRunId: active.chainRunId } : {}),
|
|
288
|
+
...(active.panelRunId ? { panelRunId: active.panelRunId } : {}),
|
|
289
|
+
...(active.judgeRunId ? { judgeRunId: active.judgeRunId } : {}),
|
|
248
290
|
report,
|
|
249
291
|
error: `Cancellation requested with ${method}.`,
|
|
250
292
|
});
|
|
@@ -264,6 +306,7 @@ export class FusionOrchestrator {
|
|
|
264
306
|
|
|
265
307
|
const active = this.runStore.getActiveRun();
|
|
266
308
|
if (!active) {
|
|
309
|
+
this.stopReconcileLoop();
|
|
267
310
|
clearFusionUi(ctx);
|
|
268
311
|
return summary;
|
|
269
312
|
}
|
|
@@ -281,6 +324,8 @@ export class FusionOrchestrator {
|
|
|
281
324
|
this.notify(ctx, message, "warning");
|
|
282
325
|
}
|
|
283
326
|
publishFusionStatus(ctx, active);
|
|
327
|
+
this.ensureReconcileLoop();
|
|
328
|
+
await this.reconcileActiveRun();
|
|
284
329
|
return summary;
|
|
285
330
|
}
|
|
286
331
|
|
|
@@ -288,6 +333,10 @@ export class FusionOrchestrator {
|
|
|
288
333
|
clearFusionUi(ctx);
|
|
289
334
|
}
|
|
290
335
|
|
|
336
|
+
dispose(): void {
|
|
337
|
+
this.stopReconcileLoop();
|
|
338
|
+
}
|
|
339
|
+
|
|
291
340
|
async showStatus(ctx: FusionCommandContext): Promise<string> {
|
|
292
341
|
this.context = ctx;
|
|
293
342
|
const report = await this.getStatusReport();
|
|
@@ -296,16 +345,29 @@ export class FusionOrchestrator {
|
|
|
296
345
|
}
|
|
297
346
|
|
|
298
347
|
async getStatusReport(): Promise<string> {
|
|
348
|
+
if (this.runStore.getActiveRun()) {
|
|
349
|
+
await this.reconcileActiveRun();
|
|
350
|
+
}
|
|
351
|
+
|
|
299
352
|
const active = this.runStore.getActiveRun();
|
|
300
353
|
let progress: FusionProgressCounts | undefined;
|
|
301
354
|
let statusWarning: string | undefined;
|
|
355
|
+
let statusDetails: FusionStatusDetails | undefined;
|
|
302
356
|
|
|
303
357
|
if (active) {
|
|
304
358
|
const targetRunId = activeRunId(active);
|
|
305
359
|
if (targetRunId) {
|
|
306
360
|
try {
|
|
307
|
-
const payload = await this.refreshStatus(
|
|
361
|
+
const payload = await this.refreshStatus(
|
|
362
|
+
targetRunId,
|
|
363
|
+
activeAsyncDir(active),
|
|
364
|
+
);
|
|
308
365
|
progress = extractFusionProgressCounts(payload);
|
|
366
|
+
statusDetails = buildFusionStatusDetails(
|
|
367
|
+
active,
|
|
368
|
+
this.activeProfile,
|
|
369
|
+
payload,
|
|
370
|
+
);
|
|
309
371
|
} catch (error: unknown) {
|
|
310
372
|
statusWarning = `Could not refresh ${targetRunId}: ${errorMessage(error)}`;
|
|
311
373
|
}
|
|
@@ -313,6 +375,7 @@ export class FusionOrchestrator {
|
|
|
313
375
|
return formatFusionStatusReport({
|
|
314
376
|
active,
|
|
315
377
|
...(progress ? { progress } : {}),
|
|
378
|
+
...(statusDetails ? { details: statusDetails } : {}),
|
|
316
379
|
warnings: this.warnings(statusWarning),
|
|
317
380
|
});
|
|
318
381
|
}
|
|
@@ -327,37 +390,197 @@ export class FusionOrchestrator {
|
|
|
327
390
|
return this.runStore.getActiveRun();
|
|
328
391
|
}
|
|
329
392
|
|
|
330
|
-
private async
|
|
393
|
+
private async reconcileActiveRun(
|
|
394
|
+
eventPayload?: unknown,
|
|
395
|
+
): Promise<FusionCommandResult> {
|
|
396
|
+
if (this.reconciling) return { status: "ignored" };
|
|
397
|
+
const active = this.runStore.getActiveRun();
|
|
398
|
+
if (!active) return { status: "ignored" };
|
|
399
|
+
|
|
400
|
+
this.reconciling = true;
|
|
401
|
+
try {
|
|
402
|
+
if (active.phase === "panel") {
|
|
403
|
+
return this.handleLegacyPanelComplete(active, eventPayload);
|
|
404
|
+
}
|
|
405
|
+
if (active.phase === "chain") {
|
|
406
|
+
return this.handleChainComplete(active, eventPayload);
|
|
407
|
+
}
|
|
408
|
+
if (active.phase === "judge") {
|
|
409
|
+
return this.handleJudgeComplete(active, eventPayload);
|
|
410
|
+
}
|
|
411
|
+
return { status: "ignored" };
|
|
412
|
+
} finally {
|
|
413
|
+
this.reconciling = false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private async handleChainComplete(
|
|
331
418
|
active: FusionRun,
|
|
332
419
|
payload: unknown,
|
|
333
420
|
): Promise<FusionCommandResult> {
|
|
334
421
|
const profile = this.activeProfile;
|
|
335
422
|
if (!profile) {
|
|
336
423
|
return this.failActiveRun(
|
|
337
|
-
"Fusion
|
|
424
|
+
"Fusion chain is active, but the profile could not be restored.",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const snapshot = await this.loadRunLifecycle({
|
|
429
|
+
run: active,
|
|
430
|
+
...(active.chainRunId ? { runId: active.chainRunId } : {}),
|
|
431
|
+
...(active.chainAsyncDir ? { asyncDir: active.chainAsyncDir } : {}),
|
|
432
|
+
eventPayload: payload,
|
|
433
|
+
});
|
|
434
|
+
const terminalPayload =
|
|
435
|
+
snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
|
|
436
|
+
if (
|
|
437
|
+
!hasResultsArray(snapshot.resultPayload) &&
|
|
438
|
+
!isTerminalSubagentState(extractSubagentState(terminalPayload))
|
|
439
|
+
) {
|
|
440
|
+
return { status: "ignored" };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const extracted = extractPanelResults(
|
|
444
|
+
snapshot.resultPayload ?? snapshot.statusPayload ?? payload,
|
|
445
|
+
{
|
|
446
|
+
panel: profile.panel,
|
|
447
|
+
limit: profile.panel.length,
|
|
448
|
+
},
|
|
449
|
+
);
|
|
450
|
+
if (!extracted.ok) {
|
|
451
|
+
return this.failActiveRun(
|
|
452
|
+
`${extracted.error.message} (${extracted.error.path})`,
|
|
338
453
|
);
|
|
339
454
|
}
|
|
340
455
|
|
|
341
|
-
const
|
|
342
|
-
active.
|
|
343
|
-
|
|
456
|
+
const updated = this.storePanelResults(
|
|
457
|
+
active.id,
|
|
458
|
+
extracted.outputs,
|
|
459
|
+
extracted.failures,
|
|
344
460
|
);
|
|
345
|
-
|
|
346
|
-
|
|
461
|
+
|
|
462
|
+
if (hasJudgeResult(snapshot.resultPayload, profile.panel.length)) {
|
|
463
|
+
const output = extractJudgeOutput(snapshot.resultPayload, {
|
|
464
|
+
resultIndex: profile.panel.length,
|
|
465
|
+
});
|
|
466
|
+
if (!output.ok) return this.failActiveRun(output.error);
|
|
467
|
+
|
|
468
|
+
const report = renderJudgeReport({
|
|
469
|
+
run: updated,
|
|
470
|
+
judgeOutput: output.output,
|
|
471
|
+
panelOutputs: storedPanelOutputs(updated),
|
|
472
|
+
failures: storedPanelFailures(updated),
|
|
473
|
+
...withJudgeModel(configuredJudgeModel(profile)),
|
|
474
|
+
});
|
|
475
|
+
return this.completeActiveRun(report);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (extracted.outputs.length === 0) {
|
|
479
|
+
const report = renderPanelFailureReport({
|
|
480
|
+
run: updated,
|
|
481
|
+
failures: extracted.failures,
|
|
482
|
+
...withJudgeModel(configuredJudgeModel(profile)),
|
|
483
|
+
});
|
|
484
|
+
return this.failActiveRun(
|
|
485
|
+
"No fusion panelists completed successfully.",
|
|
486
|
+
report,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (extracted.outputs.length === 1) {
|
|
491
|
+
const report = renderSinglePanelReport({
|
|
492
|
+
run: updated,
|
|
493
|
+
output: extracted.outputs[0]!,
|
|
494
|
+
failures: extracted.failures,
|
|
495
|
+
...withJudgeModel(configuredJudgeModel(profile)),
|
|
496
|
+
});
|
|
497
|
+
return this.completeActiveRun(report);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
try {
|
|
501
|
+
const spawnResult = await this.rpc.spawn(
|
|
502
|
+
buildJudgeSpawnParams({
|
|
503
|
+
profile,
|
|
504
|
+
prompt: active.prompt,
|
|
505
|
+
panelOutputs: extracted.outputs,
|
|
506
|
+
failedPanelists: extracted.failures,
|
|
507
|
+
}),
|
|
508
|
+
);
|
|
509
|
+
const judgeRunId = extractSubagentRunId(spawnResult);
|
|
510
|
+
if (!judgeRunId) {
|
|
511
|
+
throw new FusionArgsError(
|
|
512
|
+
"pi-subagents spawn did not return a fallback judge run ID.",
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
const judgeAsyncDir = extractSubagentAsyncDir(spawnResult);
|
|
516
|
+
const nextRun = this.runStore.updateRun(active.id, {
|
|
517
|
+
phase: "judge",
|
|
518
|
+
judgeRunId,
|
|
519
|
+
...(judgeAsyncDir ? { judgeAsyncDir } : {}),
|
|
520
|
+
panelOutputs: extracted.outputs,
|
|
521
|
+
panelFailures: extracted.failures,
|
|
522
|
+
});
|
|
523
|
+
publishFusionStatus(this.context, nextRun);
|
|
524
|
+
this.notify(
|
|
525
|
+
this.context,
|
|
526
|
+
`Fusion fallback judge started: ${judgeRunId}`,
|
|
527
|
+
"info",
|
|
528
|
+
);
|
|
529
|
+
return { status: "started", run: nextRun };
|
|
530
|
+
} catch (error: unknown) {
|
|
531
|
+
return this.failActiveRun(errorMessage(error));
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
private async handleLegacyPanelComplete(
|
|
536
|
+
active: FusionRun,
|
|
537
|
+
payload: unknown,
|
|
538
|
+
): Promise<FusionCommandResult> {
|
|
539
|
+
const profile = this.activeProfile;
|
|
540
|
+
if (!profile) {
|
|
541
|
+
return this.failActiveRun(
|
|
542
|
+
"Fusion panel completed, but the active profile was not available.",
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const snapshot = await this.loadRunLifecycle({
|
|
547
|
+
run: active,
|
|
548
|
+
...(active.panelRunId ? { runId: active.panelRunId } : {}),
|
|
549
|
+
...(active.chainAsyncDir ? { asyncDir: active.chainAsyncDir } : {}),
|
|
550
|
+
eventPayload: payload,
|
|
347
551
|
});
|
|
552
|
+
const terminalPayload =
|
|
553
|
+
snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
|
|
554
|
+
if (
|
|
555
|
+
!hasResultsArray(snapshot.resultPayload) &&
|
|
556
|
+
!isTerminalSubagentState(extractSubagentState(terminalPayload))
|
|
557
|
+
) {
|
|
558
|
+
return { status: "ignored" };
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const extracted = extractPanelResults(
|
|
562
|
+
snapshot.resultPayload ?? snapshot.statusPayload ?? payload,
|
|
563
|
+
{
|
|
564
|
+
panel: profile.panel,
|
|
565
|
+
},
|
|
566
|
+
);
|
|
348
567
|
if (!extracted.ok) {
|
|
349
568
|
return this.failActiveRun(
|
|
350
569
|
`${extracted.error.message} (${extracted.error.path})`,
|
|
351
570
|
);
|
|
352
571
|
}
|
|
353
572
|
|
|
354
|
-
|
|
355
|
-
|
|
573
|
+
const updated = this.storePanelResults(
|
|
574
|
+
active.id,
|
|
575
|
+
extracted.outputs,
|
|
576
|
+
extracted.failures,
|
|
577
|
+
);
|
|
356
578
|
|
|
357
579
|
if (extracted.outputs.length === 0) {
|
|
358
580
|
const report = renderPanelFailureReport({
|
|
359
|
-
run:
|
|
581
|
+
run: updated,
|
|
360
582
|
failures: extracted.failures,
|
|
583
|
+
...withJudgeModel(configuredJudgeModel(profile)),
|
|
361
584
|
});
|
|
362
585
|
return this.failActiveRun(
|
|
363
586
|
"No fusion panelists completed successfully.",
|
|
@@ -367,9 +590,10 @@ export class FusionOrchestrator {
|
|
|
367
590
|
|
|
368
591
|
if (extracted.outputs.length === 1) {
|
|
369
592
|
const report = renderSinglePanelReport({
|
|
370
|
-
run:
|
|
593
|
+
run: updated,
|
|
371
594
|
output: extracted.outputs[0]!,
|
|
372
595
|
failures: extracted.failures,
|
|
596
|
+
...withJudgeModel(configuredJudgeModel(profile)),
|
|
373
597
|
});
|
|
374
598
|
return this.completeActiveRun(report);
|
|
375
599
|
}
|
|
@@ -389,13 +613,17 @@ export class FusionOrchestrator {
|
|
|
389
613
|
"pi-subagents spawn did not return a judge run ID.",
|
|
390
614
|
);
|
|
391
615
|
}
|
|
392
|
-
const
|
|
616
|
+
const judgeAsyncDir = extractSubagentAsyncDir(spawnResult);
|
|
617
|
+
const nextRun = this.runStore.updateRun(active.id, {
|
|
393
618
|
phase: "judge",
|
|
394
619
|
judgeRunId,
|
|
620
|
+
...(judgeAsyncDir ? { judgeAsyncDir } : {}),
|
|
621
|
+
panelOutputs: extracted.outputs,
|
|
622
|
+
panelFailures: extracted.failures,
|
|
395
623
|
});
|
|
396
|
-
publishFusionStatus(this.context,
|
|
624
|
+
publishFusionStatus(this.context, nextRun);
|
|
397
625
|
this.notify(this.context, `Fusion judge started: ${judgeRunId}`, "info");
|
|
398
|
-
return { status: "started", run:
|
|
626
|
+
return { status: "started", run: nextRun };
|
|
399
627
|
} catch (error: unknown) {
|
|
400
628
|
return this.failActiveRun(errorMessage(error));
|
|
401
629
|
}
|
|
@@ -405,44 +633,117 @@ export class FusionOrchestrator {
|
|
|
405
633
|
active: FusionRun,
|
|
406
634
|
payload: unknown,
|
|
407
635
|
): Promise<FusionCommandResult> {
|
|
408
|
-
const
|
|
409
|
-
active
|
|
410
|
-
|
|
636
|
+
const snapshot = await this.loadRunLifecycle({
|
|
637
|
+
run: active,
|
|
638
|
+
...(active.judgeRunId ? { runId: active.judgeRunId } : {}),
|
|
639
|
+
...(active.judgeAsyncDir ? { asyncDir: active.judgeAsyncDir } : {}),
|
|
640
|
+
eventPayload: payload,
|
|
641
|
+
});
|
|
642
|
+
const terminalPayload =
|
|
643
|
+
snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
|
|
644
|
+
if (
|
|
645
|
+
!hasResultsArray(snapshot.resultPayload) &&
|
|
646
|
+
!isTerminalSubagentState(extractSubagentState(terminalPayload))
|
|
647
|
+
) {
|
|
648
|
+
return { status: "ignored" };
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
const output = extractJudgeOutput(
|
|
652
|
+
snapshot.resultPayload ?? snapshot.statusPayload ?? payload,
|
|
411
653
|
);
|
|
412
|
-
const output = extractJudgeOutput(statusPayload);
|
|
413
654
|
if (!output.ok) return this.failActiveRun(output.error);
|
|
414
655
|
|
|
415
656
|
const report = renderJudgeReport({
|
|
416
657
|
run: active,
|
|
417
658
|
judgeOutput: output.output,
|
|
418
|
-
panelOutputs:
|
|
419
|
-
failures:
|
|
659
|
+
panelOutputs: storedPanelOutputs(active),
|
|
660
|
+
failures: storedPanelFailures(active),
|
|
661
|
+
...withJudgeModel(
|
|
662
|
+
this.activeProfile
|
|
663
|
+
? configuredJudgeModel(this.activeProfile)
|
|
664
|
+
: undefined,
|
|
665
|
+
),
|
|
420
666
|
});
|
|
421
667
|
return this.completeActiveRun(report);
|
|
422
668
|
}
|
|
423
669
|
|
|
424
|
-
private async
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
670
|
+
private async loadRunLifecycle(input: {
|
|
671
|
+
run: FusionRun;
|
|
672
|
+
runId?: string;
|
|
673
|
+
asyncDir?: string;
|
|
674
|
+
eventPayload?: unknown;
|
|
675
|
+
}): Promise<RunLifecycleSnapshot> {
|
|
676
|
+
const eventPayloadMatches =
|
|
677
|
+
input.eventPayload !== undefined &&
|
|
678
|
+
extractSubagentRunId(input.eventPayload) === input.runId;
|
|
679
|
+
|
|
680
|
+
let statusPayload = readSubagentStatusArtifact(input.asyncDir);
|
|
681
|
+
if (statusPayload === undefined && input.runId) {
|
|
682
|
+
try {
|
|
683
|
+
statusPayload = await this.rpc.status({ id: input.runId });
|
|
684
|
+
} catch (error: unknown) {
|
|
685
|
+
this.installWarning = `Could not refresh subagent run ${input.runId}: ${errorMessage(error)}`;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
const progress = extractFusionProgressCounts(statusPayload);
|
|
690
|
+
if (progress) {
|
|
691
|
+
publishFusionStatus(
|
|
692
|
+
this.context,
|
|
693
|
+
input.run,
|
|
694
|
+
progress,
|
|
695
|
+
deriveFusionStatusPhase(input.run, statusPayload),
|
|
696
|
+
);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const eventHasResults = hasResultsArray(input.eventPayload);
|
|
700
|
+
if (eventPayloadMatches && eventHasResults) {
|
|
701
|
+
return { statusPayload, resultPayload: input.eventPayload };
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
const artifactResult = readSubagentResultArtifact({
|
|
705
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
706
|
+
...(input.asyncDir ? { asyncDir: input.asyncDir } : {}),
|
|
707
|
+
});
|
|
708
|
+
if (hasResultsArray(artifactResult)) {
|
|
709
|
+
return { statusPayload, resultPayload: artifactResult };
|
|
439
710
|
}
|
|
711
|
+
|
|
712
|
+
if (hasResultsArray(statusPayload)) {
|
|
713
|
+
return { statusPayload, resultPayload: statusPayload };
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
if (isTerminalSubagentState(extractSubagentState(statusPayload))) {
|
|
717
|
+
return { statusPayload, resultPayload: statusPayload };
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
if (eventPayloadMatches) {
|
|
721
|
+
return { statusPayload, resultPayload: input.eventPayload };
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return { statusPayload };
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
private storePanelResults(
|
|
728
|
+
runId: string,
|
|
729
|
+
panelOutputs: readonly PanelOutput[],
|
|
730
|
+
panelFailures: readonly FailedPanelSummary[],
|
|
731
|
+
): FusionRun {
|
|
732
|
+
return this.runStore.updateRun(runId, {
|
|
733
|
+
panelOutputs: [...panelOutputs],
|
|
734
|
+
panelFailures: [...panelFailures],
|
|
735
|
+
});
|
|
440
736
|
}
|
|
441
737
|
|
|
442
738
|
private completeActiveRun(report: string): FusionCommandResult {
|
|
443
739
|
const active = this.runStore.getActiveRun();
|
|
444
740
|
if (!active) return { status: "failed", error: "No active fusion run." };
|
|
445
|
-
const done = this.runStore.completeRun(active.id, {
|
|
741
|
+
const done = this.runStore.completeRun(active.id, {
|
|
742
|
+
...(active.chainRunId ? { chainRunId: active.chainRunId } : {}),
|
|
743
|
+
...(active.panelRunId ? { panelRunId: active.panelRunId } : {}),
|
|
744
|
+
...(active.judgeRunId ? { judgeRunId: active.judgeRunId } : {}),
|
|
745
|
+
report,
|
|
746
|
+
});
|
|
446
747
|
this.postMessage("fusion-report", report, { runId: done.id });
|
|
447
748
|
this.clearActiveRuntime();
|
|
448
749
|
this.clearUi();
|
|
@@ -457,7 +758,13 @@ export class FusionOrchestrator {
|
|
|
457
758
|
if (!active) return { status: "failed", error };
|
|
458
759
|
let failed: FusionRun;
|
|
459
760
|
try {
|
|
460
|
-
failed = this.runStore.failRun(active.id, {
|
|
761
|
+
failed = this.runStore.failRun(active.id, {
|
|
762
|
+
...(active.chainRunId ? { chainRunId: active.chainRunId } : {}),
|
|
763
|
+
...(active.panelRunId ? { panelRunId: active.panelRunId } : {}),
|
|
764
|
+
...(active.judgeRunId ? { judgeRunId: active.judgeRunId } : {}),
|
|
765
|
+
report,
|
|
766
|
+
error,
|
|
767
|
+
});
|
|
461
768
|
} catch (storeError: unknown) {
|
|
462
769
|
if (!(storeError instanceof FusionRunStoreError)) throw storeError;
|
|
463
770
|
return { status: "failed", error: errorMessage(storeError), report };
|
|
@@ -475,15 +782,33 @@ export class FusionOrchestrator {
|
|
|
475
782
|
return renderFailureReport({
|
|
476
783
|
run: active,
|
|
477
784
|
error,
|
|
478
|
-
panelOutputs:
|
|
479
|
-
failures:
|
|
785
|
+
panelOutputs: storedPanelOutputs(active),
|
|
786
|
+
failures: storedPanelFailures(active),
|
|
787
|
+
...withJudgeModel(
|
|
788
|
+
this.activeProfile
|
|
789
|
+
? configuredJudgeModel(this.activeProfile)
|
|
790
|
+
: undefined,
|
|
791
|
+
),
|
|
480
792
|
});
|
|
481
793
|
}
|
|
482
794
|
|
|
483
795
|
private clearActiveRuntime(): void {
|
|
484
796
|
this.activeProfile = undefined;
|
|
485
|
-
this.
|
|
486
|
-
|
|
797
|
+
this.stopReconcileLoop();
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
private ensureReconcileLoop(): void {
|
|
801
|
+
if (this.reconcileTimer) return;
|
|
802
|
+
this.reconcileTimer = setInterval(() => {
|
|
803
|
+
void this.reconcileActiveRun();
|
|
804
|
+
}, RECONCILE_INTERVAL_MS);
|
|
805
|
+
this.reconcileTimer.unref?.();
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
private stopReconcileLoop(): void {
|
|
809
|
+
if (!this.reconcileTimer) return;
|
|
810
|
+
clearInterval(this.reconcileTimer);
|
|
811
|
+
this.reconcileTimer = undefined;
|
|
487
812
|
}
|
|
488
813
|
|
|
489
814
|
private warnings(extra?: string): string[] {
|
|
@@ -534,10 +859,23 @@ export function extractSubagentRunId(payload: unknown): string | undefined {
|
|
|
534
859
|
return undefined;
|
|
535
860
|
}
|
|
536
861
|
|
|
862
|
+
export function extractSubagentAsyncDir(payload: unknown): string | undefined {
|
|
863
|
+
if (!isRecord(payload)) return undefined;
|
|
864
|
+
const direct = firstNonBlankString(payload.asyncDir);
|
|
865
|
+
if (direct) return direct;
|
|
866
|
+
if (isRecord(payload.details)) {
|
|
867
|
+
const details = firstNonBlankString(payload.details.asyncDir);
|
|
868
|
+
if (details) return details;
|
|
869
|
+
}
|
|
870
|
+
if (isRecord(payload.data)) return extractSubagentAsyncDir(payload.data);
|
|
871
|
+
return undefined;
|
|
872
|
+
}
|
|
873
|
+
|
|
537
874
|
export function extractJudgeOutput(
|
|
538
875
|
payload: unknown,
|
|
876
|
+
options: { resultIndex?: number } = {},
|
|
539
877
|
): { ok: true; output: string } | { ok: false; error: string } {
|
|
540
|
-
const result =
|
|
878
|
+
const result = findResult(payload, options.resultIndex);
|
|
541
879
|
if (result) {
|
|
542
880
|
const failed = resultFailed(result);
|
|
543
881
|
const output = firstNonBlankString(
|
|
@@ -556,8 +894,9 @@ export function extractJudgeOutput(
|
|
|
556
894
|
}
|
|
557
895
|
if (output) return { ok: true, output };
|
|
558
896
|
const artifactPath = extractArtifactPath(result);
|
|
559
|
-
if (artifactPath)
|
|
897
|
+
if (artifactPath) {
|
|
560
898
|
return { ok: true, output: `Output artifact: ${artifactPath}` };
|
|
899
|
+
}
|
|
561
900
|
}
|
|
562
901
|
|
|
563
902
|
const output = firstNonBlankStringFromPayload(payload);
|
|
@@ -565,48 +904,332 @@ export function extractJudgeOutput(
|
|
|
565
904
|
return { ok: false, error: "Fusion judge completed without output." };
|
|
566
905
|
}
|
|
567
906
|
|
|
907
|
+
interface FusionStatusPanelLine {
|
|
908
|
+
label: string;
|
|
909
|
+
role?: string;
|
|
910
|
+
model?: string;
|
|
911
|
+
status: string;
|
|
912
|
+
activity?: string;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
interface FusionStatusJudgeLine {
|
|
916
|
+
label: string;
|
|
917
|
+
model?: string;
|
|
918
|
+
status: string;
|
|
919
|
+
activity?: string;
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
interface FusionStatusDetails {
|
|
923
|
+
prompt: string;
|
|
924
|
+
phaseLabel?: string;
|
|
925
|
+
panelists?: readonly FusionStatusPanelLine[];
|
|
926
|
+
judge?: FusionStatusJudgeLine;
|
|
927
|
+
fallbackJudge?: FusionStatusJudgeLine;
|
|
928
|
+
}
|
|
929
|
+
|
|
568
930
|
function formatFusionStatusReport(input: {
|
|
569
931
|
active?: FusionRun;
|
|
570
932
|
last?: ReturnType<FusionRunStore["getLastRunSummary"]>;
|
|
571
933
|
progress?: FusionProgressCounts;
|
|
934
|
+
details?: FusionStatusDetails;
|
|
572
935
|
warnings: readonly string[];
|
|
573
936
|
}): string {
|
|
574
937
|
const lines = ["Fusion status"];
|
|
575
938
|
if (input.active) {
|
|
576
939
|
lines.push("State: active");
|
|
577
940
|
lines.push(`Run: ${input.active.id}`);
|
|
941
|
+
lines.push(
|
|
942
|
+
`Prompt: ${firstLine(input.details?.prompt ?? input.active.prompt)}`,
|
|
943
|
+
);
|
|
578
944
|
lines.push(`Profile: ${input.active.profileName}`);
|
|
579
|
-
lines.push(`Phase: ${input.active.phase}`);
|
|
580
|
-
if (input.active.
|
|
945
|
+
lines.push(`Phase: ${input.details?.phaseLabel ?? input.active.phase}`);
|
|
946
|
+
if (input.active.chainRunId)
|
|
947
|
+
lines.push(`Chain run: ${input.active.chainRunId}`);
|
|
948
|
+
else if (input.active.panelRunId)
|
|
581
949
|
lines.push(`Panel run: ${input.active.panelRunId}`);
|
|
582
|
-
if (input.active.judgeRunId)
|
|
583
|
-
lines.push(`
|
|
950
|
+
if (input.active.judgeRunId) {
|
|
951
|
+
lines.push(`Fallback judge run: ${input.active.judgeRunId}`);
|
|
952
|
+
}
|
|
584
953
|
lines.push(
|
|
585
954
|
`Progress: ${input.progress ? formatProgressCounts(input.progress) : "unknown"}`,
|
|
586
955
|
);
|
|
956
|
+
appendStatusDetails(lines, input.details);
|
|
587
957
|
} else if (input.last) {
|
|
588
958
|
lines.push("State: idle");
|
|
589
959
|
lines.push(`Last run: ${input.last.id}`);
|
|
960
|
+
lines.push(`Prompt: ${firstLine(input.last.prompt)}`);
|
|
590
961
|
lines.push(`Profile: ${input.last.profileName}`);
|
|
591
962
|
lines.push(`Phase: ${input.last.phase}`);
|
|
592
|
-
if (input.last.
|
|
963
|
+
if (input.last.chainRunId)
|
|
964
|
+
lines.push(`Chain run: ${input.last.chainRunId}`);
|
|
965
|
+
else if (input.last.panelRunId)
|
|
593
966
|
lines.push(`Panel run: ${input.last.panelRunId}`);
|
|
594
|
-
if (input.last.judgeRunId)
|
|
595
|
-
lines.push(`
|
|
967
|
+
if (input.last.judgeRunId) {
|
|
968
|
+
lines.push(`Fallback judge run: ${input.last.judgeRunId}`);
|
|
969
|
+
}
|
|
596
970
|
} else {
|
|
597
971
|
lines.push("State: idle");
|
|
598
972
|
lines.push("Last run: none");
|
|
599
973
|
}
|
|
600
974
|
|
|
601
975
|
if (input.warnings.length === 0) lines.push("Warnings: none");
|
|
602
|
-
else
|
|
976
|
+
else {
|
|
603
977
|
lines.push("Warnings:", ...input.warnings.map((warning) => `- ${warning}`));
|
|
978
|
+
}
|
|
604
979
|
return lines.join("\n");
|
|
605
980
|
}
|
|
606
981
|
|
|
982
|
+
function appendStatusDetails(
|
|
983
|
+
lines: string[],
|
|
984
|
+
details: FusionStatusDetails | undefined,
|
|
985
|
+
): void {
|
|
986
|
+
if (!details) return;
|
|
987
|
+
if (details.panelists && details.panelists.length > 0) {
|
|
988
|
+
lines.push("", "Panelists");
|
|
989
|
+
for (const panelist of details.panelists) {
|
|
990
|
+
lines.push(`- ${panelist.label}: ${panelist.status}`);
|
|
991
|
+
if (panelist.role) lines.push(` Role: ${panelist.role}`);
|
|
992
|
+
if (panelist.model) lines.push(` Model: ${panelist.model}`);
|
|
993
|
+
if (panelist.activity) lines.push(` Activity: ${panelist.activity}`);
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
if (details.judge) {
|
|
997
|
+
lines.push("", details.judge.label);
|
|
998
|
+
lines.push(`- Status: ${details.judge.status}`);
|
|
999
|
+
if (details.judge.model) lines.push(` Model: ${details.judge.model}`);
|
|
1000
|
+
if (details.judge.activity)
|
|
1001
|
+
lines.push(` Activity: ${details.judge.activity}`);
|
|
1002
|
+
}
|
|
1003
|
+
if (details.fallbackJudge) {
|
|
1004
|
+
lines.push("", details.fallbackJudge.label);
|
|
1005
|
+
lines.push(`- Status: ${details.fallbackJudge.status}`);
|
|
1006
|
+
if (details.fallbackJudge.model) {
|
|
1007
|
+
lines.push(` Model: ${details.fallbackJudge.model}`);
|
|
1008
|
+
}
|
|
1009
|
+
if (details.fallbackJudge.activity) {
|
|
1010
|
+
lines.push(` Activity: ${details.fallbackJudge.activity}`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
607
1015
|
function activeRunId(run: FusionRun | undefined): string | undefined {
|
|
608
1016
|
if (!run) return undefined;
|
|
609
|
-
|
|
1017
|
+
if (run.phase === "judge") return run.judgeRunId;
|
|
1018
|
+
return run.chainRunId ?? run.panelRunId;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
function activeAsyncDir(run: FusionRun | undefined): string | undefined {
|
|
1022
|
+
if (!run) return undefined;
|
|
1023
|
+
return run.phase === "judge" ? run.judgeAsyncDir : run.chainAsyncDir;
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
function buildFusionStatusDetails(
|
|
1027
|
+
run: FusionRun,
|
|
1028
|
+
profile: FusionProfile | undefined,
|
|
1029
|
+
payload: unknown,
|
|
1030
|
+
): FusionStatusDetails {
|
|
1031
|
+
const details: FusionStatusDetails = {
|
|
1032
|
+
prompt: run.prompt,
|
|
1033
|
+
phaseLabel: deriveFusionStatusPhase(run, payload),
|
|
1034
|
+
};
|
|
1035
|
+
if (!profile) return details;
|
|
1036
|
+
|
|
1037
|
+
const judgeModel = configuredJudgeModel(profile);
|
|
1038
|
+
|
|
1039
|
+
if (run.phase === "judge") {
|
|
1040
|
+
details.panelists = buildCompletedPanelStatusLines(
|
|
1041
|
+
profile.panel,
|
|
1042
|
+
storedPanelOutputs(run),
|
|
1043
|
+
storedPanelFailures(run),
|
|
1044
|
+
);
|
|
1045
|
+
details.fallbackJudge = {
|
|
1046
|
+
label: run.chainRunId ? "Fallback judge" : "Judge",
|
|
1047
|
+
...(judgeModel ? { model: judgeModel } : {}),
|
|
1048
|
+
status: describeStandaloneRunStatus(payload),
|
|
1049
|
+
};
|
|
1050
|
+
return details;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const steps = findStepsArray(payload);
|
|
1054
|
+
const panelSteps = steps.slice(0, profile.panel.length);
|
|
1055
|
+
const panelists = profile.panel.map((member, index) => {
|
|
1056
|
+
const step = panelSteps[index];
|
|
1057
|
+
const model = configuredPanelModel(member);
|
|
1058
|
+
const activity = describeStepActivity(step);
|
|
1059
|
+
return {
|
|
1060
|
+
label: member.label,
|
|
1061
|
+
...(member.role ? { role: member.role } : {}),
|
|
1062
|
+
...(model ? { model } : {}),
|
|
1063
|
+
status: describePanelStatus(step),
|
|
1064
|
+
...(activity ? { activity } : {}),
|
|
1065
|
+
};
|
|
1066
|
+
});
|
|
1067
|
+
details.panelists = panelists;
|
|
1068
|
+
|
|
1069
|
+
const judgeActivity = describeStepActivity(steps[profile.panel.length]);
|
|
1070
|
+
details.judge = {
|
|
1071
|
+
label: "Judge",
|
|
1072
|
+
...(judgeModel ? { model: judgeModel } : {}),
|
|
1073
|
+
status: describeChainJudgeStatus(steps[profile.panel.length], panelists),
|
|
1074
|
+
...(judgeActivity ? { activity: judgeActivity } : {}),
|
|
1075
|
+
};
|
|
1076
|
+
return details;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function deriveFusionStatusPhase(
|
|
1080
|
+
run: Pick<FusionRun, "phase" | "chainRunId">,
|
|
1081
|
+
payload: unknown,
|
|
1082
|
+
): string {
|
|
1083
|
+
if (run.phase === "judge") {
|
|
1084
|
+
return run.chainRunId ? "fallback judge" : "judge";
|
|
1085
|
+
}
|
|
1086
|
+
if (run.phase !== "chain") return run.phase;
|
|
1087
|
+
|
|
1088
|
+
const steps = findStepsArray(payload);
|
|
1089
|
+
if (steps.length === 0) return "chain";
|
|
1090
|
+
const judgeStatus = normalizeStatusLabel(steps.at(-1));
|
|
1091
|
+
if (judgeStatus === "running" || judgeStatus === "completed") {
|
|
1092
|
+
return "judge";
|
|
1093
|
+
}
|
|
1094
|
+
const panelSteps = steps.slice(0, -1);
|
|
1095
|
+
if (panelSteps.some((step) => normalizeStatusLabel(step) === "running")) {
|
|
1096
|
+
return "panel";
|
|
1097
|
+
}
|
|
1098
|
+
if (judgeStatus === "pending") {
|
|
1099
|
+
const allPanelsFinished = panelSteps.every((step) => {
|
|
1100
|
+
const status = normalizeStatusLabel(step);
|
|
1101
|
+
return status === "completed" || status === "failed";
|
|
1102
|
+
});
|
|
1103
|
+
return allPanelsFinished ? "judge" : "panel";
|
|
1104
|
+
}
|
|
1105
|
+
return "panel";
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function buildCompletedPanelStatusLines(
|
|
1109
|
+
panel: FusionProfile["panel"],
|
|
1110
|
+
outputs: readonly PanelOutput[],
|
|
1111
|
+
failures: readonly FailedPanelSummary[],
|
|
1112
|
+
): FusionStatusPanelLine[] {
|
|
1113
|
+
return panel.map((member, index) => {
|
|
1114
|
+
const model = configuredPanelModel(member);
|
|
1115
|
+
const output = outputs.find(
|
|
1116
|
+
(item) => item.id === member.id || item.index === index,
|
|
1117
|
+
);
|
|
1118
|
+
if (output) {
|
|
1119
|
+
return {
|
|
1120
|
+
label: member.label,
|
|
1121
|
+
...(member.role ? { role: member.role } : {}),
|
|
1122
|
+
...(model ? { model } : {}),
|
|
1123
|
+
status: "completed",
|
|
1124
|
+
};
|
|
1125
|
+
}
|
|
1126
|
+
const failure = failures.find(
|
|
1127
|
+
(item) => item.id === member.id || item.index === index,
|
|
1128
|
+
);
|
|
1129
|
+
return {
|
|
1130
|
+
label: member.label,
|
|
1131
|
+
...(member.role ? { role: member.role } : {}),
|
|
1132
|
+
...(model ? { model } : {}),
|
|
1133
|
+
status: failure ? "failed" : "unknown",
|
|
1134
|
+
...(failure ? { activity: firstLine(failure.summary) } : {}),
|
|
1135
|
+
};
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
function describePanelStatus(step: unknown): string {
|
|
1140
|
+
return normalizeStatusLabel(step);
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function describeChainJudgeStatus(
|
|
1144
|
+
step: unknown,
|
|
1145
|
+
panelists: readonly FusionStatusPanelLine[],
|
|
1146
|
+
): string {
|
|
1147
|
+
if (step === undefined) return "waiting for panel results";
|
|
1148
|
+
const normalized = normalizeStatusLabel(step);
|
|
1149
|
+
if (normalized === "pending") {
|
|
1150
|
+
const waitingOnPanel = panelists.some(
|
|
1151
|
+
(panelist) =>
|
|
1152
|
+
panelist.status !== "completed" && panelist.status !== "failed",
|
|
1153
|
+
);
|
|
1154
|
+
return waitingOnPanel ? "waiting for panel results" : "pending";
|
|
1155
|
+
}
|
|
1156
|
+
return normalized;
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
function describeStandaloneRunStatus(payload: unknown): string {
|
|
1160
|
+
const state = extractSubagentState(payload);
|
|
1161
|
+
if (state) {
|
|
1162
|
+
if (state === "complete" || state === "completed" || state === "done") {
|
|
1163
|
+
return "completed";
|
|
1164
|
+
}
|
|
1165
|
+
if (state === "running" || state === "active") return "running";
|
|
1166
|
+
if (state === "pending" || state === "queued") return "pending";
|
|
1167
|
+
if (state === "failed" || state === "paused" || state === "detached") {
|
|
1168
|
+
return "failed";
|
|
1169
|
+
}
|
|
1170
|
+
return state;
|
|
1171
|
+
}
|
|
1172
|
+
if (hasResultsArray(payload)) return "completed";
|
|
1173
|
+
return "running";
|
|
1174
|
+
}
|
|
1175
|
+
|
|
1176
|
+
function normalizeStatusLabel(step: unknown): string {
|
|
1177
|
+
if (!isRecord(step)) return "pending";
|
|
1178
|
+
if (step.success === true) return "completed";
|
|
1179
|
+
if (step.success === false) return "failed";
|
|
1180
|
+
if (step.timedOut === true || step.interrupted === true) return "failed";
|
|
1181
|
+
if (typeof step.exitCode === "number") {
|
|
1182
|
+
return step.exitCode === 0 ? "completed" : "failed";
|
|
1183
|
+
}
|
|
1184
|
+
const status = firstString(step.status, step.state);
|
|
1185
|
+
if (status === "complete" || status === "completed" || status === "done") {
|
|
1186
|
+
return "completed";
|
|
1187
|
+
}
|
|
1188
|
+
if (status === "running" || status === "active") return "running";
|
|
1189
|
+
if (status === "pending" || status === "queued") return "pending";
|
|
1190
|
+
if (status === "failed" || status === "paused" || status === "detached") {
|
|
1191
|
+
return "failed";
|
|
1192
|
+
}
|
|
1193
|
+
return "pending";
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
function findStepsArray(payload: unknown): readonly unknown[] {
|
|
1197
|
+
if (!isRecord(payload)) return [];
|
|
1198
|
+
const direct = unknownArray(payload.steps);
|
|
1199
|
+
if (direct) return direct;
|
|
1200
|
+
if (isRecord(payload.details)) {
|
|
1201
|
+
const detailsSteps = unknownArray(payload.details.steps);
|
|
1202
|
+
if (detailsSteps) return detailsSteps;
|
|
1203
|
+
}
|
|
1204
|
+
if (isRecord(payload.data)) return findStepsArray(payload.data);
|
|
1205
|
+
return [];
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
function describeStepActivity(step: unknown): string | undefined {
|
|
1209
|
+
if (!isRecord(step)) return undefined;
|
|
1210
|
+
const tools = unknownArray(step.recentTools);
|
|
1211
|
+
const lastTool = tools?.at(-1);
|
|
1212
|
+
if (isRecord(lastTool)) {
|
|
1213
|
+
const tool = firstString(lastTool.tool) ?? "tool";
|
|
1214
|
+
const args = firstString(lastTool.args);
|
|
1215
|
+
return args ? `${tool} ${summarizeActivityArg(args)}` : tool;
|
|
1216
|
+
}
|
|
1217
|
+
const recentOutput = unknownArray(step.recentOutput)
|
|
1218
|
+
?.map((item) => (typeof item === "string" ? item.trim() : ""))
|
|
1219
|
+
.find(Boolean);
|
|
1220
|
+
return recentOutput || undefined;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
function summarizeActivityArg(value: string): string {
|
|
1224
|
+
const parts = value.split("/").filter(Boolean);
|
|
1225
|
+
if (parts.length >= 3) return parts.slice(-3).join("/");
|
|
1226
|
+
return value;
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
function configuredPanelModel(
|
|
1230
|
+
member: FusionProfile["panel"][number],
|
|
1231
|
+
): string | undefined {
|
|
1232
|
+
return appendThinkingSuffix(member.model, member.thinking);
|
|
610
1233
|
}
|
|
611
1234
|
|
|
612
1235
|
function findResultsArray(payload: unknown): readonly unknown[] | undefined {
|
|
@@ -626,10 +1249,25 @@ function findResultsArray(payload: unknown): readonly unknown[] | undefined {
|
|
|
626
1249
|
return undefined;
|
|
627
1250
|
}
|
|
628
1251
|
|
|
629
|
-
function
|
|
1252
|
+
function hasResultsArray(payload: unknown): boolean {
|
|
1253
|
+
return (findResultsArray(payload)?.length ?? 0) > 0;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
function hasJudgeResult(payload: unknown, panelCount: number): boolean {
|
|
1257
|
+
return (findResultsArray(payload)?.length ?? 0) > panelCount;
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
function findResult(
|
|
630
1261
|
payload: unknown,
|
|
1262
|
+
resultIndex?: number,
|
|
631
1263
|
): Record<string, unknown> | undefined {
|
|
632
|
-
const
|
|
1264
|
+
const results = findResultsArray(payload);
|
|
1265
|
+
if (!results || results.length === 0) return undefined;
|
|
1266
|
+
if (resultIndex !== undefined) {
|
|
1267
|
+
const indexed = results[resultIndex];
|
|
1268
|
+
return isRecord(indexed) ? indexed : undefined;
|
|
1269
|
+
}
|
|
1270
|
+
const first = results[0];
|
|
633
1271
|
return isRecord(first) ? first : undefined;
|
|
634
1272
|
}
|
|
635
1273
|
|
|
@@ -642,41 +1280,115 @@ function resultFailed(result: Record<string, unknown>): boolean {
|
|
|
642
1280
|
return status === "failed" || status === "paused" || status === "detached";
|
|
643
1281
|
}
|
|
644
1282
|
|
|
1283
|
+
function extractSubagentState(payload: unknown): string | undefined {
|
|
1284
|
+
if (!isRecord(payload)) return undefined;
|
|
1285
|
+
const direct = firstString(payload.state, payload.status);
|
|
1286
|
+
if (direct) return direct;
|
|
1287
|
+
const text = firstString(
|
|
1288
|
+
payload.text,
|
|
1289
|
+
isRecord(payload.details) ? payload.details.text : undefined,
|
|
1290
|
+
);
|
|
1291
|
+
const parsedText = text ? extractStateFromText(text) : undefined;
|
|
1292
|
+
if (parsedText) return parsedText;
|
|
1293
|
+
if (isRecord(payload.details)) {
|
|
1294
|
+
const fromDetails = firstString(
|
|
1295
|
+
payload.details.state,
|
|
1296
|
+
payload.details.status,
|
|
1297
|
+
);
|
|
1298
|
+
if (fromDetails) return fromDetails;
|
|
1299
|
+
}
|
|
1300
|
+
if (isRecord(payload.data)) return extractSubagentState(payload.data);
|
|
1301
|
+
return undefined;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function extractStateFromText(text: string): string | undefined {
|
|
1305
|
+
const match = text.match(/(?:^|\n)(?:State|Status):\s*([^\n\r]+)/i);
|
|
1306
|
+
return match?.[1]?.trim() || undefined;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function isTerminalSubagentState(state: string | undefined): boolean {
|
|
1310
|
+
return (
|
|
1311
|
+
state === "complete" ||
|
|
1312
|
+
state === "completed" ||
|
|
1313
|
+
state === "done" ||
|
|
1314
|
+
state === "failed" ||
|
|
1315
|
+
state === "paused" ||
|
|
1316
|
+
state === "detached"
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
|
|
645
1320
|
function firstNonBlankStringFromPayload(payload: unknown): string | undefined {
|
|
646
1321
|
if (!isRecord(payload)) return undefined;
|
|
647
1322
|
const direct = firstNonBlankString(
|
|
648
1323
|
payload.output,
|
|
649
1324
|
payload.finalOutput,
|
|
650
1325
|
payload.summary,
|
|
651
|
-
payload.text,
|
|
652
1326
|
);
|
|
653
1327
|
if (direct) return direct;
|
|
1328
|
+
const text = firstNonBlankString(payload.text);
|
|
1329
|
+
if (text && !isStatusEnvelopeText(text)) return text;
|
|
654
1330
|
if (isRecord(payload.details)) {
|
|
655
1331
|
const fromDetails = firstNonBlankString(
|
|
656
1332
|
payload.details.output,
|
|
657
1333
|
payload.details.finalOutput,
|
|
658
1334
|
payload.details.summary,
|
|
659
|
-
payload.details.text,
|
|
660
1335
|
);
|
|
661
1336
|
if (fromDetails) return fromDetails;
|
|
1337
|
+
const detailsText = firstNonBlankString(payload.details.text);
|
|
1338
|
+
if (detailsText && !isStatusEnvelopeText(detailsText)) return detailsText;
|
|
662
1339
|
}
|
|
663
1340
|
if (isRecord(payload.data))
|
|
664
1341
|
return firstNonBlankStringFromPayload(payload.data);
|
|
665
1342
|
return undefined;
|
|
666
1343
|
}
|
|
667
1344
|
|
|
1345
|
+
function isStatusEnvelopeText(value: string): boolean {
|
|
1346
|
+
return (
|
|
1347
|
+
/(?:^|\n)Run:\s*[^\n\r]+/i.test(value) &&
|
|
1348
|
+
/(?:^|\n)(?:State|Status):\s*[^\n\r]+/i.test(value)
|
|
1349
|
+
);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
668
1352
|
function extractArtifactPath(
|
|
669
1353
|
result: Record<string, unknown>,
|
|
670
1354
|
): string | undefined {
|
|
671
1355
|
const direct = firstString(result.artifactPath, result.savedOutputPath);
|
|
672
1356
|
if (direct) return direct;
|
|
673
|
-
if (isRecord(result.artifactPaths))
|
|
1357
|
+
if (isRecord(result.artifactPaths)) {
|
|
674
1358
|
return firstString(result.artifactPaths.outputPath);
|
|
675
|
-
|
|
1359
|
+
}
|
|
1360
|
+
if (isRecord(result.outputReference)) {
|
|
676
1361
|
return firstString(result.outputReference.path);
|
|
1362
|
+
}
|
|
677
1363
|
return undefined;
|
|
678
1364
|
}
|
|
679
1365
|
|
|
1366
|
+
function storedPanelOutputs(run: FusionRun): readonly PanelOutput[] {
|
|
1367
|
+
return run.panelOutputs ?? [];
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
function storedPanelFailures(run: FusionRun): readonly FailedPanelSummary[] {
|
|
1371
|
+
return run.panelFailures ?? [];
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function configuredJudgeModel(profile: FusionProfile): string | undefined {
|
|
1375
|
+
return appendThinkingSuffix(profile.judge.model, profile.judge.thinking);
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function withJudgeModel(
|
|
1379
|
+
judgeModel: string | undefined,
|
|
1380
|
+
): { judgeModel: string } | Record<string, never> {
|
|
1381
|
+
return judgeModel ? { judgeModel } : {};
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function promptPreview(prompt: string): string {
|
|
1385
|
+
return firstLine(prompt).slice(0, 80);
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
function firstLine(value: string): string {
|
|
1389
|
+
return value.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
1390
|
+
}
|
|
1391
|
+
|
|
680
1392
|
function firstString(...values: readonly unknown[]): string | undefined {
|
|
681
1393
|
for (const value of values) {
|
|
682
1394
|
if (typeof value === "string") return value;
|