Project Files
src / orchestrator.ts
import { LMStudioClient } from "@lmstudio/sdk";
import path from "path";
import { Coder } from "./agents/coder";
import { Optimizer } from "./agents/optimizer";
import { Planner } from "./agents/planner";
import { Reviewer } from "./agents/reviewer";
import { Tester } from "./agents/tester";
import { MemoryStore } from "./memory/store";
import { completeText, slugify } from "./llm";
import { applyPatchToFile, snapshotWorkspace, resolveWithin } from "./workspace";
import type { BuildPlan, Patch } from "./types";
export interface BuildOptions {
targetDir?: string;
maxRounds?: number;
}
export interface BuildResult {
targetRoot: string;
rounds: number;
review: {
passed: boolean;
issues: string[];
};
test: {
passed: boolean;
issues: string[];
};
files: Array<{ path: string; content: string }>;
plan: BuildPlan;
}
export class AutoBuilder {
constructor(
private readonly client: any,
private readonly workspaceRoot: string,
private readonly memory = new MemoryStore(workspaceRoot)
) {}
async build(spec: string, options: BuildOptions = {}): Promise<BuildResult> {
const model = await this.client.llm.model();
const memorySummary = await this.memory.summary(12);
const targetSlug = slugify(options.targetDir ?? spec);
const targetRoot = resolveWithin(
this.workspaceRoot,
path.join(".forge-generated", targetSlug)
);
const maxRounds = options.maxRounds ?? 4;
const planner = new Planner(model, this.memory);
const coder = new Coder(model);
const reviewer = new Reviewer(model);
const tester = new Tester();
const optimizer = new Optimizer(model);
await this.memory.add("request", spec, { targetRoot });
const plan = await planner.run(spec, targetRoot, memorySummary);
let rounds = 0;
// Initial setup: if no files exist, we need to create them first
let snapshot = await snapshotWorkspace(targetRoot);
if (snapshot.length === 0) {
const initialPatches = await coder.run({
plan,
snapshot: "[]",
issues: [],
memorySummary,
});
await this.applyPatchesWithRollback(targetRoot, initialPatches);
snapshot = await snapshotWorkspace(targetRoot);
}
// Main loop for refinement
while (rounds < maxRounds) {
snapshot = await snapshotWorkspace(targetRoot);
const review = await reviewer.run({
plan,
snapshot: JSON.stringify(snapshot, null, 2),
});
const test = await tester.run(snapshot);
if (review.passed && test.passed) break;
// Prepare fixes for the current failure
const issues = [...review.issues, ...test.issues];
await this.memory.add("issues", JSON.stringify(issues), { rounds, targetRoot });
const fixes = await optimizer.run({
plan,
snapshot: JSON.stringify(snapshot, null, 2),
issues,
memorySummary,
});
try {
await this.applyPatchesWithRollback(targetRoot, fixes);
} catch (error) {
console.error("Failed to apply patches, stopping build:", error);
break;
}
rounds += 1;
}
const finalSnapshot = await snapshotWorkspace(targetRoot);
const finalReview = await reviewer.run({ plan, snapshot: JSON.stringify(finalSnapshot, null, 2) });
const finalTest = await tester.run(finalSnapshot);
const finalStatus = {
pluginName: plan.pluginName,
targetRoot,
passed: finalReview.passed && finalTest.passed,
rounds,
issues: [...finalReview.issues, ...finalTest.issues],
};
await this.memory.add("result", JSON.stringify(finalStatus), { targetRoot });
return {
targetRoot,
rounds,
review: finalReview,
test: finalTest,
files: finalSnapshot,
plan,
};
}
private async applyPatchesWithRollback(root: string, patches: Patch[]) {
for (const patch of patches) {
try {
await applyPatchToFile(root, patch);
await this.memory.add("patch", `${patch.operation} ${patch.file}`, {
file: patch.file,
operation: patch.operation,
});
} catch (error) {
await this.memory.add(
"patch-error",
`${patch.file}: ${(error as Error).message}`,
{ file: patch.file, operation: patch.operation }
);
}
}
}
}