cmte
Version:
Design by Committee™ except it's just you and LLMs
215 lines (194 loc) • 10 kB
JavaScript
/**
* A simplified output reference resolver for the Committee framework.
* Handles task output references with support for iterations.
*/
import { logger } from '../../utils/logger.js';
import { getNestedProperty } from '../../utils/nested-property.js';
export class OutputReferenceResolver {
constructor() {
this.outputs = {};
this.currentIteration = null;
this.REFERENCE_REGEX = /^([\w-]+)\.([\w-]+)(?:\[(.*?)\])?(?:\.([\w.]+))?$/;
}
/**
* Sets the current iteration context
* @param {string} iterationKey - The current iteration key
*/
setIterationContext(iterationKey) {
this.currentIteration = iterationKey;
}
/**
* Clears the current iteration context
*/
clearIterationContext() {
this.currentIteration = null;
}
/**
* Registers regular task output
* @param {string} setName - Name of the task set
* @param {string} taskName - Name of the task
* @param {any} output - The output to register
*/
registerOutput(setName, taskName, output) {
if (!this.outputs[setName]) {
this.outputs[setName] = {};
}
this.outputs[setName][taskName] = output;
}
/**
* Registers output for an iterated task
* @param {string} setName - Name of the task set
* @param {string} taskName - Name of the task
* @param {string} iteratorValue - The iteration value
* @param {any} output - The output to register
*/
registerIteratedOutput(setName, taskName, iteratorValue, output) {
if (!this.outputs[setName]) {
this.outputs[setName] = {};
}
// Ensure the entry for the task exists and is an array
// If it exists but isn't an array, overwrite it. This might hide errors
// where non-iterated and iterated outputs are mixed for the same task,
// but simplifies testing setup recovery.
if (!this.outputs[setName][taskName] || !Array.isArray(this.outputs[setName][taskName])) {
this.outputs[setName][taskName] = [];
}
this.outputs[setName][taskName].push({
iterator: iteratorValue,
output
});
}
/**
* Resolves an output reference string to its stored value.
* Handles basic references (setName.taskName.output) and iteration references (setName.taskName[key].output).
* @param {string} reference - The output reference string.
* @param {string|null} [iterationKeyOverride=null] - Optional override for the iteration context key (used by TemplateRenderer).
* @returns {*} The resolved output value.
* @throws {Error} If the reference format is invalid, the set/task/output is not found,
* or the reference is ambiguous (e.g., basic reference to iterated output).
*/
resolveReference(reference, iterationKeyOverride = null) {
logger.debug(`Resolving reference: ${reference}${iterationKeyOverride ? ` (Iteration Override: ${iterationKeyOverride})` : ''}${this.currentIteration ? ` (Current Context: ${this.currentIteration})` : ''}`);
const match = reference.match(this.REFERENCE_REGEX);
if (!match) {
// logger.debug(`[ORR_DEBUG] Invalid output reference format (will try context): ${reference}`);
throw new Error(`Invalid output reference format: ${reference}`);
}
const [, setName, taskName, iterationLookupKeyRaw, propertyPathRaw] = match;
let iterationLookupKey = iterationLookupKeyRaw;
const currentIterationContextKey = iterationKeyOverride ?? this.currentIteration;
// logger.debug(`[ORR_DEBUG] Using iteration context key: ${currentIterationContextKey || 'undefined'}`);
// --- Determine the actual property path to apply (if any) ---
let actualPropertyPath = null;
if (propertyPathRaw) { // Check if the property path group was captured at all
if (propertyPathRaw === 'output') {
// Standard .output suffix, means get the whole output
actualPropertyPath = null;
// logger.debug(`[ORR_DEBUG] Detected standard '.output', no nested path to apply.`);
} else if (propertyPathRaw.startsWith('output.')) {
// Nested property access like .output.key
actualPropertyPath = propertyPathRaw.substring(7); // Get the part after 'output.'
// logger.debug(`[ORR_DEBUG] Detected nested property path: '${actualPropertyPath}'`);
} else {
// Invalid format - path exists but doesn't start with 'output'
const errMsg = `Invalid property path in reference '${reference}'. Path must start with '.output'.`;
logger.error(errMsg); // Keep error for truly invalid path
throw new Error(errMsg);
}
} // If propertyPathRaw is undefined, actualPropertyPath remains null
// --- Check if Set and Task exist ---
if (!(setName in this.outputs)) {
logger.debug(`[ORR_DEBUG] Set not found for potential reference: ${reference}`); // Keep as debug
throw new Error(`Set not found for reference: ${reference}`);
}
if (!(taskName in this.outputs[setName])) {
logger.debug(`[ORR_DEBUG] Task output not found for potential reference: ${reference}`); // Keep as debug
throw new Error(`Task output not found for reference: ${reference}`);
}
const taskOutput = this.outputs[setName][taskName];
// logger.debug(`[ORR_DEBUG] Found task output object/array for ${setName}.${taskName}. Type: ${Array.isArray(taskOutput) ? 'Array' : typeof taskOutput}. Content preview: ${JSON.stringify(taskOutput)?.substring(0, 100)}...`);
// --- Resolve Base Output Value ---
let resolvedBaseOutput;
if (iterationLookupKey !== undefined) {
// logger.debug(`[ORR_DEBUG] Handling iteration reference. Key requested: '${iterationLookupKey}'. Using context key: '${currentIterationContextKey || 'none'}'`);
if (!Array.isArray(taskOutput)) {
const errMsg = `Cannot use iteration key '[${iterationLookupKey}]' on non-iterated output for reference: ${reference}`;
logger.error(errMsg);
throw new Error(errMsg);
}
// Special handling for [this]
if (iterationLookupKey === 'this') {
if (!currentIterationContextKey) {
const errMsg = `Cannot use [this] iteration key outside of an iteration context. Reference: ${reference}`;
logger.error(errMsg);
throw new Error(errMsg);
}
iterationLookupKey = currentIterationContextKey;
// logger.debug(`[ORR_DEBUG] Mapped [this] to actual key: '${iterationLookupKey}'`);
}
// Handle Wildcard [*]
if (iterationLookupKey === '*') {
// logger.debug(`[ORR_DEBUG] Resolving wildcard [*] reference.`);
const results = taskOutput.map(item => item.output);
// logger.debug(`[ORR_DEBUG] Wildcard results: ${JSON.stringify(results)}`);
// --- Apply Property Path (if determined) ---
if (actualPropertyPath) {
// logger.debug(`[ORR_DEBUG] Applying actual property path '${actualPropertyPath}' to EACH item in wildcard results.`);
const mappedResults = results.map(res => getNestedProperty(res, actualPropertyPath));
// logger.debug(`[ORR_DEBUG] Result after property path mapping: ${JSON.stringify(mappedResults)}`);
return mappedResults;
} else {
// logger.debug(`[ORR_DEBUG] No property path to apply, returning full wildcard results array.`);
return results; // <<< Ensure the array is returned here
}
}
// Find Specific Iteration Item
// logger.debug(`[ORR_DEBUG] Searching for specific iterator key: '${iterationLookupKey}' in array of length ${taskOutput.length}`);
const item = taskOutput.find(item => item.iterator === iterationLookupKey);
// logger.debug(`[ORR_DEBUG] Found item for key '${iterationLookupKey}': ${item ? JSON.stringify(item) : 'NOT FOUND'}`);
if (!item) {
const availableKeys = taskOutput.map(item => item.iterator).join(', ');
const errMsg = `Iteration key '${iterationLookupKey}' not found for ${setName}.${taskName}. Reference: ${reference}. Available keys: ${availableKeys}`;
logger.error(errMsg);
throw new Error(errMsg);
}
resolvedBaseOutput = item.output;
// logger.debug(`[ORR_DEBUG] Extracted iterated base output. Type: ${typeof resolvedBaseOutput}. Value preview: ${JSON.stringify(resolvedBaseOutput)?.substring(0,100)}...`);
} else {
// logger.debug(`[ORR_DEBUG] Handling non-iteration reference.`);
if (Array.isArray(taskOutput)) {
const errMsg = `Ambiguous reference: '${reference}' refers to an iterated output. Specify an iteration key (e.g., [key], [*], [this]) or use a non-iterated task output.`;
logger.error(errMsg);
throw new Error(errMsg);
}
// It's a simple, non-iterated value
resolvedBaseOutput = taskOutput;
// logger.debug(`[ORR_DEBUG] Extracted non-iterated base output. Type: ${typeof resolvedBaseOutput}. Value preview: ${JSON.stringify(resolvedBaseOutput)?.substring(0,100)}...`);
}
// --- Apply Property Path (if determined) ---
if (actualPropertyPath) {
// logger.debug(`[ORR_DEBUG] Applying actual property path '${actualPropertyPath}' to base output.`);
const finalValue = getNestedProperty(resolvedBaseOutput, actualPropertyPath);
// logger.debug(`[ORR_DEBUG] Result after property path: ${JSON.stringify(finalValue)}`);
return finalValue;
} else {
// logger.debug(`[ORR_DEBUG] No property path to apply, returning base output.`);
return resolvedBaseOutput; // Return the whole output
}
}
/**
* Returns a deep copy of all registered outputs.
* @returns {object} A deep copy of the outputs object.
*/
getAllOutputs() {
// Return a deep copy to prevent external modification of internal state
return JSON.parse(JSON.stringify(this.outputs));
}
/**
* Clears all registered outputs and the iteration context.
*/
clear() {
this.outputs = {};
this.currentIteration = null;
}
}