390 lines
13 KiB
TypeScript
390 lines
13 KiB
TypeScript
import type { LoopIteration, GodModeRaceResult, PerturbationResult } from "../core/types.js";
|
|
import { IndependentVerifier, type VerificationResult } from "./independent-verifier.js";
|
|
import { ModelRouter, type TaskComplexity, type RoutingResponse } from "./model-router.js";
|
|
import {
|
|
type FusionPanelSlug,
|
|
type FusionResult,
|
|
FUSION_PANELS,
|
|
type GodModePanelSlug,
|
|
} from "../core/fusion-types.js";
|
|
import { GodModeClassic } from "../upgrades/godmode-classic.js";
|
|
import { Parseltongue } from "../upgrades/parseltongue.js";
|
|
|
|
// ── Types ──────────────────────────────────────────────────
|
|
|
|
export type WorkflowPattern = "fan-out-synthesize" | "adversarial-verify" | "loop-until-done" | "fusion-panel" | "godmode-race" | "parseltongue-test";
|
|
export type FanOutSubtaskFlow = "direct" | "planning" | "review";
|
|
|
|
export interface FanOutSubtaskRouteContext {
|
|
task: string;
|
|
subTask: string;
|
|
index: number;
|
|
flow: FanOutSubtaskFlow;
|
|
reason: string;
|
|
route: RoutingResponse;
|
|
}
|
|
|
|
export type FanOutExecutor = (
|
|
subTask: string,
|
|
index: number,
|
|
routeContext?: FanOutSubtaskRouteContext,
|
|
) => LoopIteration | Promise<LoopIteration>;
|
|
|
|
export interface WorkflowStep {
|
|
id: string;
|
|
label: string;
|
|
agent: (ctx: Record<string, unknown>) => LoopIteration | Promise<LoopIteration>;
|
|
dependsOn?: string[];
|
|
}
|
|
|
|
export interface WorkflowDefinition {
|
|
id: string;
|
|
task: string;
|
|
pattern: WorkflowPattern;
|
|
steps: WorkflowStep[];
|
|
maxConcurrency?: number;
|
|
}
|
|
|
|
export interface MixtureOfAgentsMetadata {
|
|
provider: "fable-fan-out";
|
|
workers: number;
|
|
synthesizer: "caller-provided";
|
|
maxConcurrency: number;
|
|
}
|
|
|
|
export interface FanOutResult {
|
|
iterations: LoopIteration[];
|
|
routeContexts: FanOutSubtaskRouteContext[];
|
|
synthesized: LoopIteration;
|
|
synthesisVerdict: VerificationResult;
|
|
mixtureOfAgents: MixtureOfAgentsMetadata;
|
|
}
|
|
|
|
export interface AdversarialResult {
|
|
maker: LoopIteration;
|
|
verifier: VerificationResult;
|
|
passed: boolean;
|
|
gapSummary: string;
|
|
}
|
|
|
|
export interface LoopUntilDoneResult {
|
|
iterations: LoopIteration[];
|
|
finalVerdict: VerificationResult;
|
|
totalRounds: number;
|
|
}
|
|
|
|
export interface FusionPanelResult {
|
|
task: string;
|
|
panelSlug: FusionPanelSlug;
|
|
panelists: LoopIteration[];
|
|
synthesized: LoopIteration;
|
|
synthesisVerdict: VerificationResult;
|
|
fusionAnalysis: string;
|
|
}
|
|
|
|
// ── Dynamic Workflows ──────────────────────────────────────
|
|
|
|
export class DynamicWorkflows {
|
|
private verifier: IndependentVerifier;
|
|
private modelRouter: ModelRouter;
|
|
private godMode: GodModeClassic | null;
|
|
private parseltongue: Parseltongue | null;
|
|
|
|
constructor(verifier?: IndependentVerifier, modelRouter?: ModelRouter) {
|
|
this.verifier = verifier ?? new IndependentVerifier();
|
|
this.modelRouter = modelRouter ?? new ModelRouter();
|
|
this.godMode = null;
|
|
this.parseltongue = null;
|
|
}
|
|
|
|
/**
|
|
* Enable GodMode Classic racing workflows.
|
|
*/
|
|
enableGodMode(gm?: GodModeClassic): void {
|
|
this.godMode = gm ?? new GodModeClassic();
|
|
}
|
|
|
|
/**
|
|
* Enable Parseltongue perturbation testing workflows.
|
|
*/
|
|
enableParseltongue(pt?: Parseltongue): void {
|
|
this.parseltongue = pt ?? new Parseltongue();
|
|
}
|
|
|
|
/**
|
|
* Fan-out-and-synthesize: split work into N independent pieces,
|
|
* run an agent on each in parallel, synthesize results.
|
|
*
|
|
* Best when each sub-task benefits from its own clean context window —
|
|
* e.g., evaluating each rule in a Skill against historical examples.
|
|
*/
|
|
async fanOutAndSynthesize(
|
|
task: string,
|
|
subTasks: string[],
|
|
executor: FanOutExecutor,
|
|
synthesizer: (results: LoopIteration[], originalTask: string) => LoopIteration | Promise<LoopIteration>,
|
|
maxConcurrency: number = 3,
|
|
): Promise<FanOutResult> {
|
|
// Phase 1: Fan out — run all sub-tasks (with concurrency limit)
|
|
const routeContexts: FanOutSubtaskRouteContext[] = new Array(subTasks.length);
|
|
const iterations = await this.mapWithConcurrency(
|
|
subTasks.length,
|
|
maxConcurrency,
|
|
(index) => {
|
|
const subTask = subTasks[index];
|
|
const routeContext = this.routeFanOutSubtask(task, subTask, index);
|
|
routeContexts[index] = routeContext;
|
|
return Promise.resolve(executor(subTask, index, routeContext));
|
|
},
|
|
);
|
|
|
|
// Phase 2: Synthesize
|
|
const synthesized = await Promise.resolve(synthesizer(iterations, task));
|
|
|
|
// Phase 3: Verify the synthesis
|
|
const synthesisVerdict = this.verifier.verify(synthesized, task);
|
|
|
|
return {
|
|
iterations,
|
|
routeContexts,
|
|
synthesized,
|
|
synthesisVerdict,
|
|
mixtureOfAgents: {
|
|
provider: "fable-fan-out",
|
|
workers: subTasks.length,
|
|
synthesizer: "caller-provided",
|
|
maxConcurrency,
|
|
},
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Adversarial verification: for a maker agent, spawn an independent verifier
|
|
* with no exposure to the maker's reasoning. The structural fix for
|
|
* self-preferential bias.
|
|
*
|
|
* The maker cannot see the verifier's criteria evaluation; the verifier only
|
|
* sees the artifact and the rubric. This guarantees independent assessment.
|
|
*/
|
|
async adversarialVerify(
|
|
makerIteration: LoopIteration,
|
|
task: string,
|
|
verifierConfig?: { criteria?: Array<{ id: string; label: string; description: string }>; minPassRate?: number },
|
|
): Promise<AdversarialResult> {
|
|
// Create an independent verifier with optional custom criteria
|
|
const verifier = verifierConfig
|
|
? new IndependentVerifier({
|
|
criteria: verifierConfig.criteria ?? [
|
|
{ id: "correctness", label: "Correctness", description: "Output is technically correct" },
|
|
{ id: "completeness", label: "Completeness", description: "All requirements met" },
|
|
{ id: "consistency", label: "Consistency", description: "No contradictions within output" },
|
|
],
|
|
minPassRate: verifierConfig.minPassRate ?? 0.75,
|
|
requireAllEssential: true,
|
|
})
|
|
: this.verifier;
|
|
|
|
// The verifier sees ONLY the artifact (executed/observation), NOT the reasoning trail
|
|
const sanitized: LoopIteration = {
|
|
...makerIteration,
|
|
plan: "",
|
|
reflection: "",
|
|
refinement: "",
|
|
};
|
|
|
|
const result = verifier.verify(sanitized, task);
|
|
const passed = result.verdict === "PASS";
|
|
const gapSummary = result.gaps.length > 0 ? result.gaps.join("; ") : "All criteria met";
|
|
|
|
return { maker: makerIteration, verifier: result, passed, gapSummary };
|
|
}
|
|
|
|
/**
|
|
* Loop-until-done: loop spawning agents until a stop condition is met.
|
|
* Pairs with /goal to set a hard completion requirement.
|
|
*/
|
|
async loopUntilDone(
|
|
task: string,
|
|
executor: (iteration: number, previous: LoopIteration | null) => LoopIteration | Promise<LoopIteration>,
|
|
options?: {
|
|
maxIterations?: number;
|
|
convergenceThreshold?: number;
|
|
stopCondition?: (iteration: LoopIteration, result: VerificationResult) => boolean;
|
|
},
|
|
): Promise<LoopUntilDoneResult> {
|
|
const maxIterations = options?.maxIterations ?? 10;
|
|
const convergenceThreshold = options?.convergenceThreshold ?? 0.75;
|
|
const iterations: LoopIteration[] = [];
|
|
let finalVerdict: VerificationResult = {
|
|
verdict: "FAIL",
|
|
criteriaResults: [],
|
|
gaps: ["No iterations completed"],
|
|
matches: [],
|
|
suggestion: "No iterations ran",
|
|
};
|
|
|
|
for (let i = 0; i < maxIterations; i++) {
|
|
const previous = iterations.length > 0 ? iterations[iterations.length - 1] : null;
|
|
const iteration = await Promise.resolve(executor(i, previous));
|
|
iterations.push(iteration);
|
|
|
|
finalVerdict = this.verifier.verify(iteration, task);
|
|
const passRate = finalVerdict.criteriaResults.filter((r) => r.passed).length / Math.max(finalVerdict.criteriaResults.length, 1);
|
|
|
|
// Check stop conditions
|
|
if (options?.stopCondition && options.stopCondition(iteration, finalVerdict)) {
|
|
break;
|
|
}
|
|
if (finalVerdict.verdict === "PASS" && passRate >= convergenceThreshold) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return { iterations, finalVerdict, totalRounds: iterations.length };
|
|
}
|
|
|
|
/**
|
|
* Fusion Panel: dispatch the same task to N panelists in parallel,
|
|
* then synthesize results with the judge model.
|
|
*
|
|
* Implements the draft → critique → fuse pattern from fusion-fable:
|
|
* - All panelists get the same prompt, independently
|
|
* - No "lenses" or personas — just raw independent execution
|
|
* - Judge synthesizes with structured analysis
|
|
*/
|
|
async fusionPanel(
|
|
task: string,
|
|
executor: (prompt: string, panelistIndex: number) => LoopIteration | Promise<LoopIteration>,
|
|
judge: (panelists: LoopIteration[], originalTask: string) => LoopIteration | Promise<LoopIteration>,
|
|
panelSlug: FusionPanelSlug = "opus4.8-4.8",
|
|
maxConcurrency: number = 3,
|
|
): Promise<FusionPanelResult> {
|
|
const panel = FUSION_PANELS[panelSlug];
|
|
const panelistCount = panel.modelIds.length;
|
|
|
|
// Phase 1: Run all panelists in parallel (with concurrency limit)
|
|
const panelists = await this.mapWithConcurrency(
|
|
panelistCount,
|
|
maxConcurrency,
|
|
(index) => Promise.resolve(executor(task, index)),
|
|
);
|
|
|
|
// Phase 2: Judge synthesizes
|
|
const synthesized = await Promise.resolve(judge(panelists, task));
|
|
|
|
// Phase 3: Verify synthesis
|
|
const synthesisVerdict = this.verifier.verify(synthesized, task);
|
|
|
|
return {
|
|
task,
|
|
panelSlug,
|
|
panelists,
|
|
synthesized,
|
|
synthesisVerdict,
|
|
fusionAnalysis: synthesized.observation ?? "Fusion complete",
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Get the recommended grader model for a workflow subtask.
|
|
* Routes by complexity — cheap graders for simple tasks.
|
|
*/
|
|
getGraderForWorkflow(complexity: TaskComplexity): string {
|
|
return this.modelRouter.getGraderForWorkflow(complexity);
|
|
}
|
|
|
|
// ── GodMode Race ────────────────────────────────────────────
|
|
|
|
/**
|
|
* GodMode Race: Run N models in parallel, return best/first result.
|
|
* Inspired by G0DM0D3's GODMODE CLASSIC.
|
|
*/
|
|
async godmodeRace(
|
|
task: string,
|
|
panelSlug?: GodModePanelSlug,
|
|
): Promise<GodModeRaceResult> {
|
|
if (!this.godMode) {
|
|
this.enableGodMode();
|
|
}
|
|
const gm = this.godMode!;
|
|
return await gm.race(task, panelSlug);
|
|
}
|
|
|
|
// ── Parseltongue Test ───────────────────────────────────────
|
|
|
|
/**
|
|
* Parseltongue Test: Run perturbation suite against a safety gate.
|
|
* Inspired by G0DM0D3's Parseltongue red-teaming engine.
|
|
*/
|
|
async parseltongueTest(
|
|
input: string,
|
|
gateFn: (perturbed: string) => Promise<boolean> | boolean,
|
|
options?: {
|
|
category?: "encoding" | "injection" | "obfuscation" | "framing" | "logic_trap" | "adversarial";
|
|
intensity?: "low" | "medium" | "high";
|
|
},
|
|
): Promise<{
|
|
results: PerturbationResult[];
|
|
summary: { totalTests: number; bypassesDetected: number; gateWeaknesses: string[] };
|
|
}> {
|
|
if (!this.parseltongue) {
|
|
this.enableParseltongue();
|
|
}
|
|
const pt = this.parseltongue!;
|
|
return await pt.testGate(input, gateFn, options);
|
|
}
|
|
|
|
private routeFanOutSubtask(task: string, subTask: string, index: number): FanOutSubtaskRouteContext {
|
|
const lower = `${subTask} ${task}`.toLowerCase();
|
|
const isPlanning = /\b(plan|design|architecture|strategy|roadmap|approach|proposal|scope|spec|mvp)\b/i.test(lower);
|
|
const isReview = /\b(review|audit|check|verify|validation|assessment|safety|test|diff)\b/i.test(lower);
|
|
|
|
const flow: FanOutSubtaskFlow = isPlanning
|
|
? "planning"
|
|
: isReview
|
|
? "review"
|
|
: "direct";
|
|
|
|
const complexity = flow === "direct" ? "simple" : "medium";
|
|
const domain: "code" | "planning" | "review" =
|
|
flow === "planning" ? "planning" : flow === "review" ? "review" : "code";
|
|
|
|
const route = this.modelRouter.route({
|
|
task: `${task} ${subTask}`,
|
|
domain,
|
|
complexity,
|
|
requiresVision: false,
|
|
});
|
|
|
|
return {
|
|
task,
|
|
subTask,
|
|
index,
|
|
flow,
|
|
reason: flow === "direct" ? "Direct/simple fanout -> cheap flow" : `Plan/review fanout -> routed as ${flow}`,
|
|
route,
|
|
};
|
|
}
|
|
|
|
private async mapWithConcurrency<T>(
|
|
count: number,
|
|
maxConcurrency: number,
|
|
worker: (index: number) => Promise<T>,
|
|
): Promise<T[]> {
|
|
const results: T[] = new Array(count);
|
|
let nextIndex = 0;
|
|
const workerCount = Math.max(1, Math.min(maxConcurrency, count));
|
|
|
|
await Promise.all(
|
|
Array.from({ length: workerCount }, async () => {
|
|
while (nextIndex < count) {
|
|
const index = nextIndex++;
|
|
results[index] = await worker(index);
|
|
}
|
|
}),
|
|
);
|
|
|
|
return results;
|
|
}
|
|
}
|