train-skill-tool.js 11.3 KB
/**
 * train-skill-tool.js — Master 训练新 Skill 工具
 *
 * 让 Master Agent 可以触发完整的 Hunt→Digest→Evolve/SkillCreate 流水线。
 * 训练产物写入 data/trained_skills/user_{id}/{skillName}/,按用户隔离。
 *
 * 使用场景:
 * - 用户说"训练一个新 skill"
 * - 用户说"使用 innovator 帮我创建 xxx skill"
 * - 用户说"帮我重训/改进 xxx skill"
 */

const fs = require('fs');
const path = require('path');

/**
 * 创建 train_skill 工具定义
 */
function createTrainSkillTool() {
  // 延迟创建 JobManager(模块加载时 .data 目录可能尚未就绪)
  let _jobManager = null;
  function getJobManager() {
    if (!_jobManager) {
      const { JobManager } = require('../../services/job-manager');
      _jobManager = new JobManager({
        storePath: path.resolve(__dirname, '../../../.data/jobs-train.json'),
      });
    }
    return _jobManager;
  }

  return {
    name: 'train_skill',
    description: `训练一个新 Skill 或重训一个已有 Skill。触发完整的 Hunt→Digest→Evolve 流水线:
- Hunt: 猎取领域知识(网络研究、资料收集)
- Digest: 消化提炼为评估基准(benchmark)
- Evolve/SkillCreate: 基于知识库和基准生成高质量 SKILL.md

适用场景:
- 用户说"训练/创建一个新 skill"
- 用户说"使用 innovator 训练 xxx"
- 用户说"帮我做一个新的 xxx skill"

注意:训练任务通常需要 5-10 分钟,此工具异步执行并返回 jobId。
用 list_retrain_jobs 工具可查看进度。`,

    parameters: {
      type: 'object',
      required: ['skillName', 'description'],
      properties: {
        skillName: {
          type: 'string',
          description: 'Skill 名称(英文 kebab-case 或 snake_case,如 genlogo-prompt、code_reviewer)',
        },
        description: {
          type: 'string',
          description: 'Skill 的功能描述——它要做什么、用户输入什么、期望输出什么',
        },
        instructions: {
          type: 'string',
          description: '用户的额外训练要求或补充说明(可选)',
        },
        mode: {
          type: 'string',
          description: '"create"(新建 skill,默认)或 "retrain"(重训已有 skill)',
        },
      },
    },

    async execute(params, context) {
      const { skillName, description, instructions = '', mode = 'create' } = params;
      const config = require('../../config');
      const jobManager = getJobManager();
      const userId = context.userId || 'anonymous';

      if (mode === 'retrain') {
        return _executeRetrain(skillName, description, instructions, jobManager, config, userId);
      }

      return _executeCreate(skillName, description, instructions, jobManager, config, userId);
    },
  };
}

/**
 * 执行新 Skill 创建(Hunt→Digest→SkillCreate)
 */
async function _executeCreate(skillName, description, instructions, jobManager, config, userId) {
  const { InnovatorService } = require('../../services/innovator-service');
  const { initTrainedSkillDir } = require('../../services/skill-dir-manager');
  const skillDb = require('../../services/skill-db');

  // 1. 初始化用户训练 skill 目录
  const skillDir = initTrainedSkillDir(skillName, userId);
  console.log(`[TrainSkill] 初始化目录: ${skillDir}`);

  // 2. 创建 Job
  const job = jobManager.createJob({
    type: 'train-create',
    skillName,
    description,
    status: 'queued',
    progress: '准备启动训练流水线...',
    userId: String(userId),
  });

  // 3. 注册到 DB(如果不存在)
  try {
    skillDb.registerSkill({
      skillName,
      description,
      domain: skillName,
      scope: '',
      keywords: skillName.split(/[-_]/).join(','),
      directory: skillDir,
      userId: String(userId),
      storageType: 'trained',
    });
  } catch (err) {
    if (err.message.includes('already exists')) {
      // 更新已有条目的 directory 指向
      try { skillDb.updateSkill(skillName, { directory: skillDir, userId: String(userId), storageType: 'trained' }); } catch {}
    } else {
      console.warn(`[TrainSkill] registerSkill 警告: ${err.message}`);
    }
  }

  // 4. 异步执行 pipeline(不阻塞 Master 回复)
  _runTrainPipeline(job, skillName, description, instructions, jobManager, config, userId, skillDir).catch(err => {
    console.error(`[TrainSkill] Pipeline 失败: ${err.message}`);
    jobManager.updateJob(job.jobId, {
      status: 'failed',
      error: err.message,
      progress: `训练失败: ${err.message}`,
    });
  });

  return {
    status: 'started',
    jobId: job.jobId,
    skillName,
    userId: String(userId),
    skillDir,
    pipeline: 'Hunt → Digest → SkillCreate',
    message: `已启动 "${skillName}" 的训练流水线。训练通常需要 5-10 分钟。`,
    tip: '使用 list_retrain_jobs 工具可查看进度。',
  };
}

/**
 * 执行已有 Skill 重训(ReHunt→ReDigest→Evolve)
 */
async function _executeRetrain(skillName, description, instructions, jobManager, config, userId) {
  const { InnovatorService } = require('../../services/innovator-service');
  const { RetrainPipeline } = require('../../services/retrain-pipeline');
  const { resolveTrainedSkillDir } = require('../../services/skill-dir-manager');

  const skillsDir = config.skillsCodeDir;
  const innovator = new InnovatorService({
    skillsDir,
    skillsDataDir: config.skillsDataDir,
    trainedSkillsDir: config.trainedSkillsDir,
  });

  const skill = innovator.registry.getSkill(skillName);
  if (!skill) {
    return {
      status: 'error',
      message: `Skill "${skillName}" 不存在。请先用 mode="create" 创建。`,
    };
  }

  // 创建 Job
  const job = jobManager.createJob({
    type: 'retrain',
    skillName,
    description: instructions || description,
    status: 'queued',
    progress: '准备启动重训流水线...',
    userId: String(userId),
  });
  innovator.registry.updateSkill(skillName, { status: 'training' });

  // 异步执行 retrain pipeline
  const pipeline = new RetrainPipeline({
    job,
    skill,
    innovator,
    jobManager,
    instructions: instructions || description,
    userId,
  });
  pipeline.run().catch(err => {
    console.error(`[TrainSkill] Retrain 失败: ${err.message}`);
    jobManager.updateJob(job.jobId, {
      status: 'failed',
      error: err.message,
      progress: `重训失败: ${err.message}`,
    });
    try { innovator.registry.updateSkill(skillName, { status: 'enable' }); } catch {}
  });

  return {
    status: 'started',
    jobId: job.jobId,
    skillName,
    userId: String(userId),
    pipeline: 'ReHunt → ReDigest → Evolve',
    message: `已启动 "${skillName}" 的重训流水线 (目标 ≥95 分)。`,
    tip: '使用 list_retrain_jobs 工具可查看进度。',
  };
}

/**
 * 新 Skill 训练流水线(RL 闭环版本)
 *
 * 使用 EvolutionLoop 实现 Perceive → Plan → Execute → Evaluate → Reflect 循环,
 * 替代原有的线性 Hunt→Digest→SkillCreate 直通流程。
 *
 * 产物写入 data/trained_skills/user_{id}/{skillName}/ 目录
 */
async function _runTrainPipeline(job, skillName, description, instructions, jobManager, config, userId, skillDir) {
  const { ConversationLogger } = require('../../services/conversation-logger');
  const taskMgr = require('../../services/skill-task-manager');
  const { randomUUID } = require('crypto');
  const { loadConfigFromSkills } = require('../core/skill-config-loader');
  const AgentManager = require('../core/agent-manager');
  const { InnovatorService } = require('../../services/innovator-service');
  const { EvolutionLoop } = require('../../services/evolution-loop');

  const skillsDir = config.skillsCodeDir;
  const conversationsDir = config.conversationsDir || path.resolve(__dirname, '../../../conversations');
  // 日志按用户隔离
  const userConvDir = path.join(conversationsDir, `user-${userId}`);
  const conversationId = `train-${job.jobId}`;
  const messageId = `msg-${Date.now()}-${randomUUID().substring(0, 8)}`;
  const logger = new ConversationLogger(userConvDir, conversationId, messageId, { userId });

  taskMgr.initTask(skillDir, {
    skillName,
    description,
    source: 'master-train',
    jobId: job.jobId,
    userId: String(userId),
    pipelineType: 'evolution',
  });
  logger.ensureMeta(`[Train] ${skillName}: ${description.substring(0, 80)}`);
  logger.logUserMessage(`[Train] 训练新 Skill (RL Loop): ${skillName}\n${description}\n${instructions}`);

  // 加载 Agent 配置 — 使用系统 skills 目录(l3-hunt 等 agent 在 data/skills/ 下)
  const backends = JSON.parse(fs.readFileSync(
    path.join(path.resolve(__dirname, '../../config'), 'backends.json'), 'utf-8'
  ));
  const agentConfig = loadConfigFromSkills(skillsDir, backends);
  // Agent 的 workDir 设为用户训练目录的父目录(user_{id}/),
  // 这样 agent 引用 {skillName}/hunt/knowhow.md 就指向正确路径
  const userTrainDir = path.dirname(skillDir);
  const manager = new AgentManager(agentConfig, userTrainDir);

  try {
    // ── EvolutionLoop: RL 闭环 ──
    const loop = new EvolutionLoop({
      skillName,
      description,
      skillDir,
      instructions,
      manager,
      jobManager,
      job,
      logger,
      taskMgr,
      maxIterations: 5,
      goalScore: 90,
      onData: (agent, chunk) => {
        if (chunk?.trim()) logger.logStreamChunk(agent, chunk);
      },
    });

    jobManager.updateJob(job.jobId, {
      status: 'evolving',
      progress: '🔄 RL 进化循环启动...',
      skillName,
      conversationId,
    });

    const result = await loop.run();

    // ── Publish ──
    const innovator = new InnovatorService({
      skillsDir,
      skillsDataDir: config.skillsDataDir,
      trainedSkillsDir: config.trainedSkillsDir,
    });
    innovator.autoRegisterNewSkills();

    logger.logResult({
      type: 'train_skill',
      jobId: job.jobId,
      skillName,
      userId: String(userId),
      skillDir,
      evolutionResult: {
        status: result.status,
        finalScore: result.finalScore,
        bestScore: result.bestScore,
        iterations: result.iterations,
      },
    });

    const statusEmoji = result.status === 'goal_achieved' ? '✅' : '⚠️';
    const statusText = result.status === 'goal_achieved' 
      ? `达标 (${result.finalScore}分)`
      : result.status === 'stagnant'
        ? `停滞 (最佳 ${result.bestScore}分)`
        : `迭代用尽 (最佳 ${result.bestScore}分)`;

    jobManager.updateJob(job.jobId, {
      status: 'completed',
      progress: `${statusEmoji} 训练完成:${skillName} — ${statusText}`,
      conversationId,
      result: {
        skillName,
        userId: String(userId),
        skillDir,
        output: `Skill 训练完成 (RL Loop: ${result.iterations}轮, ${statusText})`,
        evolutionStatus: result.status,
        finalScore: result.finalScore,
        bestScore: result.bestScore,
        iterations: result.iterations,
      },
    });

    try { await manager.closeAllSessions(); } catch {}
    console.log(`[TrainSkill] ${statusEmoji} ${skillName} 训练完成 → ${skillDir} (${statusText})`);

  } catch (err) {
    logger.logError(err.message);
    const nextStep = taskMgr.getNextPendingStep(skillDir);
    if (nextStep) taskMgr.updateStep(skillDir, nextStep, 'failed', err.message);
    try { await manager.closeAllSessions(); } catch {}
    throw err;
  }
}

module.exports = { createTrainSkillTool };