@alexeiled/pi-fusion 0.6.2 → 0.7.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.
- package/README.md +7 -5
- package/agents/fusion-composer.md +3 -0
- package/agents/fusion-judge.md +3 -0
- package/agents/fusion-panelist.md +3 -0
- package/docs/user-guide.md +22 -4
- package/package.json +1 -1
- package/src/caller-contract.ts +80 -0
- package/src/config.ts +32 -2
- package/src/fusion-rpc.ts +59 -13
- package/src/lifecycle-reconcile.ts +248 -0
- package/src/orchestrator.ts +78 -61
- package/src/panel-completion.ts +57 -7
- package/src/report.ts +20 -0
- package/src/result-extract.ts +18 -3
- package/src/run-builder.ts +63 -13
- package/src/run-store.ts +17 -0
- package/src/types.ts +14 -0
package/src/orchestrator.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import { applyClaudeAliasShorthand } from "./claude-aliases.js";
|
|
2
|
+
import {
|
|
3
|
+
detectCallerOutputContract,
|
|
4
|
+
validateCallerOutput,
|
|
5
|
+
} from "./caller-contract.js";
|
|
2
6
|
import {
|
|
3
7
|
buildInlinePanelProfile,
|
|
4
8
|
loadFusionConfig,
|
|
@@ -17,6 +21,10 @@ import {
|
|
|
17
21
|
extractPanelResults,
|
|
18
22
|
type ExtractPanelResultsSuccess,
|
|
19
23
|
} from "./result-extract.js";
|
|
24
|
+
import {
|
|
25
|
+
reconcileIndexedLifecycleResult,
|
|
26
|
+
reconcilePanelResults,
|
|
27
|
+
} from "./lifecycle-reconcile.js";
|
|
20
28
|
import { appendThinkingSuffix, buildPanelSpawnParams } from "./run-builder.js";
|
|
21
29
|
import { FusionRunStore, FusionRunStoreError } from "./run-store.js";
|
|
22
30
|
import {
|
|
@@ -188,6 +196,8 @@ export class FusionOrchestrator {
|
|
|
188
196
|
return { status: "failed", error: message };
|
|
189
197
|
}
|
|
190
198
|
|
|
199
|
+
const outputContract =
|
|
200
|
+
args.outputContract ?? detectCallerOutputContract(args.prompt);
|
|
191
201
|
let run: FusionRun;
|
|
192
202
|
try {
|
|
193
203
|
run = this.runStore.startRun({
|
|
@@ -199,6 +209,7 @@ export class FusionOrchestrator {
|
|
|
199
209
|
...(args.operationId !== undefined
|
|
200
210
|
? { operationId: args.operationId }
|
|
201
211
|
: {}),
|
|
212
|
+
...(outputContract ? { outputContract } : {}),
|
|
202
213
|
phase: "panel",
|
|
203
214
|
});
|
|
204
215
|
} catch (error: unknown) {
|
|
@@ -219,7 +230,7 @@ export class FusionOrchestrator {
|
|
|
219
230
|
|
|
220
231
|
try {
|
|
221
232
|
const spawnResult = await this.rpc.spawn(
|
|
222
|
-
buildPanelSpawnParams(resolved.profile, args.prompt),
|
|
233
|
+
buildPanelSpawnParams(resolved.profile, args.prompt, outputContract),
|
|
223
234
|
);
|
|
224
235
|
const spawnError = extractSubagentFailure(spawnResult);
|
|
225
236
|
if (spawnError) throw new FusionArgsError(spawnError);
|
|
@@ -585,19 +596,37 @@ export class FusionOrchestrator {
|
|
|
585
596
|
);
|
|
586
597
|
}
|
|
587
598
|
|
|
588
|
-
const observedPanels =
|
|
599
|
+
const observedPanels = reconcilePanelResults(
|
|
589
600
|
extracted,
|
|
590
601
|
snapshot.statusPayload,
|
|
591
602
|
profile,
|
|
603
|
+
lifecyclePayload,
|
|
604
|
+
{ allowedTrailingResults: 1 },
|
|
592
605
|
);
|
|
606
|
+
if (!observedPanels.ok) {
|
|
607
|
+
return this.failActiveRun(
|
|
608
|
+
`${observedPanels.error.message} (${observedPanels.error.path})`,
|
|
609
|
+
);
|
|
610
|
+
}
|
|
593
611
|
const updated = this.storePanelResults(
|
|
594
612
|
active.id,
|
|
595
613
|
observedPanels.outputs,
|
|
596
614
|
observedPanels.failures,
|
|
597
615
|
);
|
|
598
616
|
|
|
599
|
-
|
|
600
|
-
|
|
617
|
+
const judgePayload =
|
|
618
|
+
hasJudgeResult(snapshot.resultPayload, profile.panel.length)
|
|
619
|
+
? snapshot.resultPayload
|
|
620
|
+
: snapshot.statusPayload;
|
|
621
|
+
if (hasJudgeResult(judgePayload, profile.panel.length)) {
|
|
622
|
+
const lifecycleConflict = reconcileIndexedLifecycleResult(
|
|
623
|
+
snapshot.resultPayload,
|
|
624
|
+
snapshot.statusPayload,
|
|
625
|
+
profile.panel.length,
|
|
626
|
+
"judge",
|
|
627
|
+
);
|
|
628
|
+
if (lifecycleConflict) return this.failActiveRun(lifecycleConflict);
|
|
629
|
+
const output = extractJudgeOutput(judgePayload, {
|
|
601
630
|
resultIndex: profile.panel.length,
|
|
602
631
|
});
|
|
603
632
|
if (!output.ok) return this.failActiveRun(output.error);
|
|
@@ -608,13 +637,18 @@ export class FusionOrchestrator {
|
|
|
608
637
|
snapshot.statusPayload,
|
|
609
638
|
),
|
|
610
639
|
extractRunObservation(
|
|
611
|
-
findResult(
|
|
612
|
-
snapshot.resultPayload,
|
|
640
|
+
findResult(judgePayload, profile.panel.length) ?? judgePayload,
|
|
613
641
|
),
|
|
614
642
|
);
|
|
615
643
|
const observed = this.runStore.updateRun(updated.id, {
|
|
616
644
|
judgeObservation,
|
|
617
645
|
});
|
|
646
|
+
const callerContract =
|
|
647
|
+
observed.outputContract ?? detectCallerOutputContract(observed.prompt);
|
|
648
|
+
if (callerContract) {
|
|
649
|
+
const validation = validateCallerOutput(callerContract, output.output);
|
|
650
|
+
if (!validation.ok) return this.failActiveRun(validation.error);
|
|
651
|
+
}
|
|
618
652
|
const report = renderJudgeReport({
|
|
619
653
|
run: observed,
|
|
620
654
|
judgeOutput: output.output,
|
|
@@ -713,11 +747,18 @@ export class FusionOrchestrator {
|
|
|
713
747
|
);
|
|
714
748
|
}
|
|
715
749
|
|
|
716
|
-
const observedPanels =
|
|
750
|
+
const observedPanels = reconcilePanelResults(
|
|
717
751
|
extracted,
|
|
718
752
|
snapshot.statusPayload,
|
|
719
753
|
profile,
|
|
754
|
+
lifecyclePayload,
|
|
755
|
+
{ ...(stoppedPanelIndices ? { stoppedPanelIndices } : {}) },
|
|
720
756
|
);
|
|
757
|
+
if (!observedPanels.ok) {
|
|
758
|
+
return this.failActiveRun(
|
|
759
|
+
`${observedPanels.error.message} (${observedPanels.error.path})`,
|
|
760
|
+
);
|
|
761
|
+
}
|
|
721
762
|
const stored = this.storePanelResults(
|
|
722
763
|
active.id,
|
|
723
764
|
observedPanels.outputs,
|
|
@@ -875,8 +916,25 @@ export class FusionOrchestrator {
|
|
|
875
916
|
return this.failActiveRun(lifecycleError);
|
|
876
917
|
}
|
|
877
918
|
|
|
919
|
+
const lifecycleConflict = reconcileIndexedLifecycleResult(
|
|
920
|
+
snapshot.resultPayload,
|
|
921
|
+
snapshot.statusPayload,
|
|
922
|
+
0,
|
|
923
|
+
"judge",
|
|
924
|
+
);
|
|
925
|
+
if (lifecycleConflict) return this.failActiveRun(lifecycleConflict);
|
|
926
|
+
|
|
878
927
|
const output = extractJudgeOutput(lifecyclePayload);
|
|
879
|
-
if (!output.ok)
|
|
928
|
+
if (!output.ok) {
|
|
929
|
+
const stepError = extractSubagentFailure(
|
|
930
|
+
findStepsArray(snapshot.statusPayload)[0],
|
|
931
|
+
);
|
|
932
|
+
const errors = [lifecycleError, stepError, output.error].filter(
|
|
933
|
+
(error, index, all): error is string =>
|
|
934
|
+
Boolean(error) && all.indexOf(error) === index,
|
|
935
|
+
);
|
|
936
|
+
return this.failActiveRun(errors.join(" "));
|
|
937
|
+
}
|
|
880
938
|
|
|
881
939
|
// The panel and chain handlers already treat a missing profile as fatal.
|
|
882
940
|
// Without the same guard here the run "succeeds" with a degraded report:
|
|
@@ -888,6 +946,13 @@ export class FusionOrchestrator {
|
|
|
888
946
|
);
|
|
889
947
|
}
|
|
890
948
|
|
|
949
|
+
const callerContract =
|
|
950
|
+
active.outputContract ?? detectCallerOutputContract(active.prompt);
|
|
951
|
+
if (callerContract) {
|
|
952
|
+
const validation = validateCallerOutput(callerContract, output.output);
|
|
953
|
+
if (!validation.ok) return this.failActiveRun(validation.error);
|
|
954
|
+
}
|
|
955
|
+
|
|
891
956
|
const judgeModel = configuredJudgeModel(profile);
|
|
892
957
|
const judgeObservation = mergeRunObservations(
|
|
893
958
|
extractRunObservation(
|
|
@@ -1575,15 +1640,17 @@ function hasResultsArray(payload: unknown): boolean {
|
|
|
1575
1640
|
}
|
|
1576
1641
|
|
|
1577
1642
|
function hasJudgeResult(payload: unknown, panelCount: number): boolean {
|
|
1578
|
-
|
|
1643
|
+
const count =
|
|
1644
|
+
findResultsArray(payload)?.length ?? findStepsArray(payload).length;
|
|
1645
|
+
return count > panelCount;
|
|
1579
1646
|
}
|
|
1580
1647
|
|
|
1581
1648
|
function findResult(
|
|
1582
1649
|
payload: unknown,
|
|
1583
1650
|
resultIndex?: number,
|
|
1584
1651
|
): Record<string, unknown> | undefined {
|
|
1585
|
-
const results = findResultsArray(payload);
|
|
1586
|
-
if (
|
|
1652
|
+
const results = findResultsArray(payload) ?? findStepsArray(payload);
|
|
1653
|
+
if (results.length === 0) return undefined;
|
|
1587
1654
|
if (resultIndex !== undefined) {
|
|
1588
1655
|
const indexed = results[resultIndex];
|
|
1589
1656
|
return isRecord(indexed) ? indexed : undefined;
|
|
@@ -1752,56 +1819,6 @@ function extractArtifactPath(
|
|
|
1752
1819
|
return undefined;
|
|
1753
1820
|
}
|
|
1754
1821
|
|
|
1755
|
-
function mergePanelObservations(
|
|
1756
|
-
result: ExtractPanelResultsSuccess,
|
|
1757
|
-
statusPayload: unknown,
|
|
1758
|
-
profile: FusionProfile,
|
|
1759
|
-
): ExtractPanelResultsSuccess {
|
|
1760
|
-
const status = extractPanelResults(statusPayload, {
|
|
1761
|
-
panel: profile.panel,
|
|
1762
|
-
completedOnly: true,
|
|
1763
|
-
limit: profile.panel.length,
|
|
1764
|
-
});
|
|
1765
|
-
if (!status.ok) return result;
|
|
1766
|
-
|
|
1767
|
-
const observations = new Map<number, PanelOutput["observation"]>();
|
|
1768
|
-
for (const output of status.outputs) {
|
|
1769
|
-
observations.set(output.index, output.observation);
|
|
1770
|
-
}
|
|
1771
|
-
for (const failure of status.failures) {
|
|
1772
|
-
observations.set(failure.index, failure.observation);
|
|
1773
|
-
}
|
|
1774
|
-
|
|
1775
|
-
return {
|
|
1776
|
-
...result,
|
|
1777
|
-
outputs: result.outputs.map((output) =>
|
|
1778
|
-
withMergedObservation(output, observations.get(output.index)),
|
|
1779
|
-
),
|
|
1780
|
-
failures: result.failures.map((failure) =>
|
|
1781
|
-
withMergedObservation(failure, observations.get(failure.index)),
|
|
1782
|
-
),
|
|
1783
|
-
};
|
|
1784
|
-
}
|
|
1785
|
-
|
|
1786
|
-
function withMergedObservation<T extends PanelOutput | FailedPanelSummary>(
|
|
1787
|
-
item: T,
|
|
1788
|
-
statusObservation: PanelOutput["observation"] | undefined,
|
|
1789
|
-
): T {
|
|
1790
|
-
const observation = mergeRunObservations(statusObservation, item.observation);
|
|
1791
|
-
return hasObservationData(observation) ? { ...item, observation } : item;
|
|
1792
|
-
}
|
|
1793
|
-
|
|
1794
|
-
function hasObservationData(observation: PanelOutput["observation"]): boolean {
|
|
1795
|
-
return Boolean(
|
|
1796
|
-
observation &&
|
|
1797
|
-
(observation.model ||
|
|
1798
|
-
observation.durationMs !== undefined ||
|
|
1799
|
-
observation.usage ||
|
|
1800
|
-
observation.attempts ||
|
|
1801
|
-
observation.providerFailures),
|
|
1802
|
-
);
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
1822
|
function shouldStopWhenPanelAgrees(
|
|
1806
1823
|
profile: FusionProfile,
|
|
1807
1824
|
outputs: readonly PanelOutput[],
|
package/src/panel-completion.ts
CHANGED
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
detectCallerOutputContract,
|
|
3
|
+
validateCallerOutput,
|
|
4
|
+
} from "./caller-contract.js";
|
|
5
|
+
import {
|
|
6
|
+
renderFailureReport,
|
|
7
|
+
renderPanelFailureReport,
|
|
8
|
+
renderSinglePanelReport,
|
|
9
|
+
} from "./report.js";
|
|
2
10
|
import {
|
|
3
11
|
appendThinkingSuffix,
|
|
4
12
|
buildJudgeSpawnParams,
|
|
@@ -50,14 +58,53 @@ export function decidePanelCompletion(
|
|
|
50
58
|
};
|
|
51
59
|
}
|
|
52
60
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
61
|
+
const intentionalStops =
|
|
62
|
+
input.profile.stopWhenPanelAgrees === true &&
|
|
63
|
+
input.panelOutputs.length >= 2 &&
|
|
64
|
+
input.panelFailures.length > 0 &&
|
|
65
|
+
input.panelFailures.every(
|
|
66
|
+
({ reason }) => reason === "stopped-after-agreement",
|
|
67
|
+
);
|
|
57
68
|
if (
|
|
58
|
-
input.panelOutputs.length
|
|
59
|
-
|
|
69
|
+
input.panelOutputs.length < input.profile.panel.length &&
|
|
70
|
+
!intentionalStops
|
|
60
71
|
) {
|
|
72
|
+
const error = `Only ${input.panelOutputs.length} of ${input.profile.panel.length} fusion panelists completed successfully; ${input.panelFailures.length} panelist result(s) are also missing.`;
|
|
73
|
+
const report = renderFailureReport({
|
|
74
|
+
run: input.run,
|
|
75
|
+
error,
|
|
76
|
+
panelOutputs: input.panelOutputs,
|
|
77
|
+
failures: input.panelFailures,
|
|
78
|
+
...withJudgeModel(judgeModel),
|
|
79
|
+
synthesis: resolveSynthesisMode(input.profile),
|
|
80
|
+
panel: input.profile.panel,
|
|
81
|
+
});
|
|
82
|
+
return { kind: "fail", error, report };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (input.panelOutputs.length === 1) {
|
|
86
|
+
const callerContract =
|
|
87
|
+
input.run.outputContract ??
|
|
88
|
+
detectCallerOutputContract(input.run.prompt);
|
|
89
|
+
if (callerContract) {
|
|
90
|
+
const validation = validateCallerOutput(
|
|
91
|
+
callerContract,
|
|
92
|
+
input.panelOutputs[0]!.output,
|
|
93
|
+
);
|
|
94
|
+
if (!validation.ok) {
|
|
95
|
+
const report = renderFailureReport({
|
|
96
|
+
run: input.run,
|
|
97
|
+
error: validation.error,
|
|
98
|
+
panelOutputs: input.panelOutputs,
|
|
99
|
+
failures: input.panelFailures,
|
|
100
|
+
...withJudgeModel(judgeModel),
|
|
101
|
+
synthesis: resolveSynthesisMode(input.profile),
|
|
102
|
+
panel: input.profile.panel,
|
|
103
|
+
});
|
|
104
|
+
return { kind: "fail", error: validation.error, report };
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
61
108
|
const report = renderSinglePanelReport({
|
|
62
109
|
run: input.run,
|
|
63
110
|
output: input.panelOutputs[0]!,
|
|
@@ -75,6 +122,9 @@ export function decidePanelCompletion(
|
|
|
75
122
|
panelOutputs: input.panelOutputs,
|
|
76
123
|
failedPanelists: input.panelFailures,
|
|
77
124
|
runId: input.run.id,
|
|
125
|
+
...(input.run.outputContract
|
|
126
|
+
? { callerContract: input.run.outputContract }
|
|
127
|
+
: {}),
|
|
78
128
|
}),
|
|
79
129
|
missingRunIdError: input.fallbackJudge
|
|
80
130
|
? "pi-subagents spawn did not return a fallback judge run ID."
|
package/src/report.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
detectCallerOutputContract,
|
|
3
|
+
validateCallerOutput,
|
|
4
|
+
} from "./caller-contract.js";
|
|
1
5
|
import {
|
|
2
6
|
buildBlindLabelMap,
|
|
3
7
|
type FailedPanelSummary,
|
|
@@ -23,6 +27,7 @@ type ReportRun = Pick<
|
|
|
23
27
|
| "panelRunId"
|
|
24
28
|
| "judgeRunId"
|
|
25
29
|
| "panelStopReason"
|
|
30
|
+
| "outputContract"
|
|
26
31
|
> &
|
|
27
32
|
Partial<Pick<FusionRun, "phase" | "createdAt" | "updatedAt">>;
|
|
28
33
|
|
|
@@ -227,6 +232,9 @@ export function renderPanelFailureReport(
|
|
|
227
232
|
export function renderSinglePanelReport(
|
|
228
233
|
input: RenderSinglePanelReportInput,
|
|
229
234
|
): string {
|
|
235
|
+
const exactOutput = validExactCallerOutput(input.run, input.output.output);
|
|
236
|
+
if (exactOutput) return exactOutput;
|
|
237
|
+
|
|
230
238
|
const panelName = formatPanelName(input.output);
|
|
231
239
|
const sections: ReportSection[] = [
|
|
232
240
|
{
|
|
@@ -291,6 +299,15 @@ export function renderSinglePanelReport(
|
|
|
291
299
|
* Rewrites the neutral names the judge saw back to the configured member names.
|
|
292
300
|
* Longest label first so "Candidate AA" is not partly replaced by "Candidate A".
|
|
293
301
|
*/
|
|
302
|
+
function validExactCallerOutput(
|
|
303
|
+
run: ReportRun,
|
|
304
|
+
output: string,
|
|
305
|
+
): string | undefined {
|
|
306
|
+
const contract = run.outputContract ?? detectCallerOutputContract(run.prompt);
|
|
307
|
+
if (!contract) return undefined;
|
|
308
|
+
return validateCallerOutput(contract, output).ok ? output.trim() : undefined;
|
|
309
|
+
}
|
|
310
|
+
|
|
294
311
|
function restoreBlindLabels(
|
|
295
312
|
judgeOutput: string,
|
|
296
313
|
panelOutputs: readonly PanelOutput[],
|
|
@@ -317,6 +334,9 @@ function restoreBlindLabels(
|
|
|
317
334
|
export function renderJudgeReport(input: RenderJudgeReportInput): string {
|
|
318
335
|
const panelOutputs = input.panelOutputs ?? [];
|
|
319
336
|
const failures = input.failures ?? [];
|
|
337
|
+
const exactOutput = validExactCallerOutput(input.run, input.judgeOutput);
|
|
338
|
+
if (exactOutput) return exactOutput;
|
|
339
|
+
|
|
320
340
|
const judgeOutput = input.blindPanelLabels
|
|
321
341
|
? restoreBlindLabels(input.judgeOutput, panelOutputs, failures)
|
|
322
342
|
: input.judgeOutput;
|
package/src/result-extract.ts
CHANGED
|
@@ -63,13 +63,19 @@ export function extractPanelResults(
|
|
|
63
63
|
|
|
64
64
|
const outputs: PanelOutput[] = [];
|
|
65
65
|
const failures: FailedPanelSummary[] = [];
|
|
66
|
+
const fallbackFailureReason = failureReason(container.payload);
|
|
66
67
|
const results =
|
|
67
68
|
options.limit === undefined
|
|
68
69
|
? container.results
|
|
69
70
|
: container.results.slice(0, options.limit);
|
|
70
71
|
for (const [index, rawResult] of results.entries()) {
|
|
71
72
|
if (options.completedOnly && !isCompletedResult(rawResult)) continue;
|
|
72
|
-
const child = normalizeChildResult(
|
|
73
|
+
const child = normalizeChildResult(
|
|
74
|
+
rawResult,
|
|
75
|
+
index,
|
|
76
|
+
options,
|
|
77
|
+
fallbackFailureReason,
|
|
78
|
+
);
|
|
73
79
|
if (!child.ok) return child;
|
|
74
80
|
if (child.status === "success") outputs.push(child.output);
|
|
75
81
|
else failures.push(child.failure);
|
|
@@ -84,6 +90,7 @@ export function extractPanelResults(
|
|
|
84
90
|
},
|
|
85
91
|
index,
|
|
86
92
|
options,
|
|
93
|
+
fallbackFailureReason,
|
|
87
94
|
);
|
|
88
95
|
if (!child.ok) return child;
|
|
89
96
|
if (child.status === "failed") failures.push(child.failure);
|
|
@@ -197,6 +204,7 @@ function normalizeChildResult(
|
|
|
197
204
|
rawResult: unknown,
|
|
198
205
|
index: number,
|
|
199
206
|
options: ExtractPanelResultsOptions,
|
|
207
|
+
fallbackFailureReason?: FailedPanelSummary["reason"],
|
|
200
208
|
):
|
|
201
209
|
| { ok: true; status: "success"; output: PanelOutput }
|
|
202
210
|
| { ok: true; status: "failed"; failure: FailedPanelSummary }
|
|
@@ -275,7 +283,9 @@ function normalizeChildResult(
|
|
|
275
283
|
summary: stoppedAfterAgreement
|
|
276
284
|
? "Stopped after strong panel agreement."
|
|
277
285
|
: failureSummary(rawResult, artifactPath),
|
|
278
|
-
reason:
|
|
286
|
+
reason:
|
|
287
|
+
failureReason(rawResult, stoppedAfterAgreement) ??
|
|
288
|
+
fallbackFailureReason,
|
|
279
289
|
observation,
|
|
280
290
|
artifactPath,
|
|
281
291
|
sessionPath,
|
|
@@ -392,7 +402,12 @@ function failureReason(
|
|
|
392
402
|
stoppedAfterAgreement = false,
|
|
393
403
|
): FailedPanelSummary["reason"] {
|
|
394
404
|
if (stoppedAfterAgreement) return "stopped-after-agreement";
|
|
395
|
-
if (
|
|
405
|
+
if (
|
|
406
|
+
result.timedOut === true ||
|
|
407
|
+
/(?:timed out|timeout)/i.test(firstNonBlankString(result.error) ?? "")
|
|
408
|
+
) {
|
|
409
|
+
return "timeout";
|
|
410
|
+
}
|
|
396
411
|
if (result.interrupted === true) return "interrupted";
|
|
397
412
|
return undefined;
|
|
398
413
|
}
|
package/src/run-builder.ts
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
callerOutputContractInstructions,
|
|
3
|
+
detectCallerOutputContract,
|
|
4
|
+
} from "./caller-contract.js";
|
|
1
5
|
import {
|
|
2
6
|
PANEL_DECISION_CLOSE,
|
|
3
7
|
PANEL_DECISION_OPEN,
|
|
@@ -8,6 +12,7 @@ import {
|
|
|
8
12
|
memberLabel,
|
|
9
13
|
panelItemLabel,
|
|
10
14
|
resolveSynthesisMode,
|
|
15
|
+
type CallerOutputContract,
|
|
11
16
|
THINKING_LEVELS,
|
|
12
17
|
type FailedPanelSummary,
|
|
13
18
|
type FusionProfile,
|
|
@@ -25,6 +30,13 @@ export const FUSION_ACCEPTANCE_DISABLED = {
|
|
|
25
30
|
|
|
26
31
|
export type FusionAcceptanceDisabled = typeof FUSION_ACCEPTANCE_DISABLED;
|
|
27
32
|
|
|
33
|
+
const DEFAULT_STAGE_TIMEOUT_MS = 900_000;
|
|
34
|
+
const DEFAULT_TOOL_BUDGET: ToolBudget = {
|
|
35
|
+
soft: 8,
|
|
36
|
+
hard: 12,
|
|
37
|
+
block: "*",
|
|
38
|
+
};
|
|
39
|
+
|
|
28
40
|
export interface PanelSubagentTaskParams {
|
|
29
41
|
agent: string;
|
|
30
42
|
task: string;
|
|
@@ -34,6 +46,8 @@ export interface PanelSubagentTaskParams {
|
|
|
34
46
|
skill: false;
|
|
35
47
|
acceptance: FusionAcceptanceDisabled;
|
|
36
48
|
model?: string;
|
|
49
|
+
/** Per-task cap so a panelist finalises before the workflow timeout. */
|
|
50
|
+
toolBudget: ToolBudget;
|
|
37
51
|
}
|
|
38
52
|
|
|
39
53
|
export interface PanelWorkflowTaskParams extends PanelSubagentTaskParams {
|
|
@@ -85,6 +99,7 @@ export interface BuildJudgeSpawnParamsInput {
|
|
|
85
99
|
* and its position bias.
|
|
86
100
|
*/
|
|
87
101
|
runId: string;
|
|
102
|
+
callerContract?: CallerOutputContract;
|
|
88
103
|
}
|
|
89
104
|
|
|
90
105
|
const PANEL_OUTPUT_CONTRACT = [
|
|
@@ -152,6 +167,7 @@ export function appendThinkingSuffix(
|
|
|
152
167
|
export function buildPanelSpawnParams(
|
|
153
168
|
profile: FusionProfile,
|
|
154
169
|
prompt: string,
|
|
170
|
+
callerContract?: CallerOutputContract,
|
|
155
171
|
): PanelSpawnParams {
|
|
156
172
|
const concurrency = profile.concurrency ?? profile.panel.length;
|
|
157
173
|
const tasks: PanelWorkflowTaskParams[] = profile.panel.map(
|
|
@@ -161,6 +177,8 @@ export function buildPanelSpawnParams(
|
|
|
161
177
|
member,
|
|
162
178
|
prompt,
|
|
163
179
|
profile.stopWhenPanelAgrees === true,
|
|
180
|
+
profile.panelToolBudget ?? DEFAULT_TOOL_BUDGET,
|
|
181
|
+
callerContract,
|
|
164
182
|
),
|
|
165
183
|
}),
|
|
166
184
|
);
|
|
@@ -176,9 +194,7 @@ export function buildPanelSpawnParams(
|
|
|
176
194
|
output: true,
|
|
177
195
|
outputMode: "inline",
|
|
178
196
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
179
|
-
|
|
180
|
-
? { timeoutMs: profile.timeoutMs }
|
|
181
|
-
: {}),
|
|
197
|
+
timeoutMs: resolveStageTimeout(profile.panelTimeoutMs, profile.timeoutMs),
|
|
182
198
|
};
|
|
183
199
|
}
|
|
184
200
|
|
|
@@ -197,9 +213,7 @@ export function buildJudgeSpawnParams(
|
|
|
197
213
|
skill: false,
|
|
198
214
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
199
215
|
...(model ? { model } : {}),
|
|
200
|
-
|
|
201
|
-
? { toolBudget: input.profile.judgeToolBudget }
|
|
202
|
-
: {}),
|
|
216
|
+
toolBudget: input.profile.judgeToolBudget ?? DEFAULT_TOOL_BUDGET,
|
|
203
217
|
};
|
|
204
218
|
|
|
205
219
|
return {
|
|
@@ -209,9 +223,10 @@ export function buildJudgeSpawnParams(
|
|
|
209
223
|
output: true,
|
|
210
224
|
outputMode: "inline",
|
|
211
225
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
226
|
+
timeoutMs: resolveStageTimeout(
|
|
227
|
+
input.profile.judgeTimeoutMs,
|
|
228
|
+
input.profile.timeoutMs,
|
|
229
|
+
),
|
|
215
230
|
};
|
|
216
231
|
}
|
|
217
232
|
|
|
@@ -260,20 +275,35 @@ function buildPanelWorkflowScript(
|
|
|
260
275
|
.join("\n");
|
|
261
276
|
}
|
|
262
277
|
|
|
278
|
+
function resolveStageTimeout(
|
|
279
|
+
stageTimeoutMs: number | undefined,
|
|
280
|
+
legacyTimeoutMs: number | undefined,
|
|
281
|
+
): number {
|
|
282
|
+
return stageTimeoutMs ?? legacyTimeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
|
|
283
|
+
}
|
|
284
|
+
|
|
263
285
|
function buildPanelTaskParams(
|
|
264
286
|
member: PanelMemberConfig,
|
|
265
287
|
prompt: string,
|
|
266
288
|
includeDecisionRecord: boolean,
|
|
289
|
+
toolBudget: ToolBudget,
|
|
290
|
+
callerContract?: CallerOutputContract,
|
|
267
291
|
): PanelSubagentTaskParams {
|
|
268
292
|
const model = appendThinkingSuffix(member.model, member.thinking);
|
|
269
293
|
return {
|
|
270
294
|
agent: member.agent,
|
|
271
|
-
task: buildPanelTask(
|
|
295
|
+
task: buildPanelTask(
|
|
296
|
+
member,
|
|
297
|
+
prompt,
|
|
298
|
+
includeDecisionRecord,
|
|
299
|
+
callerContract,
|
|
300
|
+
),
|
|
272
301
|
output: true,
|
|
273
302
|
outputMode: "inline",
|
|
274
303
|
progress: true,
|
|
275
304
|
skill: false,
|
|
276
305
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
306
|
+
toolBudget,
|
|
277
307
|
...(model ? { model } : {}),
|
|
278
308
|
};
|
|
279
309
|
}
|
|
@@ -282,8 +312,11 @@ function buildPanelTask(
|
|
|
282
312
|
member: PanelMemberConfig,
|
|
283
313
|
prompt: string,
|
|
284
314
|
includeDecisionRecord: boolean,
|
|
315
|
+
callerContractOverride?: CallerOutputContract,
|
|
285
316
|
): string {
|
|
286
317
|
const role = member.role?.trim() || "independent analysis and critique";
|
|
318
|
+
const callerContract =
|
|
319
|
+
callerContractForPrompt(prompt, callerContractOverride);
|
|
287
320
|
return [
|
|
288
321
|
`Panel member: ${memberLabel(member)} (${member.id})`,
|
|
289
322
|
`Role: ${role}`,
|
|
@@ -299,8 +332,10 @@ function buildPanelTask(
|
|
|
299
332
|
"- Be concise and cite evidence when you inspect files.",
|
|
300
333
|
"",
|
|
301
334
|
"Output contract:",
|
|
302
|
-
...
|
|
303
|
-
|
|
335
|
+
...(callerContract
|
|
336
|
+
? callerOutputContractInstructions(callerContract)
|
|
337
|
+
: PANEL_OUTPUT_CONTRACT),
|
|
338
|
+
...(includeDecisionRecord && !callerContract
|
|
304
339
|
? [
|
|
305
340
|
"",
|
|
306
341
|
"Decision record:",
|
|
@@ -356,6 +391,13 @@ function formatMemberTask(member: PanelMemberConfig, prompt: string): string[] {
|
|
|
356
391
|
];
|
|
357
392
|
}
|
|
358
393
|
|
|
394
|
+
function callerContractForPrompt(
|
|
395
|
+
prompt: string,
|
|
396
|
+
explicit?: CallerOutputContract,
|
|
397
|
+
): CallerOutputContract | undefined {
|
|
398
|
+
return explicit ?? detectCallerOutputContract(prompt);
|
|
399
|
+
}
|
|
400
|
+
|
|
359
401
|
function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
|
|
360
402
|
const sortedOutputs = [...input.panelOutputs].sort(comparePanelItems);
|
|
361
403
|
const sortedFailures = [...input.failedPanelists].sort(comparePanelItems);
|
|
@@ -366,6 +408,10 @@ function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
|
|
|
366
408
|
? buildBlindLabelMap([...sortedOutputs, ...sortedFailures])
|
|
367
409
|
: undefined;
|
|
368
410
|
const merging = resolveSynthesisMode(input.profile) === "merge";
|
|
411
|
+
const callerContract = callerContractForPrompt(
|
|
412
|
+
input.prompt,
|
|
413
|
+
input.callerContract,
|
|
414
|
+
);
|
|
369
415
|
return [
|
|
370
416
|
...(merging
|
|
371
417
|
? COMPOSER_INSTRUCTIONS
|
|
@@ -399,7 +445,11 @@ function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
|
|
|
399
445
|
// right" wording contradicts the composer's "do not rank panelists".
|
|
400
446
|
...(merging ? [] : [...CONTESTED_CLAIMS_INSTRUCTIONS, ""]),
|
|
401
447
|
"Output contract:",
|
|
402
|
-
...(
|
|
448
|
+
...(callerContract
|
|
449
|
+
? callerOutputContractInstructions(callerContract)
|
|
450
|
+
: merging
|
|
451
|
+
? COMPOSER_OUTPUT_CONTRACT
|
|
452
|
+
: JUDGE_OUTPUT_CONTRACT),
|
|
403
453
|
].join("\n");
|
|
404
454
|
}
|
|
405
455
|
|
package/src/run-store.ts
CHANGED
|
@@ -24,6 +24,7 @@ export type FusionRunSummary = Omit<
|
|
|
24
24
|
| "prompt"
|
|
25
25
|
| "profileName"
|
|
26
26
|
| "operationId"
|
|
27
|
+
| "outputContract"
|
|
27
28
|
| "phase"
|
|
28
29
|
| "createdAt"
|
|
29
30
|
| "updatedAt"
|
|
@@ -43,6 +44,7 @@ export interface FusionRunStartInput {
|
|
|
43
44
|
inlinePanel?: string[];
|
|
44
45
|
baseProfileName?: string;
|
|
45
46
|
operationId?: string;
|
|
47
|
+
outputContract?: FusionRun["outputContract"];
|
|
46
48
|
phase?: Exclude<FusionPhase, FusionTerminalPhase>;
|
|
47
49
|
createdAt?: number;
|
|
48
50
|
}
|
|
@@ -162,6 +164,9 @@ export class FusionRunStore {
|
|
|
162
164
|
...(input.operationId !== undefined
|
|
163
165
|
? { operationId: input.operationId }
|
|
164
166
|
: {}),
|
|
167
|
+
...(input.outputContract !== undefined
|
|
168
|
+
? { outputContract: input.outputContract }
|
|
169
|
+
: {}),
|
|
165
170
|
phase: input.phase ?? "chain",
|
|
166
171
|
createdAt,
|
|
167
172
|
updatedAt: createdAt,
|
|
@@ -379,6 +384,9 @@ function toRunSummary(
|
|
|
379
384
|
prompt: run.prompt,
|
|
380
385
|
profileName: run.profileName,
|
|
381
386
|
...(run.operationId !== undefined ? { operationId: run.operationId } : {}),
|
|
387
|
+
...(run.outputContract !== undefined
|
|
388
|
+
? { outputContract: run.outputContract }
|
|
389
|
+
: {}),
|
|
382
390
|
phase: run.phase,
|
|
383
391
|
createdAt: run.createdAt,
|
|
384
392
|
updatedAt: run.updatedAt,
|
|
@@ -404,6 +412,9 @@ function cloneRun(run: FusionRun): FusionRun {
|
|
|
404
412
|
? { baseProfileName: run.baseProfileName }
|
|
405
413
|
: {}),
|
|
406
414
|
...(run.operationId !== undefined ? { operationId: run.operationId } : {}),
|
|
415
|
+
...(run.outputContract !== undefined
|
|
416
|
+
? { outputContract: run.outputContract }
|
|
417
|
+
: {}),
|
|
407
418
|
phase: run.phase,
|
|
408
419
|
createdAt: run.createdAt,
|
|
409
420
|
updatedAt: run.updatedAt,
|
|
@@ -462,6 +473,12 @@ function isFusionRunState(value: unknown): value is FusionRun {
|
|
|
462
473
|
if (value.operationId !== undefined && !isNonEmptyString(value.operationId)) {
|
|
463
474
|
return false;
|
|
464
475
|
}
|
|
476
|
+
if (
|
|
477
|
+
value.outputContract !== undefined &&
|
|
478
|
+
value.outputContract !== "plan-review-v1"
|
|
479
|
+
) {
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
465
482
|
if (
|
|
466
483
|
value.inlinePanel !== undefined &&
|
|
467
484
|
(!Array.isArray(value.inlinePanel) ||
|