/** * Arcadia Suite - Blackboard Service * * Gerencia o estado compartilhado entre os agentes colaborativos. * Implementa o padrão Blackboard para comunicação entre agentes. * * @author Arcadia Development Team * @version 1.0.0 */ import { db } from "../../db/index"; import { blackboardTasks, blackboardArtifacts, blackboardAgentLogs, type BlackboardTask, type BlackboardArtifact, type InsertBlackboardTask, type InsertBlackboardArtifact } from "@shared/schema"; import { eq, desc, and, isNull, sql } from "drizzle-orm"; import { EventEmitter } from "events"; export type TaskStatus = "pending" | "in_progress" | "completed" | "failed" | "blocked"; export type TaskType = "main_task" | "subtask"; export type AgentName = "dispatcher" | "architect" | "generator" | "validator" | "executor" | "evolution" | "researcher"; export type ArtifactType = "spec" | "code" | "test" | "doc" | "config" | "analysis"; export interface TaskUpdate { taskId: number; status: TaskStatus; agentName: AgentName; result?: any; } class BlackboardService extends EventEmitter { private pollingAgents: Map = new Map(); async createMainTask( title: string, description: string, userId: string, context?: any ): Promise { const [task] = await db.insert(blackboardTasks).values({ type: "main_task", title, description, status: "pending", priority: 10, userId, context: context || {}, }).returning(); await this.logAction(task.id, "dispatcher", "created", `Nova tarefa criada: ${title}`); this.emit("task:created", task); return task; } async createSubtask( parentId: number, title: string, description: string, assignedAgent: AgentName, dependencies?: number[], context?: any ): Promise { const [task] = await db.insert(blackboardTasks).values({ type: "subtask", parentId, title, description, status: "pending", assignedAgent, dependencies: dependencies || [], context: context || {}, }).returning(); await this.logAction(task.id, assignedAgent, "created", `Subtarefa criada: ${title}`); this.emit("subtask:created", task); return task; } async getTask(taskId: number): Promise { const [task] = await db.select().from(blackboardTasks).where(eq(blackboardTasks.id, taskId)); return task || null; } async getMainTask(taskId: number): Promise { const task = await this.getTask(taskId); if (!task) return null; if (task.type === "main_task") return task; if (task.parentId) return this.getMainTask(task.parentId); return null; } async getSubtasks(parentId: number): Promise { return db.select() .from(blackboardTasks) .where(eq(blackboardTasks.parentId, parentId)) .orderBy(blackboardTasks.priority); } async getPendingTasksForAgent(agentName: AgentName): Promise { const tasks = await db.select() .from(blackboardTasks) .where( and( eq(blackboardTasks.assignedAgent, agentName), eq(blackboardTasks.status, "pending") ) ) .orderBy(desc(blackboardTasks.priority)); const readyTasks: BlackboardTask[] = []; for (const task of tasks) { const deps = (task.dependencies as number[]) || []; if (deps.length === 0) { readyTasks.push(task); continue; } const depTasks = await Promise.all(deps.map(id => this.getTask(id))); const allDepsCompleted = depTasks.every(t => t?.status === "completed"); if (allDepsCompleted) { readyTasks.push(task); } } return readyTasks; } async claimTask(taskId: number, agentName: AgentName): Promise { const result = await db.update(blackboardTasks) .set({ status: "in_progress", assignedAgent: agentName, startedAt: new Date(), updatedAt: new Date() }) .where( and( eq(blackboardTasks.id, taskId), eq(blackboardTasks.status, "pending") ) ) .returning(); if (result.length > 0) { await this.logAction(taskId, agentName, "claimed", `Agente assumiu a tarefa`); this.emit("task:claimed", { taskId, agentName }); return true; } return false; } async completeTask( taskId: number, agentName: AgentName, result?: any ): Promise { const [task] = await db.update(blackboardTasks) .set({ status: "completed", result: result || {}, completedAt: new Date(), updatedAt: new Date() }) .where(eq(blackboardTasks.id, taskId)) .returning(); await this.logAction(taskId, agentName, "completed", `Tarefa concluída`); this.emit("task:completed", task); if (task.parentId) { await this.checkMainTaskCompletion(task.parentId); } return task; } async failTask( taskId: number, agentName: AgentName, errorMessage: string ): Promise { const [task] = await db.update(blackboardTasks) .set({ status: "failed", errorMessage, completedAt: new Date(), updatedAt: new Date() }) .where(eq(blackboardTasks.id, taskId)) .returning(); await this.logAction(taskId, agentName, "failed", errorMessage); this.emit("task:failed", task); return task; } private async checkMainTaskCompletion(mainTaskId: number): Promise { const subtasks = await this.getSubtasks(mainTaskId); const allCompleted = subtasks.every(t => t.status === "completed"); const anyFailed = subtasks.some(t => t.status === "failed"); if (anyFailed) { await db.update(blackboardTasks) .set({ status: "failed", updatedAt: new Date() }) .where(eq(blackboardTasks.id, mainTaskId)); const [task] = await db.select().from(blackboardTasks).where(eq(blackboardTasks.id, mainTaskId)); this.emit("maintask:failed", task); } else if (allCompleted && subtasks.length > 0) { const artifacts = await this.getArtifactsForTask(mainTaskId); await db.update(blackboardTasks) .set({ status: "completed", result: { subtasks: subtasks.length, artifacts: artifacts.length }, completedAt: new Date(), updatedAt: new Date() }) .where(eq(blackboardTasks.id, mainTaskId)); const [task] = await db.select().from(blackboardTasks).where(eq(blackboardTasks.id, mainTaskId)); this.emit("maintask:completed", task); } } async addArtifact( taskId: number, type: ArtifactType, name: string, content: string, createdBy: AgentName, metadata?: any ): Promise { const mainTask = await this.getMainTask(taskId); const targetTaskId = mainTask?.id || taskId; const [artifact] = await db.insert(blackboardArtifacts).values({ taskId: targetTaskId, type, name, content, createdBy, metadata: metadata || {}, }).returning(); await this.logAction(taskId, createdBy, "artifact_created", `Artefato criado: ${name}`); this.emit("artifact:created", artifact); return artifact; } async getArtifactsForTask(taskId: number, type?: ArtifactType): Promise { const mainTask = await this.getMainTask(taskId); const targetTaskId = mainTask?.id || taskId; if (type) { return db.select() .from(blackboardArtifacts) .where(and( eq(blackboardArtifacts.taskId, targetTaskId), eq(blackboardArtifacts.type, type) )) .orderBy(desc(blackboardArtifacts.createdAt)); } return db.select() .from(blackboardArtifacts) .where(eq(blackboardArtifacts.taskId, targetTaskId)) .orderBy(desc(blackboardArtifacts.createdAt)); } async getLatestArtifact(taskId: number, type: ArtifactType): Promise { const artifacts = await this.getArtifactsForTask(taskId, type); return artifacts[0] || null; } async logAction( taskId: number | null, agentName: AgentName, action: string, thought: string, observation?: string, metadata?: any ): Promise { await db.insert(blackboardAgentLogs).values({ taskId, agentName, action, thought, observation, metadata: metadata || {}, }); } async getTaskLogs(taskId: number): Promise { return db.select() .from(blackboardAgentLogs) .where(eq(blackboardAgentLogs.taskId, taskId)) .orderBy(blackboardAgentLogs.createdAt); } async getTaskWithDetails(taskId: number): Promise<{ task: BlackboardTask; subtasks: BlackboardTask[]; artifacts: BlackboardArtifact[]; logs: any[]; } | null> { const task = await this.getTask(taskId); if (!task) return null; const [subtasks, artifacts, logs] = await Promise.all([ this.getSubtasks(taskId), this.getArtifactsForTask(taskId), this.getTaskLogs(taskId) ]); return { task, subtasks, artifacts, logs }; } async getRecentTasks(userId?: string, limit = 20): Promise { if (userId) { return db.select() .from(blackboardTasks) .where(and( eq(blackboardTasks.userId, userId), eq(blackboardTasks.type, "main_task") )) .orderBy(desc(blackboardTasks.createdAt)) .limit(limit); } return db.select() .from(blackboardTasks) .where(eq(blackboardTasks.type, "main_task")) .orderBy(desc(blackboardTasks.createdAt)) .limit(limit); } async getStats(): Promise<{ totalTasks: number; pendingTasks: number; completedTasks: number; failedTasks: number; artifactsCount: number; }> { const [tasks] = await db.select({ total: sql`count(*)`, pending: sql`sum(case when status = 'pending' then 1 else 0 end)`, completed: sql`sum(case when status = 'completed' then 1 else 0 end)`, failed: sql`sum(case when status = 'failed' then 1 else 0 end)`, }).from(blackboardTasks); const [artifacts] = await db.select({ count: sql`count(*)` }).from(blackboardArtifacts); return { totalTasks: Number(tasks.total) || 0, pendingTasks: Number(tasks.pending) || 0, completedTasks: Number(tasks.completed) || 0, failedTasks: Number(tasks.failed) || 0, artifactsCount: Number(artifacts.count) || 0, }; } // ============================================================ // GUARDRAILS E POLÍTICAS DE SEGURANÇA // ============================================================ private readonly MAX_RETRIES = 2; private readonly BLOCKED_PATHS = ["node_modules", ".git", "vendor", ".env"]; private readonly MAX_FILE_SIZE = 100000; // 100KB async retryTask(taskId: number, reason: string): Promise { const task = await this.getTask(taskId); if (!task) return null; const context = task.context as any || {}; const retries = context.retries || 0; if (retries >= this.MAX_RETRIES) { await this.logAction(taskId, "dispatcher", "max_retries", `Máximo de tentativas atingido (${this.MAX_RETRIES})`); return null; } const [updated] = await db.update(blackboardTasks) .set({ status: "pending", errorMessage: null, context: { ...context, retries: retries + 1, lastRetryReason: reason }, updatedAt: new Date(), }) .where(eq(blackboardTasks.id, taskId)) .returning(); await this.logAction(taskId, "dispatcher", "retry", `Tarefa reiniciada (tentativa ${retries + 1}): ${reason}`); this.emit("task:retried", updated); return updated; } validateFilePath(filePath: string): { valid: boolean; error?: string } { for (const blocked of this.BLOCKED_PATHS) { if (filePath.includes(blocked)) { return { valid: false, error: `Caminho bloqueado: ${blocked}` }; } } if (filePath.startsWith("/") || filePath.includes("..")) { return { valid: false, error: "Caminho absoluto ou traversal não permitido" }; } const allowedDirs = ["client/src", "server", "shared"]; const isAllowed = allowedDirs.some(dir => filePath.startsWith(dir)); if (!isAllowed) { return { valid: false, error: `Deve estar em: ${allowedDirs.join(", ")}` }; } return { valid: true }; } validateContent(content: string): { valid: boolean; error?: string } { if (content.length > this.MAX_FILE_SIZE) { return { valid: false, error: `Conteúdo muito grande (máx ${this.MAX_FILE_SIZE} bytes)` }; } const dangerousPatterns = [ /process\.env\.[A-Z_]+\s*=/, /eval\s*\(/, /Function\s*\(/, /child_process/, ]; for (const pattern of dangerousPatterns) { if (pattern.test(content)) { return { valid: false, error: "Padrão potencialmente perigoso detectado" }; } } return { valid: true }; } async createRetrySubtask( mainTaskId: number, failedTaskId: number, agentName: AgentName, reason: string ): Promise { const failedTask = await this.getTask(failedTaskId); return this.createSubtask( mainTaskId, `Corrigir: ${failedTask?.title || "tarefa"}`, `Correção necessária: ${reason}`, agentName, [], { phase: "retry", originalTaskId: failedTaskId, retryReason: reason } ); } } export const blackboardService = new BlackboardService();