@alexeiled/pi-fusion 0.6.1 → 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 +8 -6
- 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 +23 -5
- 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 +84 -24
- 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 {
|
|
@@ -43,7 +57,6 @@ export interface PanelWorkflowTaskParams extends PanelSubagentTaskParams {
|
|
|
43
57
|
export interface PanelSpawnParams {
|
|
44
58
|
workflowScript: string;
|
|
45
59
|
async: true;
|
|
46
|
-
clarify: false;
|
|
47
60
|
context: "fresh" | "fork";
|
|
48
61
|
output: true;
|
|
49
62
|
outputMode: "inline";
|
|
@@ -51,22 +64,28 @@ export interface PanelSpawnParams {
|
|
|
51
64
|
timeoutMs?: number;
|
|
52
65
|
}
|
|
53
66
|
|
|
54
|
-
export interface
|
|
67
|
+
export interface JudgeWorkflowTaskParams {
|
|
55
68
|
agent: string;
|
|
56
69
|
task: string;
|
|
57
|
-
async: true;
|
|
58
|
-
clarify: false;
|
|
59
|
-
context: "fresh" | "fork";
|
|
60
70
|
output: true;
|
|
61
71
|
outputMode: "inline";
|
|
62
72
|
skill: false;
|
|
63
73
|
acceptance: FusionAcceptanceDisabled;
|
|
64
74
|
model?: string;
|
|
65
|
-
timeoutMs?: number;
|
|
66
75
|
/** Per-task cap on judge tool calls; see `FusionProfile.judgeToolBudget`. */
|
|
67
76
|
toolBudget?: ToolBudget;
|
|
68
77
|
}
|
|
69
78
|
|
|
79
|
+
export interface JudgeSpawnParams {
|
|
80
|
+
workflowScript: string;
|
|
81
|
+
async: true;
|
|
82
|
+
context: "fresh" | "fork";
|
|
83
|
+
output: true;
|
|
84
|
+
outputMode: "inline";
|
|
85
|
+
acceptance: FusionAcceptanceDisabled;
|
|
86
|
+
timeoutMs?: number;
|
|
87
|
+
}
|
|
88
|
+
|
|
70
89
|
export type { FailedPanelSummary, PanelOutput } from "./types.js";
|
|
71
90
|
|
|
72
91
|
export interface BuildJudgeSpawnParamsInput {
|
|
@@ -80,6 +99,7 @@ export interface BuildJudgeSpawnParamsInput {
|
|
|
80
99
|
* and its position bias.
|
|
81
100
|
*/
|
|
82
101
|
runId: string;
|
|
102
|
+
callerContract?: CallerOutputContract;
|
|
83
103
|
}
|
|
84
104
|
|
|
85
105
|
const PANEL_OUTPUT_CONTRACT = [
|
|
@@ -147,6 +167,7 @@ export function appendThinkingSuffix(
|
|
|
147
167
|
export function buildPanelSpawnParams(
|
|
148
168
|
profile: FusionProfile,
|
|
149
169
|
prompt: string,
|
|
170
|
+
callerContract?: CallerOutputContract,
|
|
150
171
|
): PanelSpawnParams {
|
|
151
172
|
const concurrency = profile.concurrency ?? profile.panel.length;
|
|
152
173
|
const tasks: PanelWorkflowTaskParams[] = profile.panel.map(
|
|
@@ -156,6 +177,8 @@ export function buildPanelSpawnParams(
|
|
|
156
177
|
member,
|
|
157
178
|
prompt,
|
|
158
179
|
profile.stopWhenPanelAgrees === true,
|
|
180
|
+
profile.panelToolBudget ?? DEFAULT_TOOL_BUDGET,
|
|
181
|
+
callerContract,
|
|
159
182
|
),
|
|
160
183
|
}),
|
|
161
184
|
);
|
|
@@ -167,14 +190,11 @@ export function buildPanelSpawnParams(
|
|
|
167
190
|
profile.stopWhenPanelAgrees === true,
|
|
168
191
|
),
|
|
169
192
|
async: true,
|
|
170
|
-
clarify: false,
|
|
171
193
|
context: profile.context ?? "fresh",
|
|
172
194
|
output: true,
|
|
173
195
|
outputMode: "inline",
|
|
174
196
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
175
|
-
|
|
176
|
-
? { timeoutMs: profile.timeoutMs }
|
|
177
|
-
: {}),
|
|
197
|
+
timeoutMs: resolveStageTimeout(profile.panelTimeoutMs, profile.timeoutMs),
|
|
178
198
|
};
|
|
179
199
|
}
|
|
180
200
|
|
|
@@ -185,23 +205,28 @@ export function buildJudgeSpawnParams(
|
|
|
185
205
|
input.profile.judge.model,
|
|
186
206
|
input.profile.judge.thinking,
|
|
187
207
|
);
|
|
188
|
-
|
|
208
|
+
const task: JudgeWorkflowTaskParams = {
|
|
189
209
|
agent: resolveSynthesisAgent(input.profile),
|
|
190
210
|
task: buildJudgeTask(input),
|
|
191
|
-
async: true,
|
|
192
|
-
clarify: false,
|
|
193
|
-
context: input.profile.context ?? "fresh",
|
|
194
211
|
output: true,
|
|
195
212
|
outputMode: "inline",
|
|
196
213
|
skill: false,
|
|
197
214
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
198
215
|
...(model ? { model } : {}),
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
216
|
+
toolBudget: input.profile.judgeToolBudget ?? DEFAULT_TOOL_BUDGET,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
workflowScript: `return runs.run("judge", ${JSON.stringify(task)});`,
|
|
221
|
+
async: true,
|
|
222
|
+
context: input.profile.context ?? "fresh",
|
|
223
|
+
output: true,
|
|
224
|
+
outputMode: "inline",
|
|
225
|
+
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
226
|
+
timeoutMs: resolveStageTimeout(
|
|
227
|
+
input.profile.judgeTimeoutMs,
|
|
228
|
+
input.profile.timeoutMs,
|
|
229
|
+
),
|
|
205
230
|
};
|
|
206
231
|
}
|
|
207
232
|
|
|
@@ -250,20 +275,35 @@ function buildPanelWorkflowScript(
|
|
|
250
275
|
.join("\n");
|
|
251
276
|
}
|
|
252
277
|
|
|
278
|
+
function resolveStageTimeout(
|
|
279
|
+
stageTimeoutMs: number | undefined,
|
|
280
|
+
legacyTimeoutMs: number | undefined,
|
|
281
|
+
): number {
|
|
282
|
+
return stageTimeoutMs ?? legacyTimeoutMs ?? DEFAULT_STAGE_TIMEOUT_MS;
|
|
283
|
+
}
|
|
284
|
+
|
|
253
285
|
function buildPanelTaskParams(
|
|
254
286
|
member: PanelMemberConfig,
|
|
255
287
|
prompt: string,
|
|
256
288
|
includeDecisionRecord: boolean,
|
|
289
|
+
toolBudget: ToolBudget,
|
|
290
|
+
callerContract?: CallerOutputContract,
|
|
257
291
|
): PanelSubagentTaskParams {
|
|
258
292
|
const model = appendThinkingSuffix(member.model, member.thinking);
|
|
259
293
|
return {
|
|
260
294
|
agent: member.agent,
|
|
261
|
-
task: buildPanelTask(
|
|
295
|
+
task: buildPanelTask(
|
|
296
|
+
member,
|
|
297
|
+
prompt,
|
|
298
|
+
includeDecisionRecord,
|
|
299
|
+
callerContract,
|
|
300
|
+
),
|
|
262
301
|
output: true,
|
|
263
302
|
outputMode: "inline",
|
|
264
303
|
progress: true,
|
|
265
304
|
skill: false,
|
|
266
305
|
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
306
|
+
toolBudget,
|
|
267
307
|
...(model ? { model } : {}),
|
|
268
308
|
};
|
|
269
309
|
}
|
|
@@ -272,8 +312,11 @@ function buildPanelTask(
|
|
|
272
312
|
member: PanelMemberConfig,
|
|
273
313
|
prompt: string,
|
|
274
314
|
includeDecisionRecord: boolean,
|
|
315
|
+
callerContractOverride?: CallerOutputContract,
|
|
275
316
|
): string {
|
|
276
317
|
const role = member.role?.trim() || "independent analysis and critique";
|
|
318
|
+
const callerContract =
|
|
319
|
+
callerContractForPrompt(prompt, callerContractOverride);
|
|
277
320
|
return [
|
|
278
321
|
`Panel member: ${memberLabel(member)} (${member.id})`,
|
|
279
322
|
`Role: ${role}`,
|
|
@@ -289,8 +332,10 @@ function buildPanelTask(
|
|
|
289
332
|
"- Be concise and cite evidence when you inspect files.",
|
|
290
333
|
"",
|
|
291
334
|
"Output contract:",
|
|
292
|
-
...
|
|
293
|
-
|
|
335
|
+
...(callerContract
|
|
336
|
+
? callerOutputContractInstructions(callerContract)
|
|
337
|
+
: PANEL_OUTPUT_CONTRACT),
|
|
338
|
+
...(includeDecisionRecord && !callerContract
|
|
294
339
|
? [
|
|
295
340
|
"",
|
|
296
341
|
"Decision record:",
|
|
@@ -346,6 +391,13 @@ function formatMemberTask(member: PanelMemberConfig, prompt: string): string[] {
|
|
|
346
391
|
];
|
|
347
392
|
}
|
|
348
393
|
|
|
394
|
+
function callerContractForPrompt(
|
|
395
|
+
prompt: string,
|
|
396
|
+
explicit?: CallerOutputContract,
|
|
397
|
+
): CallerOutputContract | undefined {
|
|
398
|
+
return explicit ?? detectCallerOutputContract(prompt);
|
|
399
|
+
}
|
|
400
|
+
|
|
349
401
|
function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
|
|
350
402
|
const sortedOutputs = [...input.panelOutputs].sort(comparePanelItems);
|
|
351
403
|
const sortedFailures = [...input.failedPanelists].sort(comparePanelItems);
|
|
@@ -356,6 +408,10 @@ function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
|
|
|
356
408
|
? buildBlindLabelMap([...sortedOutputs, ...sortedFailures])
|
|
357
409
|
: undefined;
|
|
358
410
|
const merging = resolveSynthesisMode(input.profile) === "merge";
|
|
411
|
+
const callerContract = callerContractForPrompt(
|
|
412
|
+
input.prompt,
|
|
413
|
+
input.callerContract,
|
|
414
|
+
);
|
|
359
415
|
return [
|
|
360
416
|
...(merging
|
|
361
417
|
? COMPOSER_INSTRUCTIONS
|
|
@@ -389,7 +445,11 @@ function buildJudgeTask(input: BuildJudgeSpawnParamsInput): string {
|
|
|
389
445
|
// right" wording contradicts the composer's "do not rank panelists".
|
|
390
446
|
...(merging ? [] : [...CONTESTED_CLAIMS_INSTRUCTIONS, ""]),
|
|
391
447
|
"Output contract:",
|
|
392
|
-
...(
|
|
448
|
+
...(callerContract
|
|
449
|
+
? callerOutputContractInstructions(callerContract)
|
|
450
|
+
: merging
|
|
451
|
+
? COMPOSER_OUTPUT_CONTRACT
|
|
452
|
+
: JUDGE_OUTPUT_CONTRACT),
|
|
393
453
|
].join("\n");
|
|
394
454
|
}
|
|
395
455
|
|