workflow-generation-pipeline.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. /**
  2. * WorkflowGenerationPipeline — workflow-driven code generation
  3. *
  4. * Delegates to WorkflowExecutor (backed by VL-Workflow-Engine) for execution.
  5. * The workflow JSON (e.g., meta-direct-codegen.json) defines the DAG of LLM
  6. * calls, loops, branches, and file writes.
  7. *
  8. * Fallback: If workflow execution fails, transparently falls back
  9. * to the original JS GenerationPipeline.
  10. */
  11. import path from 'path';
  12. import fs from 'fs';
  13. import { resolveSeedWorkflowDir } from '../utils/bundle-paths.js';
  14. // Workflow file search order for generation (large/complex projects)
  15. const GENERATION_WORKFLOW_NAMES = [
  16. 'fullstack-codegen-v2.json',
  17. 'fullstack-codegen.json',
  18. 'fullstack-parallel.json',
  19. '9-file-codegen.json',
  20. '3-file-codegen.json',
  21. ];
  22. // Simplified workflow for small projects (Meta-direct path)
  23. const META_DIRECT_WORKFLOW = 'meta-direct-codegen.json';
  24. // Complexity threshold: below this, use meta-direct path
  25. const SMALL_PROJECT_THRESHOLD = {
  26. maxPages: 5,
  27. maxServices: 3,
  28. maxDescriptionLength: 2000,
  29. };
  30. export class WorkflowGenerationPipeline {
  31. /**
  32. * @param {Object} config - VL-Code configuration
  33. */
  34. constructor(config) {
  35. this.config = config;
  36. this._fallbackPipeline = null;
  37. }
  38. /**
  39. * Run the generation pipeline.
  40. * @param {Object} params - { userRequest, targetLang, mode }
  41. * @param {Object} callbacks - { onStepStart, onStepDone, onFileWritten, onToken, onDone, onError, onMetadataReady }
  42. */
  43. async run(params, callbacks = {}) {
  44. const useMetaDirect = params.mode === 'meta-direct' || (
  45. params.mode !== 'auxiliary' && this._isSmallProject(params)
  46. );
  47. const workflowFile = useMetaDirect
  48. ? this._findWorkflowByName(META_DIRECT_WORKFLOW)
  49. : this._findGenerationWorkflow();
  50. if (workflowFile) {
  51. callbacks.onStepStart?.({
  52. id: 'mode-select',
  53. title: useMetaDirect ? 'Using Meta-Direct path (small project)' : 'Using auxiliary-file path (complex project)',
  54. });
  55. try {
  56. return await this._runViaEngine(workflowFile, params, callbacks);
  57. } catch (err) {
  58. callbacks.onError?.(`Engine execution failed (${err.message}), falling back to JS pipeline`);
  59. }
  60. }
  61. // Fallback to original JS pipeline
  62. return this._runFallback(params, callbacks);
  63. }
  64. /**
  65. * Execute the generation workflow via WorkflowExecutor (VL-Workflow-Engine).
  66. */
  67. async _runViaEngine(workflowFile, params, callbacks) {
  68. const cb = callbacks;
  69. const wfJson = JSON.parse(fs.readFileSync(workflowFile, 'utf-8'));
  70. cb.onStepStart?.({ id: 'engine', title: 'Starting workflow engine execution...' });
  71. const { WorkflowExecutor } = await import('./workflow-executor.js');
  72. const executor = new WorkflowExecutor(this.config);
  73. await executor.execute(wfJson, {
  74. userRequest: params.userRequest || '',
  75. targetLang: params.targetLang || 'en',
  76. }, {
  77. onNodeStart: (info) => {
  78. cb.onStepStart?.({ id: info.nodeId, title: info.title || info.nodeId });
  79. },
  80. onNodeDone: (info) => {
  81. cb.onStepDone?.({ id: info.nodeId, title: info.title || info.nodeId });
  82. },
  83. onNodeError: (info) => {
  84. cb.onError?.(`Step ${info.nodeId} failed: ${info.error || 'unknown error'}`);
  85. },
  86. onToolStart: (info) => cb.onToolStart?.(info),
  87. onToolDone: (info) => cb.onToolDone?.(info),
  88. onToolError: (info) => cb.onToolError?.(info),
  89. onToolMessage: (info) => cb.onToolMessage?.(info),
  90. onToken: (token) => cb.onToken?.(token),
  91. onFileWritten: (fp) => cb.onFileWritten?.(fp),
  92. onDone: (summary) => {
  93. cb.onDone?.({
  94. filesWritten: summary.filesWritten,
  95. stageCount: summary.stageCount,
  96. });
  97. },
  98. onError: (msg) => cb.onError?.(msg),
  99. onText: (text) => {
  100. cb.onText?.(text);
  101. cb.onToken?.(text);
  102. },
  103. });
  104. }
  105. /**
  106. * Fallback to the original JS GenerationPipeline.
  107. */
  108. async _runFallback(params, callbacks) {
  109. if (!this._fallbackPipeline) {
  110. const { GenerationPipeline } = await import('./generation-pipeline.js');
  111. this._fallbackPipeline = new GenerationPipeline(this.config);
  112. }
  113. return this._fallbackPipeline.run(params, callbacks);
  114. }
  115. /**
  116. * Find a generation workflow JSON file.
  117. */
  118. _findGenerationWorkflow() {
  119. const searchDirs = [
  120. path.join(this.config.workDir, '.vl-code', 'workflows'),
  121. resolveSeedWorkflowDir(),
  122. ];
  123. for (const dir of searchDirs) {
  124. for (const name of GENERATION_WORKFLOW_NAMES) {
  125. const candidate = path.join(dir, name);
  126. if (fs.existsSync(candidate)) return candidate;
  127. }
  128. }
  129. return null;
  130. }
  131. /**
  132. * Find a specific workflow by name.
  133. */
  134. _findWorkflowByName(name) {
  135. const searchDirs = [
  136. path.join(this.config.workDir, '.vl-code', 'workflows'),
  137. resolveSeedWorkflowDir(),
  138. ];
  139. for (const dir of searchDirs) {
  140. const candidate = path.join(dir, name);
  141. if (fs.existsSync(candidate)) return candidate;
  142. }
  143. return null;
  144. }
  145. /**
  146. * Heuristic to determine if a project is "small" (use Meta-direct path).
  147. */
  148. _isSmallProject(params) {
  149. const req = params.userRequest || '';
  150. if (req.length > SMALL_PROJECT_THRESHOLD.maxDescriptionLength) return false;
  151. const pageKeywords = req.match(/page|screen|view|form|list|dashboard/gi) || [];
  152. const serviceKeywords = req.match(/service|api|backend|database/gi) || [];
  153. if (pageKeywords.length > SMALL_PROJECT_THRESHOLD.maxPages) return false;
  154. if (serviceKeywords.length > SMALL_PROJECT_THRESHOLD.maxServices) return false;
  155. return true;
  156. }
  157. }