/** * WorkflowGenerationPipeline — workflow-driven code generation * * Delegates to WorkflowExecutor (backed by VL-Workflow-Engine) for execution. * The workflow JSON (e.g., meta-direct-codegen.json) defines the DAG of LLM * calls, loops, branches, and file writes. * * Fallback: If workflow execution fails, transparently falls back * to the original JS GenerationPipeline. */ import path from 'path'; import fs from 'fs'; import { resolveSeedWorkflowDir } from '../utils/bundle-paths.js'; // Workflow file search order for generation (large/complex projects) const GENERATION_WORKFLOW_NAMES = [ 'fullstack-codegen-v2.json', 'fullstack-codegen.json', 'fullstack-parallel.json', '9-file-codegen.json', '3-file-codegen.json', ]; // Simplified workflow for small projects (Meta-direct path) const META_DIRECT_WORKFLOW = 'meta-direct-codegen.json'; // Complexity threshold: below this, use meta-direct path const SMALL_PROJECT_THRESHOLD = { maxPages: 5, maxServices: 3, maxDescriptionLength: 2000, }; export class WorkflowGenerationPipeline { /** * @param {Object} config - VL-Code configuration */ constructor(config) { this.config = config; this._fallbackPipeline = null; } /** * Run the generation pipeline. * @param {Object} params - { userRequest, targetLang, mode } * @param {Object} callbacks - { onStepStart, onStepDone, onFileWritten, onToken, onDone, onError, onMetadataReady } */ async run(params, callbacks = {}) { const useMetaDirect = params.mode === 'meta-direct' || ( params.mode !== 'auxiliary' && this._isSmallProject(params) ); const workflowFile = useMetaDirect ? this._findWorkflowByName(META_DIRECT_WORKFLOW) : this._findGenerationWorkflow(); if (workflowFile) { callbacks.onStepStart?.({ id: 'mode-select', title: useMetaDirect ? 'Using Meta-Direct path (small project)' : 'Using auxiliary-file path (complex project)', }); try { return await this._runViaEngine(workflowFile, params, callbacks); } catch (err) { callbacks.onError?.(`Engine execution failed (${err.message}), falling back to JS pipeline`); } } // Fallback to original JS pipeline return this._runFallback(params, callbacks); } /** * Execute the generation workflow via WorkflowExecutor (VL-Workflow-Engine). */ async _runViaEngine(workflowFile, params, callbacks) { const cb = callbacks; const wfJson = JSON.parse(fs.readFileSync(workflowFile, 'utf-8')); cb.onStepStart?.({ id: 'engine', title: 'Starting workflow engine execution...' }); const { WorkflowExecutor } = await import('./workflow-executor.js'); const executor = new WorkflowExecutor(this.config); await executor.execute(wfJson, { userRequest: params.userRequest || '', targetLang: params.targetLang || 'en', }, { onNodeStart: (info) => { cb.onStepStart?.({ id: info.nodeId, title: info.title || info.nodeId }); }, onNodeDone: (info) => { cb.onStepDone?.({ id: info.nodeId, title: info.title || info.nodeId }); }, onNodeError: (info) => { cb.onError?.(`Step ${info.nodeId} failed: ${info.error || 'unknown error'}`); }, onToolStart: (info) => cb.onToolStart?.(info), onToolDone: (info) => cb.onToolDone?.(info), onToolError: (info) => cb.onToolError?.(info), onToolMessage: (info) => cb.onToolMessage?.(info), onToken: (token) => cb.onToken?.(token), onFileWritten: (fp) => cb.onFileWritten?.(fp), onDone: (summary) => { cb.onDone?.({ filesWritten: summary.filesWritten, stageCount: summary.stageCount, }); }, onError: (msg) => cb.onError?.(msg), onText: (text) => { cb.onText?.(text); cb.onToken?.(text); }, }); } /** * Fallback to the original JS GenerationPipeline. */ async _runFallback(params, callbacks) { if (!this._fallbackPipeline) { const { GenerationPipeline } = await import('./generation-pipeline.js'); this._fallbackPipeline = new GenerationPipeline(this.config); } return this._fallbackPipeline.run(params, callbacks); } /** * Find a generation workflow JSON file. */ _findGenerationWorkflow() { const searchDirs = [ path.join(this.config.workDir, '.vl-code', 'workflows'), resolveSeedWorkflowDir(), ]; for (const dir of searchDirs) { for (const name of GENERATION_WORKFLOW_NAMES) { const candidate = path.join(dir, name); if (fs.existsSync(candidate)) return candidate; } } return null; } /** * Find a specific workflow by name. */ _findWorkflowByName(name) { const searchDirs = [ path.join(this.config.workDir, '.vl-code', 'workflows'), resolveSeedWorkflowDir(), ]; for (const dir of searchDirs) { const candidate = path.join(dir, name); if (fs.existsSync(candidate)) return candidate; } return null; } /** * Heuristic to determine if a project is "small" (use Meta-direct path). */ _isSmallProject(params) { const req = params.userRequest || ''; if (req.length > SMALL_PROJECT_THRESHOLD.maxDescriptionLength) return false; const pageKeywords = req.match(/page|screen|view|form|list|dashboard/gi) || []; const serviceKeywords = req.match(/service|api|backend|database/gi) || []; if (pageKeywords.length > SMALL_PROJECT_THRESHOLD.maxPages) return false; if (serviceKeywords.length > SMALL_PROJECT_THRESHOLD.maxServices) return false; return true; } }