Versão inicial do Agents Orchestrator

Painel administrativo web para orquestração de agentes Claude Code
com suporte a execução de tarefas, agendamento cron, pipelines
sequenciais e terminal com streaming em tempo real via WebSocket.
This commit is contained in:
Frederico Castro
2026-02-26 00:23:56 -03:00
commit 723a08d2e1
24 changed files with 8433 additions and 0 deletions

189
src/agents/executor.js Normal file
View File

@@ -0,0 +1,189 @@
import { spawn } from 'child_process';
import { existsSync } from 'fs';
import { v4 as uuidv4 } from 'uuid';
const CLAUDE_BIN = '/home/fred/.local/bin/claude';
const DEFAULT_MODEL = 'claude-sonnet-4-6';
const activeExecutions = new Map();
function cleanEnv() {
const env = { ...process.env };
delete env.CLAUDECODE;
delete env.ANTHROPIC_API_KEY;
return env;
}
function buildArgs(agentConfig, prompt) {
const model = agentConfig.model || DEFAULT_MODEL;
const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose', '--model', model];
if (agentConfig.systemPrompt) {
args.push('--system-prompt', agentConfig.systemPrompt);
}
return args;
}
function buildPrompt(task, instructions) {
const parts = [];
if (task) parts.push(task);
if (instructions) parts.push(`\nInstruções adicionais:\n${instructions}`);
return parts.join('\n');
}
function parseStreamLine(line) {
const trimmed = line.trim();
if (!trimmed) return null;
try {
return JSON.parse(trimmed);
} catch {
return { type: 'text', content: trimmed };
}
}
function extractText(event) {
if (!event) return null;
if (event.type === 'assistant' && event.message?.content) {
return event.message.content
.filter((b) => b.type === 'text')
.map((b) => b.text)
.join('');
}
if (event.type === 'content_block_delta' && event.delta?.text) {
return event.delta.text;
}
if (event.type === 'content_block_start' && event.content_block?.text) {
return event.content_block.text;
}
if (event.type === 'result') {
if (typeof event.result === 'string') return event.result;
if (event.result?.content) {
return event.result.content
.filter((b) => b.type === 'text')
.map((b) => b.text)
.join('');
}
}
if (event.type === 'text') return event.content || null;
return null;
}
export function execute(agentConfig, task, callbacks = {}) {
const executionId = uuidv4();
const { onData, onError, onComplete } = callbacks;
const prompt = buildPrompt(task.description || task, task.instructions);
const args = buildArgs(agentConfig, prompt);
const spawnOptions = {
env: cleanEnv(),
stdio: ['ignore', 'pipe', 'pipe'],
};
if (agentConfig.workingDirectory && agentConfig.workingDirectory.trim()) {
if (!existsSync(agentConfig.workingDirectory)) {
const err = new Error(`Diretório de trabalho não encontrado: ${agentConfig.workingDirectory}`);
if (onError) onError(err, executionId);
return executionId;
}
spawnOptions.cwd = agentConfig.workingDirectory;
}
console.log(`[executor] Iniciando: ${executionId}`);
console.log(`[executor] Modelo: ${agentConfig.model || DEFAULT_MODEL}`);
console.log(`[executor] cwd: ${spawnOptions.cwd || process.cwd()}`);
const child = spawn(CLAUDE_BIN, args, spawnOptions);
let hadError = false;
activeExecutions.set(executionId, {
process: child,
agentConfig,
task,
startedAt: new Date().toISOString(),
executionId,
});
let outputBuffer = '';
let errorBuffer = '';
let fullText = '';
child.stdout.on('data', (chunk) => {
const raw = chunk.toString();
const lines = (outputBuffer + raw).split('\n');
outputBuffer = lines.pop();
for (const line of lines) {
const parsed = parseStreamLine(line);
if (!parsed) continue;
const text = extractText(parsed);
if (text) {
fullText += text;
if (onData) onData({ type: 'chunk', content: text }, executionId);
}
}
});
child.stderr.on('data', (chunk) => {
errorBuffer += chunk.toString();
});
child.on('error', (err) => {
console.log(`[executor][error] ${err.message}`);
hadError = true;
activeExecutions.delete(executionId);
if (onError) onError(err, executionId);
});
child.on('close', (code) => {
console.log(`[executor][close] code=${code} hadError=${hadError}`);
activeExecutions.delete(executionId);
if (hadError) return;
if (outputBuffer.trim()) {
const parsed = parseStreamLine(outputBuffer);
if (parsed) {
const text = extractText(parsed);
if (text) fullText += text;
}
}
if (onComplete) {
onComplete(
{
executionId,
exitCode: code,
result: fullText,
stderr: errorBuffer,
},
executionId,
);
}
});
return executionId;
}
export function cancel(executionId) {
const execution = activeExecutions.get(executionId);
if (!execution) return false;
execution.process.kill('SIGTERM');
activeExecutions.delete(executionId);
return true;
}
export function getActiveExecutions() {
return Array.from(activeExecutions.entries()).map(([id, exec]) => ({
executionId: id,
startedAt: exec.startedAt,
agentConfig: exec.agentConfig,
}));
}