Advertisement

← vibesdk

worker/agents/core/stateMigration.ts

10.2 KB Download file
import { AgentState, FileState } from './state';
import { StructuredLogger } from '../../logger';
import { TemplateDetails } from 'worker/services/sandbox/sandboxTypes';
import { generateNanoId } from '../../utils/idGenerator';
import { generateProjectName } from '../utils/templateCustomizer';
import { MAX_AGENT_QUERY_LENGTH } from 'worker/api/controllers/agent/types';
import type { InferenceMetadata } from '../inferutils/config.types';
import { getBehaviorTypeForProject } from './features';

// Type guards for legacy state detection
type LegacyFileFormat = {
    file_path?: string;
    file_contents?: string;
    file_purpose?: string;
};

type StateWithDeprecatedFields = AgentState & {
    latestScreenshot?: unknown;
    templateDetails?: TemplateDetails;
    agentMode?: string;
    inferenceContext?: unknown;
};

function hasLegacyFileFormat(file: unknown): file is LegacyFileFormat {
    if (typeof file !== 'object' || file === null) return false;
    return 'file_path' in file || 'file_contents' in file || 'file_purpose' in file;
}

function hasField<K extends string>(state: AgentState, key: K): state is AgentState & Record<K, unknown> {
    return key in state;
}

function isStateWithTemplateDetails(state: AgentState): state is StateWithDeprecatedFields & { templateDetails: TemplateDetails } {
    return 'templateDetails' in state;
}

function isStateWithAgentMode(state: AgentState): state is StateWithDeprecatedFields & { agentMode: string } {
    return 'agentMode' in state;
}

function isRecord(value: unknown): value is Record<string, unknown> {
    return typeof value === 'object' && value !== null;
}

function extractInferenceMetadata(value: unknown): InferenceMetadata | null {
    if (!isRecord(value)) return null;

    const agentId = value.agentId;
    const userId = value.userId;
    if (typeof agentId !== 'string' || agentId.trim() === '') return null;
    if (typeof userId !== 'string' || userId.trim() === '') return null;

    return { agentId, userId };
}

export class StateMigration {
    static migrateCommon(state: AgentState): AgentState | null {
        const stateRecord = state as unknown as Record<string, unknown>;

        // Check if we already have valid metadata
        if (state.metadata?.agentId && state.metadata?.userId) {
            return null; // No migration needed
        }

        // Try to extract from legacy inferenceContext
        const hasLegacyInferenceContext = hasField(state, 'inferenceContext');
        if (hasLegacyInferenceContext) {
            const rawInferenceContext = stateRecord.inferenceContext;
            const extractedMetadata = extractInferenceMetadata(rawInferenceContext);

            if (extractedMetadata) {
                // Create new state with migrated metadata
                const nextStateRecord: Record<string, unknown> = {
                    ...stateRecord,
                    metadata: extractedMetadata,
                };
                // Remove the old inferenceContext field
                delete nextStateRecord.inferenceContext;

                return nextStateRecord as unknown as AgentState;
            }
        }

        return null; // No migration possible
    }

    // Mostly for phasic behavior
    static migratePhasic(state: AgentState, logger: StructuredLogger): AgentState | null {
        let needsMigration = false;
        const stateRecord = state as unknown as Record<string, unknown>;

        // If the query is too long, truncate it to avoid performance issues
        if (state.query && state.query.length > MAX_AGENT_QUERY_LENGTH) {
            logger.warn("Large prompt detected. Truncating query to avoid performance issues");
            state.query = state.query.slice(0, MAX_AGENT_QUERY_LENGTH);
            needsMigration = true;
        }

        //------------------------------------------------------------------------------------
        // Migrate files from old schema
        //------------------------------------------------------------------------------------
        const migrateFile = (file: FileState | unknown): FileState => {
            if (hasLegacyFileFormat(file)) {
                return {
                    filePath: (file as FileState).filePath || file.file_path || '',
                    fileContents: (file as FileState).fileContents || file.file_contents || '',
                    filePurpose: (file as FileState).filePurpose || file.file_purpose || '',
                    lastDiff: (file as FileState).lastDiff || '',
                };
            }
            return file as FileState;
        };

        const migratedFilesMap: Record<string, FileState> = {};
        for (const [key, file] of Object.entries(state.generatedFilesMap)) {
            const migratedFile = migrateFile(file);

            migratedFilesMap[key] = {
                ...migratedFile,
            };

            if (migratedFile !== file) {
                needsMigration = true;
            }
        }

        //------------------------------------------------------------------------------------
        // Migrate deprecated props
        //------------------------------------------------------------------------------------  
        const stateHasDeprecatedProps = hasField(state, 'latestScreenshot');
        if (stateHasDeprecatedProps) {
            needsMigration = true;
        }

        const stateHasProjectUpdatesAccumulator = hasField(state, 'projectUpdatesAccumulator');
        if (!stateHasProjectUpdatesAccumulator) {
            needsMigration = true;
        }

        //------------------------------------------------------------------------------------
        // Migrate templateDetails -> templateName
        //------------------------------------------------------------------------------------
        let migratedTemplateName = state.templateName;
        const hasTemplateDetails = isStateWithTemplateDetails(state);
        if (hasTemplateDetails) {
            migratedTemplateName = state.templateDetails.name;
            needsMigration = true;
            logger.info('Migrating templateDetails to templateName', { templateName: migratedTemplateName });
        }

        //------------------------------------------------------------------------------------
        // Migrate projectName -> generate if missing
        //------------------------------------------------------------------------------------
        let migratedProjectName = state.projectName;
        if (!state.projectName) {
            // Generate project name for older apps
            migratedProjectName = generateProjectName(
                state.blueprint?.projectName || migratedTemplateName || state.query,
                generateNanoId(),
                20
            );
            needsMigration = true;
            logger.info('Generating missing projectName', { projectName: migratedProjectName });
        }

        let migratedProjectType = state.projectType;
        const hasProjectType = hasField(state, 'projectType');
        if (!hasProjectType || migratedProjectType !== 'app' && migratedProjectType !== 'presentation' && migratedProjectType !== 'general' && migratedProjectType !== 'workflow') {
            migratedProjectType = 'app';
            needsMigration = true;
            logger.info('Adding default projectType for legacy state', { projectType: migratedProjectType });
        }

        let migratedBehaviorType = state.behaviorType;
        const rawBehaviorType = stateRecord.behaviorType;
        const hasValidBehaviorType = rawBehaviorType === 'phasic' || rawBehaviorType === 'agentic' || rawBehaviorType === 'think';

        if (!hasField(state, 'behaviorType') || !hasValidBehaviorType) {
            // Derive default behaviorType from the (migrated) projectType
            // via the feature registry. Falls back to 'phasic' if the
            // helper is unavailable. This keeps the migration consistent
            // with `DEFAULT_FEATURE_DEFINITIONS` (single source of truth).
            try {
                migratedBehaviorType = getBehaviorTypeForProject(migratedProjectType);
            } catch {
                migratedBehaviorType = 'phasic';
            }
            needsMigration = true;
            logger.info('Adding default behaviorType for legacy state', { behaviorType: migratedBehaviorType });
        }

        if (isStateWithAgentMode(state)) {
            migratedBehaviorType = state.agentMode === 'smart' ? 'agentic' : 'phasic';
            needsMigration = true;
            logger.info('Migrating agentMode to behaviorType', {
                oldMode: state.agentMode,
                newType: migratedBehaviorType,
            });
        }

        const migratedProjectUpdatesAccumulator = stateHasProjectUpdatesAccumulator && Array.isArray((stateRecord as Record<string, unknown>).projectUpdatesAccumulator)
            ? (stateRecord.projectUpdatesAccumulator as string[])
            : [];

        if (needsMigration) {
            logger.info('Migrating state: schema format fixes and legacy field cleanup', {
                generatedFilesCount: Object.keys(migratedFilesMap).length,
            });

            const nextStateRecord: Record<string, unknown> = {
                ...stateRecord,
                behaviorType: migratedBehaviorType,
                projectType: migratedProjectType,
                generatedFilesMap: migratedFilesMap,
                projectUpdatesAccumulator: migratedProjectUpdatesAccumulator,
                templateName: migratedTemplateName,
                projectName: migratedProjectName,
            };

            const newState = nextStateRecord as unknown as AgentState;

            const stateWithDeprecated = newState as StateWithDeprecatedFields;
            if (stateHasDeprecatedProps) {
                delete stateWithDeprecated.latestScreenshot;
            }
            if (hasTemplateDetails) {
                delete stateWithDeprecated.templateDetails;
            }
            if (isStateWithAgentMode(state)) {
                delete stateWithDeprecated.agentMode;
            }

            return newState;
        }

        return null;
    }
}