432 lines
13 KiB
JavaScript
432 lines
13 KiB
JavaScript
/**
|
||
* GLADA · 主执行循环 · execution-loop.js
|
||
*
|
||
* 核心闭环:
|
||
* 取任务 → 加载上下文 → 逐步执行 → 记录 → Git 提交 → 通知
|
||
*
|
||
* 这是 GLADA 的心脏。它不断从任务队列中取出任务,
|
||
* 按步骤逐一执行,每一步都经过:
|
||
* 1. 深度上下文构建
|
||
* 2. LLM 代码生成
|
||
* 3. 回归防护
|
||
* 4. 自动测试
|
||
* 5. Git 提交
|
||
* 6. 开发记录
|
||
*
|
||
* 任务来源(双通道):
|
||
* A. core/task-queue 中央任务队列(type: 'glada-task')
|
||
* B. CAB pending 目录 + GLADA 本地队列(兼容)
|
||
*
|
||
* 版权:国作登字-2026-A-00037559
|
||
* 签发:铸渊 · ICE-GL-ZY001
|
||
*/
|
||
|
||
'use strict';
|
||
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
|
||
const taskReceiver = require('./task-receiver');
|
||
const contextBuilder = require('./context-builder');
|
||
const stepExecutor = require('./step-executor');
|
||
const gitOperator = require('./git-operator');
|
||
const notifier = require('./notifier');
|
||
const skillDistiller = require('./skill-distiller');
|
||
const reflector = require('./reflector');
|
||
|
||
const ROOT = path.resolve(__dirname, '..');
|
||
const EXECUTION_LOG_DIR = path.join(ROOT, 'glada', 'logs', 'executions');
|
||
|
||
// 尝试加载中央任务队列(容错:如果不存在则跳过)
|
||
let taskQueue = null;
|
||
try {
|
||
taskQueue = require('../core/task-queue');
|
||
} catch {
|
||
console.warn('[GLADA] ⚠️ core/task-queue 加载失败,仅使用本地队列');
|
||
}
|
||
|
||
/**
|
||
* 执行单个 GLADA 任务(完整的步骤循环)
|
||
* @param {Object} gladaTask - GLADA 任务对象
|
||
* @param {Object} [options] - 执行选项
|
||
* @returns {Promise<Object>} 执行结果
|
||
*/
|
||
async function executeTask(gladaTask, options = {}) {
|
||
const taskId = gladaTask.glada_task_id;
|
||
const startTime = Date.now();
|
||
|
||
console.log(`\n${'═'.repeat(60)}`);
|
||
console.log(`[GLADA] 🚀 开始执行任务: ${taskId}`);
|
||
console.log(`[GLADA] 📋 标题: ${gladaTask.plan.title}`);
|
||
console.log(`[GLADA] 📊 步骤数: ${gladaTask.plan.steps.length}`);
|
||
console.log(`${'═'.repeat(60)}\n`);
|
||
|
||
// 更新任务状态为 running
|
||
gladaTask.status = 'running';
|
||
taskReceiver.updateTask(taskId, { status: 'running' });
|
||
|
||
// 通知任务开始
|
||
await notifier.notify(gladaTask, 'started');
|
||
|
||
// 记录原始分支
|
||
let originalBranch;
|
||
try {
|
||
originalBranch = gitOperator.getCurrentBranch();
|
||
} catch {
|
||
originalBranch = 'main';
|
||
}
|
||
|
||
// 创建任务分支
|
||
let taskBranch;
|
||
try {
|
||
taskBranch = gitOperator.createTaskBranch(taskId);
|
||
gladaTask.completion.git_branch = taskBranch;
|
||
} catch (err) {
|
||
console.error(`[GLADA] ⚠️ 创建 Git 分支失败: ${err.message}`);
|
||
taskBranch = null;
|
||
}
|
||
|
||
const executionResult = {
|
||
task_id: taskId,
|
||
status: 'running',
|
||
steps_completed: 0,
|
||
steps_failed: 0,
|
||
total_files_changed: [],
|
||
git_commits: [],
|
||
errors: [],
|
||
started_at: new Date().toISOString(),
|
||
completed_at: null,
|
||
duration_ms: 0
|
||
};
|
||
|
||
// 逐步执行
|
||
for (const step of gladaTask.plan.steps) {
|
||
// 跳过已完成的步骤(支持断点续传)
|
||
if (step.status === 'completed') {
|
||
console.log(`[GLADA] ⏭️ 跳过已完成步骤 ${step.step_id}`);
|
||
executionResult.steps_completed++;
|
||
continue;
|
||
}
|
||
|
||
console.log(`\n${'─'.repeat(40)}`);
|
||
console.log(`[GLADA] 📌 步骤 ${step.step_id}/${gladaTask.plan.steps.length}: ${step.description}`);
|
||
console.log(`${'─'.repeat(40)}`);
|
||
|
||
// 1. 构建深度上下文
|
||
const context = contextBuilder.buildContext(gladaTask);
|
||
const systemPrompt = contextBuilder.contextToSystemPrompt(context);
|
||
|
||
// 2. 执行步骤
|
||
step.status = 'running';
|
||
step.started_at = new Date().toISOString();
|
||
|
||
const stepResult = await stepExecutor.executeStep(step, systemPrompt, gladaTask, {
|
||
model: options.model, // null = let model-router auto-select
|
||
maxTokens: options.maxTokens || 8192,
|
||
skipTests: options.skipTests
|
||
});
|
||
|
||
// 3. 记录执行日志
|
||
const logEntry = {
|
||
step_id: step.step_id,
|
||
action: step.description,
|
||
status: stepResult.status,
|
||
reasoning: stepResult.reasoning,
|
||
files_changed: stepResult.files_changed,
|
||
error: stepResult.error,
|
||
duration_ms: stepResult.duration_ms,
|
||
timestamp: new Date().toISOString()
|
||
};
|
||
|
||
gladaTask.execution_log.push(logEntry);
|
||
writeExecutionLog(taskId, logEntry);
|
||
|
||
// 4. 更新步骤状态
|
||
if (stepResult.status === 'completed') {
|
||
step.status = 'completed';
|
||
step.completed_at = stepResult.completed_at;
|
||
step.result = stepResult.summary;
|
||
step.files_changed = stepResult.files_changed;
|
||
step.reasoning = stepResult.reasoning;
|
||
|
||
executionResult.steps_completed++;
|
||
executionResult.total_files_changed.push(...stepResult.files_changed);
|
||
|
||
// 5. Git 提交
|
||
if (taskBranch && stepResult.files_changed.length > 0) {
|
||
try {
|
||
const commitHash = gitOperator.commitStep(
|
||
taskId,
|
||
step.step_id,
|
||
stepResult.summary || step.description,
|
||
stepResult.files_changed
|
||
);
|
||
if (commitHash) {
|
||
executionResult.git_commits.push(`${commitHash}: 步骤${step.step_id} - ${step.description}`);
|
||
}
|
||
} catch (err) {
|
||
console.warn(`[GLADA] ⚠️ Git 提交失败: ${err.message}`);
|
||
}
|
||
}
|
||
} else {
|
||
step.status = stepResult.status === 'rolled_back' ? 'rolled_back' : 'failed';
|
||
step.error = stepResult.error;
|
||
executionResult.steps_failed++;
|
||
executionResult.errors.push(`步骤${step.step_id}: ${stepResult.error}`);
|
||
|
||
// 如果设置了失败即停止
|
||
if (options.stopOnFailure !== false) {
|
||
console.warn(`[GLADA] ⛔ 步骤 ${step.step_id} 失败,停止执行`);
|
||
break;
|
||
}
|
||
}
|
||
|
||
// 保存任务状态
|
||
taskReceiver.updateTask(taskId, {
|
||
plan: gladaTask.plan,
|
||
execution_log: gladaTask.execution_log
|
||
});
|
||
|
||
// 步骤间通知
|
||
await notifier.notify(gladaTask, 'step_completed');
|
||
}
|
||
|
||
// 推送到远程
|
||
if (taskBranch) {
|
||
gitOperator.pushBranch(taskBranch);
|
||
}
|
||
|
||
// 最终状态
|
||
const allCompleted = gladaTask.plan.steps.every(s => s.status === 'completed');
|
||
executionResult.status = allCompleted ? 'completed' : 'partial';
|
||
executionResult.completed_at = new Date().toISOString();
|
||
executionResult.duration_ms = Date.now() - startTime;
|
||
|
||
// 去重文件列表
|
||
executionResult.total_files_changed = [...new Set(executionResult.total_files_changed)];
|
||
|
||
// 更新任务完成信息
|
||
gladaTask.status = allCompleted ? 'completed' : 'failed';
|
||
gladaTask.completion = {
|
||
...gladaTask.completion,
|
||
completed_at: executionResult.completed_at,
|
||
total_files_changed: executionResult.total_files_changed,
|
||
git_branch: taskBranch,
|
||
git_commits: executionResult.git_commits,
|
||
notification_sent: false
|
||
};
|
||
|
||
// 保存最终状态
|
||
taskReceiver.updateTask(taskId, {
|
||
status: gladaTask.status,
|
||
plan: gladaTask.plan,
|
||
completion: gladaTask.completion,
|
||
execution_log: gladaTask.execution_log
|
||
});
|
||
|
||
// 发送完成通知
|
||
const eventType = allCompleted ? 'completed' : 'failed';
|
||
await notifier.notify(gladaTask, eventType);
|
||
gladaTask.completion.notification_sent = true;
|
||
taskReceiver.updateTask(taskId, { completion: gladaTask.completion });
|
||
|
||
// ── Hermes-inspired 后处理:Skill 蒸馏 + 自我反思 ──
|
||
|
||
// 1. Skill 蒸馏:从成功的执行中提炼可复用模板
|
||
try {
|
||
const distillResult = skillDistiller.distillAndSave(gladaTask);
|
||
if (distillResult.saved) {
|
||
console.log(`[GLADA] 🧪 Skill 蒸馏完成: ${distillResult.path}`);
|
||
}
|
||
} catch (err) {
|
||
console.warn(`[GLADA] ⚠️ Skill 蒸馏失败(不影响主流程): ${err.message}`);
|
||
}
|
||
|
||
// 2. 自我反思:分析执行过程,写入经验枝干
|
||
try {
|
||
const reflectResult = reflector.reflectOffline(gladaTask);
|
||
if (reflectResult.saved) {
|
||
console.log(`[GLADA] 🪞 自我反思完成: ${reflectResult.path}`);
|
||
console.log(`[GLADA] 📊 信心评分: ${Math.round(reflectResult.reflection.confidence_score * 100)}%`);
|
||
}
|
||
} catch (err) {
|
||
console.warn(`[GLADA] ⚠️ 自我反思失败(不影响主流程): ${err.message}`);
|
||
}
|
||
|
||
// 如果全部完成,归档
|
||
if (allCompleted) {
|
||
taskReceiver.archiveTask(taskId, gladaTask.completion);
|
||
}
|
||
|
||
// 切回原始分支
|
||
if (originalBranch && taskBranch) {
|
||
try {
|
||
gitOperator.checkoutBranch(originalBranch);
|
||
} catch {
|
||
// 忽略
|
||
}
|
||
}
|
||
|
||
console.log(`\n${'═'.repeat(60)}`);
|
||
console.log(`[GLADA] ${allCompleted ? '✅' : '⚠️'} 任务执行${allCompleted ? '完成' : '部分完成'}: ${taskId}`);
|
||
console.log(`[GLADA] 📊 完成: ${executionResult.steps_completed}, 失败: ${executionResult.steps_failed}`);
|
||
console.log(`[GLADA] ⏱️ 耗时: ${Math.round(executionResult.duration_ms / 1000)}s`);
|
||
console.log(`${'═'.repeat(60)}\n`);
|
||
|
||
return executionResult;
|
||
}
|
||
|
||
/**
|
||
* 写入执行日志
|
||
* @param {string} taskId - 任务 ID
|
||
* @param {Object} entry - 日志条目
|
||
*/
|
||
function writeExecutionLog(taskId, entry) {
|
||
fs.mkdirSync(EXECUTION_LOG_DIR, { recursive: true });
|
||
|
||
const logFile = path.join(EXECUTION_LOG_DIR, `${taskId}.jsonl`);
|
||
fs.appendFileSync(logFile, JSON.stringify(entry) + '\n', 'utf-8');
|
||
}
|
||
|
||
/**
|
||
* 从中央任务队列获取 glada-task 类型的任务
|
||
* @returns {Object|null} GLADA 格式的任务,或 null
|
||
*/
|
||
function dequeueFromCentralQueue() {
|
||
if (!taskQueue) return null;
|
||
|
||
try {
|
||
taskQueue.loadQueue();
|
||
const queueTask = taskQueue.dequeue();
|
||
|
||
if (!queueTask || queueTask.type !== 'glada-task') {
|
||
return null;
|
||
}
|
||
|
||
// 标记为运行中
|
||
// 将中央队列任务转换为 GLADA 执行格式
|
||
console.log(`[GLADA] 📥 从中央任务队列取出: ${queueTask.task_id}`);
|
||
|
||
// 如果任务有 cab_spec,用它转换;否则构建最小执行任务
|
||
if (queueTask.cab_spec) {
|
||
return taskReceiver.convertToGladaTask(queueTask.cab_spec);
|
||
}
|
||
|
||
// 最小格式转换
|
||
return {
|
||
glada_task_id: `GLADA-${queueTask.task_id}`,
|
||
source_task_id: queueTask.task_id,
|
||
source: 'central-queue',
|
||
status: 'pending',
|
||
created_at: queueTask.created_at || new Date().toISOString(),
|
||
updated_at: new Date().toISOString(),
|
||
plan: {
|
||
title: queueTask.description || queueTask.task_id,
|
||
description: queueTask.description || '',
|
||
steps: (queueTask.steps || [queueTask.description || 'Execute task']).map((step, i) => ({
|
||
step_id: i + 1,
|
||
description: typeof step === 'string' ? step : step.description || `步骤 ${i + 1}`,
|
||
status: 'pending',
|
||
started_at: null,
|
||
completed_at: null,
|
||
result: null,
|
||
files_changed: [],
|
||
reasoning: null
|
||
})),
|
||
priority: queueTask.priority || 'normal'
|
||
},
|
||
architecture: queueTask.architecture || {},
|
||
constraints: queueTask.constraints || { no_touch_files: [], required_tests: true },
|
||
reasoning_context: {},
|
||
execution_log: [],
|
||
completion: {
|
||
completed_at: null,
|
||
total_files_changed: [],
|
||
git_branch: null,
|
||
git_commits: [],
|
||
notification_sent: false
|
||
}
|
||
};
|
||
} catch (err) {
|
||
console.error(`[GLADA] ⚠️ 中央队列读取失败: ${err.message}`);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 主循环:持续监听并执行任务
|
||
* 双通道获取任务:中央 task-queue → 本地 GLADA 队列
|
||
* @param {Object} [options] - 循环选项
|
||
* @param {number} [options.pollIntervalMs=30000] - 轮询间隔(毫秒)
|
||
* @param {boolean} [options.singleRun=false] - 只执行一个任务就退出
|
||
*/
|
||
async function startLoop(options = {}) {
|
||
const pollInterval = options.pollIntervalMs || 30000;
|
||
const singleRun = options.singleRun || false;
|
||
|
||
console.log(`[GLADA] 🔄 执行循环已启动 (轮询间隔: ${pollInterval / 1000}s)`);
|
||
if (taskQueue) {
|
||
console.log(`[GLADA] 📡 已连接中央任务队列 (core/task-queue)`);
|
||
}
|
||
|
||
const poll = async () => {
|
||
try {
|
||
// 双通道:先查中央队列,再查本地队列
|
||
let task = dequeueFromCentralQueue();
|
||
|
||
if (!task) {
|
||
task = taskReceiver.receiveNextTask();
|
||
}
|
||
|
||
if (task) {
|
||
// 如果来自中央队列,同时保存到本地以便断点续传
|
||
if (task.source === 'central-queue') {
|
||
const queueDir = taskReceiver.GLADA_QUEUE_DIR;
|
||
fs.mkdirSync(queueDir, { recursive: true });
|
||
fs.writeFileSync(
|
||
path.join(queueDir, `${task.glada_task_id}.json`),
|
||
JSON.stringify(task, null, 2),
|
||
'utf-8'
|
||
);
|
||
}
|
||
|
||
await executeTask(task, options);
|
||
|
||
// 中央队列完成回写
|
||
if (task.source === 'central-queue' && taskQueue) {
|
||
try {
|
||
const allCompleted = task.plan.steps.every(s => s.status === 'completed');
|
||
if (allCompleted) {
|
||
taskQueue.complete(task.source_task_id, { glada_task_id: task.glada_task_id });
|
||
} else {
|
||
taskQueue.fail(task.source_task_id, 'GLADA execution incomplete');
|
||
}
|
||
} catch {
|
||
// 回写失败不阻塞主流程
|
||
}
|
||
}
|
||
|
||
if (singleRun) {
|
||
console.log('[GLADA] 单次运行模式,退出');
|
||
return;
|
||
}
|
||
}
|
||
} catch (err) {
|
||
console.error(`[GLADA] ❌ 循环异常: ${err.message}`);
|
||
}
|
||
|
||
// 继续轮询
|
||
if (!singleRun) {
|
||
setTimeout(poll, pollInterval);
|
||
}
|
||
};
|
||
|
||
await poll();
|
||
}
|
||
|
||
module.exports = {
|
||
executeTask,
|
||
writeExecutionLog,
|
||
startLoop
|
||
};
|