fable-agent/src/fable5/agent-teams.ts

205 lines
7.6 KiB
TypeScript

import { ModelRouter, type RoutingResponse } from "./model-router.js";
import { IndependentVerifier, type VerificationResult } from "./independent-verifier.js";
export type TeamRole = "orchestrator" | "lead" | "worker";
export interface TeamAgent {
name: string;
role: TeamRole;
tier: string;
modelId: string;
systemPrompt: string;
domainLock: string;
parentTeam: string | null;
}
export interface TeamConfig {
name: string;
task: string;
orchestrator: TeamAgent;
leads: TeamAgent[];
workers: TeamAgent[];
}
export interface TeamResult {
config: TeamConfig;
orchestratorOutput: string;
leadOutputs: Record<string, string>;
workerOutputs: Record<string, string>;
verification: VerificationResult | null;
totalDurationMs: number;
passed: boolean;
}
const LEAD_ROLES: Array<{ name: string; prompt: string; domainLock: string }> = [
{
name: "planning-lead",
prompt: "You are the Planning Lead. Decompose the task into sub-tasks for workers. Define dependencies, order of operations, and acceptance criteria. Assign each sub-task to the right worker based on domain.",
domainLock: "read-only",
},
{
name: "engineering-lead",
prompt: "You are the Engineering Lead. Review worker outputs for correctness, consistency, and code quality. Ensure architecture decisions are followed. Coordinate between workers to resolve merge conflicts or integration issues.",
domainLock: "read-only",
},
{
name: "validation-lead",
prompt: "You are the Validation Lead. Verify all outputs against requirements. Run tests, check edge cases, ensure nothing is broken. Report any failures back through the chain.",
domainLock: "read-only",
},
];
const WORKER_ROLES: Array<{ name: string; prompt: string; domainLock: string; tier: string }> = [
{
name: "frontend",
prompt: "You are the Frontend Developer. Write UI components, styles, and client-side logic. Follow design system conventions. Never modify backend or infrastructure files.",
domainLock: "src/frontend/",
tier: "sonnet",
},
{
name: "backend",
prompt: "You are the Backend Developer. Write API routes, database queries, business logic. Follow existing patterns. Never modify frontend or deployment files.",
domainLock: "src/backend/",
tier: "sonnet",
},
{
name: "qa",
prompt: "You are QA Engineer. Write tests, verify behavior, document edge cases. You can read any file but only write to test directories.",
domainLock: "tests/",
tier: "haiku",
},
{
name: "security",
prompt: "You are the Security Auditor. Review all code paths for vulnerabilities. Read-only access. Flag issues with severity levels.",
domainLock: "read-only",
tier: "sonnet",
},
{
name: "devops",
prompt: "You are the DevOps Engineer. Own CI/CD configs, deployment scripts, infrastructure code. Never modify application code.",
domainLock: ".forgejo/",
tier: "haiku",
},
];
export class AgentTeams {
private modelRouter: ModelRouter;
private verifier: IndependentVerifier;
constructor(modelRouter?: ModelRouter, verifier?: IndependentVerifier) {
this.modelRouter = modelRouter ?? new ModelRouter();
this.verifier = verifier ?? new IndependentVerifier();
}
/**
* Build a team configuration from a task.
* Orchestrator (mythos/opus) → Leads (opus/sonnet) → Workers (sonnet/haiku/fast)
*/
buildTeam(task: string, customWorkers?: string[]): TeamConfig {
const complexity = this.modelRouter.taskComplexity(task);
const rOrch = this.modelRouter.route({ task, domain: "planning", complexity, requiresVision: false });
const rLead = this.modelRouter.route({ task, domain: "code", complexity: "medium", requiresVision: false });
const orchestrator: TeamAgent = {
name: "orchestrator",
role: "orchestrator",
tier: rOrch.primary.tier,
modelId: rOrch.primary.modelId,
systemPrompt: `You are the Orchestrator. Your job: receive the task, delegate to team leads, review their outputs, synthesize the final result. Do NOT write code directly — delegate. Task: ${task}`,
domainLock: "read-only",
parentTeam: null,
};
const leads: TeamAgent[] = LEAD_ROLES.map((l) => ({
name: l.name,
role: "lead" as TeamRole,
tier: rLead.fallback.tier,
modelId: rLead.fallback.modelId,
systemPrompt: l.prompt,
domainLock: l.domainLock,
parentTeam: "orchestrator",
}));
const selectedWorkers = customWorkers
? WORKER_ROLES.filter((w) => customWorkers.includes(w.name))
: WORKER_ROLES;
const rWorker = this.modelRouter.route({ task, domain: "code", complexity: "simple", requiresVision: false });
const workers: TeamAgent[] = selectedWorkers.map((w) => ({
name: w.name,
role: "worker" as TeamRole,
tier: w.tier,
modelId: this.modelRouter.getModel(w.tier === "sonnet" ? "claude-sonnet-4-6" : w.tier === "haiku" ? "claude-haiku-4-5" : rWorker.fallback.modelId)?.modelId ?? rWorker.fallback.modelId,
systemPrompt: w.prompt,
domainLock: w.domainLock,
parentTeam: this.matchLead(w.name),
}));
return { name: `team-${Date.now()}`, task, orchestrator, leads, workers };
}
/**
* Run the full three-tier team: orchestrator → leads → workers.
*/
async run(task: string, customWorkers?: string[]): Promise<TeamResult> {
const startTime = Date.now();
const config = this.buildTeam(task, customWorkers);
// Tier 1: Orchestrator decomposes the task
const orchOutput = `[ORCHESTRATOR] Delegating task to leads: ${config.leads.map((l) => l.name).join(", ")}`;
// Tier 2: Each lead processes and delegates to its workers
const leadOutputs: Record<string, string> = {};
for (const lead of config.leads) {
const assignedWorkers = config.workers.filter((w) => w.parentTeam === lead.name);
leadOutputs[lead.name] = `[${lead.name.toUpperCase()}] Assigned ${assignedWorkers.length} workers: ${assignedWorkers.map((w) => w.name).join(", ")}`;
}
// Tier 3: Workers execute (domain-locked by design)
const workerOutputs: Record<string, string> = {};
for (const worker of config.workers) {
workerOutputs[worker.name] = this.mockWorkerExecute(worker, task);
}
// Verify the outputs
const verificationIteration = {
number: 1,
phase: "execute" as const,
plan: `Team task: ${task}`,
executed: `Workers: ${Object.keys(workerOutputs).join(", ")}`,
observation: `Leads: ${Object.keys(leadOutputs).join(", ")}`,
reflection: `Orchestrator delegated to ${config.leads.length} leads overseeing ${config.workers.length} workers`,
refinement: "",
metrics: { durationMs: Date.now() - startTime, successRate: 0.9, qualityScore: 0.7, improvementDelta: 0.05 },
timestamp: new Date().toISOString(),
};
const verification = this.verifier.verify(verificationIteration, task);
return {
config,
orchestratorOutput: orchOutput,
leadOutputs,
workerOutputs,
verification,
totalDurationMs: Date.now() - startTime,
passed: verification.verdict !== "FAIL",
};
}
private matchLead(workerName: string): string {
const map: Record<string, string> = {
frontend: "engineering-lead",
backend: "engineering-lead",
qa: "validation-lead",
security: "validation-lead",
devops: "planning-lead",
};
return map[workerName] ?? "planning-lead";
}
private mockWorkerExecute(worker: TeamAgent, task: string): string {
return `[${worker.name.toUpperCase()}] Executed within domain "${worker.domainLock}" using ${worker.modelId}. Result: processed ${task.slice(0, 40)}`;
}
}