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