@alexeiled/pi-fusion 0.6.0 → 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 +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 +60 -131
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.42.1 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.42.1 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,45 +36,12 @@ 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
|
-
|
|
47
|
-
async: true;
|
|
48
|
-
clarify: false;
|
|
49
|
-
concurrency: number;
|
|
50
|
-
context: "fresh" | "fork";
|
|
51
|
-
output: true;
|
|
52
|
-
outputMode: "inline";
|
|
53
|
-
acceptance: FusionAcceptanceDisabled;
|
|
54
|
-
timeoutMs?: number;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface FusionChainParallelStepParams {
|
|
58
|
-
parallel: PanelChainTaskParams[];
|
|
59
|
-
concurrency: number;
|
|
60
|
-
failFast: false;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export interface FusionChainJudgeStepParams {
|
|
64
|
-
agent: string;
|
|
65
|
-
task: string;
|
|
66
|
-
label: "Judge";
|
|
67
|
-
phase: "Judge";
|
|
68
|
-
output: true;
|
|
69
|
-
outputMode: "inline";
|
|
70
|
-
skill: false;
|
|
71
|
-
acceptance: FusionAcceptanceDisabled;
|
|
72
|
-
model?: string;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
export interface FusionChainSpawnParams {
|
|
76
|
-
chain: [FusionChainParallelStepParams, FusionChainJudgeStepParams];
|
|
77
|
-
task: string;
|
|
44
|
+
workflowScript: string;
|
|
78
45
|
async: true;
|
|
79
46
|
clarify: false;
|
|
80
47
|
context: "fresh" | "fork";
|
|
@@ -181,57 +148,24 @@ export function buildPanelSpawnParams(
|
|
|
181
148
|
profile: FusionProfile,
|
|
182
149
|
prompt: string,
|
|
183
150
|
): PanelSpawnParams {
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
151
|
+
const concurrency = profile.concurrency ?? profile.panel.length;
|
|
152
|
+
const tasks: PanelWorkflowTaskParams[] = profile.panel.map(
|
|
153
|
+
(member, index) => ({
|
|
154
|
+
key: `panel-${index + 1}`,
|
|
155
|
+
...buildPanelTaskParams(
|
|
187
156
|
member,
|
|
188
157
|
prompt,
|
|
189
158
|
profile.stopWhenPanelAgrees === true,
|
|
190
159
|
),
|
|
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,
|
|
160
|
+
}),
|
|
212
161
|
);
|
|
162
|
+
|
|
213
163
|
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(),
|
|
164
|
+
workflowScript: buildPanelWorkflowScript(
|
|
165
|
+
tasks,
|
|
166
|
+
concurrency,
|
|
167
|
+
profile.stopWhenPanelAgrees === true,
|
|
168
|
+
),
|
|
235
169
|
async: true,
|
|
236
170
|
clarify: false,
|
|
237
171
|
context: profile.context ?? "fresh",
|
|
@@ -271,6 +205,51 @@ export function buildJudgeSpawnParams(
|
|
|
271
205
|
};
|
|
272
206
|
}
|
|
273
207
|
|
|
208
|
+
function buildPanelWorkflowScript(
|
|
209
|
+
tasks: readonly PanelWorkflowTaskParams[],
|
|
210
|
+
concurrency: number,
|
|
211
|
+
stopWhenAgrees: boolean,
|
|
212
|
+
): string {
|
|
213
|
+
const serializedTasks = JSON.stringify(tasks);
|
|
214
|
+
const effectiveConcurrency = stopWhenAgrees
|
|
215
|
+
? Math.min(concurrency, 2)
|
|
216
|
+
: concurrency;
|
|
217
|
+
const stopLogic = stopWhenAgrees
|
|
218
|
+
? [
|
|
219
|
+
"const decisions = results",
|
|
220
|
+
" .filter((result) => result && result.ok === true)",
|
|
221
|
+
" .map((result) => {",
|
|
222
|
+
" const text = typeof result.output === \"string\" ? result.output : \"\";",
|
|
223
|
+
" const match = text.match(/<fusion-panel-decision>([\\s\\S]*?)<\\/fusion-panel-decision>\\s*$/);",
|
|
224
|
+
" if (!match) return undefined;",
|
|
225
|
+
" try { return JSON.parse(match[1]); } catch { return undefined; }",
|
|
226
|
+
" })",
|
|
227
|
+
" .filter((decision) => decision && typeof decision.recommendation === \"string\" && decision.confidence === \"high\" && decision.needsMoreEvidence === false);",
|
|
228
|
+
"if (decisions.length >= 2 && results.length < tasks.length) {",
|
|
229
|
+
" const recommendation = decisions[0].recommendation.trim().toLocaleLowerCase().replace(/[^\\p{L}\\p{N}]+/gu, \" \" ).trim(),",
|
|
230
|
+
" agrees = recommendation && decisions.every((decision) => decision.recommendation.trim().toLocaleLowerCase().replace(/[^\\p{L}\\p{N}]+/gu, \" \" ).trim() === recommendation);",
|
|
231
|
+
" if (agrees) {",
|
|
232
|
+
" emit({ type: \"pi-fusion-panel-stop\", indices: tasks.slice(results.length).map((_, index) => results.length + index) });",
|
|
233
|
+
" return results;",
|
|
234
|
+
" }",
|
|
235
|
+
"}",
|
|
236
|
+
].join("\n")
|
|
237
|
+
: "";
|
|
238
|
+
|
|
239
|
+
return [
|
|
240
|
+
`const tasks = ${serializedTasks};`,
|
|
241
|
+
`const concurrency = ${effectiveConcurrency};`,
|
|
242
|
+
"const results = [];",
|
|
243
|
+
"for (let index = 0; index < tasks.length; index += concurrency) {",
|
|
244
|
+
" results.push(...await runs.all(tasks.slice(index, index + concurrency)));",
|
|
245
|
+
stopWhenAgrees ? " " + stopLogic.replaceAll("\n", "\n ") : "",
|
|
246
|
+
"}",
|
|
247
|
+
"return results;",
|
|
248
|
+
]
|
|
249
|
+
.filter(Boolean)
|
|
250
|
+
.join("\n");
|
|
251
|
+
}
|
|
252
|
+
|
|
274
253
|
function buildPanelTaskParams(
|
|
275
254
|
member: PanelMemberConfig,
|
|
276
255
|
prompt: string,
|
|
@@ -289,26 +268,6 @@ function buildPanelTaskParams(
|
|
|
289
268
|
};
|
|
290
269
|
}
|
|
291
270
|
|
|
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
271
|
function buildPanelTask(
|
|
313
272
|
member: PanelMemberConfig,
|
|
314
273
|
prompt: string,
|
|
@@ -461,30 +420,6 @@ function formatFacetAssignments(
|
|
|
461
420
|
});
|
|
462
421
|
}
|
|
463
422
|
|
|
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
423
|
function formatPanelStatus(
|
|
489
424
|
outputs: readonly PanelOutput[],
|
|
490
425
|
failures: readonly FailedPanelSummary[],
|
|
@@ -625,12 +560,6 @@ function firstLine(value: string): string {
|
|
|
625
560
|
return value.split(/\r?\n/, 1)[0]?.trim() || "unknown failure";
|
|
626
561
|
}
|
|
627
562
|
|
|
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
563
|
function hasThinkingSuffix(model: string): boolean {
|
|
635
564
|
const colonIndex = model.lastIndexOf(":");
|
|
636
565
|
if (colonIndex === -1) return false;
|