UNPKG

meld

Version:

Meld: A template language for LLM prompts

396 lines (348 loc) 11.7 kB
import { IStateService } from '@services/state/StateService/IStateService.js'; import { IStateTrackingService } from '@tests/utils/debug/StateTrackingService/IStateTrackingService.js'; import { stateLogger as logger } from '@core/utils/logger.js'; /** * Variable type supported by the State service */ export type VariableType = 'text' | 'data' | 'path' | 'command'; /** * Options for copying variables between state objects */ export interface VariableCopyOptions { /** * Skip variables that already exist in the target state * @default false */ skipExisting?: boolean; /** * Track context boundary before and after copying (requires tracking service) * @default true */ trackContextBoundary?: boolean; /** * Track each variable crossing (requires tracking service) * @default true */ trackVariableCrossing?: boolean; } /** * Utility for consistently copying variables between state objects */ export class StateVariableCopier { private trackingService?: IStateTrackingService; /** * Create a new StateVariableCopier * @param trackingService Optional tracking service for debugging */ constructor(trackingService?: IStateTrackingService) { this.trackingService = trackingService; } /** * Copy all variables from source state to target state * @param sourceState Source state containing variables * @param targetState Target state to receive variables * @param options Additional options for copying * @returns Number of variables copied */ public copyAllVariables( sourceState: IStateService, targetState: IStateService, options: VariableCopyOptions = {} ): number { const { skipExisting = false, trackContextBoundary = true, trackVariableCrossing = true } = options; let totalCopied = 0; // Track boundary if requested and tracking service exists if (trackContextBoundary && this.trackingService) { let filePath: string | undefined; try { filePath = sourceState.getCurrentFilePath?.() || undefined; } catch (error) { logger.debug('Error getting current file path', { error }); } this.trackContextBoundary(sourceState, targetState, filePath); } // Copy text variables totalCopied += this.copyVariableType( sourceState, targetState, 'text', skipExisting, trackVariableCrossing ); // Copy data variables totalCopied += this.copyVariableType( sourceState, targetState, 'data', skipExisting, trackVariableCrossing ); // Copy path variables totalCopied += this.copyVariableType( sourceState, targetState, 'path', skipExisting, trackVariableCrossing ); // Copy commands totalCopied += this.copyVariableType( sourceState, targetState, 'command', skipExisting, trackVariableCrossing ); // Track boundary again if requested and tracking service exists if (trackContextBoundary && this.trackingService) { let filePath: string | undefined; try { filePath = sourceState.getCurrentFilePath?.() || undefined; } catch (error) { logger.debug('Error getting current file path', { error }); } this.trackContextBoundary(sourceState, targetState, filePath); } return totalCopied; } /** * Copy variables of a specific type between states */ private copyVariableType( sourceState: IStateService, targetState: IStateService, variableType: VariableType, skipExisting: boolean, trackVariableCrossing: boolean ): number { let getMethod: keyof IStateService; let setMethod: keyof IStateService; let copied = 0; // Select the appropriate methods based on variable type switch (variableType) { case 'text': getMethod = 'getAllTextVars'; setMethod = 'setTextVar'; break; case 'data': getMethod = 'getAllDataVars'; setMethod = 'setDataVar'; break; case 'path': getMethod = 'getAllPathVars'; setMethod = 'setPathVar'; break; case 'command': getMethod = 'getAllCommands'; setMethod = 'setCommand'; break; } // Check if methods exist if (typeof sourceState[getMethod] !== 'function' || typeof targetState[setMethod] !== 'function') { return 0; } try { // Get all variables of the specified type const variables = (sourceState[getMethod] as Function)(); // Copy each variable variables.forEach((value: any, name: string) => { // Skip if variable exists and skipExisting is true if (skipExisting) { // Determine the appropriate get method for checking existence let existsMethod: keyof IStateService; switch (variableType) { case 'text': existsMethod = 'getTextVar'; break; case 'data': existsMethod = 'getDataVar'; break; case 'path': existsMethod = 'getPathVar'; break; case 'command': existsMethod = 'getCommand'; break; } // Check if variable exists in target state if (typeof targetState[existsMethod] === 'function' && (targetState[existsMethod] as Function)(name) !== undefined) { return; } } // Set the variable (targetState[setMethod] as Function)(name, value); copied++; // Track variable crossing if requested and tracking service exists if (trackVariableCrossing && this.trackingService) { this.trackVariableCrossing(name, variableType, sourceState, targetState); } }); return copied; } catch (error) { logger.debug(`Error copying ${variableType} variables`, { error }); return 0; } } /** * Copy specific variables by name from source state to target state * @param sourceState Source state containing variables * @param targetState Target state to receive variables * @param variableNames List of variable names to copy with optional aliases * @param options Additional options for copying * @returns Number of variables copied */ public copySpecificVariables( sourceState: IStateService, targetState: IStateService, variableNames: Array<{ name: string; alias?: string }>, options: VariableCopyOptions = {} ): number { const { skipExisting = false, trackContextBoundary = true, trackVariableCrossing = true } = options; let totalCopied = 0; // Track boundary if requested and tracking service exists if (trackContextBoundary && this.trackingService) { let filePath: string | undefined; try { filePath = sourceState.getCurrentFilePath?.() || undefined; } catch (error) { logger.debug('Error getting current file path', { error }); } this.trackContextBoundary(sourceState, targetState, filePath); } for (const { name, alias } of variableNames) { // Try to copy as a text variable const textValue = sourceState.getTextVar?.(name); if (textValue !== undefined) { // Skip if variable exists and skipExisting is true if (skipExisting && targetState.getTextVar?.(alias || name) !== undefined) { continue; } targetState.setTextVar(alias || name, textValue); totalCopied++; if (trackVariableCrossing && this.trackingService) { this.trackVariableCrossing(name, 'text', sourceState, targetState, alias); } continue; } // Try to copy as a data variable const dataValue = sourceState.getDataVar?.(name); if (dataValue !== undefined) { // Skip if variable exists and skipExisting is true if (skipExisting && targetState.getDataVar?.(alias || name) !== undefined) { continue; } targetState.setDataVar(alias || name, dataValue); totalCopied++; if (trackVariableCrossing && this.trackingService) { this.trackVariableCrossing(name, 'data', sourceState, targetState, alias); } continue; } // Try to copy as a path variable const pathValue = sourceState.getPathVar?.(name); if (pathValue !== undefined) { // Skip if variable exists and skipExisting is true if (skipExisting && targetState.getPathVar?.(alias || name) !== undefined) { continue; } targetState.setPathVar(alias || name, pathValue); totalCopied++; if (trackVariableCrossing && this.trackingService) { this.trackVariableCrossing(name, 'path', sourceState, targetState, alias); } continue; } // Try to copy as a command const commandValue = sourceState.getCommand?.(name); if (commandValue !== undefined) { // Skip if variable exists and skipExisting is true if (skipExisting && targetState.getCommand?.(alias || name) !== undefined) { continue; } targetState.setCommand(alias || name, commandValue); totalCopied++; if (trackVariableCrossing && this.trackingService) { this.trackVariableCrossing(name, 'command', sourceState, targetState, alias); } continue; } } // Track boundary again if requested and tracking service exists if (trackContextBoundary && this.trackingService) { let filePath: string | undefined; try { filePath = sourceState.getCurrentFilePath?.() || undefined; } catch (error) { logger.debug('Error getting current file path', { error }); } this.trackContextBoundary(sourceState, targetState, filePath); } return totalCopied; } /** * Track context boundary for debugging */ private trackContextBoundary( sourceState: IStateService, targetState: IStateService, filePath?: string ): void { if (!this.trackingService) return; try { const sourceId = sourceState.getStateId?.() || 'unknown'; const targetId = targetState.getStateId?.() || 'unknown'; // Default to 'import' as boundary type since we're copying variables this.trackingService.trackContextBoundary( sourceId, targetId, 'import', filePath ); } catch (error) { logger.debug('Error tracking context boundary', { error }); } } /** * Track variable crossing for debugging */ private trackVariableCrossing( name: string, type: VariableType, sourceState: IStateService, targetState: IStateService, alias?: string ): void { if (!this.trackingService) return; try { const sourceId = sourceState.getStateId?.() || 'unknown'; const targetId = targetState.getStateId?.() || 'unknown'; this.trackingService.trackVariableCrossing( sourceId, targetId, name, type, alias ); } catch (error) { logger.debug('Error tracking variable crossing', { error }); } } } /** * Create a StateVariableCopier instance with optional tracking */ export function createStateVariableCopier( trackingService?: IStateTrackingService ): StateVariableCopier { return new StateVariableCopier(trackingService); }