refactor: implement dynamic role and level handling with migration support

This commit is contained in:
Lauren ten Hoor
2026-02-15 18:46:00 +08:00
parent 0e24a68882
commit a85f4fd33e
10 changed files with 278 additions and 227 deletions

View File

@@ -4,23 +4,63 @@
* Uses an LLM to understand model capabilities and assign optimal models to DevClaw roles.
*/
import { runCommand } from "../run-command.js";
import { ROLE_REGISTRY } from "../roles/index.js";
import type { ModelAssignment } from "./smart-model-selector.js";
export type ModelAssignment = {
developer: {
junior: string;
medior: string;
senior: string;
};
tester: {
junior: string;
medior: string;
senior: string;
};
architect: {
junior: string;
senior: string;
};
};
/**
* Build a ModelAssignment where every role/level maps to the same model.
*/
function singleModelAssignment(model: string): ModelAssignment {
const result: ModelAssignment = {};
for (const [roleId, config] of Object.entries(ROLE_REGISTRY)) {
result[roleId] = {};
for (const level of config.levels) {
result[roleId][level] = model;
}
}
return result;
}
/**
* Build the JSON format example for the LLM prompt, derived from registry.
*/
function buildJsonExample(): string {
const obj: Record<string, Record<string, string>> = {};
for (const [roleId, config] of Object.entries(ROLE_REGISTRY)) {
obj[roleId] = {};
for (const level of config.levels) {
obj[roleId][level] = "provider/model-name";
}
}
return JSON.stringify(obj, null, 2);
}
/**
* Validate that a parsed assignment has all required roles and levels.
*/
function validateAssignment(assignment: Record<string, unknown>, fallbackModel: string): ModelAssignment | null {
const result: ModelAssignment = {};
for (const [roleId, config] of Object.entries(ROLE_REGISTRY)) {
const roleData = assignment[roleId] as Record<string, string> | undefined;
if (!roleData) {
// Backfill missing roles from the first available role or fallback
result[roleId] = {};
for (const level of config.levels) {
result[roleId][level] = fallbackModel;
}
continue;
}
result[roleId] = {};
for (const level of config.levels) {
if (!roleData[level]) {
console.error(`Missing ${roleId}.${level} in LLM assignment`);
return null;
}
result[roleId][level] = roleData[level];
}
}
return result;
}
/**
* Use an LLM to intelligently select and assign models to DevClaw roles.
@@ -35,16 +75,12 @@ export async function selectModelsWithLLM(
// If only one model, assign it to all roles
if (availableModels.length === 1) {
const model = availableModels[0].model;
return {
developer: { junior: model, medior: model, senior: model },
tester: { junior: model, medior: model, senior: model },
architect: { junior: model, senior: model },
};
return singleModelAssignment(availableModels[0].model);
}
// Create a prompt for the LLM
const modelList = availableModels.map((m) => m.model).join("\n");
const jsonExample = buildJsonExample();
const prompt = `You are an AI model expert. Analyze the following authenticated AI models and assign them to DevClaw development roles based on their capabilities.
@@ -65,22 +101,7 @@ Rules:
6. Stable versions (no date) > snapshot versions (with date like 20250514)
Return ONLY a JSON object in this exact format (no markdown, no explanation):
{
"developer": {
"junior": "provider/model-name",
"medior": "provider/model-name",
"senior": "provider/model-name"
},
"tester": {
"junior": "provider/model-name",
"medior": "provider/model-name",
"senior": "provider/model-name"
},
"architect": {
"junior": "provider/model-name",
"senior": "provider/model-name"
}
}`;
${jsonExample}`;
try {
const sessionId = sessionKey ?? "devclaw-model-selection";
@@ -127,28 +148,14 @@ Return ONLY a JSON object in this exact format (no markdown, no explanation):
// Log what we got for debugging
console.log("LLM returned:", JSON.stringify(assignment, null, 2));
// Validate the structure
// Backfill architect if LLM didn't return it (graceful upgrade)
if (!assignment.architect) {
assignment.architect = {
senior: assignment.developer?.senior ?? availableModels[0].model,
junior: assignment.developer?.medior ?? availableModels[0].model,
};
}
if (
!assignment.developer?.junior ||
!assignment.developer?.medior ||
!assignment.developer?.senior ||
!assignment.tester?.junior ||
!assignment.tester?.medior ||
!assignment.tester?.senior
) {
// Validate and backfill
const validated = validateAssignment(assignment, availableModels[0].model);
if (!validated) {
console.error("Invalid assignment structure. Got:", assignment);
throw new Error(`Invalid assignment structure from LLM. Missing fields in: ${JSON.stringify(Object.keys(assignment))}`);
}
return assignment as ModelAssignment;
return validated;
} catch (err) {
console.error("LLM model selection failed:", (err as Error).message);
return null;

View File

@@ -3,23 +3,25 @@
*
* Uses an LLM to intelligently analyze and assign models to DevClaw roles.
*/
import { getAllRoleIds, getLevelsForRole } from "../roles/index.js";
import { ROLE_REGISTRY } from "../roles/index.js";
export type ModelAssignment = {
developer: {
junior: string;
medior: string;
senior: string;
};
tester: {
junior: string;
medior: string;
senior: string;
};
architect: {
junior: string;
senior: string;
};
};
/** Model assignment: role → level → model ID. Derived from registry structure. */
export type ModelAssignment = Record<string, Record<string, string>>;
/**
* Build a ModelAssignment where every role/level maps to the same model.
*/
function singleModelAssignment(model: string): ModelAssignment {
const result: ModelAssignment = {};
for (const [roleId, config] of Object.entries(ROLE_REGISTRY)) {
result[roleId] = {};
for (const level of config.levels) {
result[roleId][level] = model;
}
}
return result;
}
/**
* Intelligently assign available models to DevClaw roles using an LLM.
@@ -42,12 +44,7 @@ export async function assignModels(
// If only one model, use it for everything
if (authenticated.length === 1) {
const model = authenticated[0].model;
return {
developer: { junior: model, medior: model, senior: model },
tester: { junior: model, medior: model, senior: model },
architect: { junior: model, senior: model },
};
return singleModelAssignment(authenticated[0].model);
}
// Multiple models: use LLM-based selection
@@ -68,15 +65,16 @@ export function formatAssignment(assignment: ModelAssignment): string {
const lines = [
"| Role | Level | Model |",
"|-----------|----------|--------------------------|",
`| DEVELOPER | senior | ${assignment.developer.senior.padEnd(24)} |`,
`| DEVELOPER | medior | ${assignment.developer.medior.padEnd(24)} |`,
`| DEVELOPER | junior | ${assignment.developer.junior.padEnd(24)} |`,
`| TESTER | senior | ${assignment.tester.senior.padEnd(24)} |`,
`| TESTER | medior | ${assignment.tester.medior.padEnd(24)} |`,
`| TESTER | junior | ${assignment.tester.junior.padEnd(24)} |`,
`| ARCHITECT | senior | ${assignment.architect.senior.padEnd(24)} |`,
`| ARCHITECT | junior | ${assignment.architect.junior.padEnd(24)} |`,
];
for (const roleId of getAllRoleIds()) {
const roleModels = assignment[roleId];
if (!roleModels) continue;
const displayName = ROLE_REGISTRY[roleId]?.displayName ?? roleId.toUpperCase();
for (const level of getLevelsForRole(roleId)) {
const model = roleModels[level] ?? "";
lines.push(`| ${displayName.padEnd(9)} | ${level.padEnd(8)} | ${model.padEnd(24)} |`);
}
}
return lines.join("\n");
}