Melhorias no frontend, pipeline e executor
- Estilos CSS expandidos com novos componentes visuais - Editor de fluxo visual para pipelines (flow-editor.js) - Melhorias na UI de agentes e pipelines - Sumarização automática entre steps de pipeline - Retry com backoff no executor - Utilitários adicionais no frontend
This commit is contained in:
@@ -1,9 +1,12 @@
|
||||
import { spawn } from 'child_process';
|
||||
import { existsSync } from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { settingsStore } from '../store/db.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const AGENT_SETTINGS = path.resolve(__dirname, '..', '..', 'data', 'agent-settings.json');
|
||||
const CLAUDE_BIN = resolveBin();
|
||||
const activeExecutions = new Map();
|
||||
const MAX_OUTPUT_SIZE = 512 * 1024;
|
||||
@@ -48,9 +51,7 @@ function cleanEnv(agentSecrets) {
|
||||
const env = { ...process.env };
|
||||
delete env.CLAUDECODE;
|
||||
delete env.ANTHROPIC_API_KEY;
|
||||
if (!env.CLAUDE_CODE_MAX_OUTPUT_TOKENS) {
|
||||
env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '128000';
|
||||
}
|
||||
env.CLAUDE_CODE_MAX_OUTPUT_TOKENS = '128000';
|
||||
if (agentSecrets && typeof agentSecrets === 'object') {
|
||||
Object.assign(env, agentSecrets);
|
||||
}
|
||||
@@ -61,6 +62,10 @@ function buildArgs(agentConfig, prompt) {
|
||||
const model = agentConfig.model || 'claude-sonnet-4-6';
|
||||
const args = ['-p', prompt, '--output-format', 'stream-json', '--verbose', '--model', model];
|
||||
|
||||
if (existsSync(AGENT_SETTINGS)) {
|
||||
args.push('--settings', AGENT_SETTINGS);
|
||||
}
|
||||
|
||||
if (agentConfig.systemPrompt) {
|
||||
args.push('--system-prompt', agentConfig.systemPrompt);
|
||||
}
|
||||
@@ -358,6 +363,10 @@ export function resume(agentConfig, sessionId, message, callbacks = {}) {
|
||||
'--permission-mode', agentConfig.permissionMode || 'bypassPermissions',
|
||||
];
|
||||
|
||||
if (existsSync(AGENT_SETTINGS)) {
|
||||
args.push('--settings', AGENT_SETTINGS);
|
||||
}
|
||||
|
||||
if (agentConfig.maxTurns && agentConfig.maxTurns > 0) {
|
||||
args.push('--max-turns', String(agentConfig.maxTurns));
|
||||
}
|
||||
@@ -412,6 +421,65 @@ export function getActiveExecutions() {
|
||||
}));
|
||||
}
|
||||
|
||||
export function summarize(text, threshold = 1500) {
|
||||
return new Promise((resolve) => {
|
||||
if (!text || text.length <= threshold) {
|
||||
resolve(text);
|
||||
return;
|
||||
}
|
||||
|
||||
const prompt = `Resuma o conteúdo abaixo de forma estruturada e concisa. Preserve TODAS as informações críticas:
|
||||
- Decisões técnicas e justificativas
|
||||
- Trechos de código essenciais
|
||||
- Dados, números e métricas
|
||||
- Problemas encontrados e soluções
|
||||
- Recomendações e próximos passos
|
||||
|
||||
Organize o resumo usando <tags> XML (ex: <decisoes>, <codigo>, <problemas>, <recomendacoes>).
|
||||
NÃO omita informações que seriam necessárias para outro profissional continuar o trabalho.
|
||||
|
||||
<conteudo_para_resumir>
|
||||
${text}
|
||||
</conteudo_para_resumir>`;
|
||||
|
||||
const args = [
|
||||
'-p', prompt,
|
||||
'--output-format', 'text',
|
||||
'--model', 'claude-haiku-4-5-20251001',
|
||||
'--max-turns', '1',
|
||||
'--permission-mode', 'bypassPermissions',
|
||||
];
|
||||
|
||||
if (existsSync(AGENT_SETTINGS)) {
|
||||
args.push('--settings', AGENT_SETTINGS);
|
||||
}
|
||||
|
||||
const child = spawn(CLAUDE_BIN, args, {
|
||||
env: cleanEnv(),
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let output = '';
|
||||
const timer = setTimeout(() => {
|
||||
child.kill('SIGTERM');
|
||||
}, 120000);
|
||||
|
||||
child.stdout.on('data', (chunk) => { output += chunk.toString(); });
|
||||
|
||||
child.on('close', () => {
|
||||
clearTimeout(timer);
|
||||
const result = output.trim();
|
||||
console.log(`[executor] Sumarização: ${text.length} → ${result.length} chars`);
|
||||
resolve(result || text);
|
||||
});
|
||||
|
||||
child.on('error', () => {
|
||||
clearTimeout(timer);
|
||||
resolve(text);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function getBinPath() {
|
||||
return CLAUDE_BIN;
|
||||
}
|
||||
|
||||
@@ -273,6 +273,23 @@ export function getRecentExecutions(limit = 20) {
|
||||
return recentExecBuffer.slice(0, Math.min(limit, MAX_RECENT));
|
||||
}
|
||||
|
||||
async function executeWithRetry(agentId, taskDescription, metadata, maxRetries = 10, baseDelay = 30000) {
|
||||
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
||||
try {
|
||||
executeTask(agentId, taskDescription, null, null, metadata);
|
||||
return;
|
||||
} catch (err) {
|
||||
if (err.message.includes('Limite de execuções simultâneas') && attempt < maxRetries) {
|
||||
const delay = baseDelay + Math.random() * 10000;
|
||||
console.log(`[manager] Agendamento aguardando slot (tentativa ${attempt}/${maxRetries}), retry em ${(delay / 1000).toFixed(0)}s`);
|
||||
await new Promise(resolve => setTimeout(resolve, delay));
|
||||
continue;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function scheduleTask(agentId, taskDescription, cronExpression, wsCallback) {
|
||||
const agent = agentsStore.getById(agentId);
|
||||
if (!agent) throw new Error(`Agente ${agentId} não encontrado`);
|
||||
@@ -296,7 +313,9 @@ export function scheduleTask(agentId, taskDescription, cronExpression, wsCallbac
|
||||
schedulesStore.save(items);
|
||||
|
||||
scheduler.schedule(scheduleId, cronExpression, () => {
|
||||
executeTask(agentId, taskDescription, null, null, { source: 'schedule', scheduleId });
|
||||
executeWithRetry(agentId, taskDescription, { source: 'schedule', scheduleId }).catch(err => {
|
||||
console.log(`[manager] Agendamento ${scheduleId} falhou após retries: ${err.message}`);
|
||||
});
|
||||
}, false);
|
||||
|
||||
return { scheduleId, agentId, agentName: agent.agent_name, taskDescription, cronExpression };
|
||||
@@ -314,7 +333,9 @@ export function updateScheduleTask(scheduleId, data, wsCallback) {
|
||||
const cronExpression = data.cronExpression || stored.cronExpression;
|
||||
|
||||
scheduler.updateSchedule(scheduleId, cronExpression, () => {
|
||||
executeTask(agentId, taskDescription, null, null, { source: 'schedule', scheduleId });
|
||||
executeWithRetry(agentId, taskDescription, { source: 'schedule', scheduleId }).catch(err => {
|
||||
console.log(`[manager] Agendamento ${scheduleId} falhou após retries: ${err.message}`);
|
||||
});
|
||||
});
|
||||
|
||||
schedulesStore.update(scheduleId, { agentId, agentName: agent.agent_name, taskDescription, cronExpression });
|
||||
@@ -424,10 +445,8 @@ export function importAgent(data) {
|
||||
|
||||
export function restoreSchedules() {
|
||||
scheduler.restoreSchedules((agentId, taskDescription, scheduleId) => {
|
||||
try {
|
||||
executeTask(agentId, taskDescription, null, null, { source: 'schedule', scheduleId });
|
||||
} catch (err) {
|
||||
executeWithRetry(agentId, taskDescription, { source: 'schedule', scheduleId }).catch(err => {
|
||||
console.log(`[manager] Erro ao executar tarefa agendada: ${err.message}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ function executeStepAsPromise(agentConfig, prompt, pipelineState, wsCallback, pi
|
||||
costUsd: result.costUsd || 0,
|
||||
durationMs: result.durationMs || 0,
|
||||
numTurns: result.numTurns || 0,
|
||||
sessionId: result.sessionId || '',
|
||||
});
|
||||
},
|
||||
}
|
||||
@@ -237,7 +238,7 @@ export async function executePipeline(pipelineId, initialInput, wsCallback, opti
|
||||
|
||||
totalCost += stepResult.costUsd;
|
||||
currentInput = stepResult.text;
|
||||
results.push({ stepId: step.id, agentName: agent.agent_name, result: stepResult.text });
|
||||
results.push({ stepId: step.id, agentId: step.agentId, agentName: agent.agent_name, result: stepResult.text, sessionId: stepResult.sessionId });
|
||||
|
||||
const current = executionsStore.getById(historyRecord.id);
|
||||
const savedSteps = current ? (current.steps || []) : [];
|
||||
@@ -266,6 +267,19 @@ export async function executePipeline(pipelineId, initialInput, wsCallback, opti
|
||||
costUsd: stepResult.costUsd,
|
||||
});
|
||||
}
|
||||
|
||||
if (i < steps.length - 1 && !pipelineState.canceled) {
|
||||
if (wsCallback) {
|
||||
wsCallback({ type: 'pipeline_summarizing', pipelineId, stepIndex: i, originalLength: currentInput.length });
|
||||
}
|
||||
const summarized = await executor.summarize(currentInput);
|
||||
if (summarized !== currentInput) {
|
||||
if (wsCallback) {
|
||||
wsCallback({ type: 'pipeline_summarized', pipelineId, stepIndex: i, originalLength: currentInput.length, summarizedLength: summarized.length });
|
||||
}
|
||||
currentInput = summarized;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
activePipelines.delete(executionId);
|
||||
@@ -285,7 +299,17 @@ export async function executePipeline(pipelineId, initialInput, wsCallback, opti
|
||||
if (wsCallback) wsCallback({ type: 'report_generated', pipelineId, reportFile: report.filename });
|
||||
}
|
||||
} catch (e) { console.error('[pipeline] Erro ao gerar relatório:', e.message); }
|
||||
if (wsCallback) wsCallback({ type: 'pipeline_complete', pipelineId, executionId, results, totalCostUsd: totalCost });
|
||||
const lastResult = results.length > 0 ? results[results.length - 1] : null;
|
||||
if (wsCallback) wsCallback({
|
||||
type: 'pipeline_complete',
|
||||
pipelineId,
|
||||
executionId,
|
||||
results,
|
||||
totalCostUsd: totalCost,
|
||||
lastAgentId: lastResult?.agentId || '',
|
||||
lastAgentName: lastResult?.agentName || '',
|
||||
lastSessionId: lastResult?.sessionId || '',
|
||||
});
|
||||
}
|
||||
|
||||
return { executionId, results };
|
||||
|
||||
Reference in New Issue
Block a user