UNPKG

cmte

Version:

Design by Committee™ except it's just you and LLMs

176 lines (166 loc) 6.24 kB
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'); } }