cmte
Version:
Design by Committee™ except it's just you and LLMs
176 lines (166 loc) • 6.24 kB
JavaScript
import fs from 'fs/promises';
import path from 'path';
import { renderTemplate } from "./two-phase-utils.js";
import { logger } from "../utils/logger.js";
/**
* Configuration for the two-phase processor
*/
/**
* Result of the two-phase process
*/
/**
* Runs a two-phase process (thinking then response) using the provided LLM client
*/
export async function runTwoPhaseProcess(config) {
logger.info(`Starting two-phase process for ${config.role} in ${config.phase} phase`, {
dryRun: config.dryRun,
savePrompts: config.savePrompts,
apiDryRun: config.apiDryRun,
useHaikuModel: config.useHaikuModel,
useLocalLLM: config.useLocalLLM,
llmClientType: config.useLocalLLM ? 'LocalLLMClient' : 'ClaudeClient'
});
// Get the appropriate LLM client
const llmClient = config.llmClient;
// Ensure output directories exist
await fs.mkdir(path.dirname(config.outputPath.thinking.output), {
recursive: true
});
await fs.mkdir(path.dirname(config.outputPath.response.output), {
recursive: true
});
// Phase 1: Thinking
logger.debug('Starting thinking phase');
const thinkingPrompt = await renderTemplate(config.thinkingPromptTemplate, config.context, config.fileContext, config.modelConfig?.useXML, false // Not an LLM response
);
// Save thinking prompt if requested or in dry run mode
if ((config.savePrompts || config.dryRun) && config.outputPath.thinking.prompt) {
await fs.mkdir(path.dirname(config.outputPath.thinking.prompt), {
recursive: true
});
await fs.writeFile(config.outputPath.thinking.prompt, thinkingPrompt);
logger.debug('Saved thinking prompt', {
path: config.outputPath.thinking.prompt
});
}
logger.info('Sending thinking prompt to LLM...', {
role: config.role,
phase: config.phase,
contextKeys: Object.keys(config.context),
outputPath: config.outputPath.thinking.output,
dryRun: config.dryRun
});
const thinking = await llmClient.completePrompt(thinkingPrompt, {
model: config.modelConfig?.thinkingModel,
temperature: config.modelConfig?.temperature,
maxTokens: config.modelConfig?.maxTokens,
useXML: config.modelConfig?.useXML,
savePrompt: config.savePrompts || config.dryRun,
dryRun: config.dryRun,
apiDryRun: config.apiDryRun,
useHaikuModel: config.useHaikuModel,
outputPath: config.outputPath.thinking.output
});
logger.info('Received thinking response from LLM', {
responseLength: thinking?.length || 0,
firstChars: thinking?.substring(0, 100),
dryRun: config.dryRun
});
logger.info('Writing thinking response to file', {
path: config.outputPath.thinking.output,
content: thinking
});
await fs.writeFile(config.outputPath.thinking.output, thinking);
logger.debug('Thinking phase complete');
// Phase 2: Response
logger.debug('Starting response phase');
const responseContext = {
...config.context,
thinking
};
const responsePrompt = await renderTemplate(config.responsePromptTemplate, responseContext, config.fileContext, config.modelConfig?.useXML, true // This is an LLM response
);
// Save response prompt if requested or in dry run mode
if ((config.savePrompts || config.dryRun) && config.outputPath.response.prompt) {
await fs.mkdir(path.dirname(config.outputPath.response.prompt), {
recursive: true
});
await fs.writeFile(config.outputPath.response.prompt, responsePrompt);
logger.debug('Saved response prompt', {
path: config.outputPath.response.prompt
});
}
logger.info('Sending response prompt to LLM...', {
role: config.role,
phase: config.phase,
contextKeys: Object.keys(responseContext),
outputPath: config.outputPath.response.output,
dryRun: config.dryRun
});
const response = await llmClient.completePrompt(responsePrompt, {
model: config.modelConfig?.responseModel,
temperature: config.modelConfig?.temperature,
maxTokens: config.modelConfig?.maxTokens,
useXML: config.modelConfig?.useXML,
savePrompt: config.savePrompts || config.dryRun,
dryRun: config.dryRun,
apiDryRun: config.apiDryRun,
useHaikuModel: config.useHaikuModel,
outputPath: config.outputPath.response.output
});
logger.info('Received response from LLM', {
responseLength: response?.length || 0,
firstChars: response?.substring(0, 100),
dryRun: config.dryRun
});
logger.info('Writing response to file', {
path: config.outputPath.response.output,
content: response
});
await fs.writeFile(config.outputPath.response.output, response);
logger.debug('Response phase complete');
return {
thinking,
response
};
}
/**
* Validates the two-phase processor configuration
* @param config Configuration to validate
* @throws Error if the configuration is invalid
*/
export function validateConfig(config) {
if (!config.role) {
throw new Error('Role is required in two-phase processor config');
}
if (!['service', 'architect', 'pm'].includes(config.role)) {
throw new Error(`Invalid role: ${config.role}. Must be 'service', 'architect', or 'pm'`);
}
if (!config.phase) {
throw new Error('Phase is required in two-phase processor config');
}
if (!config.thinkingPromptTemplate) {
throw new Error('Thinking prompt template is required in two-phase processor config');
}
if (!config.responsePromptTemplate) {
throw new Error('Response prompt template is required in two-phase processor config');
}
if (!config.context) {
throw new Error('Context is required in two-phase processor config');
}
if (!config.outputPath) {
throw new Error('Output path is required in two-phase processor config');
}
if (!config.outputPath.thinking) {
throw new Error('Thinking output path is required in two-phase processor config');
}
if (!config.outputPath.thinking.output) {
throw new Error('Thinking output file path is required in two-phase processor config');
}
if (!config.outputPath.response) {
throw new Error('Response output path is required in two-phase processor config');
}
if (!config.outputPath.response.output) {
throw new Error('Response output file path is required in two-phase processor config');
}
}