@maestrofrontier/frontier 1.4.5 → 1.5.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/.agents/plugins/marketplace.json +21 -21
- package/.codex-plugin/plugin.json +29 -29
- package/.cursorrules +197 -194
- package/AGENTS.md +3 -3
- package/README.md +368 -368
- package/bin/maestro.cjs +75 -75
- package/commands/compress.md +36 -36
- package/commands/frontier.md +124 -124
- package/commands/terse.md +23 -23
- package/docs/codex.md +167 -167
- package/docs/orchestration.md +168 -168
- package/frontier/cli.cjs +279 -252
- package/frontier/config.cjs +468 -468
- package/frontier/dispatch.cjs +267 -255
- package/frontier/judge.cjs +92 -92
- package/frontier/run.cjs +201 -180
- package/frontier/schema.cjs +112 -112
- package/frontier/semaphore.cjs +49 -49
- package/frontier/synthesize.cjs +79 -79
- package/hooks/frontier-autorun.cjs +127 -120
- package/hooks/hooks.json +103 -103
- package/hooks/maestro-doctrine-guard.cjs +81 -81
- package/hooks/maestro-gate-reminder.cjs +22 -7
- package/hooks/maestro-gate-telemetry.cjs +79 -77
- package/hooks/maestro-phase-scope.cjs +118 -118
- package/hooks/maestro-statusline-sync.cjs +152 -152
- package/hooks/maestro-subagent-guard.cjs +148 -148
- package/hooks/maestro-terse-mode.cjs +189 -189
- package/hooks/maestro-toolbudget-advisory.cjs +127 -127
- package/integrations/README.md +111 -111
- package/integrations/cline/skills/frontier/SKILL.md +75 -75
- package/integrations/codex/prompts/frontier.md +70 -70
- package/integrations/codex/prompts/update.md +39 -39
- package/integrations/codex/skills/maestro-frontier/SKILL.md +122 -122
- package/integrations/codex/skills/maestro-settings/SKILL.md +55 -55
- package/integrations/codex/skills/maestro-terse/SKILL.md +58 -58
- package/integrations/codex/skills/maestro-update/SKILL.md +31 -31
- package/integrations/cursor/commands/frontier.md +63 -63
- package/integrations/cursor/commands/update.md +34 -34
- package/integrations/gemini/commands/frontier.toml +76 -76
- package/integrations/windsurf/workflows/frontier.md +70 -70
- package/package.json +58 -58
- package/scripts/install.cjs +1014 -1014
- package/settings/cli.cjs +140 -140
- package/settings/config.cjs +309 -309
- package/skills/maestro-frontier/SKILL.md +122 -122
- package/skills/maestro-settings/SKILL.md +55 -55
- package/skills/maestro-terse/SKILL.md +58 -58
- package/skills/maestro-update/SKILL.md +31 -31
- package/skills/terse/SKILL.md +74 -74
package/frontier/judge.cjs
CHANGED
|
@@ -1,92 +1,92 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Maestro Frontier — judge stage: build prompt + invoke Opus to produce Analysis.
|
|
3
|
-
|
|
4
|
-
'use strict';
|
|
5
|
-
|
|
6
|
-
const { parseAnalysis } = require('./schema.cjs');
|
|
7
|
-
const dispatch = require('./dispatch.cjs');
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Build the judge prompt for Opus.
|
|
11
|
-
* @param {string} userPrompt
|
|
12
|
-
* @param {import('./schema.cjs').PanelResponse[]} responses
|
|
13
|
-
* @param {object} cfg
|
|
14
|
-
* @returns {string}
|
|
15
|
-
*/
|
|
16
|
-
function buildJudgePrompt(userPrompt, responses, cfg) {
|
|
17
|
-
const sections = responses.map(
|
|
18
|
-
r => `### Response from ${r.model}\n${r.content}`
|
|
19
|
-
).join('\n\n');
|
|
20
|
-
|
|
21
|
-
const domainNote = (cfg.excluded_domains && cfg.excluded_domains.length > 0)
|
|
22
|
-
? `\nDisregard any claims sourced only from the following domains: ${cfg.excluded_domains.join(', ')}.\n`
|
|
23
|
-
: '';
|
|
24
|
-
|
|
25
|
-
return `You are a neutral JUDGE evaluating multiple AI panel responses to a user question.
|
|
26
|
-
|
|
27
|
-
USER QUESTION:
|
|
28
|
-
${userPrompt}
|
|
29
|
-
|
|
30
|
-
PANEL RESPONSES:
|
|
31
|
-
${sections}
|
|
32
|
-
${domainNote}
|
|
33
|
-
INSTRUCTIONS:
|
|
34
|
-
COMPARE the panel responses — do NOT merge or summarize them. Perform a structured analysis:
|
|
35
|
-
- consensus: points that all or most models agree on
|
|
36
|
-
- contradictions: topics where models disagree, with each model's specific stance
|
|
37
|
-
- partial_coverage: points that only some models covered (with which models)
|
|
38
|
-
- unique_insights: insights raised by exactly one model (with which model)
|
|
39
|
-
- blind_spots: important points that NO model addressed
|
|
40
|
-
|
|
41
|
-
OUTPUT IS ONLY a single JSON object with EXACTLY these keys:
|
|
42
|
-
consensus (string[])
|
|
43
|
-
contradictions (array of {topic: string, stances: [{model: string, stance: string}]})
|
|
44
|
-
partial_coverage (array of {models: string[], point: string})
|
|
45
|
-
unique_insights (array of {model: string, insight: string})
|
|
46
|
-
blind_spots (string[])
|
|
47
|
-
|
|
48
|
-
No prose before or after. No markdown fence. No extra keys. Output the raw JSON object only.`;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Run the judge stage. Returns Analysis or undefined on any failure (degrades gracefully).
|
|
53
|
-
* @param {string} userPrompt
|
|
54
|
-
* @param {import('./schema.cjs').PanelResponse[]} responses
|
|
55
|
-
* @param {object} cfg
|
|
56
|
-
* @param {{ spawn?: Function }} [deps]
|
|
57
|
-
* @returns {Promise<import('./schema.cjs').Analysis | undefined>}
|
|
58
|
-
*/
|
|
59
|
-
async function runJudge(userPrompt, responses, cfg, deps) {
|
|
60
|
-
const spawn = (deps && deps.spawn) || dispatch.spawnOne;
|
|
61
|
-
let r;
|
|
62
|
-
try {
|
|
63
|
-
r = await spawn(
|
|
64
|
-
buildJudgePrompt(userPrompt, responses, cfg),
|
|
65
|
-
cfg.adapters[cfg.judgeModel],
|
|
66
|
-
{ timeoutMs: cfg.timeoutMs, fusionDepth: 1 }
|
|
67
|
-
);
|
|
68
|
-
} catch {
|
|
69
|
-
return undefined;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!r || !r.ok || !r.content) return undefined;
|
|
73
|
-
|
|
74
|
-
// Primary parse
|
|
75
|
-
try {
|
|
76
|
-
return parseAnalysis(r.content);
|
|
77
|
-
} catch {
|
|
78
|
-
// substring recovery: find outermost { ... }
|
|
79
|
-
const first = r.content.indexOf('{');
|
|
80
|
-
const last = r.content.lastIndexOf('}');
|
|
81
|
-
if (first !== -1 && last > first) {
|
|
82
|
-
try {
|
|
83
|
-
return parseAnalysis(r.content.slice(first, last + 1));
|
|
84
|
-
} catch {
|
|
85
|
-
return undefined;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
module.exports = { buildJudgePrompt, runJudge };
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro Frontier — judge stage: build prompt + invoke Opus to produce Analysis.
|
|
3
|
+
|
|
4
|
+
'use strict';
|
|
5
|
+
|
|
6
|
+
const { parseAnalysis } = require('./schema.cjs');
|
|
7
|
+
const dispatch = require('./dispatch.cjs');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Build the judge prompt for Opus.
|
|
11
|
+
* @param {string} userPrompt
|
|
12
|
+
* @param {import('./schema.cjs').PanelResponse[]} responses
|
|
13
|
+
* @param {object} cfg
|
|
14
|
+
* @returns {string}
|
|
15
|
+
*/
|
|
16
|
+
function buildJudgePrompt(userPrompt, responses, cfg) {
|
|
17
|
+
const sections = responses.map(
|
|
18
|
+
r => `### Response from ${r.model}\n${r.content}`
|
|
19
|
+
).join('\n\n');
|
|
20
|
+
|
|
21
|
+
const domainNote = (cfg.excluded_domains && cfg.excluded_domains.length > 0)
|
|
22
|
+
? `\nDisregard any claims sourced only from the following domains: ${cfg.excluded_domains.join(', ')}.\n`
|
|
23
|
+
: '';
|
|
24
|
+
|
|
25
|
+
return `You are a neutral JUDGE evaluating multiple AI panel responses to a user question.
|
|
26
|
+
|
|
27
|
+
USER QUESTION:
|
|
28
|
+
${userPrompt}
|
|
29
|
+
|
|
30
|
+
PANEL RESPONSES:
|
|
31
|
+
${sections}
|
|
32
|
+
${domainNote}
|
|
33
|
+
INSTRUCTIONS:
|
|
34
|
+
COMPARE the panel responses — do NOT merge or summarize them. Perform a structured analysis:
|
|
35
|
+
- consensus: points that all or most models agree on
|
|
36
|
+
- contradictions: topics where models disagree, with each model's specific stance
|
|
37
|
+
- partial_coverage: points that only some models covered (with which models)
|
|
38
|
+
- unique_insights: insights raised by exactly one model (with which model)
|
|
39
|
+
- blind_spots: important points that NO model addressed
|
|
40
|
+
|
|
41
|
+
OUTPUT IS ONLY a single JSON object with EXACTLY these keys:
|
|
42
|
+
consensus (string[])
|
|
43
|
+
contradictions (array of {topic: string, stances: [{model: string, stance: string}]})
|
|
44
|
+
partial_coverage (array of {models: string[], point: string})
|
|
45
|
+
unique_insights (array of {model: string, insight: string})
|
|
46
|
+
blind_spots (string[])
|
|
47
|
+
|
|
48
|
+
No prose before or after. No markdown fence. No extra keys. Output the raw JSON object only.`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Run the judge stage. Returns Analysis or undefined on any failure (degrades gracefully).
|
|
53
|
+
* @param {string} userPrompt
|
|
54
|
+
* @param {import('./schema.cjs').PanelResponse[]} responses
|
|
55
|
+
* @param {object} cfg
|
|
56
|
+
* @param {{ spawn?: Function }} [deps]
|
|
57
|
+
* @returns {Promise<import('./schema.cjs').Analysis | undefined>}
|
|
58
|
+
*/
|
|
59
|
+
async function runJudge(userPrompt, responses, cfg, deps) {
|
|
60
|
+
const spawn = (deps && deps.spawn) || dispatch.spawnOne;
|
|
61
|
+
let r;
|
|
62
|
+
try {
|
|
63
|
+
r = await spawn(
|
|
64
|
+
buildJudgePrompt(userPrompt, responses, cfg),
|
|
65
|
+
cfg.adapters[cfg.judgeModel],
|
|
66
|
+
{ timeoutMs: cfg.timeoutMs, fusionDepth: 1 }
|
|
67
|
+
);
|
|
68
|
+
} catch {
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (!r || !r.ok || !r.content) return undefined;
|
|
73
|
+
|
|
74
|
+
// Primary parse
|
|
75
|
+
try {
|
|
76
|
+
return parseAnalysis(r.content);
|
|
77
|
+
} catch {
|
|
78
|
+
// substring recovery: find outermost { ... }
|
|
79
|
+
const first = r.content.indexOf('{');
|
|
80
|
+
const last = r.content.lastIndexOf('}');
|
|
81
|
+
if (first !== -1 && last > first) {
|
|
82
|
+
try {
|
|
83
|
+
return parseAnalysis(r.content.slice(first, last + 1));
|
|
84
|
+
} catch {
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { buildJudgePrompt, runJudge };
|
package/frontier/run.cjs
CHANGED
|
@@ -1,180 +1,201 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Maestro Frontier — mode router (off / single / fusion).
|
|
3
|
-
// runFrontier({prompt, state, cfg, deps}) -> Promise<FusionResult>.
|
|
4
|
-
//
|
|
5
|
-
// Re-grounding note (Fable T2): the engine fans a prompt to model CLIs
|
|
6
|
-
// that do NOT share this session's file/context. A member or synthesis
|
|
7
|
-
// output asserting "I can't see X" / "no such file" reflects that
|
|
8
|
-
// subprocess's blank context, not ground truth — re-ground such claims
|
|
9
|
-
// against live context before relaying them to the user (AGENTS.md S7.7).
|
|
10
|
-
|
|
11
|
-
'use strict';
|
|
12
|
-
|
|
13
|
-
const { DEFAULTS } = require('./config.cjs');
|
|
14
|
-
const { resolvePanel, resolveJudgeModel, resolveSynthModel } = require('./config.cjs');
|
|
15
|
-
const { classify, toFailedModel } = require('./schema.cjs');
|
|
16
|
-
const dispatch = require('./dispatch.cjs');
|
|
17
|
-
const judge = require('./judge.cjs');
|
|
18
|
-
const synthesize = require('./synthesize.cjs');
|
|
19
|
-
|
|
20
|
-
const MODEL_ALIASES = {
|
|
21
|
-
chatgpt: 'gpt-5.5',
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const PRESET_ALIASES = {
|
|
25
|
-
'chatgpt-duo': 'gpt-duo',
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
/** @param {string} model @returns {string} */
|
|
29
|
-
function canonicalModelId(model) {
|
|
30
|
-
return MODEL_ALIASES[model] || model;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** @param {string} preset @returns {string} */
|
|
34
|
-
function canonicalPresetId(preset) {
|
|
35
|
-
return PRESET_ALIASES[preset] || preset;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** @param {object} state @returns {object} */
|
|
39
|
-
function normalizeStateAliases(state) {
|
|
40
|
-
const normalized = { ...state };
|
|
41
|
-
if (normalized.model) normalized.model = canonicalModelId(normalized.model);
|
|
42
|
-
if (normalized.preset) normalized.preset = canonicalPresetId(normalized.preset);
|
|
43
|
-
if (Array.isArray(normalized.models)) {
|
|
44
|
-
normalized.models = normalized.models.map(canonicalModelId);
|
|
45
|
-
}
|
|
46
|
-
if (normalized.judgeModel) normalized.judgeModel = canonicalModelId(normalized.judgeModel);
|
|
47
|
-
if (normalized.synthModel) normalized.synthModel = canonicalModelId(normalized.synthModel);
|
|
48
|
-
return normalized;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* @param {{ prompt:string, state:object, cfg?:object, deps?:object }} opts
|
|
53
|
-
* @returns {Promise<object>}
|
|
54
|
-
*/
|
|
55
|
-
async function runFrontier({ prompt, state, cfg, deps }) {
|
|
56
|
-
cfg = cfg || DEFAULTS;
|
|
57
|
-
deps = deps || {};
|
|
58
|
-
state = normalizeStateAliases(state || {});
|
|
59
|
-
|
|
60
|
-
const spawnOne
|
|
61
|
-
const fanOut
|
|
62
|
-
const runJudge
|
|
63
|
-
const runSynth
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
model: state.model,
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Maestro Frontier — mode router (off / single / fusion).
|
|
3
|
+
// runFrontier({prompt, state, cfg, deps}) -> Promise<FusionResult>.
|
|
4
|
+
//
|
|
5
|
+
// Re-grounding note (Fable T2): the engine fans a prompt to model CLIs
|
|
6
|
+
// that do NOT share this session's file/context. A member or synthesis
|
|
7
|
+
// output asserting "I can't see X" / "no such file" reflects that
|
|
8
|
+
// subprocess's blank context, not ground truth — re-ground such claims
|
|
9
|
+
// against live context before relaying them to the user (AGENTS.md S7.7).
|
|
10
|
+
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
const { DEFAULTS } = require('./config.cjs');
|
|
14
|
+
const { resolvePanel, resolveJudgeModel, resolveSynthModel } = require('./config.cjs');
|
|
15
|
+
const { classify, toFailedModel } = require('./schema.cjs');
|
|
16
|
+
const dispatch = require('./dispatch.cjs');
|
|
17
|
+
const judge = require('./judge.cjs');
|
|
18
|
+
const synthesize = require('./synthesize.cjs');
|
|
19
|
+
|
|
20
|
+
const MODEL_ALIASES = {
|
|
21
|
+
chatgpt: 'gpt-5.5',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const PRESET_ALIASES = {
|
|
25
|
+
'chatgpt-duo': 'gpt-duo',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** @param {string} model @returns {string} */
|
|
29
|
+
function canonicalModelId(model) {
|
|
30
|
+
return MODEL_ALIASES[model] || model;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** @param {string} preset @returns {string} */
|
|
34
|
+
function canonicalPresetId(preset) {
|
|
35
|
+
return PRESET_ALIASES[preset] || preset;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @param {object} state @returns {object} */
|
|
39
|
+
function normalizeStateAliases(state) {
|
|
40
|
+
const normalized = { ...state };
|
|
41
|
+
if (normalized.model) normalized.model = canonicalModelId(normalized.model);
|
|
42
|
+
if (normalized.preset) normalized.preset = canonicalPresetId(normalized.preset);
|
|
43
|
+
if (Array.isArray(normalized.models)) {
|
|
44
|
+
normalized.models = normalized.models.map(canonicalModelId);
|
|
45
|
+
}
|
|
46
|
+
if (normalized.judgeModel) normalized.judgeModel = canonicalModelId(normalized.judgeModel);
|
|
47
|
+
if (normalized.synthModel) normalized.synthModel = canonicalModelId(normalized.synthModel);
|
|
48
|
+
return normalized;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @param {{ prompt:string, state:object, cfg?:object, deps?:object }} opts
|
|
53
|
+
* @returns {Promise<object>}
|
|
54
|
+
*/
|
|
55
|
+
async function runFrontier({ prompt, state, cfg, deps }) {
|
|
56
|
+
cfg = cfg || DEFAULTS;
|
|
57
|
+
deps = deps || {};
|
|
58
|
+
state = normalizeStateAliases(state || {});
|
|
59
|
+
|
|
60
|
+
const spawnOne = deps.spawnOne || dispatch.spawnOne;
|
|
61
|
+
const fanOut = deps.fanOut || dispatch.fanOut;
|
|
62
|
+
const runJudge = deps.runJudge || judge.runJudge;
|
|
63
|
+
const runSynth = deps.runSynth || synthesize.runSynth;
|
|
64
|
+
const onProgress = (deps && typeof deps.onProgress === 'function') ? deps.onProgress : null;
|
|
65
|
+
|
|
66
|
+
const startMs = Date.now();
|
|
67
|
+
|
|
68
|
+
/** Emit a progress event; swallows any error thrown by the callback. */
|
|
69
|
+
function emit(eventObj) {
|
|
70
|
+
if (!onProgress) return;
|
|
71
|
+
try { onProgress(eventObj); } catch (_) {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const rawDepth = parseInt(process.env.FUSION_DEPTH || '0', 10);
|
|
75
|
+
const depth = isNaN(rawDepth) ? 0 : rawDepth;
|
|
76
|
+
|
|
77
|
+
// ---- OFF ----
|
|
78
|
+
if (state.mode === 'off') {
|
|
79
|
+
return { status: 'off', mode: 'off', final: null };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---- RECURSION GUARD (single + fusion) ----
|
|
83
|
+
if (depth >= 1) {
|
|
84
|
+
const base = {
|
|
85
|
+
status: 'error',
|
|
86
|
+
mode: state.mode,
|
|
87
|
+
error: 'fusion depth exceeded (one-level cap)',
|
|
88
|
+
failure_reason: 'fusion_invocation_capped',
|
|
89
|
+
};
|
|
90
|
+
if (state.mode === 'fusion') return { ...base, preset: state.preset };
|
|
91
|
+
if (state.mode === 'single') return { ...base, model: state.model };
|
|
92
|
+
return base;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- SINGLE ----
|
|
96
|
+
if (state.mode === 'single') {
|
|
97
|
+
const adapter = cfg.adapters && cfg.adapters[state.model];
|
|
98
|
+
if (!adapter) {
|
|
99
|
+
return {
|
|
100
|
+
status: 'error',
|
|
101
|
+
mode: 'single',
|
|
102
|
+
model: state.model,
|
|
103
|
+
error: 'unknown model: ' + state.model,
|
|
104
|
+
failure_reason: 'unexpected_error',
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
emit({ phase: 'single-start', model: state.model });
|
|
108
|
+
const resp = await spawnOne(prompt, adapter, { timeoutMs: cfg.timeoutMs, fusionDepth: depth + 1 });
|
|
109
|
+
if (!resp.ok) {
|
|
110
|
+
return {
|
|
111
|
+
status: 'error',
|
|
112
|
+
mode: 'single',
|
|
113
|
+
model: state.model,
|
|
114
|
+
error: resp.error,
|
|
115
|
+
failure_reason: classify([toFailedModel(resp)]),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
emit({ phase: 'done', models: 1, ms: Date.now() - startMs });
|
|
119
|
+
return { status: 'ok', mode: 'single', model: state.model, final: resp.content, response: resp };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- FUSION ----
|
|
123
|
+
if (state.mode === 'fusion') {
|
|
124
|
+
// resolve panel
|
|
125
|
+
let panelIds;
|
|
126
|
+
try {
|
|
127
|
+
panelIds = resolvePanel(state, cfg);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
return {
|
|
130
|
+
status: 'error',
|
|
131
|
+
mode: 'fusion',
|
|
132
|
+
preset: state.preset,
|
|
133
|
+
error: e.message,
|
|
134
|
+
failure_reason: 'unexpected_error',
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// BUDGET (opt-in)
|
|
139
|
+
if (cfg.tokenBudget && cfg.tokenBudget > 0) {
|
|
140
|
+
const projected = Math.ceil(prompt.length / 4) * panelIds.length;
|
|
141
|
+
if (projected > cfg.tokenBudget) {
|
|
142
|
+
return {
|
|
143
|
+
status: 'error',
|
|
144
|
+
mode: 'fusion',
|
|
145
|
+
preset: state.preset,
|
|
146
|
+
error: 'projected token budget exceeded (' + projected + '>' + cfg.tokenBudget + ')',
|
|
147
|
+
failure_reason: 'unexpected_error',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
emit({ phase: 'panel-start', models: panelIds });
|
|
153
|
+
const panel = await fanOut(prompt, panelIds, cfg, { fusionDepth: depth + 1, onProgress });
|
|
154
|
+
const ok = panel.filter(p => p.ok);
|
|
155
|
+
const failed = panel.filter(p => !p.ok);
|
|
156
|
+
|
|
157
|
+
emit({ phase: 'panel-done', ok: ok.length, failed: failed.length });
|
|
158
|
+
|
|
159
|
+
if (ok.length === 0) {
|
|
160
|
+
return {
|
|
161
|
+
status: 'error',
|
|
162
|
+
mode: 'fusion',
|
|
163
|
+
preset: state.preset,
|
|
164
|
+
error: 'all panels failed',
|
|
165
|
+
failure_reason: classify(failed.map(toFailedModel)),
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Resolve judge/synth model (explicit flag -> preset override -> default)
|
|
170
|
+
// and hand the judge/synth stages a cfg pinned to those models, so a
|
|
171
|
+
// preset like gpt-duo runs entirely on its own provider.
|
|
172
|
+
const stageCfg = {
|
|
173
|
+
...cfg,
|
|
174
|
+
judgeModel: resolveJudgeModel(state, cfg),
|
|
175
|
+
synthModel: resolveSynthModel(state, cfg),
|
|
176
|
+
};
|
|
177
|
+
emit({ phase: 'judge-start', model: stageCfg.judgeModel });
|
|
178
|
+
const analysis = await runJudge(prompt, ok, stageCfg);
|
|
179
|
+
emit({ phase: 'synth-start', model: stageCfg.synthModel });
|
|
180
|
+
let final = await runSynth(prompt, { analysis, responses: ok }, stageCfg);
|
|
181
|
+
if (!final) {
|
|
182
|
+
// synth-fail fallback: longest ok response
|
|
183
|
+
final = ok.reduce((a, b) => b.content.length > a.content.length ? b : a).content;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (failed.length > 0 && ok.length > 0) {
|
|
187
|
+
emit({ phase: 'degraded', failed: failed.length });
|
|
188
|
+
}
|
|
189
|
+
emit({ phase: 'done', models: ok.length, ms: Date.now() - startMs });
|
|
190
|
+
|
|
191
|
+
const result = { status: 'ok', mode: 'fusion', preset: state.preset, final, responses: ok };
|
|
192
|
+
if (analysis !== undefined) result.analysis = analysis;
|
|
193
|
+
if (failed.length) result.failed_models = failed.map(toFailedModel);
|
|
194
|
+
return result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// ---- UNKNOWN MODE ----
|
|
198
|
+
return { status: 'error', mode: state.mode, error: 'unknown mode', failure_reason: 'unexpected_error' };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = { runFrontier, canonicalModelId, canonicalPresetId, normalizeStateAliases };
|