@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.
- package/README.md +24 -11
- package/agents/fusion-composer.md +49 -0
- package/agents/fusion-judge.md +7 -0
- package/agents/fusion-panelist-full.md +53 -0
- package/agents/fusion-panelist-web.md +41 -0
- package/agents/fusion-panelist.md +2 -0
- package/docs/user-guide.md +208 -21
- package/package.json +1 -1
- package/skills/fusion-review/SKILL.md +69 -18
- package/src/commands.ts +1 -0
- package/src/config.ts +167 -5
- package/src/fusion-args.ts +36 -2
- package/src/index.ts +11 -2
- package/src/orchestrator.ts +146 -19
- package/src/panel-completion.ts +17 -6
- package/src/report.ts +233 -78
- package/src/result-extract.ts +15 -1
- package/src/run-builder.ts +298 -153
- package/src/run-store.ts +31 -0
- package/src/types.ts +89 -1
package/src/orchestrator.ts
CHANGED
|
@@ -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
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
...(
|
|
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
|
|
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
|
-
|
|
818
|
-
|
|
819
|
-
|
|
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
|
|
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
|
|
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
|
|
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);
|
package/src/panel-completion.ts
CHANGED
|
@@ -4,11 +4,12 @@ import {
|
|
|
4
4
|
buildJudgeSpawnParams,
|
|
5
5
|
type JudgeSpawnParams,
|
|
6
6
|
} from "./run-builder.js";
|
|
7
|
-
import
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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."
|