/** * Fusion Executor — multi-model panel execution inspired by: * - OpenRouter Fusion API (compound models via orchestration API) * - fusion-fable (fan-out panel → judge → structured synthesis) * - Fable 5's implicit multi-model orchestration capability * * The core pattern is the same across all three: * DRAFT → Run N panelists in parallel, same prompt, independent context * CRITIQUE → Judge evaluates every answer (consensus, contradictions, blind spots) * FUSE → Judge writes a grounded final answer from the analysis * * As a PhaseExecutor, this can be used directly in the feedback loop: * const fusion = new FusionExecutor(); * const result = await feedbackLoop.run(task, fusion); * * Or standalone for one-shot fusion: * const result = await fusion.fuse(task, "opus4.8-gpt5.5"); * // { analysis, finalAnswer, panelists } */ import type { LoopIteration } from "../../core/types.js"; import type { PhaseExecutor } from "../../tier2-primitives/loops/feedback-loop.js"; import { SafetyBoundary } from "../../upgrades/safety-boundary.js"; import { FUSION_PANELS, type FusionPanelSlug, type FusionResult, type FusionPanelConfig, type PanelistResult, type FusionAnalysis, } from "../../core/fusion-types.js"; import { callProxyChat } from "../../core/proxy-caller.js"; // ─── Configuration ────────────────────────────────────────── export interface FusionExecutorConfig { /** Default panel composition */ panel: FusionPanelSlug; /** Safety boundary for model routing */ safetyBoundary?: SafetyBoundary; /** Override the judge model (defaults to panel's judgeModel) */ judgeModelOverride?: string; /** Custom API call for a panelist model */ callModel?: (modelId: string, prompt: string, maxTokens: number) => Promise; /** Enable verbose logging */ verbose?: boolean; } // ─── Fusion Executor ──────────────────────────────────────── export class FusionExecutor implements PhaseExecutor { private config: FusionExecutorConfig; private panel: FusionPanelConfig; private safetyBoundary: SafetyBoundary; private totalCalls = 0; /** Stored from plan() so refine() can access it */ private currentTask = ""; constructor(config?: Partial) { this.config = { panel: "opus4.8-4.8", verbose: false, ...config, }; this.panel = FUSION_PANELS[this.config.panel]; this.safetyBoundary = this.config.safetyBoundary ?? new SafetyBoundary(); } // ── PhaseExecutor Interface ───────────────────────────── async plan(task: string, previousIteration: LoopIteration | null): Promise { // Fusion doesn't need a separate plan phase — the panel dispatch IS the plan this.currentTask = task; if (previousIteration) { return this.fuseWithContext(task, previousIteration); } return this.fuse(task, this.config.panel).then(r => r.finalAnswer); } async execute(plan: string): Promise { return plan; // Fusion already produced the answer } async observe(executed: string): Promise { return executed; } async reflect(observation: string, _previousIteration: LoopIteration | null): Promise { // Self-critique: reflect on the fused answer return this.critiqueAnswer(observation); } async refine(reflection: string): Promise { // Refine: re-fuse incorporating the reflection return this.fuseWithReflection(this.currentTask, reflection); } // ── Core Fusion API ───────────────────────────────────── /** * Run a complete fusion cycle: dispatch panel → analyze → synthesize. */ async fuse(task: string, panelSlug?: FusionPanelSlug): Promise { const panel = panelSlug ? FUSION_PANELS[panelSlug] : this.panel; const startTime = Date.now(); // 1. DRAFT — dispatch panelists in parallel const panelResults = await this.dispatchPanel(task, panel); // 2. CRITIQUE — analyze panelist responses const analysis = this.analyzePanel(panelResults); // 3. FUSE — synthesize final answer const finalAnswer = analysis.blindSpots.length > 0 ? await this.synthesizeFinal(task, panelResults, analysis) : panelResults[0]?.raw ?? "No panelists returned results."; return { task, panelSlug: panel.slug, panelSize: panel.modelIds.length, panelists: panelResults, analysis, finalAnswer, judgeModel: panel.judgeModel, totalDurationMs: Date.now() - startTime, error: null, }; } /** * One-shot fusion: just call a single model (OpenRouter Fusion API or direct). */ async callOnce(prompt: string, modelId?: string): Promise { const model = modelId ?? "claude-opus-4-8"; const route = this.safetyBoundary.route("reasoning"); return this.makeModelCall(model, prompt, 8192); } // ── Private: Panel Dispatch ────────────────────────────── private async dispatchPanel( task: string, panel: FusionPanelConfig, ): Promise { // Validate models through safety boundary const validModels = panel.modelIds.filter(id => { const route = this.safetyBoundary.route("reasoning"); return id !== "claude-fable-5" && id !== "mythos-5" && route.modelId !== "none"; }); if (validModels.length === 0) { return [this.emptyResult("No available models in panel")]; } // Build judge prompt using Fable 5-style framing const prompt = this.buildPanelPrompt(task); // Dispatch in parallel const startTime = Date.now(); const results = await Promise.allSettled( validModels.map((modelId, i) => this.callPanelist(modelId, prompt, panel, i)) ); const panelResults: PanelistResult[] = []; for (let i = 0; i < results.length; i++) { const r = results[i]; if (r.status === "fulfilled") { panelResults.push(r.value); } else { panelResults.push({ panelistIndex: i, modelId: validModels[i] ?? "unknown", modelName: validModels[i] ?? "unknown", raw: "", durationMs: Date.now() - startTime, tokenCount: 0, error: r.reason?.toString() ?? "Unknown error", }); } } return panelResults; } private async callPanelist( modelId: string, prompt: string, panel: FusionPanelConfig, index: number, ): Promise { const startTime = Date.now(); const raw = await this.makeModelCall(modelId, prompt, panel.maxTokensPerPanelist); return { panelistIndex: index, modelId, modelName: modelId, raw, durationMs: Date.now() - startTime, tokenCount: Math.ceil(raw.length / 4), error: null, }; } // ── Private: Analysis ─────────────────────────────────── private analyzePanel(panelists: PanelistResult[]): FusionAnalysis { const nonEmpty = panelists.filter(p => p.raw.length > 0 && !p.error); if (nonEmpty.length === 0) { return { consensus: [], contradictions: [], partialCoverage: [], uniqueInsights: [], blindSpots: ["No panelists produced results"], }; } if (nonEmpty.length === 1) { return { consensus: ["Single panelist — all findings treated as consensus"], contradictions: [], partialCoverage: [], uniqueInsights: [], blindSpots: ["Single-panelist fusion: no cross-verification available"], }; } // Extract common themes (simple heuristic: shared keywords/patterns) const allWords = nonEmpty.map(p => new Set(p.raw.toLowerCase().split(/\W+/).filter(w => w.length > 4))); const commonWords = [...allWords[0]].filter(w => allWords.every(s => s.has(w))); const consensus = commonWords.length > 0 ? [`Panel shares vocabulary around: ${commonWords.slice(0, 10).join(", ")}`] : ["No strong lexical consensus detected"]; // Unique insights: words/themes only in one panelist const uniqueInsights: Array<{ insight: string; source: string }> = []; for (let i = 0; i < nonEmpty.length; i++) { const unique = [...allWords[i]].filter(w => allWords.every((s, j) => j === i || !s.has(w)) ); if (unique.length > 0) { uniqueInsights.push({ insight: `Unique terms: ${unique.slice(0, 5).join(", ")}`, source: nonEmpty[i].modelId, }); } } // Blind spots: what no one mentioned const blindSpots = nonEmpty.length < panelists.length ? [`${panelists.length - nonEmpty.length} panelist(s) failed to respond`] : []; return { consensus, contradictions: [], partialCoverage: [`All ${nonEmpty.length} panelists produced responses`], uniqueInsights, blindSpots, }; } // ── Private: Synthesis ───────────────────────────────── private async synthesizeFinal( task: string, panelists: PanelistResult[], analysis: FusionAnalysis, ): Promise { const synthesisPrompt = this.buildSynthesisPrompt(task, panelists, analysis); return this.makeModelCall(this.panel.judgeModel, synthesisPrompt, 16384); } private async fuseWithContext(task: string, prev: LoopIteration): Promise { const prompt = [ `Previous iteration feedback:`, ` Reflection: ${prev.reflection ?? "none"}`, ` Refinement: ${prev.refinement ?? "none"}`, ``, `Continue the fusion for: ${task}`, ].join("\n"); return this.makeModelCall(this.panel.judgeModel, prompt, 8192); } private async fuseWithReflection(task: string, reflection: string): Promise { const prompt = [ `Reflection on previous fusion: ${reflection}`, ``, `Re-fuse with correction: ${task}`, ].join("\n"); return this.makeModelCall(this.panel.judgeModel, prompt, 8192); } private async critiqueAnswer(answer: string): Promise { const prompt = [ `Critique the following answer. Identify:`, ` 1. What is strongest about it`, ` 2. What is weakest or missing`, ` 3. What would you add or change`, ``, `Answer:`, answer, ].join("\n"); return this.makeModelCall(this.panel.judgeModel, prompt, 4096); } // ── Prompt Builders ───────────────────────────────────── /** * Fable 5-inspired prompt structure: * Goal-oriented (not step-oriented). Context + goal framing. * "I'm working on [larger goal]. With that in mind: [specific request]." */ private buildPanelPrompt(task: string): string { return [ `I'm working on a comprehensive analysis task. With that in mind:`, ``, task, ``, `Provide your complete, detailed response. Include specific reasoning,`, `evidence, and actionable conclusions. Be thorough.`, ].join("\n"); } private buildSynthesisPrompt( task: string, panelists: PanelistResult[], analysis: FusionAnalysis, ): string { const panelBlocks = panelists .filter(p => p.raw.length > 0 && !p.error) .map((p, i) => [ `--- Panelist ${i + 1}: ${p.modelId} ---`, p.raw, ].join("\n")) .join("\n\n"); return [ `You are the judge. A panel of models has independently answered the following task.`, ``, `Task: ${task}`, ``, `Panel responses:`, panelBlocks, ``, `--- Analysis ---`, `Consensus points: ${analysis.consensus.join("; ") || "none identified"}`, `Contradictions: ${analysis.contradictions.length}`, `Unique insights: ${analysis.uniqueInsights.map(i => i.insight).join("; ") || "none"}`, `Blind spots: ${analysis.blindSpots.join("; ") || "none identified"}`, ``, `Now produce a FINAL ANSWER that:`, `1. Synthesizes the consensus into a coherent position`, `2. Flags contradictions and explains why they exist`, `3. Incorporates unique insights from individual panelists`, `4. Acknowledges blind spots and limitations`, `5. Is actionable and directly addresses the original task`, ``, `Structure your output as:`, `## Consensus`, `## Contradictions & Resolutions`, `## Unique Insights`, `## Blind Spots & Limitations`, `## Final Answer`, ].join("\n"); } // ── Model Call ────────────────────────────────────────── private async makeModelCall( modelId: string, prompt: string, maxTokens: number, ): Promise { if (this.config.callModel) { return this.config.callModel(modelId, prompt, maxTokens); } this.totalCalls++; const prefix = `[Fusion call #${this.totalCalls} to ${modelId}]`; if (this.config.verbose) { console.error(`${prefix} sending ${prompt.length} chars, max ${maxTokens} tokens`); } if (modelId === "openrouter-fusion") { return `${prefix} OpenRouter Fusion not available through proxy`; } try { const result = await callProxyChat(modelId, prompt, maxTokens); if (this.config.verbose) { console.error(`${prefix} got ${result.length} chars back`); } return result; } catch (e) { const msg = (e as Error).message; console.error(`${prefix} ERROR: ${msg}`); return `${prefix} Error: ${msg}`; } } private emptyResult(reason: string): PanelistResult { return { panelistIndex: 0, modelId: "none", modelName: "none", raw: "", durationMs: 0, tokenCount: 0, error: reason, }; } }