@alexeiled/pi-fusion 0.5.1 → 0.6.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.
@@ -1,4 +1,6 @@
1
+ import { applyClaudeAliasShorthand } from "./claude-aliases.js";
1
2
  import {
3
+ buildInlinePanelProfile,
2
4
  loadFusionConfig,
3
5
  resolveProfile as resolveFusionProfile,
4
6
  type ResolvedFusionProfile,
@@ -21,6 +23,7 @@ import {
21
23
  clearFusionUi,
22
24
  extractFusionProgressCounts,
23
25
  formatProgressCounts,
26
+ isTerminalFusionProgress,
24
27
  publishFusionStatus,
25
28
  type FusionProgressCounts,
26
29
  type FusionUi,
@@ -29,12 +32,14 @@ import {
29
32
  readSubagentResultArtifact,
30
33
  readSubagentStatusArtifact,
31
34
  } from "./subagent-artifacts.js";
32
- import type {
33
- FailedPanelSummary,
34
- FusionProfile,
35
- FusionRun,
36
- PanelOutput,
37
- ParsedFusionArgs,
35
+ import {
36
+ memberLabel,
37
+ resolveSynthesisMode,
38
+ type FailedPanelSummary,
39
+ type FusionProfile,
40
+ type FusionRun,
41
+ type PanelOutput,
42
+ type ParsedFusionArgs,
38
43
  } from "./types.js";
39
44
  import {
40
45
  extractRunObservation,
@@ -149,9 +154,30 @@ export class FusionOrchestrator {
149
154
  }
150
155
 
151
156
  let resolved: ResolvedFusionProfile;
157
+ let baseProfileName: string | undefined;
152
158
  try {
153
159
  const config = await this.loadConfig(ctx);
154
160
  resolved = this.resolveProfile(config, args.profile);
161
+ baseProfileName = resolved.name;
162
+ if (args.panel?.length) {
163
+ // The named profile still supplies the judge and every other setting;
164
+ // only the panel is replaced. Inline models skip the alias pass that
165
+ // runs at config load, so re-run it over the assembled profile.
166
+ const inlineName = `${resolved.name} (inline panel)`;
167
+ const aliased = await applyClaudeAliasShorthand(
168
+ {
169
+ defaultProfile: inlineName,
170
+ profiles: {
171
+ [inlineName]: buildInlinePanelProfile(
172
+ resolved.profile,
173
+ args.panel,
174
+ ),
175
+ },
176
+ },
177
+ ctx,
178
+ );
179
+ resolved = this.resolveProfile(aliased, inlineName);
180
+ }
155
181
  this.configWarning = undefined;
156
182
  } catch (error: unknown) {
157
183
  const message = errorMessage(error);
@@ -165,7 +191,12 @@ export class FusionOrchestrator {
165
191
  run = this.runStore.startRun({
166
192
  prompt: args.prompt,
167
193
  profileName: resolved.name,
168
- ...(args.operationId !== undefined ? { operationId: args.operationId } : {}),
194
+ ...(args.panel?.length
195
+ ? { inlinePanel: args.panel, baseProfileName: baseProfileName }
196
+ : {}),
197
+ ...(args.operationId !== undefined
198
+ ? { operationId: args.operationId }
199
+ : {}),
169
200
  phase: "panel",
170
201
  });
171
202
  } catch (error: unknown) {
@@ -188,6 +219,8 @@ export class FusionOrchestrator {
188
219
  const spawnResult = await this.rpc.spawn(
189
220
  buildPanelSpawnParams(resolved.profile, args.prompt),
190
221
  );
222
+ const spawnError = extractSubagentFailure(spawnResult);
223
+ if (spawnError) throw new FusionArgsError(spawnError);
191
224
  const panelRunId = extractSubagentRunId(spawnResult);
192
225
  if (!panelRunId) {
193
226
  throw new FusionArgsError(
@@ -345,6 +378,12 @@ export class FusionOrchestrator {
345
378
  ? configuredJudgeModel(this.activeProfile)
346
379
  : undefined,
347
380
  ),
381
+ ...(this.activeProfile
382
+ ? {
383
+ synthesis: resolveSynthesisMode(this.activeProfile),
384
+ panel: this.activeProfile.panel,
385
+ }
386
+ : {}),
348
387
  });
349
388
  const cancelled = this.runStore.cancelRun(active.id, {
350
389
  ...(active.chainRunId ? { chainRunId: active.chainRunId } : {}),
@@ -376,10 +415,19 @@ export class FusionOrchestrator {
376
415
 
377
416
  try {
378
417
  const config = await this.loadConfig(ctx);
379
- this.activeProfile = this.resolveProfile(
418
+ // An inline run's profileName is a display name that no config defines,
419
+ // so rebuild it from the base profile plus the persisted entries. Looking
420
+ // the display name up throws, which used to leave activeProfile undefined
421
+ // and fail the run the moment its panel completed.
422
+ const base = this.resolveProfile(
380
423
  config,
381
- active.profileName,
424
+ active.inlinePanel?.length
425
+ ? active.baseProfileName
426
+ : active.profileName,
382
427
  ).profile;
428
+ this.activeProfile = active.inlinePanel?.length
429
+ ? buildInlinePanelProfile(base, active.inlinePanel)
430
+ : base;
383
431
  this.configWarning = undefined;
384
432
  } catch (error: unknown) {
385
433
  const message = `Could not restore fusion profile "${active.profileName}": ${errorMessage(error)}`;
@@ -517,13 +565,17 @@ export class FusionOrchestrator {
517
565
  return { status: "ignored" };
518
566
  }
519
567
 
520
- const extracted = extractPanelResults(
521
- snapshot.resultPayload ?? snapshot.statusPayload ?? payload,
522
- {
523
- panel: profile.panel,
524
- limit: profile.panel.length,
525
- },
526
- );
568
+ const lifecyclePayload =
569
+ snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
570
+ const lifecycleError = extractSubagentFailure(lifecyclePayload);
571
+ if (!hasLifecycleResults(lifecyclePayload) && lifecycleError) {
572
+ return this.failActiveRun(lifecycleError);
573
+ }
574
+
575
+ const extracted = extractPanelResults(lifecyclePayload, {
576
+ panel: profile.panel,
577
+ limit: profile.panel.length,
578
+ });
527
579
  if (!extracted.ok) {
528
580
  return this.failActiveRun(
529
581
  `${extracted.error.message} (${extracted.error.path})`,
@@ -567,6 +619,8 @@ export class FusionOrchestrator {
567
619
  failures: storedPanelFailures(observed),
568
620
  ...withJudgeModel(configuredJudgeModel(profile)),
569
621
  judgeObservation,
622
+ ...(profile.blindPanelLabels ? { blindPanelLabels: true } : {}),
623
+ synthesis: resolveSynthesisMode(profile),
570
624
  });
571
625
  return this.completeActiveRun(report);
572
626
  }
@@ -628,16 +682,20 @@ export class FusionOrchestrator {
628
682
  return { status: "ignored" };
629
683
  }
630
684
 
631
- const extracted = extractPanelResults(
632
- snapshot.resultPayload ?? snapshot.statusPayload ?? payload,
633
- {
634
- panel: profile.panel,
635
- limit: profile.panel.length,
636
- ...(active.panelStoppedIndices
637
- ? { stoppedPanelIndices: active.panelStoppedIndices }
638
- : {}),
639
- },
640
- );
685
+ const lifecyclePayload =
686
+ snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
687
+ const lifecycleError = extractSubagentFailure(lifecyclePayload);
688
+ if (!hasLifecycleResults(lifecyclePayload) && lifecycleError) {
689
+ return this.failActiveRun(lifecycleError);
690
+ }
691
+
692
+ const extracted = extractPanelResults(lifecyclePayload, {
693
+ panel: profile.panel,
694
+ limit: profile.panel.length,
695
+ ...(active.panelStoppedIndices
696
+ ? { stoppedPanelIndices: active.panelStoppedIndices }
697
+ : {}),
698
+ });
641
699
  if (!extracted.ok) {
642
700
  return this.failActiveRun(
643
701
  `${extracted.error.message} (${extracted.error.path})`,
@@ -742,6 +800,8 @@ export class FusionOrchestrator {
742
800
 
743
801
  try {
744
802
  const spawnResult = await this.rpc.spawn(decision.params);
803
+ const spawnError = extractSubagentFailure(spawnResult);
804
+ if (spawnError) throw new FusionArgsError(spawnError);
745
805
  const judgeRunId = extractSubagentRunId(spawnResult);
746
806
  if (!judgeRunId) {
747
807
  throw new FusionArgsError(decision.missingRunIdError);
@@ -789,14 +849,27 @@ export class FusionOrchestrator {
789
849
  return { status: "ignored" };
790
850
  }
791
851
 
792
- const output = extractJudgeOutput(
793
- snapshot.resultPayload ?? snapshot.statusPayload ?? payload,
794
- );
852
+ const lifecyclePayload =
853
+ snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
854
+ const lifecycleError = extractSubagentFailure(lifecyclePayload);
855
+ if (!hasLifecycleResults(lifecyclePayload) && lifecycleError) {
856
+ return this.failActiveRun(lifecycleError);
857
+ }
858
+
859
+ const output = extractJudgeOutput(lifecyclePayload);
795
860
  if (!output.ok) return this.failActiveRun(output.error);
796
861
 
797
- const judgeModel = this.activeProfile
798
- ? configuredJudgeModel(this.activeProfile)
799
- : undefined;
862
+ // The panel and chain handlers already treat a missing profile as fatal.
863
+ // Without the same guard here the run "succeeds" with a degraded report:
864
+ // blind labels are never restored, and the judge model is dropped.
865
+ const profile = this.activeProfile;
866
+ if (!profile) {
867
+ return this.failActiveRun(
868
+ "Fusion judge completed, but the active profile was not available.",
869
+ );
870
+ }
871
+
872
+ const judgeModel = configuredJudgeModel(profile);
800
873
  const judgeObservation = mergeRunObservations(
801
874
  extractRunObservation(
802
875
  findStepsArray(snapshot.statusPayload)[0] ?? snapshot.statusPayload,
@@ -817,6 +890,8 @@ export class FusionOrchestrator {
817
890
  failures: storedPanelFailures(observed),
818
891
  ...withJudgeModel(judgeModel),
819
892
  judgeObservation,
893
+ ...(profile.blindPanelLabels ? { blindPanelLabels: true } : {}),
894
+ synthesis: resolveSynthesisMode(profile),
820
895
  });
821
896
  return this.completeActiveRun(report);
822
897
  }
@@ -850,12 +925,18 @@ export class FusionOrchestrator {
850
925
  );
851
926
  }
852
927
 
928
+ const statusIsTerminal =
929
+ isTerminalSubagentState(extractSubagentState(statusPayload)) ||
930
+ isTerminalFusionProgress(statusPayload);
931
+ const eventIsTerminal =
932
+ isTerminalSubagentState(extractSubagentState(input.eventPayload)) ||
933
+ isTerminalFusionProgress(input.eventPayload);
853
934
  const eventHasResults = hasResultsArray(input.eventPayload);
854
935
  if (eventPayloadMatches && eventHasResults) {
855
936
  return {
856
937
  statusPayload,
857
938
  resultPayload: input.eventPayload,
858
- resultIsTerminal: true,
939
+ resultIsTerminal: eventIsTerminal || statusIsTerminal,
859
940
  };
860
941
  }
861
942
 
@@ -872,17 +953,14 @@ export class FusionOrchestrator {
872
953
  }
873
954
 
874
955
  if (hasResultsArray(statusPayload)) {
875
- const resultIsTerminal =
876
- eventPayloadMatches ||
877
- isTerminalSubagentState(extractSubagentState(statusPayload));
878
956
  return {
879
957
  statusPayload,
880
958
  resultPayload: statusPayload,
881
- resultIsTerminal,
959
+ resultIsTerminal: statusIsTerminal,
882
960
  };
883
961
  }
884
962
 
885
- if (isTerminalSubagentState(extractSubagentState(statusPayload))) {
963
+ if (statusIsTerminal) {
886
964
  return {
887
965
  statusPayload,
888
966
  resultPayload: statusPayload,
@@ -894,9 +972,7 @@ export class FusionOrchestrator {
894
972
  return {
895
973
  statusPayload,
896
974
  resultPayload: input.eventPayload,
897
- resultIsTerminal: isTerminalSubagentState(
898
- extractSubagentState(input.eventPayload),
899
- ),
975
+ resultIsTerminal: eventIsTerminal,
900
976
  };
901
977
  }
902
978
 
@@ -968,6 +1044,12 @@ export class FusionOrchestrator {
968
1044
  ? configuredJudgeModel(this.activeProfile)
969
1045
  : undefined,
970
1046
  ),
1047
+ ...(this.activeProfile
1048
+ ? {
1049
+ synthesis: resolveSynthesisMode(this.activeProfile),
1050
+ panel: this.activeProfile.panel,
1051
+ }
1052
+ : {}),
971
1053
  });
972
1054
  }
973
1055
 
@@ -1248,7 +1330,7 @@ function buildFusionStatusDetails(
1248
1330
  const activity = describeStepActivity(step);
1249
1331
  const metrics = describeStepMetrics(step);
1250
1332
  return {
1251
- label: member.label,
1333
+ label: memberLabel(member),
1252
1334
  ...(member.role ? { role: member.role } : {}),
1253
1335
  ...(model ? { model } : {}),
1254
1336
  status: describePanelStatus(step),
@@ -1313,7 +1395,7 @@ function buildCompletedPanelStatusLines(
1313
1395
  );
1314
1396
  if (output) {
1315
1397
  return {
1316
- label: member.label,
1398
+ label: memberLabel(member),
1317
1399
  ...(member.role ? { role: member.role } : {}),
1318
1400
  ...(model ? { model } : {}),
1319
1401
  status: "completed",
@@ -1323,7 +1405,7 @@ function buildCompletedPanelStatusLines(
1323
1405
  (item) => item.id === member.id || item.index === index,
1324
1406
  );
1325
1407
  return {
1326
- label: member.label,
1408
+ label: memberLabel(member),
1327
1409
  ...(member.role ? { role: member.role } : {}),
1328
1410
  ...(model ? { model } : {}),
1329
1411
  status: failure ? "failed" : "unknown",
@@ -1526,6 +1608,38 @@ function isTerminalSubagentState(state: string | undefined): boolean {
1526
1608
  );
1527
1609
  }
1528
1610
 
1611
+ function hasLifecycleResults(payload: unknown): boolean {
1612
+ return hasResultsArray(payload) || findStepsArray(payload).length > 0;
1613
+ }
1614
+
1615
+ function extractSubagentFailure(payload: unknown): string | undefined {
1616
+ if (!isRecord(payload)) return undefined;
1617
+ const direct = firstNonBlankString(payload.error, payload.errorMessage);
1618
+ if (direct) return direct;
1619
+ if (isRecord(payload.details)) {
1620
+ const detailsError = firstNonBlankString(
1621
+ payload.details.error,
1622
+ payload.details.errorMessage,
1623
+ );
1624
+ if (detailsError) return detailsError;
1625
+ }
1626
+ if (payload.isError === true && Array.isArray(payload.content)) {
1627
+ for (const item of payload.content) {
1628
+ const contentText = isRecord(item)
1629
+ ? firstNonBlankString(item.text)
1630
+ : undefined;
1631
+ if (contentText) return contentText;
1632
+ }
1633
+ }
1634
+ const text = firstNonBlankString(
1635
+ payload.text,
1636
+ isRecord(payload.details) ? payload.details.text : undefined,
1637
+ );
1638
+ if (text && /^error(?:\s|:)/i.test(text)) return text;
1639
+ if (isRecord(payload.data)) return extractSubagentFailure(payload.data);
1640
+ return undefined;
1641
+ }
1642
+
1529
1643
  function firstNonBlankStringFromPayload(payload: unknown): string | undefined {
1530
1644
  if (!isRecord(payload)) return undefined;
1531
1645
  const direct = firstNonBlankString(
@@ -4,11 +4,12 @@ import {
4
4
  buildJudgeSpawnParams,
5
5
  type JudgeSpawnParams,
6
6
  } from "./run-builder.js";
7
- import type {
8
- FailedPanelSummary,
9
- FusionProfile,
10
- FusionRun,
11
- PanelOutput,
7
+ import {
8
+ resolveSynthesisMode,
9
+ type FailedPanelSummary,
10
+ type FusionProfile,
11
+ type FusionRun,
12
+ type PanelOutput,
12
13
  } from "./types.js";
13
14
 
14
15
  export type PanelCompletionDecision =
@@ -39,6 +40,8 @@ export function decidePanelCompletion(
39
40
  run: input.run,
40
41
  failures: input.panelFailures,
41
42
  ...withJudgeModel(judgeModel),
43
+ synthesis: resolveSynthesisMode(input.profile),
44
+ panel: input.profile.panel,
42
45
  });
43
46
  return {
44
47
  kind: "fail",
@@ -47,7 +50,14 @@ export function decidePanelCompletion(
47
50
  };
48
51
  }
49
52
 
50
- if (input.panelOutputs.length === 1) {
53
+ // Under `select` every panelist answered the whole question, so a lone
54
+ // survivor is a complete if thin answer. Under `merge` it answered ONE facet:
55
+ // returning it as the answer would be wrong, not thin. Run the composer so
56
+ // the report names the facets nobody covered.
57
+ if (
58
+ input.panelOutputs.length === 1 &&
59
+ resolveSynthesisMode(input.profile) !== "merge"
60
+ ) {
51
61
  const report = renderSinglePanelReport({
52
62
  run: input.run,
53
63
  output: input.panelOutputs[0]!,
@@ -64,6 +74,7 @@ export function decidePanelCompletion(
64
74
  prompt: input.run.prompt,
65
75
  panelOutputs: input.panelOutputs,
66
76
  failedPanelists: input.panelFailures,
77
+ runId: input.run.id,
67
78
  }),
68
79
  missingRunIdError: input.fallbackJudge
69
80
  ? "pi-subagents spawn did not return a fallback judge run ID."