UNPKG

cmte

Version:

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

479 lines (433 loc) 16 kB
/** * PhaseExecutor - Executes a phase with human input management */ import path from 'path'; import { SetExecutor } from './set-executor.js'; import { logger } from '../../utils/logger.js'; import fs from 'fs'; import { CommitteePaths } from '../../utils/paths.js'; import { getLLMClient } from "../llm/factory.js"; /** * Options for the PhaseExecutor */ /** * Executor for phase components */ export class PhaseExecutor { constructor(options) { this.registry = options.registry; this.workflowPath = options.workflowPath; this.outputPath = options.outputPath; this.state = options.state; this.phaseName = options.phaseName; this.phaseIteration = options.phaseIteration; this.savePrompts = options.savePrompts || false; this.dryRun = options.dryRun || false; this.apiDryRun = options.apiDryRun || false; this.useLiteModel = options.useLiteModel || false; this.useLocalLLM = options.useLocalLLM ?? false; this.modelConfig = options.modelConfig; this.llmClient = options.llmClient || getLLMClient(this.useLocalLLM, { lite: this.useLiteModel, apiDryRun: this.apiDryRun, savePrompts: this.savePrompts, model: this.modelConfig?.model }); logger.info('Phase executor initialized', { phaseName: this.phaseName, phaseIteration: this.phaseIteration, savePrompts: this.savePrompts, dryRun: this.dryRun, apiDryRun: this.apiDryRun, useLiteModel: this.useLiteModel, useLocalLLM: this.useLocalLLM, llmClientType: this.useLocalLLM ? 'LocalLLMAdapter' : 'ClaudeAdapter', modelConfig: this.modelConfig }); } /** * Get whether prompts should be saved */ shouldSavePrompts() { return this.savePrompts; } /** * Get whether this is a dry run */ isDryRun() { return this.dryRun; } /** * Get whether this is an API dry run */ isApiDryRun() { return this.apiDryRun; } /** * Get whether to use lite models */ useLiteModels() { return this.useLiteModel; } /** * Get the ID of a set reference */ getSetId(setRef) { if ('useSet' in setRef) { return setRef.useSet; } else if ('template' in setRef) { return setRef.template; } throw new Error(`Invalid set reference type: Set must have either 'useSet' or 'template' property. Found properties: ${Object.keys(setRef).join(', ')}. Context: ${JSON.stringify(setRef, null, 2)}`); } /** * Execute a set reference */ async executeSetReference(setRef, context, setExecutor, iterationName) { if ('useSet' in setRef) { // Handle SetReference const set = await this.registry.loadSet(setRef.useSet); // Log set execution logger.info(`Adding set to phase ${this.phaseName}: ${setRef.name || set.name}`, { parallel: set.execution === 'parallel' }); // Apply variables from the set reference const setContext = { ...context, ...(setRef.variables || {}) }; // Validate required input if (setRef.requiredInput) { this.validateRequiredInput(setRef.requiredInput, setContext); } // Create a new set executor with the correct set name const newSetExecutor = new SetExecutor({ registry: setExecutor.registry, workflowPath: setExecutor.workflowPath, outputPath: setExecutor.outputPath, state: setExecutor.state, phaseName: setExecutor.phaseName, setName: setRef.name || set.name, savePrompts: setExecutor.savePrompts, dryRun: setExecutor.dryRun, apiDryRun: setExecutor.apiDryRun, useLiteModel: setExecutor.useLiteModel, useLocalLLM: setExecutor.useLocalLLM, llmClient: setExecutor.llmClient }); // Log set start logger.info(`Starting set execution: ${setRef.name || set.name}`); // Execute the set const result = await newSetExecutor.executeSet(set, setContext); // Log set completion logger.info(`Completed set execution: ${setRef.name || set.name}`); // Validate required output if (setRef.requiredOutput) { this.validateRequiredOutput(setRef.requiredOutput, result.context); } return result; } else if ('template' in setRef) { // Handle TemplateReference const templateSet = await this.registry.loadSet(setRef.template); if (setRef.for_each) { // Get the array to iterate over from the context const items = context[setRef.for_each]; if (!Array.isArray(items)) { throw new Error(`for_each value '${setRef.for_each}' is not an array in context`); } let updatedContext = { ...context }; let updatedState = this.state; // Execute the template for each item for (const item of items) { // Create context for this iteration const iterationContext = { ...updatedContext, item, ...(setRef.variables || {}) }; // Validate required input if (setRef.requiredInput) { this.validateRequiredInput(setRef.requiredInput, iterationContext); } // Create a new set executor with the correct set name const newSetExecutor = new SetExecutor({ registry: setExecutor.registry, workflowPath: setExecutor.workflowPath, outputPath: setExecutor.outputPath, state: setExecutor.state, phaseName: setExecutor.phaseName, setName: setRef.name || templateSet.name, savePrompts: setExecutor.savePrompts, dryRun: setExecutor.dryRun, apiDryRun: setExecutor.apiDryRun, useLiteModel: setExecutor.useLiteModel, useLocalLLM: setExecutor.useLocalLLM, llmClient: setExecutor.llmClient }); // Execute the template set const result = await newSetExecutor.executeSet(templateSet, iterationContext); // Merge the result into the context and update the state updatedContext = { ...updatedContext, ...result.context }; updatedState = result.state; // Validate required output if (setRef.requiredOutput) { this.validateRequiredOutput(setRef.requiredOutput, updatedContext); } } return { context: updatedContext, state: updatedState }; } else { // Execute the template once const templateContext = { ...context, ...(setRef.variables || {}) }; // Validate required input if (setRef.requiredInput) { this.validateRequiredInput(setRef.requiredInput, templateContext); } // Create a new set executor with the correct set name const newSetExecutor = new SetExecutor({ registry: setExecutor.registry, workflowPath: setExecutor.workflowPath, outputPath: setExecutor.outputPath, state: setExecutor.state, phaseName: setExecutor.phaseName, setName: setRef.name || templateSet.name, savePrompts: setExecutor.savePrompts, dryRun: setExecutor.dryRun, apiDryRun: setExecutor.apiDryRun, useLiteModel: setExecutor.useLiteModel, useLocalLLM: setExecutor.useLocalLLM, llmClient: setExecutor.llmClient }); // Execute the template set const result = await newSetExecutor.executeSet(templateSet, templateContext); // Validate required output if (setRef.requiredOutput) { this.validateRequiredOutput(setRef.requiredOutput, result.context); } return result; } } throw new Error('Invalid set reference type'); } /** * Execute a phase * @param phase The phase to execute * @param context The context to use for execution * @returns The updated context and state after execution */ async executePhase(phase, context = {}) { try { logger.info(`Executing phase: ${phase.name}`, { dryRun: this.dryRun, savePrompts: this.savePrompts, apiDryRun: this.apiDryRun, useLiteModel: this.useLiteModel, workflowPath: this.workflowPath, outputPath: this.outputPath }); // Check phase condition first if (phase.condition) { const conditionResult = await this.evaluateCondition(phase.condition, context); if (!conditionResult) { logger.info(`Skipping phase ${phase.name} due to condition: ${phase.condition}`); return { context, state: this.state }; } } // Merge initial context with state context const mergedContext = { ...(this.state.context || {}), ...context }; logger.debug('Phase context', { phaseName: phase.name, contextKeys: Object.keys(mergedContext), stateContextKeys: Object.keys(this.state.context || {}) }); // Validate that all required input is available if (phase.requiredInput) { this.validateRequiredInput(phase.requiredInput, mergedContext); } // Create a new set executor with the appropriate path components const setExecutor = new SetExecutor({ registry: this.registry, workflowPath: this.workflowPath, outputPath: this.outputPath, state: this.state, phaseName: this.phaseName, setName: `${this.phaseName}-set`, savePrompts: this.savePrompts || this.dryRun, dryRun: this.dryRun, apiDryRun: this.apiDryRun, useLiteModel: this.useLiteModel, useLocalLLM: this.useLocalLLM, llmClient: this.llmClient }); logger.info(`Created set executor for phase ${phase.name}`, { phaseName: this.phaseName, savePrompts: this.savePrompts || this.dryRun, dryRun: this.dryRun }); let updatedContext = mergedContext; let updatedState = this.state; // Handle parallel vs sequential execution if (phase.execution === 'parallel') { logger.info(`Executing sets in parallel for phase ${phase.name}`); // Execute all sets in parallel const setPromises = phase.set.map(async (setRef, index) => { const setId = this.getSetId(setRef); logger.info(`Starting parallel set execution: ${setId}`, { index, totalSets: phase.set.length, dryRun: this.dryRun }); return this.executeSetReference(setRef, updatedContext, setExecutor); }); // Wait for all sets to complete const results = await Promise.all(setPromises); // Merge all results results.forEach(result => { updatedContext = { ...updatedContext, ...result.context }; updatedState = result.state; }); } else { // Sequential execution (default) logger.info(`Executing sets sequentially for phase ${phase.name}`); for (let i = 0; i < phase.set.length; i++) { const setRef = phase.set[i]; const setId = this.getSetId(setRef); logger.info(`Starting sequential set execution: ${setId}`, { setNumber: i + 1, totalSets: phase.set.length, dryRun: this.dryRun }); const result = await this.executeSetReference(setRef, updatedContext, setExecutor); updatedContext = { ...updatedContext, ...result.context }; updatedState = result.state; logger.info(`Completed set execution: ${setId}`, { setNumber: i + 1, totalSets: phase.set.length, contextKeys: Object.keys(result.context) }); } } // Validate that all required output is present if (phase.requiredOutput) { this.validateRequiredOutput(phase.requiredOutput, updatedContext); } logger.info(`Completed phase execution: ${phase.name}`, { contextKeys: Object.keys(updatedContext), dryRun: this.dryRun }); return { context: updatedContext, state: updatedState }; } catch (error) { // Add phase context to error error.phase = phase.name; throw error; } } /** * Check if a phase requires human input and set up the state accordingly * @param phase The phase to check * @param state The current resume state * @returns The updated state with human input requirements */ checkHumanInputRequirements(phase, state) { logger.info(`Checking human input requirements for phase: ${phase.name}`); logger.info(`Phase input requirements: ${JSON.stringify(phase.humanInputRequired)}`); logger.info(`Current state: ${JSON.stringify(state)}`); if (!phase.humanInputRequired || phase.humanInputRequired.length === 0) { logger.info('No human input required for this phase'); return state; } // Resolve file paths to absolute paths const absolutePaths = phase.humanInputRequired.map(filePath => { const resolved = path.isAbsolute(filePath) ? filePath : path.resolve(this.workflowPath, filePath); logger.info(`Resolving path: ${filePath} -> ${resolved}`); return resolved; }); logger.info(`Resolved absolute paths: ${JSON.stringify(absolutePaths)}`); // Update the state with files awaiting review const updatedState = setFilesAwaitingReview(state, absolutePaths); logger.info(`Updated state: ${JSON.stringify(updatedState)}`); return updatedState; } /** * Validate that all required input variables are present in the context * @param requiredInput The list of required input variables * @param context The context to validate * @throws Error if any required input is missing */ validateRequiredInput(requiredInput, context) { const missingInput = requiredInput.filter(input => !(input in context)); if (missingInput.length > 0) { throw new Error(`Missing required input variables: ${missingInput.join(', ')}`); } } /** * Validate that all required output variables are present in the context * @param requiredOutput The list of required output variables * @param context The context to validate * @throws Error if any required output is missing */ validateRequiredOutput(requiredOutput, context) { const missingOutput = requiredOutput.filter(output => !(output in context)); if (missingOutput.length > 0) { throw new Error(`Missing required output variables: ${missingOutput.join(', ')}`); } } /** * Evaluate a condition expression */ async evaluateCondition(condition, context) { // For now, implement a simple evaluation that checks if a context variable is truthy // This can be expanded to support more complex conditions const contextVar = condition.trim(); return Boolean(context[contextVar]); } /** * Generate the phase-level output file */ async generatePhaseOutput(phase, context) { const components = { phase: phase.name, phaseIteration: this.phaseIteration, set: phase.name // Using phase name as set since this is phase-level output }; const outputPath = CommitteePaths.constructOutputPath(this.outputPath, components); // Create a summary of the phase execution const summary = { name: phase.name, phaseIteration: this.phaseIteration, timestamp: new Date().toISOString(), context: context }; // Ensure directory exists await fs.promises.mkdir(path.dirname(outputPath), { recursive: true }); // Write the phase output file await fs.promises.writeFile(outputPath, `# Phase: ${phase.name}\n\n${JSON.stringify(summary, null, 2)}`); } }