@alexeiled/pi-fusion 0.6.0 → 0.6.2
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 +2 -2
- package/docs/user-guide.md +9 -3
- package/package.json +1 -1
- package/src/orchestrator.ts +71 -5
- package/src/result-extract.ts +15 -1
- package/src/run-builder.ts +74 -135
package/README.md
CHANGED
|
@@ -177,7 +177,7 @@ Requirements:
|
|
|
177
177
|
|
|
178
178
|
- Pi
|
|
179
179
|
- Node.js 22.19+
|
|
180
|
-
- `pi-subagents`
|
|
180
|
+
- `pi-subagents` 0.43.0 or later
|
|
181
181
|
|
|
182
182
|
```bash
|
|
183
183
|
pi install npm:pi-subagents
|
|
@@ -210,7 +210,7 @@ For commands, config, and troubleshooting details, see [`docs/user-guide.md`](./
|
|
|
210
210
|
- Fusion sends your prompt and any inspected snippets to every panel model, and to the judge, through `pi-subagents`.
|
|
211
211
|
- Reports include available per-panel and judge time, aggregate model time, usage, estimated cost, and model failure details. Missing provider usage is shown as unknown. `$0.0000` is a known zero cost.
|
|
212
212
|
- `Model` is lifecycle metadata. `Configured model` is the profile request. Both appear when the run differs from the request.
|
|
213
|
-
- `stopWhenPanelAgrees` is an opt-in profile setting. It requires matching high-confidence decision records with no request for more evidence,
|
|
213
|
+
- `stopWhenPanelAgrees` is an opt-in profile setting. It requires matching high-confidence decision records with no request for more evidence, evaluates panelists in pairs so it can avoid starting later work, records skipped panelists in the report, and still runs the judge.
|
|
214
214
|
- Panel answers reach the judge in an order seeded from the run id, not in config order. A fixed order advantages the same member on every run, because judges favour whichever candidate they see first or last.
|
|
215
215
|
- Panelists can search the web by opting in to the `fusion-panelist-web` agent, which requires `pi-web-providers`. Defaults stay local-only on purpose: tool names are a strict allowlist, so an agent declaring a tool whose extension is missing fails every task that uses it.
|
|
216
216
|
- `synthesis: "merge"` switches from picking the best answer to merging answers that covered different facets, using the `fusion-composer` agent. Panel members get facets through their optional `question` field. See the user guide.
|
package/docs/user-guide.md
CHANGED
|
@@ -29,7 +29,7 @@ prompts, or both. **Mixing models is the main lever.** The config that
|
|
|
29
29
|
`/fusion init` writes sets no `model`. By default you therefore get one model in
|
|
30
30
|
three roles. Give each member its own `model` to get the real benefit.
|
|
31
31
|
|
|
32
|
-
Older runs created as a single `pi-subagents` chain remain supported when restored.
|
|
32
|
+
Fusion launches new panels through `pi-subagents` `workflowScript`; the panel and judge remain separate durable runs. Older runs created as a single `pi-subagents` chain remain supported when restored.
|
|
33
33
|
|
|
34
34
|
The base Pi session stays in control. Fusion is a tool for decisions, not a replacement for normal coding.
|
|
35
35
|
|
|
@@ -149,7 +149,7 @@ Profile:
|
|
|
149
149
|
|
|
150
150
|
- `panel`: one or more panel members
|
|
151
151
|
- `judge`: judge agent config
|
|
152
|
-
- `concurrency`: max parallel panelists
|
|
152
|
+
- `concurrency`: max parallel panelists. When `stopWhenPanelAgrees` is on, Fusion evaluates the first two panelists before launching another batch so it can avoid work after strong agreement.
|
|
153
153
|
- `timeoutMs`: async subagent timeout in milliseconds
|
|
154
154
|
- `context`: `fresh` or `fork`
|
|
155
155
|
- `stopWhenPanelAgrees`: optional boolean, default `false`. When it is on, Fusion can stop the panelists that have not finished yet. All four conditions must hold: two or more finished panelists give the same normalized recommendation, every one of them reports `high` confidence, none of them asks for more evidence, and work remains. The judge still runs over the answers already collected. This policy is fixed on purpose. There is no threshold to tune.
|
|
@@ -451,10 +451,16 @@ For an economical mixed panel, give each member a fast or inexpensive frontier,
|
|
|
451
451
|
|
|
452
452
|
`pi-subagents RPC is unavailable`
|
|
453
453
|
|
|
454
|
-
- install `pi-subagents`
|
|
454
|
+
- install or update `pi-subagents` to 0.43.0 or later
|
|
455
455
|
- reload Pi
|
|
456
456
|
- retry `/fusion status`
|
|
457
457
|
|
|
458
|
+
`RPC spawn no longer accepts top-level chain or parallel inputs; use workflowScript.`
|
|
459
|
+
|
|
460
|
+
- update `pi-fusion` to 0.6.1 or later
|
|
461
|
+
- reload Pi so it loads the updated extension
|
|
462
|
+
- retry `/fusion`; do not change your Fusion profile
|
|
463
|
+
|
|
458
464
|
`Unknown fusion profile`
|
|
459
465
|
|
|
460
466
|
- verify `defaultProfile`
|
package/package.json
CHANGED
package/src/orchestrator.ts
CHANGED
|
@@ -51,6 +51,7 @@ import type { SubagentsTargetParams } from "./subagents-rpc.js";
|
|
|
51
51
|
export const SUBAGENT_ASYNC_COMPLETE_EVENT = "subagent:async-complete";
|
|
52
52
|
|
|
53
53
|
const RECONCILE_INTERVAL_MS = 2_000;
|
|
54
|
+
const WORKFLOW_RESULT_ARTIFACT_GRACE_MS = 5_000;
|
|
54
55
|
|
|
55
56
|
export type FusionNotifyType = "info" | "warning" | "error";
|
|
56
57
|
|
|
@@ -105,6 +106,7 @@ interface RunLifecycleSnapshot {
|
|
|
105
106
|
statusPayload?: unknown;
|
|
106
107
|
resultPayload?: unknown;
|
|
107
108
|
resultIsTerminal: boolean;
|
|
109
|
+
resultArtifactPending?: boolean;
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
export class FusionOrchestrator {
|
|
@@ -556,6 +558,7 @@ export class FusionOrchestrator {
|
|
|
556
558
|
...(active.chainAsyncDir ? { asyncDir: active.chainAsyncDir } : {}),
|
|
557
559
|
eventPayload: payload,
|
|
558
560
|
});
|
|
561
|
+
if (snapshot.resultArtifactPending) return { status: "ignored" };
|
|
559
562
|
const terminalPayload =
|
|
560
563
|
snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
|
|
561
564
|
if (
|
|
@@ -655,8 +658,12 @@ export class FusionOrchestrator {
|
|
|
655
658
|
: {}),
|
|
656
659
|
eventPayload: payload,
|
|
657
660
|
});
|
|
661
|
+
if (snapshot.resultArtifactPending) return { status: "ignored" };
|
|
658
662
|
|
|
659
|
-
|
|
663
|
+
const panelIsTerminal =
|
|
664
|
+
snapshot.resultIsTerminal ||
|
|
665
|
+
isTerminalSubagentState(extractSubagentState(snapshot.statusPayload));
|
|
666
|
+
if (!active.panelStopReason && !panelIsTerminal) {
|
|
660
667
|
const partial = extractPanelResults(snapshot.statusPayload, {
|
|
661
668
|
panel: profile.panel,
|
|
662
669
|
completedOnly: true,
|
|
@@ -689,12 +696,16 @@ export class FusionOrchestrator {
|
|
|
689
696
|
return this.failActiveRun(lifecycleError);
|
|
690
697
|
}
|
|
691
698
|
|
|
699
|
+
const workflowStoppedIndices = extractWorkflowStoppedPanelIndices(
|
|
700
|
+
lifecyclePayload,
|
|
701
|
+
);
|
|
702
|
+
const stoppedPanelIndices =
|
|
703
|
+
active.panelStoppedIndices ??
|
|
704
|
+
(workflowStoppedIndices.length > 0 ? workflowStoppedIndices : undefined);
|
|
692
705
|
const extracted = extractPanelResults(lifecyclePayload, {
|
|
693
706
|
panel: profile.panel,
|
|
694
707
|
limit: profile.panel.length,
|
|
695
|
-
...(
|
|
696
|
-
? { stoppedPanelIndices: active.panelStoppedIndices }
|
|
697
|
-
: {}),
|
|
708
|
+
...(stoppedPanelIndices ? { stoppedPanelIndices } : {}),
|
|
698
709
|
});
|
|
699
710
|
if (!extracted.ok) {
|
|
700
711
|
return this.failActiveRun(
|
|
@@ -707,11 +718,18 @@ export class FusionOrchestrator {
|
|
|
707
718
|
snapshot.statusPayload,
|
|
708
719
|
profile,
|
|
709
720
|
);
|
|
710
|
-
const
|
|
721
|
+
const stored = this.storePanelResults(
|
|
711
722
|
active.id,
|
|
712
723
|
observedPanels.outputs,
|
|
713
724
|
observedPanels.failures,
|
|
714
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;
|
|
715
733
|
|
|
716
734
|
return this.finishPanelCompletion(
|
|
717
735
|
updated,
|
|
@@ -840,6 +858,7 @@ export class FusionOrchestrator {
|
|
|
840
858
|
...(active.judgeAsyncDir ? { asyncDir: active.judgeAsyncDir } : {}),
|
|
841
859
|
eventPayload: payload,
|
|
842
860
|
});
|
|
861
|
+
if (snapshot.resultArtifactPending) return { status: "ignored" };
|
|
843
862
|
const terminalPayload =
|
|
844
863
|
snapshot.resultPayload ?? snapshot.statusPayload ?? payload;
|
|
845
864
|
if (
|
|
@@ -952,6 +971,17 @@ export class FusionOrchestrator {
|
|
|
952
971
|
};
|
|
953
972
|
}
|
|
954
973
|
|
|
974
|
+
if (
|
|
975
|
+
statusIsTerminal &&
|
|
976
|
+
isWorkflowResultArtifactPending(statusPayload)
|
|
977
|
+
) {
|
|
978
|
+
return {
|
|
979
|
+
statusPayload,
|
|
980
|
+
resultIsTerminal: false,
|
|
981
|
+
resultArtifactPending: true,
|
|
982
|
+
};
|
|
983
|
+
}
|
|
984
|
+
|
|
955
985
|
if (hasResultsArray(statusPayload)) {
|
|
956
986
|
return {
|
|
957
987
|
statusPayload,
|
|
@@ -1608,10 +1638,46 @@ function isTerminalSubagentState(state: string | undefined): boolean {
|
|
|
1608
1638
|
);
|
|
1609
1639
|
}
|
|
1610
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
|
+
|
|
1611
1654
|
function hasLifecycleResults(payload: unknown): boolean {
|
|
1612
1655
|
return hasResultsArray(payload) || findStepsArray(payload).length > 0;
|
|
1613
1656
|
}
|
|
1614
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
|
+
|
|
1615
1681
|
function extractSubagentFailure(payload: unknown): string | undefined {
|
|
1616
1682
|
if (!isRecord(payload)) return undefined;
|
|
1617
1683
|
const direct = firstNonBlankString(payload.error, payload.errorMessage);
|
package/src/result-extract.ts
CHANGED
|
@@ -75,6 +75,20 @@ export function extractPanelResults(
|
|
|
75
75
|
else failures.push(child.failure);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
+
for (const index of options.stoppedPanelIndices ?? []) {
|
|
79
|
+
if (index < results.length || index >= (options.limit ?? Infinity)) continue;
|
|
80
|
+
const child = normalizeChildResult(
|
|
81
|
+
{
|
|
82
|
+
success: false,
|
|
83
|
+
error: "Stopped after strong panel agreement.",
|
|
84
|
+
},
|
|
85
|
+
index,
|
|
86
|
+
options,
|
|
87
|
+
);
|
|
88
|
+
if (!child.ok) return child;
|
|
89
|
+
if (child.status === "failed") failures.push(child.failure);
|
|
90
|
+
}
|
|
91
|
+
|
|
78
92
|
const runId = firstString(container.payload.runId, container.payload.id);
|
|
79
93
|
return {
|
|
80
94
|
ok: true,
|
|
@@ -197,7 +211,7 @@ function normalizeChildResult(
|
|
|
197
211
|
}
|
|
198
212
|
|
|
199
213
|
const member = options.panel?.[index];
|
|
200
|
-
const agent = firstString(
|
|
214
|
+
const agent = firstString(member?.agent, rawResult.agent);
|
|
201
215
|
if (!agent) {
|
|
202
216
|
return error(
|
|
203
217
|
"missing-result-field",
|
package/src/run-builder.ts
CHANGED
|
@@ -36,17 +36,13 @@ export interface PanelSubagentTaskParams {
|
|
|
36
36
|
model?: string;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
export interface
|
|
40
|
-
|
|
41
|
-
label: string;
|
|
42
|
-
phase: "Panel";
|
|
39
|
+
export interface PanelWorkflowTaskParams extends PanelSubagentTaskParams {
|
|
40
|
+
key: string;
|
|
43
41
|
}
|
|
44
42
|
|
|
45
43
|
export interface PanelSpawnParams {
|
|
46
|
-
|
|
44
|
+
workflowScript: string;
|
|
47
45
|
async: true;
|
|
48
|
-
clarify: false;
|
|
49
|
-
concurrency: number;
|
|
50
46
|
context: "fresh" | "fork";
|
|
51
47
|
output: true;
|
|
52
48
|
outputMode: "inline";
|
|
@@ -54,50 +50,26 @@ export interface PanelSpawnParams {
|
|
|
54
50
|
timeoutMs?: number;
|
|
55
51
|
}
|
|
56
52
|
|
|
57
|
-
export interface
|
|
58
|
-
parallel: PanelChainTaskParams[];
|
|
59
|
-
concurrency: number;
|
|
60
|
-
failFast: false;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface FusionChainJudgeStepParams {
|
|
53
|
+
export interface JudgeWorkflowTaskParams {
|
|
64
54
|
agent: string;
|
|
65
55
|
task: string;
|
|
66
|
-
label: "Judge";
|
|
67
|
-
phase: "Judge";
|
|
68
56
|
output: true;
|
|
69
57
|
outputMode: "inline";
|
|
70
58
|
skill: false;
|
|
71
59
|
acceptance: FusionAcceptanceDisabled;
|
|
72
60
|
model?: string;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
export interface FusionChainSpawnParams {
|
|
76
|
-
chain: [FusionChainParallelStepParams, FusionChainJudgeStepParams];
|
|
77
|
-
task: string;
|
|
78
|
-
async: true;
|
|
79
|
-
clarify: false;
|
|
80
|
-
context: "fresh" | "fork";
|
|
81
|
-
output: true;
|
|
82
|
-
outputMode: "inline";
|
|
83
|
-
acceptance: FusionAcceptanceDisabled;
|
|
84
|
-
timeoutMs?: number;
|
|
61
|
+
/** Per-task cap on judge tool calls; see `FusionProfile.judgeToolBudget`. */
|
|
62
|
+
toolBudget?: ToolBudget;
|
|
85
63
|
}
|
|
86
64
|
|
|
87
65
|
export interface JudgeSpawnParams {
|
|
88
|
-
|
|
89
|
-
task: string;
|
|
66
|
+
workflowScript: string;
|
|
90
67
|
async: true;
|
|
91
|
-
clarify: false;
|
|
92
68
|
context: "fresh" | "fork";
|
|
93
69
|
output: true;
|
|
94
70
|
outputMode: "inline";
|
|
95
|
-
skill: false;
|
|
96
71
|
acceptance: FusionAcceptanceDisabled;
|
|
97
|
-
model?: string;
|
|
98
72
|
timeoutMs?: number;
|
|
99
|
-
/** Per-task cap on judge tool calls; see `FusionProfile.judgeToolBudget`. */
|
|
100
|
-
toolBudget?: ToolBudget;
|
|
101
73
|
}
|
|
102
74
|
|
|
103
75
|
export type { FailedPanelSummary, PanelOutput } from "./types.js";
|
|
@@ -181,59 +153,25 @@ export function buildPanelSpawnParams(
|
|
|
181
153
|
profile: FusionProfile,
|
|
182
154
|
prompt: string,
|
|
183
155
|
): PanelSpawnParams {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
156
|
+
const concurrency = profile.concurrency ?? profile.panel.length;
|
|
157
|
+
const tasks: PanelWorkflowTaskParams[] = profile.panel.map(
|
|
158
|
+
(member, index) => ({
|
|
159
|
+
key: `panel-${index + 1}`,
|
|
160
|
+
...buildPanelTaskParams(
|
|
187
161
|
member,
|
|
188
162
|
prompt,
|
|
189
163
|
profile.stopWhenPanelAgrees === true,
|
|
190
164
|
),
|
|
191
|
-
),
|
|
192
|
-
async: true,
|
|
193
|
-
clarify: false,
|
|
194
|
-
concurrency: profile.concurrency ?? profile.panel.length,
|
|
195
|
-
context: profile.context ?? "fresh",
|
|
196
|
-
output: true,
|
|
197
|
-
outputMode: "inline",
|
|
198
|
-
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
199
|
-
...(profile.timeoutMs !== undefined
|
|
200
|
-
? { timeoutMs: profile.timeoutMs }
|
|
201
|
-
: {}),
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function buildFusionChainSpawnParams(
|
|
206
|
-
profile: FusionProfile,
|
|
207
|
-
prompt: string,
|
|
208
|
-
): FusionChainSpawnParams {
|
|
209
|
-
const model = appendThinkingSuffix(
|
|
210
|
-
profile.judge.model,
|
|
211
|
-
profile.judge.thinking,
|
|
165
|
+
}),
|
|
212
166
|
);
|
|
167
|
+
|
|
213
168
|
return {
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
concurrency: profile.concurrency ?? profile.panel.length,
|
|
220
|
-
failFast: false,
|
|
221
|
-
},
|
|
222
|
-
{
|
|
223
|
-
agent: profile.judge.agent,
|
|
224
|
-
task: buildChainJudgeTask(profile.panel),
|
|
225
|
-
label: "Judge",
|
|
226
|
-
phase: "Judge",
|
|
227
|
-
output: true,
|
|
228
|
-
outputMode: "inline",
|
|
229
|
-
skill: false,
|
|
230
|
-
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
231
|
-
...(model ? { model } : {}),
|
|
232
|
-
},
|
|
233
|
-
],
|
|
234
|
-
task: prompt.trim(),
|
|
169
|
+
workflowScript: buildPanelWorkflowScript(
|
|
170
|
+
tasks,
|
|
171
|
+
concurrency,
|
|
172
|
+
profile.stopWhenPanelAgrees === true,
|
|
173
|
+
),
|
|
235
174
|
async: true,
|
|
236
|
-
clarify: false,
|
|
237
175
|
context: profile.context ?? "fresh",
|
|
238
176
|
output: true,
|
|
239
177
|
outputMode: "inline",
|
|
@@ -251,12 +189,9 @@ export function buildJudgeSpawnParams(
|
|
|
251
189
|
input.profile.judge.model,
|
|
252
190
|
input.profile.judge.thinking,
|
|
253
191
|
);
|
|
254
|
-
|
|
192
|
+
const task: JudgeWorkflowTaskParams = {
|
|
255
193
|
agent: resolveSynthesisAgent(input.profile),
|
|
256
194
|
task: buildJudgeTask(input),
|
|
257
|
-
async: true,
|
|
258
|
-
clarify: false,
|
|
259
|
-
context: input.profile.context ?? "fresh",
|
|
260
195
|
output: true,
|
|
261
196
|
outputMode: "inline",
|
|
262
197
|
skill: false,
|
|
@@ -265,12 +200,66 @@ export function buildJudgeSpawnParams(
|
|
|
265
200
|
...(input.profile.judgeToolBudget
|
|
266
201
|
? { toolBudget: input.profile.judgeToolBudget }
|
|
267
202
|
: {}),
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
return {
|
|
206
|
+
workflowScript: `return runs.run("judge", ${JSON.stringify(task)});`,
|
|
207
|
+
async: true,
|
|
208
|
+
context: input.profile.context ?? "fresh",
|
|
209
|
+
output: true,
|
|
210
|
+
outputMode: "inline",
|
|
211
|
+
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
268
212
|
...(input.profile.timeoutMs !== undefined
|
|
269
213
|
? { timeoutMs: input.profile.timeoutMs }
|
|
270
214
|
: {}),
|
|
271
215
|
};
|
|
272
216
|
}
|
|
273
217
|
|
|
218
|
+
function buildPanelWorkflowScript(
|
|
219
|
+
tasks: readonly PanelWorkflowTaskParams[],
|
|
220
|
+
concurrency: number,
|
|
221
|
+
stopWhenAgrees: boolean,
|
|
222
|
+
): string {
|
|
223
|
+
const serializedTasks = JSON.stringify(tasks);
|
|
224
|
+
const effectiveConcurrency = stopWhenAgrees
|
|
225
|
+
? Math.min(concurrency, 2)
|
|
226
|
+
: concurrency;
|
|
227
|
+
const stopLogic = stopWhenAgrees
|
|
228
|
+
? [
|
|
229
|
+
"const decisions = results",
|
|
230
|
+
" .filter((result) => result && result.ok === true)",
|
|
231
|
+
" .map((result) => {",
|
|
232
|
+
" const text = typeof result.output === \"string\" ? result.output : \"\";",
|
|
233
|
+
" const match = text.match(/<fusion-panel-decision>([\\s\\S]*?)<\\/fusion-panel-decision>\\s*$/);",
|
|
234
|
+
" if (!match) return undefined;",
|
|
235
|
+
" try { return JSON.parse(match[1]); } catch { return undefined; }",
|
|
236
|
+
" })",
|
|
237
|
+
" .filter((decision) => decision && typeof decision.recommendation === \"string\" && decision.confidence === \"high\" && decision.needsMoreEvidence === false);",
|
|
238
|
+
"if (decisions.length >= 2 && results.length < tasks.length) {",
|
|
239
|
+
" const recommendation = decisions[0].recommendation.trim().toLocaleLowerCase().replace(/[^\\p{L}\\p{N}]+/gu, \" \" ).trim(),",
|
|
240
|
+
" agrees = recommendation && decisions.every((decision) => decision.recommendation.trim().toLocaleLowerCase().replace(/[^\\p{L}\\p{N}]+/gu, \" \" ).trim() === recommendation);",
|
|
241
|
+
" if (agrees) {",
|
|
242
|
+
" emit({ type: \"pi-fusion-panel-stop\", indices: tasks.slice(results.length).map((_, index) => results.length + index) });",
|
|
243
|
+
" return results;",
|
|
244
|
+
" }",
|
|
245
|
+
"}",
|
|
246
|
+
].join("\n")
|
|
247
|
+
: "";
|
|
248
|
+
|
|
249
|
+
return [
|
|
250
|
+
`const tasks = ${serializedTasks};`,
|
|
251
|
+
`const concurrency = ${effectiveConcurrency};`,
|
|
252
|
+
"const results = [];",
|
|
253
|
+
"for (let index = 0; index < tasks.length; index += concurrency) {",
|
|
254
|
+
" results.push(...await runs.all(tasks.slice(index, index + concurrency)));",
|
|
255
|
+
stopWhenAgrees ? " " + stopLogic.replaceAll("\n", "\n ") : "",
|
|
256
|
+
"}",
|
|
257
|
+
"return results;",
|
|
258
|
+
]
|
|
259
|
+
.filter(Boolean)
|
|
260
|
+
.join("\n");
|
|
261
|
+
}
|
|
262
|
+
|
|
274
263
|
function buildPanelTaskParams(
|
|
275
264
|
member: PanelMemberConfig,
|
|
276
265
|
prompt: string,
|
|
@@ -289,26 +278,6 @@ function buildPanelTaskParams(
|
|
|
289
278
|
};
|
|
290
279
|
}
|
|
291
280
|
|
|
292
|
-
function buildPanelChainTaskParams(
|
|
293
|
-
member: PanelMemberConfig,
|
|
294
|
-
index: number,
|
|
295
|
-
): PanelChainTaskParams {
|
|
296
|
-
const model = appendThinkingSuffix(member.model, member.thinking);
|
|
297
|
-
return {
|
|
298
|
-
agent: member.agent,
|
|
299
|
-
task: buildPanelTask(member, "{task}", false),
|
|
300
|
-
as: chainOutputName(member, index),
|
|
301
|
-
label: memberLabel(member),
|
|
302
|
-
phase: "Panel",
|
|
303
|
-
output: true,
|
|
304
|
-
outputMode: "inline",
|
|
305
|
-
progress: true,
|
|
306
|
-
skill: false,
|
|
307
|
-
acceptance: FUSION_ACCEPTANCE_DISABLED,
|
|
308
|
-
...(model ? { model } : {}),
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
281
|
function buildPanelTask(
|
|
313
282
|
member: PanelMemberConfig,
|
|
314
283
|
prompt: string,
|
|
@@ -461,30 +430,6 @@ function formatFacetAssignments(
|
|
|
461
430
|
});
|
|
462
431
|
}
|
|
463
432
|
|
|
464
|
-
function buildChainJudgeTask(panel: readonly PanelMemberConfig[]): string {
|
|
465
|
-
return [
|
|
466
|
-
"You are the fusion judge.",
|
|
467
|
-
"Read-only synthesis only. Leave files, git state, and the workspace untouched. Do not ask other agents. Do not run subagents.",
|
|
468
|
-
"Synthesize the panel results. Preserve disagreement instead of forcing consensus.",
|
|
469
|
-
"",
|
|
470
|
-
"Original task:",
|
|
471
|
-
"{task}",
|
|
472
|
-
"",
|
|
473
|
-
"All listed panelists completed successfully.",
|
|
474
|
-
"",
|
|
475
|
-
"Panel outputs:",
|
|
476
|
-
...panel.flatMap((member, index) => [
|
|
477
|
-
`## ${memberLabel(member)} (${member.id})`,
|
|
478
|
-
`Agent: ${member.agent}`,
|
|
479
|
-
"",
|
|
480
|
-
`{outputs.${chainOutputName(member, index)}}`,
|
|
481
|
-
"",
|
|
482
|
-
]),
|
|
483
|
-
"Output contract:",
|
|
484
|
-
...JUDGE_OUTPUT_CONTRACT,
|
|
485
|
-
].join("\n");
|
|
486
|
-
}
|
|
487
|
-
|
|
488
433
|
function formatPanelStatus(
|
|
489
434
|
outputs: readonly PanelOutput[],
|
|
490
435
|
failures: readonly FailedPanelSummary[],
|
|
@@ -625,12 +570,6 @@ function firstLine(value: string): string {
|
|
|
625
570
|
return value.split(/\r?\n/, 1)[0]?.trim() || "unknown failure";
|
|
626
571
|
}
|
|
627
572
|
|
|
628
|
-
function chainOutputName(member: PanelMemberConfig, index: number): string {
|
|
629
|
-
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(member.id)
|
|
630
|
-
? member.id
|
|
631
|
-
: `panel_${index + 1}`;
|
|
632
|
-
}
|
|
633
|
-
|
|
634
573
|
function hasThinkingSuffix(model: string): boolean {
|
|
635
574
|
const colonIndex = model.lastIndexOf(":");
|
|
636
575
|
if (colonIndex === -1) return false;
|