arcadia-suite-sv/server/blackboard/PipelineOrchestrator.ts

702 lines
25 KiB
TypeScript

import { db } from "../../db/index";
import { blackboardService, type AgentName } from "./service";
import { jobQueueService } from "../governance/jobQueue";
import { governanceService } from "../governance/service";
import { xosDevPipelines, xosStagingChanges, type XosDevPipeline } from "@shared/schema";
import { eq, sql } from "drizzle-orm";
import { EventEmitter } from "events";
import { randomUUID } from "crypto";
export type PipelinePhase = "design" | "codegen" | "validation" | "staging" | "evolution";
export type PipelineStatus = "queued" | "running" | "staging_review" | "completed" | "failed";
export interface PipelineBudget {
maxTokens: number;
maxTimeMs: number;
maxCalls: number;
usedTokens: number;
usedTimeMs: number;
usedCalls: number;
exceeded: boolean;
}
export interface RunbookDecision {
phase: string;
agent: string;
decision: string;
timestamp: string;
details?: any;
}
export interface PipelineRunbook {
context: string;
decisions: RunbookDecision[];
validations: any;
approval: any;
deployment: any;
}
const DEFAULT_BUDGET: PipelineBudget = {
maxTokens: 100000,
maxTimeMs: 600000,
maxCalls: 100,
usedTokens: 0,
usedTimeMs: 0,
usedCalls: 0,
exceeded: false,
};
const PHASE_CONFIG: Record<PipelinePhase, { agent: AgentName; next?: PipelinePhase; contextPhase: string }> = {
design: { agent: "architect", next: "codegen", contextPhase: "design" },
codegen: { agent: "generator", next: "validation", contextPhase: "codegen" },
validation: { agent: "validator", next: "staging", contextPhase: "validation" },
staging: { agent: "executor", next: "evolution", contextPhase: "deploy" },
evolution: { agent: "evolution", contextPhase: "evolution" },
};
const ALL_PHASES: PipelinePhase[] = ["design", "codegen", "validation", "staging", "evolution"];
class PipelineOrchestrator extends EventEmitter {
private activeMonitors: Map<number, NodeJS.Timeout> = new Map();
private processingMonitors: Set<number> = new Set();
async createPipeline(prompt: string, userId: string = "system", metadata?: any): Promise<XosDevPipeline> {
const correlationId = randomUUID();
const initialPhases: Record<string, any> = {};
for (const phase of ALL_PHASES) {
initialPhases[phase] = { status: "pending" };
}
const budget: PipelineBudget = {
...DEFAULT_BUDGET,
...(metadata?.budget || {}),
};
const runbook: PipelineRunbook = {
context: prompt,
decisions: [],
validations: null,
approval: null,
deployment: null,
};
const [pipeline] = await db.insert(xosDevPipelines).values({
correlationId,
prompt,
status: "queued",
currentPhase: "queued",
userId,
phases: initialPhases,
metadata: metadata || {},
budget: budget as any,
runbook: runbook as any,
}).returning();
await governanceService.recordAudit({
agentName: "pipeline-orchestrator",
action: "pipeline_created",
target: `Pipeline #${pipeline.id}`,
decision: "executed",
justification: `Pipeline criado com correlationId: ${correlationId}`,
metadata: { correlationId, pipelineId: pipeline.id },
});
this.emit("pipeline:created", pipeline);
return pipeline;
}
async startPipeline(pipelineId: number): Promise<XosDevPipeline> {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline) throw new Error(`Pipeline ${pipelineId} não encontrado`);
const mainTask = await blackboardService.createMainTask(
`Pipeline #${pipelineId}: ${pipeline.prompt.slice(0, 80)}`,
pipeline.prompt,
pipeline.userId || "system",
{ pipelineId, source: "pipeline-orchestrator", correlationId: pipeline.correlationId }
);
await this.updatePipeline(pipelineId, {
status: "running",
currentPhase: "design",
mainTaskId: mainTask.id,
startedAt: new Date(),
});
await this.addRunbookDecision(pipelineId, "init", "orchestrator", "Pipeline iniciado", { mainTaskId: mainTask.id });
await this.startPhase(pipelineId, mainTask.id, "design");
this.monitorPipeline(pipelineId, mainTask.id);
const updated = await this.getPipeline(pipelineId);
this.emit("pipeline:started", updated);
return updated!;
}
private async startPhase(pipelineId: number, mainTaskId: number, phase: PipelinePhase): Promise<void> {
const config = PHASE_CONFIG[phase];
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline) return;
const budgetCheck = await this.checkBudget(pipelineId);
if (!budgetCheck.ok) {
await this.failPipeline(pipelineId, `Budget excedido: ${budgetCheck.reason}`);
return;
}
const subtask = await blackboardService.createSubtask(
mainTaskId,
`[Pipeline #${pipelineId}] Fase: ${phase}`,
`Executar fase "${phase}" do pipeline para: ${pipeline.prompt}`,
config.agent,
[],
{ phase: config.contextPhase, pipelineId, correlationId: pipeline.correlationId }
);
const phases = (pipeline.phases as Record<string, any>) || {};
phases[phase] = { status: "running", taskId: subtask.id, startedAt: new Date().toISOString() };
await this.updatePipeline(pipelineId, {
currentPhase: phase,
phases,
});
await this.addRunbookDecision(pipelineId, phase, config.agent, `Fase ${phase} iniciada`, { taskId: subtask.id });
await this.trackBudgetCall(pipelineId);
this.emit("pipeline:phase_started", { pipelineId, phase, taskId: subtask.id, correlationId: pipeline.correlationId });
}
private monitorPipeline(pipelineId: number, mainTaskId: number): void {
if (this.activeMonitors.has(pipelineId)) return;
const interval = setInterval(async () => {
if (this.processingMonitors.has(pipelineId)) return;
this.processingMonitors.add(pipelineId);
try {
await this.checkPhaseProgress(pipelineId, mainTaskId);
} catch (error) {
console.error(`[PipelineOrchestrator] Erro no monitor #${pipelineId}:`, error);
} finally {
this.processingMonitors.delete(pipelineId);
}
}, 3000);
this.activeMonitors.set(pipelineId, interval);
}
private stopMonitor(pipelineId: number): void {
const interval = this.activeMonitors.get(pipelineId);
if (interval) {
clearInterval(interval);
this.activeMonitors.delete(pipelineId);
}
}
private async checkPhaseProgress(pipelineId: number, mainTaskId: number): Promise<void> {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline || pipeline.status === "completed" || pipeline.status === "failed") {
this.stopMonitor(pipelineId);
return;
}
const budgetCheck = await this.checkBudget(pipelineId);
if (!budgetCheck.ok) {
await this.failPipeline(pipelineId, `Budget excedido: ${budgetCheck.reason}`);
return;
}
const currentPhase = pipeline.currentPhase;
if (!currentPhase || currentPhase === "queued") return;
const phases = (pipeline.phases as Record<string, any>) || {};
const phaseData = phases[currentPhase];
if (!phaseData?.taskId) return;
const task = await blackboardService.getTask(phaseData.taskId);
if (!task) return;
await this.trackBudgetTime(pipelineId);
if (task.status === "completed") {
phases[currentPhase] = {
...phaseData,
status: "completed",
completedAt: new Date().toISOString(),
result: task.result,
};
await this.updatePipeline(pipelineId, { phases });
await this.addRunbookDecision(pipelineId, currentPhase, PHASE_CONFIG[currentPhase as PipelinePhase]?.agent || "unknown", `Fase ${currentPhase} concluída`, task.result);
this.emit("pipeline:phase_completed", { pipelineId, phase: currentPhase, correlationId: pipeline.correlationId });
if (currentPhase === "staging") {
await this.handleStagingComplete(pipelineId, mainTaskId);
return;
}
if (currentPhase === "validation") {
const validationResult = task.result as any;
await this.updateRunbookValidation(pipelineId, validationResult);
}
const config = PHASE_CONFIG[currentPhase as PipelinePhase];
if (config?.next) {
await this.startPhase(pipelineId, mainTaskId, config.next);
} else {
await this.completePipeline(pipelineId);
}
} else if (task.status === "failed") {
phases[currentPhase] = {
...phaseData,
status: "failed",
completedAt: new Date().toISOString(),
};
await this.failPipeline(pipelineId, task.errorMessage || `Falha na fase ${currentPhase}`, phases);
}
}
private async handleStagingComplete(pipelineId: number, mainTaskId: number): Promise<void> {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline) return;
const details = await blackboardService.getTaskWithDetails(mainTaskId);
if (!details) return;
const codeArtifacts = details.artifacts?.filter((a: any) => a.type === "code") || [];
for (const artifact of codeArtifacts) {
let originalContent: string | null = null;
try {
const { toolManager } = await import("../autonomous/tools");
const readResult = await toolManager.execute("read_file", { path: artifact.name });
if (readResult.success && readResult.data?.content) {
originalContent = readResult.data.content;
}
} catch {}
await db.insert(xosStagingChanges).values({
pipelineId,
taskId: mainTaskId,
correlationId: pipeline.correlationId,
filePath: artifact.name,
content: artifact.content || "",
originalContent,
action: originalContent ? "modify" : "create",
status: "pending",
});
}
if (codeArtifacts.length > 0) {
await this.updatePipeline(pipelineId, {
status: "staging_review",
currentPhase: "staging",
});
await this.addRunbookDecision(pipelineId, "staging", "executor", `${codeArtifacts.length} arquivos aguardando revisão`);
this.emit("pipeline:staging_ready", { pipelineId, files: codeArtifacts.length, correlationId: pipeline.correlationId });
} else {
await this.startPhase(pipelineId, mainTaskId, "evolution");
}
}
async approveStagingChanges(pipelineId: number, reviewedBy: string = "user", selectedFiles?: string[]): Promise<{ applied: string[]; errors: string[]; skipped: string[] }> {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline) {
throw new Error("Pipeline não encontrado");
}
const changes = await db.select().from(xosStagingChanges)
.where(eq(xosStagingChanges.pipelineId, pipelineId));
let pendingChanges = changes.filter(c => c.status === "pending");
if (pendingChanges.length === 0) {
throw new Error("Nenhuma mudança pendente para aprovar");
}
const applied: string[] = [];
const errors: string[] = [];
const skipped: string[] = [];
if (selectedFiles && selectedFiles.length > 0) {
const selected = pendingChanges.filter(c => selectedFiles.includes(c.filePath));
pendingChanges = selected;
}
const sortedPending = [...pendingChanges].sort((a, b) => b.id - a.id);
const fileMap = new Map<string, typeof pendingChanges[0]>();
const duplicateIds: number[] = [];
for (const change of sortedPending) {
if (fileMap.has(change.filePath)) {
duplicateIds.push(change.id);
} else {
fileMap.set(change.filePath, change);
}
}
if (duplicateIds.length > 0) {
for (const dupId of duplicateIds) {
await db.update(xosStagingChanges).set({
status: "applied",
reviewedBy,
reviewedAt: new Date(),
appliedAt: new Date(),
}).where(eq(xosStagingChanges.id, dupId));
}
}
const uniqueChanges = Array.from(fileMap.values());
const { toolManager } = await import("../autonomous/tools");
const { PROTECTED_FILES } = await import("./agents/ExecutorAgent");
for (const change of uniqueChanges) {
if (PROTECTED_FILES.includes(change.filePath)) {
errors.push(`${change.filePath}: arquivo protegido`);
await db.update(xosStagingChanges).set({
status: "failed",
reviewedBy,
reviewedAt: new Date(),
}).where(eq(xosStagingChanges.id, change.id));
const dupsForProtected = sortedPending.filter(c => c.filePath === change.filePath && c.id !== change.id);
for (const dup of dupsForProtected) {
await db.update(xosStagingChanges).set({ status: "failed", reviewedBy, reviewedAt: new Date() })
.where(eq(xosStagingChanges.id, dup.id));
}
continue;
}
const result = await toolManager.execute("write_file", {
path: change.filePath,
content: change.content,
createDirs: true,
});
if (result.success) {
applied.push(change.filePath);
await db.update(xosStagingChanges).set({
status: "applied",
reviewedBy,
reviewedAt: new Date(),
appliedAt: new Date(),
}).where(eq(xosStagingChanges.id, change.id));
} else {
errors.push(`${change.filePath}: ${result.error}`);
await db.update(xosStagingChanges).set({
status: "failed",
reviewedBy,
reviewedAt: new Date(),
}).where(eq(xosStagingChanges.id, change.id));
}
}
if (applied.length > 0) {
await toolManager.execute("git_local_commit", {
message: `[Pipeline #${pipelineId}] ${pipeline.prompt.slice(0, 60)}`,
files: applied,
});
}
await this.updateRunbookApproval(pipelineId, { reviewedBy, applied, errors, skipped, approvedAt: new Date().toISOString() });
await this.addRunbookDecision(pipelineId, "approval", reviewedBy, `Aprovado: ${applied.length} arquivos`, { applied, errors, skipped });
await governanceService.recordAudit({
agentName: reviewedBy,
action: "staging_approved",
target: `Pipeline #${pipelineId}`,
decision: "approved",
justification: `${applied.length} arquivos aplicados, ${errors.length} erros, ${skipped.length} ignorados`,
metadata: { correlationId: pipeline.correlationId, pipelineId },
});
const remainingPending = await this.getPendingStagingCount(pipelineId);
if (remainingPending === 0 && (pipeline.status === "staging_review" || pipeline.status === "running")) {
if (pipeline.mainTaskId && pipeline.status === "staging_review") {
await this.startPhase(pipelineId, pipeline.mainTaskId, "evolution");
await this.updatePipeline(pipelineId, { status: "running", currentPhase: "evolution" });
} else {
await this.updatePipeline(pipelineId, { status: "completed" });
}
}
this.emit("pipeline:staging_approved", { pipelineId, applied, errors, skipped, correlationId: pipeline.correlationId });
return { applied, errors, skipped };
}
async rollbackPipeline(pipelineId: number, rolledBackBy: string = "user"): Promise<{ restored: string[]; errors: string[] }> {
const changes = await db.select().from(xosStagingChanges)
.where(eq(xosStagingChanges.pipelineId, pipelineId));
const appliedChanges = changes.filter(c => c.status === "applied" && c.originalContent !== null);
const restored: string[] = [];
const errors: string[] = [];
const { toolManager } = await import("../autonomous/tools");
for (const change of appliedChanges) {
try {
if (change.originalContent) {
const result = await toolManager.execute("write_file", {
path: change.filePath,
content: change.originalContent,
});
if (result.success) {
restored.push(change.filePath);
await db.update(xosStagingChanges).set({
status: "rolled_back",
rolledBackAt: new Date(),
}).where(eq(xosStagingChanges.id, change.id));
} else {
errors.push(`${change.filePath}: ${result.error}`);
}
} else if (change.action === "create") {
restored.push(change.filePath);
await db.update(xosStagingChanges).set({
status: "rolled_back",
rolledBackAt: new Date(),
}).where(eq(xosStagingChanges.id, change.id));
}
} catch (err: any) {
errors.push(`${change.filePath}: ${err.message}`);
}
}
if (restored.length > 0) {
await toolManager.execute("git_local_commit", {
message: `[Rollback Pipeline #${pipelineId}] Revertido por ${rolledBackBy}`,
files: restored,
});
}
await this.updatePipeline(pipelineId, { status: "failed", error: `Rollback executado por ${rolledBackBy}` });
await this.addRunbookDecision(pipelineId, "rollback", rolledBackBy, `Rollback: ${restored.length} arquivos restaurados`, { restored, errors });
await governanceService.recordAudit({
agentName: rolledBackBy,
action: "pipeline_rollback",
target: `Pipeline #${pipelineId}`,
decision: "executed",
justification: `${restored.length} arquivos restaurados ao estado original`,
metadata: { correlationId: (await this.getPipeline(pipelineId))?.correlationId, pipelineId },
});
this.emit("pipeline:rolled_back", { pipelineId, restored, errors });
return { restored, errors };
}
async rejectStagingChanges(pipelineId: number, reviewedBy: string = "user"): Promise<void> {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline) {
throw new Error("Pipeline não encontrado");
}
await db.update(xosStagingChanges).set({
status: "rejected",
reviewedBy,
reviewedAt: new Date(),
}).where(eq(xosStagingChanges.pipelineId, pipelineId));
await this.updatePipeline(pipelineId, {
status: "failed",
error: "Alterações rejeitadas pelo usuário",
});
await this.addRunbookDecision(pipelineId, "rejection", reviewedBy, "Alterações rejeitadas pelo usuário");
this.stopMonitor(pipelineId);
this.emit("pipeline:staging_rejected", { pipelineId });
}
private async completePipeline(pipelineId: number): Promise<void> {
const pipeline = await this.getPipeline(pipelineId);
const elapsed = pipeline?.startedAt ? Date.now() - new Date(pipeline.startedAt).getTime() : 0;
await this.addRunbookDecision(pipelineId, "completion", "orchestrator", `Pipeline concluído em ${Math.round(elapsed / 1000)}s`);
await this.updatePipeline(pipelineId, {
status: "completed",
completedAt: new Date(),
});
this.stopMonitor(pipelineId);
this.emit("pipeline:completed", { pipelineId, correlationId: pipeline?.correlationId });
}
private async failPipeline(pipelineId: number, error: string, phases?: Record<string, any>): Promise<void> {
const updateData: any = { status: "failed", error };
if (phases) updateData.phases = phases;
await this.updatePipeline(pipelineId, updateData);
await this.addRunbookDecision(pipelineId, "failure", "orchestrator", `Pipeline falhou: ${error}`);
const pipeline = await this.getPipeline(pipelineId);
this.emit("pipeline:failed", { pipelineId, error, correlationId: pipeline?.correlationId });
this.stopMonitor(pipelineId);
}
private async checkBudget(pipelineId: number): Promise<{ ok: boolean; reason?: string }> {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline?.budget) return { ok: true };
const budget = pipeline.budget as PipelineBudget;
if (budget.exceeded) return { ok: false, reason: "Budget já foi excedido anteriormente" };
const elapsed = pipeline.startedAt ? Date.now() - new Date(pipeline.startedAt).getTime() : 0;
if (elapsed > budget.maxTimeMs) {
await this.markBudgetExceeded(pipelineId, budget);
return { ok: false, reason: `Tempo máximo excedido: ${Math.round(elapsed / 1000)}s > ${Math.round(budget.maxTimeMs / 1000)}s` };
}
if (budget.usedCalls >= budget.maxCalls) {
await this.markBudgetExceeded(pipelineId, budget);
return { ok: false, reason: `Limite de chamadas excedido: ${budget.usedCalls} >= ${budget.maxCalls}` };
}
return { ok: true };
}
private async trackBudgetCall(pipelineId: number): Promise<void> {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline?.budget) return;
const budget = pipeline.budget as PipelineBudget;
budget.usedCalls = (budget.usedCalls || 0) + 1;
await this.updatePipeline(pipelineId, { budget: budget as any });
}
private async trackBudgetTime(pipelineId: number): Promise<void> {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline?.budget || !pipeline.startedAt) return;
const budget = pipeline.budget as PipelineBudget;
budget.usedTimeMs = Date.now() - new Date(pipeline.startedAt).getTime();
await this.updatePipeline(pipelineId, { budget: budget as any });
}
private async markBudgetExceeded(pipelineId: number, budget: PipelineBudget): Promise<void> {
budget.exceeded = true;
await this.updatePipeline(pipelineId, { budget: budget as any });
await this.addRunbookDecision(pipelineId, "budget", "orchestrator", "Budget excedido - pipeline interrompido", budget);
}
private async addRunbookDecision(pipelineId: number, phase: string, agent: string, decision: string, details?: any): Promise<void> {
try {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline) return;
const runbook = (pipeline.runbook as PipelineRunbook) || { context: "", decisions: [], validations: null, approval: null, deployment: null };
runbook.decisions = runbook.decisions || [];
runbook.decisions.push({
phase,
agent,
decision,
timestamp: new Date().toISOString(),
details,
});
await this.updatePipeline(pipelineId, { runbook: runbook as any });
} catch {}
}
private async updateRunbookValidation(pipelineId: number, validation: any): Promise<void> {
try {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline) return;
const runbook = (pipeline.runbook as PipelineRunbook) || { context: "", decisions: [], validations: null, approval: null, deployment: null };
runbook.validations = validation;
await this.updatePipeline(pipelineId, { runbook: runbook as any });
} catch {}
}
private async updateRunbookApproval(pipelineId: number, approval: any): Promise<void> {
try {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline) return;
const runbook = (pipeline.runbook as PipelineRunbook) || { context: "", decisions: [], validations: null, approval: null, deployment: null };
runbook.approval = approval;
await this.updatePipeline(pipelineId, { runbook: runbook as any });
} catch {}
}
async getPipeline(pipelineId: number): Promise<XosDevPipeline | null> {
const [pipeline] = await db.select().from(xosDevPipelines)
.where(eq(xosDevPipelines.id, pipelineId));
return pipeline || null;
}
async getPipelineRunbook(pipelineId: number): Promise<PipelineRunbook | null> {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline?.runbook) return null;
return pipeline.runbook as PipelineRunbook;
}
async getPipelineWithDetails(pipelineId: number): Promise<any> {
const pipeline = await this.getPipeline(pipelineId);
if (!pipeline) return null;
const staging = await db.select().from(xosStagingChanges)
.where(eq(xosStagingChanges.pipelineId, pipelineId));
let taskDetails = null;
if (pipeline.mainTaskId) {
taskDetails = await blackboardService.getTaskWithDetails(pipeline.mainTaskId);
}
return {
pipeline,
staging,
task: taskDetails?.task,
subtasks: taskDetails?.subtasks || [],
artifacts: taskDetails?.artifacts || [],
logs: taskDetails?.logs || [],
};
}
async getRecentPipelines(limit: number = 20): Promise<XosDevPipeline[]> {
return db.select().from(xosDevPipelines)
.orderBy(sql`created_at DESC`)
.limit(limit);
}
async getStagingChanges(pipelineId: number): Promise<any[]> {
return db.select().from(xosStagingChanges)
.where(eq(xosStagingChanges.pipelineId, pipelineId));
}
async getPendingStagingCount(pipelineId: number): Promise<number> {
const result = await db.select({ count: sql<number>`count(*)` })
.from(xosStagingChanges)
.where(sql`${xosStagingChanges.pipelineId} = ${pipelineId} AND ${xosStagingChanges.status} = 'pending'`);
return Number(result[0]?.count || 0);
}
private async updatePipeline(pipelineId: number, data: Partial<XosDevPipeline>): Promise<void> {
await db.update(xosDevPipelines).set(data as any)
.where(eq(xosDevPipelines.id, pipelineId));
}
registerJobHandlers(): void {
for (const [phase, _config] of Object.entries(PHASE_CONFIG)) {
jobQueueService.registerHandler(`pipeline_${phase}`, async (job) => {
try {
const { pipelineId, mainTaskId } = job.payload as any;
await this.startPhase(pipelineId, mainTaskId, phase as PipelinePhase);
return { success: true, data: { phase, pipelineId } };
} catch (error: any) {
return { success: false, error: error.message };
}
});
}
console.log("[PipelineOrchestrator] Job handlers registrados para todas as fases");
}
}
export const pipelineOrchestrator = new PipelineOrchestrator();