@alexeiled/pi-fusion 0.5.2 → 0.6.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.
@@ -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,
@@ -30,12 +32,14 @@ import {
30
32
  readSubagentResultArtifact,
31
33
  readSubagentStatusArtifact,
32
34
  } from "./subagent-artifacts.js";
33
- import type {
34
- FailedPanelSummary,
35
- FusionProfile,
36
- FusionRun,
37
- PanelOutput,
38
- ParsedFusionArgs,
35
+ import {
36
+ memberLabel,
37
+ resolveSynthesisMode,
38
+ type FailedPanelSummary,
39
+ type FusionProfile,
40
+ type FusionRun,
41
+ type PanelOutput,
42
+ type ParsedFusionArgs,
39
43
  } from "./types.js";
40
44
  import {
41
45
  extractRunObservation,
@@ -47,6 +51,7 @@ import type { SubagentsTargetParams } from "./subagents-rpc.js";
47
51
  export const SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
48
52
 
49
53
  const RECONCILE_INTERVAL_MS = 2_000;
54
+ const WORKFLOW_RESULT_ARTIFACT_GRACE_MS = 5_000;
50
55
 
51
56
  export type FusionNotifyType = "info" | "warning" | "error";
52
57
 
@@ -101,6 +106,7 @@ interface RunLifecycleSnapshot {
101
106
  statusPayload?: unknown;
102
107
  resultPayload?: unknown;
103
108
  resultIsTerminal: boolean;
109
+ resultArtifactPending?: boolean;
104
110
  }
105
111
 
106
112
  export class FusionOrchestrator {
@@ -150,9 +156,30 @@ export class FusionOrchestrator {
150
156
  }
151
157
 
152
158
  let resolved: ResolvedFusionProfile;
159
+ let baseProfileName: string | undefined;
153
160
  try {
154
161
  const config = await this.loadConfig(ctx);
155
162
  resolved = this.resolveProfile(config, args.profile);
163
+ baseProfileName = resolved.name;
164
+ if (args.panel?.length) {
165
+ // The named profile still supplies the judge and every other setting;
166
+ // only the panel is replaced. Inline models skip the alias pass that
167
+ // runs at config load, so re-run it over the assembled profile.
168
+ const inlineName = `${resolved.name} (inline panel)`;
169
+ const aliased = await applyClaudeAliasShorthand(
170
+ {
171
+ defaultProfile: inlineName,
172
+ profiles: {
173
+ [inlineName]: buildInlinePanelProfile(
174
+ resolved.profile,
175
+ args.panel,
176
+ ),
177
+ },
178
+ },
179
+ ctx,
180
+ );
181
+ resolved = this.resolveProfile(aliased, inlineName);
182
+ }
156
183
  this.configWarning = undefined;
157
184
  } catch (error: unknown) {
158
185
  const message = errorMessage(error);
@@ -166,6 +193,9 @@ export class FusionOrchestrator {
166
193
  run = this.runStore.startRun({
167
194
  prompt: args.prompt,
168
195
  profileName: resolved.name,
196
+ ...(args.panel?.length
197
+ ? { inlinePanel: args.panel, baseProfileName: baseProfileName }
198
+ : {}),
169
199
  ...(args.operationId !== undefined
170
200
  ? { operationId: args.operationId }
171
201
  : {}),
@@ -350,6 +380,12 @@ export class FusionOrchestrator {
350
380
  ? configuredJudgeModel(this.activeProfile)
351
381
  : undefined,
352
382
  ),
383
+ ...(this.activeProfile
384
+ ? {
385
+ synthesis: resolveSynthesisMode(this.activeProfile),
386
+ panel: this.activeProfile.panel,
387
+ }
388
+ : {}),
353
389
  });
354
390
  const cancelled = this.runStore.cancelRun(active.id, {
355
391
  ...(active.chainRunId ? { chainRunId: active.chainRunId } : {}),
@@ -381,10 +417,19 @@ export class FusionOrchestrator {
381
417
 
382
418
  try {
383
419
  const config = await this.loadConfig(ctx);
384
- this.activeProfile = this.resolveProfile(
420
+ // An inline run's profileName is a display name that no config defines,
421
+ // so rebuild it from the base profile plus the persisted entries. Looking
422
+ // the display name up throws, which used to leave activeProfile undefined
423
+ // and fail the run the moment its panel completed.
424
+ const base = this.resolveProfile(
385
425
  config,
386
- active.profileName,
426
+ active.inlinePanel?.length
427
+ ? active.baseProfileName
428
+ : active.profileName,
387
429
  ).profile;
430
+ this.activeProfile = active.inlinePanel?.length
431
+ ? buildInlinePanelProfile(base, active.inlinePanel)
432
+ : base;
388
433
  this.configWarning = undefined;
389
434
  } catch (error: unknown) {
390
435
  const message = `Could not restore fusion profile "${active.profileName}": ${errorMessage(error)}`;
@@ -513,6 +558,7 @@ export class FusionOrchestrator {
513
558
  ...(active.chainAsyncDir ? { asyncDir: active.chainAsyncDir } : {}),
514
559
  eventPayload: payload,
515
560
  });
561
+ if (snapshot.resultArtifactPending) return { status: "ignored" };
516
562
  const terminalPayload =
517
563
  snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
518
564
  if (
@@ -576,6 +622,8 @@ export class FusionOrchestrator {
576
622
  failures: storedPanelFailures(observed),
577
623
  ...withJudgeModel(configuredJudgeModel(profile)),
578
624
  judgeObservation,
625
+ ...(profile.blindPanelLabels ? { blindPanelLabels: true } : {}),
626
+ synthesis: resolveSynthesisMode(profile),
579
627
  });
580
628
  return this.completeActiveRun(report);
581
629
  }
@@ -610,8 +658,12 @@ export class FusionOrchestrator {
610
658
  : {}),
611
659
  eventPayload: payload,
612
660
  });
661
+ if (snapshot.resultArtifactPending) return { status: "ignored" };
613
662
 
614
- if (!active.panelStopReason) {
663
+ const panelIsTerminal =
664
+ snapshot.resultIsTerminal ||
665
+ isTerminalSubagentState(extractSubagentState(snapshot.statusPayload));
666
+ if (!active.panelStopReason && !panelIsTerminal) {
615
667
  const partial = extractPanelResults(snapshot.statusPayload, {
616
668
  panel: profile.panel,
617
669
  completedOnly: true,
@@ -644,12 +696,16 @@ export class FusionOrchestrator {
644
696
  return this.failActiveRun(lifecycleError);
645
697
  }
646
698
 
699
+ const workflowStoppedIndices = extractWorkflowStoppedPanelIndices(
700
+ lifecyclePayload,
701
+ );
702
+ const stoppedPanelIndices =
703
+ active.panelStoppedIndices ??
704
+ (workflowStoppedIndices.length > 0 ? workflowStoppedIndices : undefined);
647
705
  const extracted = extractPanelResults(lifecyclePayload, {
648
706
  panel: profile.panel,
649
707
  limit: profile.panel.length,
650
- ...(active.panelStoppedIndices
651
- ? { stoppedPanelIndices: active.panelStoppedIndices }
652
- : {}),
708
+ ...(stoppedPanelIndices ? { stoppedPanelIndices } : {}),
653
709
  });
654
710
  if (!extracted.ok) {
655
711
  return this.failActiveRun(
@@ -662,11 +718,18 @@ export class FusionOrchestrator {
662
718
  snapshot.statusPayload,
663
719
  profile,
664
720
  );
665
- const updated = this.storePanelResults(
721
+ const stored = this.storePanelResults(
666
722
  active.id,
667
723
  observedPanels.outputs,
668
724
  observedPanels.failures,
669
725
  );
726
+ const updated =
727
+ workflowStoppedIndices.length > 0 && !active.panelStopReason
728
+ ? this.runStore.updateRun(stored.id, {
729
+ panelStopReason: "agreement",
730
+ panelStoppedIndices: workflowStoppedIndices,
731
+ })
732
+ : stored;
670
733
 
671
734
  return this.finishPanelCompletion(
672
735
  updated,
@@ -795,6 +858,7 @@ export class FusionOrchestrator {
795
858
  ...(active.judgeAsyncDir ? { asyncDir: active.judgeAsyncDir } : {}),
796
859
  eventPayload: payload,
797
860
  });
861
+ if (snapshot.resultArtifactPending) return { status: "ignored" };
798
862
  const terminalPayload =
799
863
  snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
800
864
  if (
@@ -814,9 +878,17 @@ export class FusionOrchestrator {
814
878
  const output = extractJudgeOutput(lifecyclePayload);
815
879
  if (!output.ok) return this.failActiveRun(output.error);
816
880
 
817
- const judgeModel = this.activeProfile
818
- ? configuredJudgeModel(this.activeProfile)
819
- : undefined;
881
+ // The panel and chain handlers already treat a missing profile as fatal.
882
+ // Without the same guard here the run "succeeds" with a degraded report:
883
+ // blind labels are never restored, and the judge model is dropped.
884
+ const profile = this.activeProfile;
885
+ if (!profile) {
886
+ return this.failActiveRun(
887
+ "Fusion judge completed, but the active profile was not available.",
888
+ );
889
+ }
890
+
891
+ const judgeModel = configuredJudgeModel(profile);
820
892
  const judgeObservation = mergeRunObservations(
821
893
  extractRunObservation(
822
894
  findStepsArray(snapshot.statusPayload)[0] ?? snapshot.statusPayload,
@@ -837,6 +909,8 @@ export class FusionOrchestrator {
837
909
  failures: storedPanelFailures(observed),
838
910
  ...withJudgeModel(judgeModel),
839
911
  judgeObservation,
912
+ ...(profile.blindPanelLabels ? { blindPanelLabels: true } : {}),
913
+ synthesis: resolveSynthesisMode(profile),
840
914
  });
841
915
  return this.completeActiveRun(report);
842
916
  }
@@ -897,6 +971,17 @@ export class FusionOrchestrator {
897
971
  };
898
972
  }
899
973
 
974
+ if (
975
+ statusIsTerminal &&
976
+ isWorkflowResultArtifactPending(statusPayload)
977
+ ) {
978
+ return {
979
+ statusPayload,
980
+ resultIsTerminal: false,
981
+ resultArtifactPending: true,
982
+ };
983
+ }
984
+
900
985
  if (hasResultsArray(statusPayload)) {
901
986
  return {
902
987
  statusPayload,
@@ -989,6 +1074,12 @@ export class FusionOrchestrator {
989
1074
  ? configuredJudgeModel(this.activeProfile)
990
1075
  : undefined,
991
1076
  ),
1077
+ ...(this.activeProfile
1078
+ ? {
1079
+ synthesis: resolveSynthesisMode(this.activeProfile),
1080
+ panel: this.activeProfile.panel,
1081
+ }
1082
+ : {}),
992
1083
  });
993
1084
  }
994
1085
 
@@ -1269,7 +1360,7 @@ function buildFusionStatusDetails(
1269
1360
  const activity = describeStepActivity(step);
1270
1361
  const metrics = describeStepMetrics(step);
1271
1362
  return {
1272
- label: member.label,
1363
+ label: memberLabel(member),
1273
1364
  ...(member.role ? { role: member.role } : {}),
1274
1365
  ...(model ? { model } : {}),
1275
1366
  status: describePanelStatus(step),
@@ -1334,7 +1425,7 @@ function buildCompletedPanelStatusLines(
1334
1425
  );
1335
1426
  if (output) {
1336
1427
  return {
1337
- label: member.label,
1428
+ label: memberLabel(member),
1338
1429
  ...(member.role ? { role: member.role } : {}),
1339
1430
  ...(model ? { model } : {}),
1340
1431
  status: "completed",
@@ -1344,7 +1435,7 @@ function buildCompletedPanelStatusLines(
1344
1435
  (item) => item.id === member.id || item.index === index,
1345
1436
  );
1346
1437
  return {
1347
- label: member.label,
1438
+ label: memberLabel(member),
1348
1439
  ...(member.role ? { role: member.role } : {}),
1349
1440
  ...(model ? { model } : {}),
1350
1441
  status: failure ? "failed" : "unknown",
@@ -1547,10 +1638,46 @@ function isTerminalSubagentState(state: string | undefined): boolean {
1547
1638
  );
1548
1639
  }
1549
1640
 
1641
+ function isWorkflowResultArtifactPending(payload: unknown): boolean {
1642
+ if (!isRecord(payload)) return false;
1643
+ const details = isRecord(payload.details) ? payload.details : undefined;
1644
+ const mode = firstString(payload.mode, details?.mode);
1645
+ const endedAtValue = payload.endedAt ?? details?.endedAt;
1646
+ if (mode === "workflow" && typeof endedAtValue === "number") {
1647
+ return Date.now() - endedAtValue < WORKFLOW_RESULT_ARTIFACT_GRACE_MS;
1648
+ }
1649
+ return isRecord(payload.data)
1650
+ ? isWorkflowResultArtifactPending(payload.data)
1651
+ : false;
1652
+ }
1653
+
1550
1654
  function hasLifecycleResults(payload: unknown): boolean {
1551
1655
  return hasResultsArray(payload) || findStepsArray(payload).length > 0;
1552
1656
  }
1553
1657
 
1658
+ function extractWorkflowStoppedPanelIndices(payload: unknown): number[] {
1659
+ if (!isRecord(payload)) return [];
1660
+ const emits = isRecord(payload.workflow)
1661
+ ? unknownArray(payload.workflow.emits)
1662
+ : undefined;
1663
+ const indices = new Set<number>();
1664
+ for (const emit of emits ?? []) {
1665
+ if (!isRecord(emit) || emit.type !== "pi-fusion-panel-stop") continue;
1666
+ for (const index of unknownArray(emit.indices) ?? []) {
1667
+ if (typeof index === "number" && Number.isInteger(index) && index >= 0) {
1668
+ indices.add(index);
1669
+ }
1670
+ }
1671
+ }
1672
+ if (indices.size > 0) return [...indices].sort((left, right) => left - right);
1673
+ if (isRecord(payload.details)) {
1674
+ const nested = extractWorkflowStoppedPanelIndices(payload.details);
1675
+ if (nested.length > 0) return nested;
1676
+ }
1677
+ if (isRecord(payload.data)) return extractWorkflowStoppedPanelIndices(payload.data);
1678
+ return [];
1679
+ }
1680
+
1554
1681
  function extractSubagentFailure(payload: unknown): string | undefined {
1555
1682
  if (!isRecord(payload)) return undefined;
1556
1683
  const direct = firstNonBlankString(payload.error, payload.errorMessage);
@@ -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."