@alexeiled/pi-fusion 0.3.0 → 0.5.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.
@@ -11,11 +11,11 @@ import {
11
11
  renderFailureReport,
12
12
  renderJudgeReport,
13
13
  } from "./report.js";
14
- import { extractPanelResults } from "./result-extract.js";
15
14
  import {
16
- appendThinkingSuffix,
17
- buildFusionChainSpawnParams,
18
- } from "./run-builder.js";
15
+ extractPanelResults,
16
+ type ExtractPanelResultsSuccess,
17
+ } from "./result-extract.js";
18
+ import { appendThinkingSuffix, buildPanelSpawnParams } from "./run-builder.js";
19
19
  import { FusionRunStore, FusionRunStoreError } from "./run-store.js";
20
20
  import {
21
21
  clearFusionUi,
@@ -36,6 +36,11 @@ import type {
36
36
  PanelOutput,
37
37
  ParsedFusionArgs,
38
38
  } from "./types.js";
39
+ import {
40
+ extractRunObservation,
41
+ hasStrongPanelAgreement,
42
+ mergeRunObservations,
43
+ } from "./run-observations.js";
39
44
  import type { SubagentsTargetParams } from "./subagents-rpc.js";
40
45
 
41
46
  export const SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
@@ -94,6 +99,7 @@ export type FusionCommandResult =
94
99
  interface RunLifecycleSnapshot {
95
100
  statusPayload?: unknown;
96
101
  resultPayload?: unknown;
102
+ resultIsTerminal: boolean;
97
103
  }
98
104
 
99
105
  export class FusionOrchestrator {
@@ -108,6 +114,7 @@ export class FusionOrchestrator {
108
114
  private configWarning: string | undefined;
109
115
  private reconcileTimer: NodeJS.Timeout | undefined;
110
116
  private reconciling = false;
117
+ private pendingCompletionPayload: unknown;
111
118
 
112
119
  constructor(deps: FusionOrchestratorDeps) {
113
120
  this.rpc = deps.rpc;
@@ -153,42 +160,102 @@ export class FusionOrchestrator {
153
160
  return { status: "failed", error: message };
154
161
  }
155
162
 
156
- const run = this.runStore.startRun({
157
- prompt: args.prompt,
158
- profileName: resolved.name,
159
- phase: "chain",
160
- });
163
+ let run: FusionRun;
164
+ try {
165
+ run = this.runStore.startRun({
166
+ prompt: args.prompt,
167
+ profileName: resolved.name,
168
+ phase: "panel",
169
+ });
170
+ } catch (error: unknown) {
171
+ if (!(error instanceof FusionRunStoreError)) throw error;
172
+ const active = this.runStore.getActiveRun();
173
+ if (active) {
174
+ this.notify(
175
+ ctx,
176
+ `Fusion run ${active.id} is already active.`,
177
+ "warning",
178
+ );
179
+ return { status: "conflict", activeRunId: active.id };
180
+ }
181
+ return { status: "failed", error: errorMessage(error) };
182
+ }
161
183
  this.activeProfile = resolved.profile;
162
184
  publishFusionStatus(ctx, run);
163
185
 
164
186
  try {
165
187
  const spawnResult = await this.rpc.spawn(
166
- buildFusionChainSpawnParams(resolved.profile, args.prompt),
188
+ buildPanelSpawnParams(resolved.profile, args.prompt),
167
189
  );
168
- const chainRunId = extractSubagentRunId(spawnResult);
169
- if (!chainRunId) {
190
+ const panelRunId = extractSubagentRunId(spawnResult);
191
+ if (!panelRunId) {
170
192
  throw new FusionArgsError(
171
- "pi-subagents spawn did not return a fusion chain run ID.",
193
+ "pi-subagents spawn did not return a fusion panel run ID.",
172
194
  );
173
195
  }
174
- const chainAsyncDir = extractSubagentAsyncDir(spawnResult);
196
+ const panelAsyncDir = extractSubagentAsyncDir(spawnResult);
197
+ const current = this.runStore.getActiveRun();
198
+ if (!current || current.id !== run.id) {
199
+ await this.stopOrphanedRun(panelRunId);
200
+ const cancelled = this.runStore.getLastRunSummary();
201
+ return cancelled?.id === run.id &&
202
+ cancelled.phase === "cancelled" &&
203
+ cancelled.report
204
+ ? { status: "cancelled", run: cancelled, report: cancelled.report }
205
+ : { status: "ignored" };
206
+ }
175
207
  const updated = this.runStore.updateRun(run.id, {
176
- chainRunId,
177
- ...(chainAsyncDir ? { chainAsyncDir } : {}),
208
+ panelRunId,
209
+ ...(panelAsyncDir ? { panelAsyncDir } : {}),
178
210
  });
179
211
  publishFusionStatus(ctx, updated);
180
212
  this.ensureReconcileLoop();
181
213
  this.notify(
182
214
  ctx,
183
- `Fusion ${resolved.name} started (${resolved.profile.panel.length} panelists): "${promptPreview(args.prompt)}" — ${chainRunId}`,
215
+ `Fusion ${resolved.name} started (${resolved.profile.panel.length} panelists): "${promptPreview(args.prompt)}" — ${panelRunId}`,
184
216
  "info",
185
217
  );
186
218
  return { status: "started", run: updated };
187
219
  } catch (error: unknown) {
220
+ const cancelled = this.runStore.getLastRunSummary();
221
+ if (
222
+ cancelled?.id === run.id &&
223
+ cancelled.phase === "cancelled" &&
224
+ cancelled.report
225
+ ) {
226
+ return {
227
+ status: "cancelled",
228
+ run: cancelled,
229
+ report: cancelled.report,
230
+ };
231
+ }
188
232
  return this.failActiveRun(errorMessage(error));
189
233
  }
190
234
  }
191
235
 
236
+ private async stopOrphanedRun(
237
+ runId: string,
238
+ kind: "panel" | "judge" = "panel",
239
+ ): Promise<void> {
240
+ try {
241
+ await this.rpc.stop({ id: runId });
242
+ return;
243
+ } catch (stopError: unknown) {
244
+ try {
245
+ await this.rpc.interrupt({ id: runId });
246
+ this.installWarning = `Orphaned ${kind} stop fell back to interrupt for ${runId}: ${errorMessage(stopError)}`;
247
+ return;
248
+ } catch (interruptError: unknown) {
249
+ this.installWarning = `Could not stop orphaned ${kind} run ${runId}: ${errorMessage(interruptError)}`;
250
+ }
251
+ }
252
+ this.notify(
253
+ this.context,
254
+ this.installWarning ?? `Could not stop orphaned ${kind} run.`,
255
+ "warning",
256
+ );
257
+ }
258
+
192
259
  async handleSubagentComplete(payload: unknown): Promise<FusionCommandResult> {
193
260
  const active = this.runStore.getActiveRun();
194
261
  if (!active) return { status: "ignored" };
@@ -262,6 +329,10 @@ export class FusionOrchestrator {
262
329
  }
263
330
  }
264
331
 
332
+ if (this.runStore.getActiveRun()?.id !== active.id) {
333
+ return { status: "ignored" };
334
+ }
335
+
265
336
  const report = renderCancelledReport({
266
337
  run: active,
267
338
  method,
@@ -384,7 +455,12 @@ export class FusionOrchestrator {
384
455
  private async reconcileActiveRun(
385
456
  eventPayload?: unknown,
386
457
  ): Promise<FusionCommandResult> {
387
- if (this.reconciling) return { status: "ignored" };
458
+ if (this.reconciling) {
459
+ if (eventPayload !== undefined) {
460
+ this.pendingCompletionPayload = eventPayload;
461
+ }
462
+ return { status: "ignored" };
463
+ }
388
464
  const active = this.runStore.getActiveRun();
389
465
  if (!active) return { status: "ignored" };
390
466
 
@@ -402,6 +478,15 @@ export class FusionOrchestrator {
402
478
  return { status: "ignored" };
403
479
  } finally {
404
480
  this.reconciling = false;
481
+ const pendingPayload = this.pendingCompletionPayload;
482
+ this.pendingCompletionPayload = undefined;
483
+ if (pendingPayload !== undefined) {
484
+ void this.reconcileActiveRun(pendingPayload).catch((error: unknown) => {
485
+ const message = `Could not reconcile completed fusion run: ${errorMessage(error)}`;
486
+ this.installWarning = message;
487
+ this.notify(this.context, message, "warning");
488
+ });
489
+ }
405
490
  }
406
491
  }
407
492
 
@@ -425,7 +510,7 @@ export class FusionOrchestrator {
425
510
  const terminalPayload =
426
511
  snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
427
512
  if (
428
- !hasResultsArray(snapshot.resultPayload) &&
513
+ !snapshot.resultIsTerminal &&
429
514
  !isTerminalSubagentState(extractSubagentState(terminalPayload))
430
515
  ) {
431
516
  return { status: "ignored" };
@@ -444,10 +529,15 @@ export class FusionOrchestrator {
444
529
  );
445
530
  }
446
531
 
532
+ const observedPanels = mergePanelObservations(
533
+ extracted,
534
+ snapshot.statusPayload,
535
+ profile,
536
+ );
447
537
  const updated = this.storePanelResults(
448
538
  active.id,
449
- extracted.outputs,
450
- extracted.failures,
539
+ observedPanels.outputs,
540
+ observedPanels.failures,
451
541
  );
452
542
 
453
543
  if (hasJudgeResult(snapshot.resultPayload, profile.panel.length)) {
@@ -456,12 +546,26 @@ export class FusionOrchestrator {
456
546
  });
457
547
  if (!output.ok) return this.failActiveRun(output.error);
458
548
 
549
+ const judgeObservation = mergeRunObservations(
550
+ extractRunObservation(
551
+ findStepsArray(snapshot.statusPayload)[profile.panel.length] ??
552
+ snapshot.statusPayload,
553
+ ),
554
+ extractRunObservation(
555
+ findResult(snapshot.resultPayload, profile.panel.length) ??
556
+ snapshot.resultPayload,
557
+ ),
558
+ );
559
+ const observed = this.runStore.updateRun(updated.id, {
560
+ judgeObservation,
561
+ });
459
562
  const report = renderJudgeReport({
460
- run: updated,
563
+ run: observed,
461
564
  judgeOutput: output.output,
462
- panelOutputs: storedPanelOutputs(updated),
463
- failures: storedPanelFailures(updated),
565
+ panelOutputs: storedPanelOutputs(observed),
566
+ failures: storedPanelFailures(observed),
464
567
  ...withJudgeModel(configuredJudgeModel(profile)),
568
+ judgeObservation,
465
569
  });
466
570
  return this.completeActiveRun(report);
467
571
  }
@@ -469,8 +573,8 @@ export class FusionOrchestrator {
469
573
  return this.finishPanelCompletion(
470
574
  updated,
471
575
  profile,
472
- extracted.outputs,
473
- extracted.failures,
576
+ observedPanels.outputs,
577
+ observedPanels.failures,
474
578
  { fallbackJudge: true },
475
579
  );
476
580
  }
@@ -489,13 +593,35 @@ export class FusionOrchestrator {
489
593
  const snapshot = await this.loadRunLifecycle({
490
594
  run: active,
491
595
  ...(active.panelRunId ? { runId: active.panelRunId } : {}),
492
- ...(active.chainAsyncDir ? { asyncDir: active.chainAsyncDir } : {}),
596
+ ...(active.panelAsyncDir
597
+ ? { asyncDir: active.panelAsyncDir }
598
+ : active.chainAsyncDir
599
+ ? { asyncDir: active.chainAsyncDir }
600
+ : {}),
493
601
  eventPayload: payload,
494
602
  });
603
+
604
+ if (!active.panelStopReason) {
605
+ const partial = extractPanelResults(snapshot.statusPayload, {
606
+ panel: profile.panel,
607
+ completedOnly: true,
608
+ });
609
+ if (
610
+ partial.ok &&
611
+ shouldStopWhenPanelAgrees(profile, partial.outputs, partial.failures)
612
+ ) {
613
+ return this.stopPanelAfterAgreement(
614
+ active,
615
+ partial,
616
+ profile.panel.length,
617
+ );
618
+ }
619
+ }
620
+
495
621
  const terminalPayload =
496
622
  snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
497
623
  if (
498
- !hasResultsArray(snapshot.resultPayload) &&
624
+ !snapshot.resultIsTerminal &&
499
625
  !isTerminalSubagentState(extractSubagentState(terminalPayload))
500
626
  ) {
501
627
  return { status: "ignored" };
@@ -505,6 +631,10 @@ export class FusionOrchestrator {
505
631
  snapshot.resultPayload ?? snapshot.statusPayload ?? payload,
506
632
  {
507
633
  panel: profile.panel,
634
+ limit: profile.panel.length,
635
+ ...(active.panelStoppedIndices
636
+ ? { stoppedPanelIndices: active.panelStoppedIndices }
637
+ : {}),
508
638
  },
509
639
  );
510
640
  if (!extracted.ok) {
@@ -513,21 +643,80 @@ export class FusionOrchestrator {
513
643
  );
514
644
  }
515
645
 
646
+ const observedPanels = mergePanelObservations(
647
+ extracted,
648
+ snapshot.statusPayload,
649
+ profile,
650
+ );
516
651
  const updated = this.storePanelResults(
517
652
  active.id,
518
- extracted.outputs,
519
- extracted.failures,
653
+ observedPanels.outputs,
654
+ observedPanels.failures,
520
655
  );
521
656
 
522
657
  return this.finishPanelCompletion(
523
658
  updated,
524
659
  profile,
525
- extracted.outputs,
526
- extracted.failures,
660
+ observedPanels.outputs,
661
+ observedPanels.failures,
527
662
  { fallbackJudge: false },
528
663
  );
529
664
  }
530
665
 
666
+ private async stopPanelAfterAgreement(
667
+ active: FusionRun,
668
+ partial: ExtractPanelResultsSuccess,
669
+ panelSize: number,
670
+ ): Promise<FusionCommandResult> {
671
+ if (!active.panelRunId) return { status: "ignored" };
672
+
673
+ let method: "stop" | "interrupt" = "stop";
674
+ try {
675
+ await this.rpc.stop({ id: active.panelRunId });
676
+ } catch (stopError: unknown) {
677
+ try {
678
+ await this.rpc.interrupt({ id: active.panelRunId });
679
+ method = "interrupt";
680
+ } catch (interruptError: unknown) {
681
+ this.installWarning = `Could not stop panel run ${active.panelRunId} after agreement: ${errorMessage(interruptError)}`;
682
+ this.notify(this.context, this.installWarning, "warning");
683
+ return { status: "ignored" };
684
+ }
685
+ this.installWarning = `Panel stop fell back to interrupt for ${active.panelRunId}: ${errorMessage(stopError)}`;
686
+ }
687
+
688
+ if (this.runStore.getActiveRun()?.id !== active.id) {
689
+ return { status: "ignored" };
690
+ }
691
+
692
+ const completedIndices = new Set([
693
+ ...partial.outputs.map((output) => output.index),
694
+ ...partial.failures.map((failure) => failure.index),
695
+ ]);
696
+ const panelStoppedIndices = Array.from(
697
+ { length: panelSize },
698
+ (_, index) => index,
699
+ ).filter((index) => !completedIndices.has(index));
700
+ const updated = this.runStore.updateRun(active.id, {
701
+ panelStopReason: "agreement",
702
+ panelStoppedIndices,
703
+ panelOutputs: partial.outputs,
704
+ panelFailures: partial.failures,
705
+ });
706
+ publishFusionStatus(
707
+ this.context,
708
+ updated,
709
+ undefined,
710
+ "stopping after panel agreement",
711
+ );
712
+ this.notify(
713
+ this.context,
714
+ `Panel agreement found; stopping remaining panelists (${method}).`,
715
+ "info",
716
+ );
717
+ return { status: "started", run: updated };
718
+ }
719
+
531
720
  private async finishPanelCompletion(
532
721
  run: FusionRun,
533
722
  profile: FusionProfile,
@@ -557,6 +746,10 @@ export class FusionOrchestrator {
557
746
  throw new FusionArgsError(decision.missingRunIdError);
558
747
  }
559
748
  const judgeAsyncDir = extractSubagentAsyncDir(spawnResult);
749
+ if (this.runStore.getActiveRun()?.id !== run.id) {
750
+ await this.stopOrphanedRun(judgeRunId, "judge");
751
+ return { status: "ignored" };
752
+ }
560
753
  const nextRun = this.runStore.updateRun(run.id, {
561
754
  phase: "judge",
562
755
  judgeRunId,
@@ -589,7 +782,7 @@ export class FusionOrchestrator {
589
782
  const terminalPayload =
590
783
  snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
591
784
  if (
592
- !hasResultsArray(snapshot.resultPayload) &&
785
+ !snapshot.resultIsTerminal &&
593
786
  !isTerminalSubagentState(extractSubagentState(terminalPayload))
594
787
  ) {
595
788
  return { status: "ignored" };
@@ -600,16 +793,29 @@ export class FusionOrchestrator {
600
793
  );
601
794
  if (!output.ok) return this.failActiveRun(output.error);
602
795
 
796
+ const judgeModel = this.activeProfile
797
+ ? configuredJudgeModel(this.activeProfile)
798
+ : undefined;
799
+ const judgeObservation = mergeRunObservations(
800
+ extractRunObservation(
801
+ findStepsArray(snapshot.statusPayload)[0] ?? snapshot.statusPayload,
802
+ ),
803
+ extractRunObservation(
804
+ findResult(
805
+ snapshot.resultPayload ?? snapshot.statusPayload ?? payload,
806
+ ) ?? snapshot.resultPayload,
807
+ ),
808
+ );
809
+ const observed = this.runStore.updateRun(active.id, {
810
+ judgeObservation,
811
+ });
603
812
  const report = renderJudgeReport({
604
- run: active,
813
+ run: observed,
605
814
  judgeOutput: output.output,
606
- panelOutputs: storedPanelOutputs(active),
607
- failures: storedPanelFailures(active),
608
- ...withJudgeModel(
609
- this.activeProfile
610
- ? configuredJudgeModel(this.activeProfile)
611
- : undefined,
612
- ),
815
+ panelOutputs: storedPanelOutputs(observed),
816
+ failures: storedPanelFailures(observed),
817
+ ...withJudgeModel(judgeModel),
818
+ judgeObservation,
613
819
  });
614
820
  return this.completeActiveRun(report);
615
821
  }
@@ -645,7 +851,11 @@ export class FusionOrchestrator {
645
851
 
646
852
  const eventHasResults = hasResultsArray(input.eventPayload);
647
853
  if (eventPayloadMatches && eventHasResults) {
648
- return { statusPayload, resultPayload: input.eventPayload };
854
+ return {
855
+ statusPayload,
856
+ resultPayload: input.eventPayload,
857
+ resultIsTerminal: true,
858
+ };
649
859
  }
650
860
 
651
861
  const artifactResult = readSubagentResultArtifact({
@@ -653,22 +863,43 @@ export class FusionOrchestrator {
653
863
  ...(input.asyncDir ? { asyncDir: input.asyncDir } : {}),
654
864
  });
655
865
  if (hasResultsArray(artifactResult)) {
656
- return { statusPayload, resultPayload: artifactResult };
866
+ return {
867
+ statusPayload,
868
+ resultPayload: artifactResult,
869
+ resultIsTerminal: true,
870
+ };
657
871
  }
658
872
 
659
873
  if (hasResultsArray(statusPayload)) {
660
- return { statusPayload, resultPayload: statusPayload };
874
+ const resultIsTerminal =
875
+ eventPayloadMatches ||
876
+ isTerminalSubagentState(extractSubagentState(statusPayload));
877
+ return {
878
+ statusPayload,
879
+ resultPayload: statusPayload,
880
+ resultIsTerminal,
881
+ };
661
882
  }
662
883
 
663
884
  if (isTerminalSubagentState(extractSubagentState(statusPayload))) {
664
- return { statusPayload, resultPayload: statusPayload };
885
+ return {
886
+ statusPayload,
887
+ resultPayload: statusPayload,
888
+ resultIsTerminal: true,
889
+ };
665
890
  }
666
891
 
667
892
  if (eventPayloadMatches) {
668
- return { statusPayload, resultPayload: input.eventPayload };
893
+ return {
894
+ statusPayload,
895
+ resultPayload: input.eventPayload,
896
+ resultIsTerminal: isTerminalSubagentState(
897
+ extractSubagentState(input.eventPayload),
898
+ ),
899
+ };
669
900
  }
670
901
 
671
- return { statusPayload };
902
+ return { statusPayload, resultIsTerminal: false };
672
903
  }
673
904
 
674
905
  private storePanelResults(
@@ -747,7 +978,11 @@ export class FusionOrchestrator {
747
978
  private ensureReconcileLoop(): void {
748
979
  if (this.reconcileTimer) return;
749
980
  this.reconcileTimer = setInterval(() => {
750
- void this.reconcileActiveRun();
981
+ void this.reconcileActiveRun().catch((error: unknown) => {
982
+ const message = `Could not reconcile fusion run: ${errorMessage(error)}`;
983
+ this.installWarning = message;
984
+ this.notify(this.context, message, "warning");
985
+ });
751
986
  }, RECONCILE_INTERVAL_MS);
752
987
  this.reconcileTimer.unref?.();
753
988
  }
@@ -895,7 +1130,9 @@ function formatFusionStatusReport(input: {
895
1130
  else if (input.active.panelRunId)
896
1131
  lines.push(`Panel run: ${input.active.panelRunId}`);
897
1132
  if (input.active.judgeRunId) {
898
- lines.push(`Fallback judge run: ${input.active.judgeRunId}`);
1133
+ lines.push(
1134
+ `${input.active.chainRunId ? "Fallback judge run" : "Judge run"}: ${input.active.judgeRunId}`,
1135
+ );
899
1136
  }
900
1137
  lines.push(
901
1138
  `Progress: ${input.progress ? formatProgressCounts(input.progress) : "unknown"}`,
@@ -912,7 +1149,9 @@ function formatFusionStatusReport(input: {
912
1149
  else if (input.last.panelRunId)
913
1150
  lines.push(`Panel run: ${input.last.panelRunId}`);
914
1151
  if (input.last.judgeRunId) {
915
- lines.push(`Fallback judge run: ${input.last.judgeRunId}`);
1152
+ lines.push(
1153
+ `${input.last.chainRunId ? "Fallback judge run" : "Judge run"}: ${input.last.judgeRunId}`,
1154
+ );
916
1155
  }
917
1156
  } else {
918
1157
  lines.push("State: idle");
@@ -967,7 +1206,10 @@ function activeRunId(run: FusionRun | undefined): string | undefined {
967
1206
 
968
1207
  function activeAsyncDir(run: FusionRun | undefined): string | undefined {
969
1208
  if (!run) return undefined;
970
- return run.phase === "judge" ? run.judgeAsyncDir : run.chainAsyncDir;
1209
+ if (run.phase === "judge") return run.judgeAsyncDir;
1210
+ return run.phase === "panel"
1211
+ ? (run.panelAsyncDir ?? run.chainAsyncDir)
1212
+ : run.chainAsyncDir;
971
1213
  }
972
1214
 
973
1215
  function buildFusionStatusDetails(
@@ -1003,22 +1245,28 @@ function buildFusionStatusDetails(
1003
1245
  const step = panelSteps[index];
1004
1246
  const model = configuredPanelModel(member);
1005
1247
  const activity = describeStepActivity(step);
1248
+ const metrics = describeStepMetrics(step);
1006
1249
  return {
1007
1250
  label: member.label,
1008
1251
  ...(member.role ? { role: member.role } : {}),
1009
1252
  ...(model ? { model } : {}),
1010
1253
  status: describePanelStatus(step),
1011
- ...(activity ? { activity } : {}),
1254
+ ...([activity, metrics].filter(Boolean).length > 0
1255
+ ? { activity: [activity, metrics].filter(Boolean).join(" · ") }
1256
+ : {}),
1012
1257
  };
1013
1258
  });
1014
1259
  details.panelists = panelists;
1015
1260
 
1016
1261
  const judgeActivity = describeStepActivity(steps[profile.panel.length]);
1262
+ const judgeMetrics = describeStepMetrics(steps[profile.panel.length]);
1017
1263
  details.judge = {
1018
1264
  label: "Judge",
1019
1265
  ...(judgeModel ? { model: judgeModel } : {}),
1020
1266
  status: describeChainJudgeStatus(steps[profile.panel.length], panelists),
1021
- ...(judgeActivity ? { activity: judgeActivity } : {}),
1267
+ ...([judgeActivity, judgeMetrics].filter(Boolean).length > 0
1268
+ ? { activity: [judgeActivity, judgeMetrics].filter(Boolean).join(" · ") }
1269
+ : {}),
1022
1270
  };
1023
1271
  return details;
1024
1272
  }
@@ -1167,6 +1415,19 @@ function describeStepActivity(step: unknown): string | undefined {
1167
1415
  return recentOutput || undefined;
1168
1416
  }
1169
1417
 
1418
+ function describeStepMetrics(step: unknown): string | undefined {
1419
+ const observation = extractRunObservation(step);
1420
+ const metrics = [
1421
+ observation.durationMs !== undefined
1422
+ ? `${(observation.durationMs / 1000).toFixed(1)}s`
1423
+ : undefined,
1424
+ observation.usage?.costUsd !== undefined
1425
+ ? `$${observation.usage.costUsd.toFixed(4)}`
1426
+ : undefined,
1427
+ ].filter((value): value is string => Boolean(value));
1428
+ return metrics.length > 0 ? metrics.join(", ") : undefined;
1429
+ }
1430
+
1170
1431
  function summarizeActivityArg(value: string): string {
1171
1432
  const parts = value.split("/").filter(Boolean);
1172
1433
  if (parts.length >= 3) return parts.slice(-3).join("/");
@@ -1310,6 +1571,71 @@ function extractArtifactPath(
1310
1571
  return undefined;
1311
1572
  }
1312
1573
 
1574
+ function mergePanelObservations(
1575
+ result: ExtractPanelResultsSuccess,
1576
+ statusPayload: unknown,
1577
+ profile: FusionProfile,
1578
+ ): ExtractPanelResultsSuccess {
1579
+ const status = extractPanelResults(statusPayload, {
1580
+ panel: profile.panel,
1581
+ completedOnly: true,
1582
+ limit: profile.panel.length,
1583
+ });
1584
+ if (!status.ok) return result;
1585
+
1586
+ const observations = new Map<number, PanelOutput["observation"]>();
1587
+ for (const output of status.outputs) {
1588
+ observations.set(output.index, output.observation);
1589
+ }
1590
+ for (const failure of status.failures) {
1591
+ observations.set(failure.index, failure.observation);
1592
+ }
1593
+
1594
+ return {
1595
+ ...result,
1596
+ outputs: result.outputs.map((output) =>
1597
+ withMergedObservation(output, observations.get(output.index)),
1598
+ ),
1599
+ failures: result.failures.map((failure) =>
1600
+ withMergedObservation(failure, observations.get(failure.index)),
1601
+ ),
1602
+ };
1603
+ }
1604
+
1605
+ function withMergedObservation<T extends PanelOutput | FailedPanelSummary>(
1606
+ item: T,
1607
+ statusObservation: PanelOutput["observation"] | undefined,
1608
+ ): T {
1609
+ const observation = mergeRunObservations(statusObservation, item.observation);
1610
+ return hasObservationData(observation) ? { ...item, observation } : item;
1611
+ }
1612
+
1613
+ function hasObservationData(observation: PanelOutput["observation"]): boolean {
1614
+ return Boolean(
1615
+ observation &&
1616
+ (observation.model ||
1617
+ observation.durationMs !== undefined ||
1618
+ observation.usage ||
1619
+ observation.attempts ||
1620
+ observation.providerFailures),
1621
+ );
1622
+ }
1623
+
1624
+ function shouldStopWhenPanelAgrees(
1625
+ profile: FusionProfile,
1626
+ outputs: readonly PanelOutput[],
1627
+ failures: readonly FailedPanelSummary[],
1628
+ ): boolean {
1629
+ return (
1630
+ profile.stopWhenPanelAgrees === true &&
1631
+ hasStrongPanelAgreement(
1632
+ outputs,
1633
+ outputs.length + failures.length,
1634
+ profile.panel.length,
1635
+ )
1636
+ );
1637
+ }
1638
+
1313
1639
  function storedPanelOutputs(run: FusionRun): readonly PanelOutput[] {
1314
1640
  return run.panelOutputs ?? [];
1315
1641
  }