run-controller.js 10.8 KB
/**
 * Evolution Run Controller — 薄编排器
 *
 * 每个 Run 只进化一个 Skill,互不干涉。
 *
 * Phase 1: 初始化 (Forage → Seed → Benchmark 生成 → 初始适应度)
 * Phase 2: 自然选择循环 (Evaluate → Select → Reproduce → BenchmarkEvolve)
 */
const path = require('path');
const fs = require('fs');
const evolutionQueries = require('./queries').evolutionQueries;
const steps = require('./steps');
const { EvolutionLogger } = require('./evolution-logger');
const { setLogSink, setAgentManager } = require('./evaluator');

class RunController {
  constructor(options = {}) {
    this.skillsDir = options.skillsDir;
    this.populationSize = options.populationSize || 5;
    this.convergenceThreshold = options.convergenceThreshold || 0.9;

    if (!options.agentManager) {
      throw new Error('RunController: options.agentManager is required');
    }
    this._agentManager = options.agentManager;
  }

  /**
   * Phase 1: 初始化单个 Skill 的进化
   *
   * @param {string} skillName - 要进化的 Skill 名称 (如 "code-review")
   */
  async runPhase1(skillName) {
    if (!skillName) throw new Error('必须指定 skillName — 每个 Run 只进化一个 Skill');

    const logger = new EvolutionLogger(null, skillName);
    setLogSink((event, data) => logger.log(event, data));
    setAgentManager(this._agentManager);
    const log = (msg) => console.log(`[RunController] ${msg}`);

    logger.phase('Phase 1: 初始化', { skillName });

    // 1. Forage — 只扫描指定 Skill
    logger.step('Forage', { skillName });
    const { forageOneSkill } = require('./forager');
    const forageResult = await forageOneSkill(skillName, this.skillsDir);
    if (!forageResult) {
      logger.error('Forage', { message: `未找到 Skill: ${skillName}` });
      logger.end({ success: false });
      return { success: false, reason: 'skill-not-found' };
    }

    // 获取该 Skill 关联的 Run
    const { runs } = await evolutionQueries.getRuns({ pageSize: 100 });
    const run = runs.find(r => r.skill_name === skillName) || runs[0];
    if (!run) {
      logger.end({ success: false });
      return { success: false, reason: 'no-run-created' };
    }

    logger.runId = run.id;
    logger.log('run_created', { runId: run.id, skillName });

    // 2. Seed — 创建策略变体
    logger.step('Seed', { populationSize: this.populationSize });
    let seededVariants = 0;
    try {
      const { seedRunPopulation } = require('./genome/seeder');
      seededVariants = await seedRunPopulation(run.id, this.populationSize);
      logger.log('seed_done', { variants: seededVariants });
    } catch (err) {
      logger.error('Seed', err);
    }

    // 3. Benchmark 生成 — 为该 Skill 生成专属测试场景
    logger.step('Benchmark 生成');
    try {
      const existing = await evolutionQueries.getBenchmarks(skillName);
      if (existing.length === 0) {
        logger.llmCall('生成 Benchmark 场景', { backend: 'evaluator' });
        const { generateRunBenchmarks } = require('./benchmark/generator');
        await generateRunBenchmarks(run.id, `${skillName} 能力评估`);
        const created = await evolutionQueries.getBenchmarks(skillName);
        logger.llmResult('Benchmark 生成', { success: true, count: created.length });
      } else {
        logger.log('benchmark_exists', { count: existing.length });
      }
    } catch (e) {
      logger.error('Benchmark 生成', e);
    }

    // 4. 初始适应度评估
    logger.step('初始适应度评估');
    try {
      logger.llmCall('6 维适应度评估', { backend: 'evaluator' });
      const { evaluateRunFitness } = require('./operators/fitness-calculator');
      const fitnessResults = await evaluateRunFitness(run.id);
      for (const r of fitnessResults) {
        logger.genome('fitness', { id: r.genomeId, fitness: r.overall, dimensions: r.dimensionScores });
      }
      logger.llmResult('适应度评估', { success: true, evaluated: fitnessResults.length });
    } catch (e) {
      logger.error('适应度评估', e);
    }

    // 更新状态
    const bestGenome = await evolutionQueries.getBestGenome(run.id);
    await evolutionQueries.updateRun(run.id, {
      status: 'initialized',
      current_generation: 0,
      best_fitness: bestGenome?.overall_fitness || null,
    });

    const elapsed = ((Date.now() - logger.startTime) / 1000).toFixed(1);
    const summary = {
      success: true, runId: run.id, skillName,
      variants: seededVariants,
      bestFitness: bestGenome?.overall_fitness,
      elapsedSeconds: parseFloat(elapsed),
    };

    logger.phase('Phase 1 完成', summary);
    logger.end(summary);

    log(`Phase 1 完成 | ${skillName} | ${elapsed}s | bestFitness=${summary.bestFitness || '—'}`);
    return summary;
  }

  /**
   * Phase 2: 自然选择循环
   *
   * 每代: Evaluate → Select → Reproduce → BenchmarkEvolve
   */
  async runPhase2(runId, generations = 5) {
    const run = await evolutionQueries.getRun(runId);
    if (!run) throw new Error(`Run ${runId} not found`);

    const logger = new EvolutionLogger(runId, run.skill_name);
    setLogSink((event, data) => logger.log(event, data));
    setAgentManager(this._agentManager);
    const log = (msg) => console.log(`[RunController] ${msg}`);

    logger.phase('Phase 2: 自然选择', { runId, skillName: run.skill_name, generations });

    await evolutionQueries.updateRun(runId, {
      status: 'running',
      max_generations: run.current_generation + generations,
      started_at: new Date(),
    });

    const generationLog = [];

    for (let gen = run.current_generation + 1; gen <= run.current_generation + generations; gen++) {
      logger.phase(`Generation ${gen}`);
      const ctx = { log, run, generation: gen, logger };

      const result = { generation: gen };

      // Evaluate
      logger.step('Evaluate (Arena + Benchmark + Fitness)');
      Object.assign(result, await steps.stepEvaluate(runId, ctx));
      if (result.fitness) {
        for (const f of (result.fitness.results || [])) {
          logger.genome('fitness', { id: f.genomeId, fitness: f.overall });
        }
      }

      // Select
      logger.step('Select');
      Object.assign(result, await steps.stepSelect(runId, ctx));
      if (result.selection) {
        logger.log('selection', {
          elites: result.selection.elites,
          survivors: result.selection.survivors,
          killed: result.selection.killed,
        });
      }

      // Reproduce
      logger.step('Reproduce (Crossover + Mutation)');
      Object.assign(result, await steps.stepReproduce(runId, ctx));
      if (result.crossover) logger.log('crossover', { children: result.crossover.count || 0 });
      if (result.mutation) logger.log('mutation', { children: result.mutation.count || 0 });

      // BenchmarkEvolve
      logger.step('Benchmark 自进化');
      Object.assign(result, await steps.stepBenchmarkEvolve(runId, ctx));

      const best = await evolutionQueries.getBestGenome(runId);
      await evolutionQueries.updateRun(runId, {
        current_generation: gen,
        best_fitness: best?.overall_fitness || null,
      });

      result.bestFitness = best?.overall_fitness;
      generationLog.push(result);

      const aliveCount = result.selection?.survivors ?? '?';
      logger.log('generation_done', { gen, bestFitness: result.bestFitness, alive: aliveCount },
        `📈 Gen ${gen} | best=${result.bestFitness ? (result.bestFitness * 100).toFixed(1) + '%' : '—'} | alive=${aliveCount}`);

      if (result.bestFitness && result.bestFitness >= this.convergenceThreshold) {
        logger.log('converged', { fitness: result.bestFitness, threshold: this.convergenceThreshold });
        await evolutionQueries.updateRun(runId, { status: 'converged' });
        break;
      }
    }

    await evolutionQueries.updateRun(runId, { status: 'completed', completed_at: new Date() });
    const elapsed = ((Date.now() - logger.startTime) / 1000).toFixed(1);

    const summary = { success: true, runId, generations: generationLog.length, elapsedSeconds: parseFloat(elapsed) };
    logger.phase('Phase 2 完成', summary);
    logger.end(summary);

    log(`Phase 2 完成 | ${generationLog.length} 代 | ${elapsed}s`);
    return { ...summary, generationLog };
  }

  /**
   * 部署进化结果: 将最佳基因组写回 SKILL.md
   *
   * @param {string} runId - Evolution Run ID
   * @returns {object} 部署结果
   */
  async deploy(runId) {
    const log = (msg) => console.log(`[RunController] ${msg}`);

    const run = await evolutionQueries.getRun(runId);
    if (!run) throw new Error(`Run ${runId} not found`);

    log('═══════════════════════════════════════');
    log(`  Deploy: ${run.skill_name}`);
    log('═══════════════════════════════════════\n');

    const bestGenome = await evolutionQueries.getBestGenome(runId);
    if (!bestGenome) {
      log('  ❌ 没有可部署的基因组');
      return { success: false, reason: 'no-genome' };
    }

    log(`  🏆 最佳: ${bestGenome.id.slice(0, 8)} (Gen ${bestGenome.generation}, fitness=${bestGenome.overall_fitness ? (bestGenome.overall_fitness * 100).toFixed(1) + '%' : '—'})`);

    const { writeGenomeToSkill } = require('./genome/deserializer');
    const path = require('path');
    const targetDir = path.join(this.skillsDir, run.skill_name);

    const result = writeGenomeToSkill(bestGenome, targetDir, {
      name: run.skill_name,
    }, { backup: true });

    log(`  ✅ 部署完成: ${result.path}${result.backedUp ? ' (原文件已备份)' : ''}`);

    return {
      success: true,
      skillName: run.skill_name,
      genomeId: bestGenome.id,
      fitness: bestGenome.overall_fitness,
      generation: bestGenome.generation,
      deployedTo: result.path,
      backedUp: result.backedUp,
    };
  }
}

// CLI
if (require.main === module) {
  const args = process.argv.slice(2);
  const controller = new RunController();

  if (args.includes('--phase2')) {
    const runId = args.find(a => !a.startsWith('--'));
    const gens = parseInt(args.find(a => a.startsWith('--gens='))?.split('=')[1]) || 5;
    if (!runId) { console.error('Usage: node run-controller.js --phase2 <runId> [--gens=5]'); process.exit(1); }
    controller.runPhase2(runId, gens)
      .then(r => { console.log('\n📋 Result:', JSON.stringify(r, null, 2)); process.exit(0); })
      .catch(e => { console.error('Fatal:', e); process.exit(1); });
  } else {
    const skillName = args.find(a => !a.startsWith('--'));
    if (!skillName) { console.error('Usage: node run-controller.js <skillName>'); process.exit(1); }
    controller.runPhase1(skillName)
      .then(r => { console.log('\n📋 Result:', JSON.stringify(r, null, 2)); process.exit(0); })
      .catch(e => { console.error('Fatal:', e); process.exit(1); });
  }
}

module.exports = { RunController };