cmte
Version:
Design by Committee™ except it's just you and LLMs
479 lines (433 loc) • 16 kB
JavaScript
/**
* 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)}`);
}
}