Histórico persistente de execuções com visualização detalhada

- Novo executionsStore em db.js com cache in-memory e escrita debounced
- Camada de cache (src/cache/index.js) com TTL e suporte opcional a Redis
- Persistência de execuções de agentes e pipelines com metadados completos
- Pipeline grava cada etapa com prompt, resultado, timestamps e status
- 4 endpoints REST: listagem paginada com filtros, detalhe, exclusão individual e limpeza total
- Componente frontend (history.js) com cards, filtros, paginação e modal de detalhe
- Timeline visual para pipelines com prompts colapsáveis por etapa
- Correção do executor: --max-turns em vez de --max-tokens, --permission-mode bypassPermissions
- Refatoração do scheduler com persistência melhorada e graceful shutdown
This commit is contained in:
Frederico Castro
2026-02-26 01:36:28 -03:00
parent 2f7a9d4c56
commit 4b6c876f36
13 changed files with 1536 additions and 398 deletions

View File

@@ -1,5 +1,5 @@
import { v4 as uuidv4 } from 'uuid';
import { agentsStore, schedulesStore } from '../store/db.js';
import { agentsStore, schedulesStore, executionsStore } from '../store/db.js';
import * as executor from './executor.js';
import * as scheduler from './scheduler.js';
@@ -12,6 +12,9 @@ const DEFAULT_CONFIG = {
allowedTools: '',
};
const MAX_RECENT = 200;
const recentExecBuffer = [];
let dailyExecutionCount = 0;
let dailyCountDate = new Date().toDateString();
@@ -61,11 +64,8 @@ export function getAgentById(id) {
export function createAgent(data) {
const errors = validateAgent(data);
if (errors.length > 0) {
throw new Error(errors.join('; '));
}
const agentData = {
if (errors.length > 0) throw new Error(errors.join('; '));
return agentsStore.create({
agent_name: data.agent_name,
description: data.description || '',
tags: sanitizeTags(data.tags),
@@ -74,15 +74,12 @@ export function createAgent(data) {
status: data.status || 'active',
assigned_host: data.assigned_host || 'localhost',
executions: [],
};
return agentsStore.create(agentData);
});
}
export function updateAgent(id, data) {
const existing = agentsStore.getById(id);
if (!existing) return null;
const updateData = {};
if (data.agent_name !== undefined) updateData.agent_name = data.agent_name;
if (data.description !== undefined) updateData.description = data.description;
@@ -90,10 +87,7 @@ export function updateAgent(id, data) {
if (data.tasks !== undefined) updateData.tasks = data.tasks;
if (data.status !== undefined) updateData.status = data.status;
if (data.assigned_host !== undefined) updateData.assigned_host = data.assigned_host;
if (data.config !== undefined) {
updateData.config = { ...existing.config, ...data.config };
}
if (data.config !== undefined) updateData.config = { ...existing.config, ...data.config };
return agentsStore.update(id, updateData);
}
@@ -106,12 +100,25 @@ export function executeTask(agentId, task, instructions, wsCallback) {
if (!agent) throw new Error(`Agente ${agentId} não encontrado`);
if (agent.status !== 'active') throw new Error(`Agente ${agentId} está inativo`);
const executionRecord = {
const taskText = typeof task === 'string' ? task : task.description;
const startedAt = new Date().toISOString();
const historyRecord = executionsStore.create({
type: 'agent',
agentId,
agentName: agent.agent_name,
task: taskText,
instructions: instructions || '',
status: 'running',
startedAt,
});
const execRecord = {
executionId: null,
agentId,
agentName: agent.agent_name,
task: typeof task === 'string' ? task : task.description,
startedAt: new Date().toISOString(),
task: taskText,
startedAt,
status: 'running',
};
@@ -120,74 +127,70 @@ export function executeTask(agentId, task, instructions, wsCallback) {
{ description: task, instructions },
{
onData: (parsed, execId) => {
if (wsCallback) {
wsCallback({
type: 'execution_output',
executionId: execId,
agentId,
data: parsed,
});
}
if (wsCallback) wsCallback({ type: 'execution_output', executionId: execId, agentId, data: parsed });
},
onError: (err, execId) => {
updateAgentExecution(agentId, execId, { status: 'error', error: err.message, endedAt: new Date().toISOString() });
if (wsCallback) {
wsCallback({
type: 'execution_error',
executionId: execId,
agentId,
data: { error: err.message },
});
}
const endedAt = new Date().toISOString();
updateExecutionRecord(agentId, execId, { status: 'error', error: err.message, endedAt });
executionsStore.update(historyRecord.id, { status: 'error', error: err.message, endedAt });
if (wsCallback) wsCallback({ type: 'execution_error', executionId: execId, agentId, data: { error: err.message } });
},
onComplete: (result, execId) => {
updateAgentExecution(agentId, execId, { status: 'completed', result, endedAt: new Date().toISOString() });
if (wsCallback) {
wsCallback({
type: 'execution_complete',
executionId: execId,
agentId,
data: result,
});
}
const endedAt = new Date().toISOString();
updateExecutionRecord(agentId, execId, { status: 'completed', result, endedAt });
executionsStore.update(historyRecord.id, {
status: 'completed',
result: result.result || '',
exitCode: result.exitCode,
endedAt,
});
if (wsCallback) wsCallback({ type: 'execution_complete', executionId: execId, agentId, data: result });
},
}
);
if (!executionId) {
executionsStore.update(historyRecord.id, { status: 'error', error: 'Limite de execuções simultâneas atingido', endedAt: new Date().toISOString() });
throw new Error('Limite de execuções simultâneas atingido');
}
executionRecord.executionId = executionId;
execRecord.executionId = executionId;
executionsStore.update(historyRecord.id, { executionId });
incrementDailyCount();
const updatedAgent = agentsStore.getById(agentId);
const executions = [...(updatedAgent.executions || []), executionRecord];
const executions = [...(updatedAgent.executions || []), execRecord];
agentsStore.update(agentId, { executions: executions.slice(-100) });
recentExecBuffer.unshift({ ...execRecord });
if (recentExecBuffer.length > MAX_RECENT) recentExecBuffer.length = MAX_RECENT;
return executionId;
}
function updateAgentExecution(agentId, executionId, updates) {
function updateRecentBuffer(executionId, updates) {
const entry = recentExecBuffer.find((e) => e.executionId === executionId);
if (entry) Object.assign(entry, updates);
}
function updateExecutionRecord(agentId, executionId, updates) {
const agent = agentsStore.getById(agentId);
if (!agent) return;
const executions = (agent.executions || []).map((exec) => {
if (exec.executionId === executionId) {
return { ...exec, ...updates };
}
return exec;
});
const executions = (agent.executions || []).map((exec) =>
exec.executionId === executionId ? { ...exec, ...updates } : exec
);
agentsStore.update(agentId, { executions });
}
export function getRecentExecutions(limit = 20) {
return recentExecBuffer.slice(0, Math.min(limit, MAX_RECENT));
}
export function scheduleTask(agentId, taskDescription, cronExpression, wsCallback) {
const agent = agentsStore.getById(agentId);
if (!agent) throw new Error(`Agente ${agentId} não encontrado`);
const scheduleId = uuidv4();
const items = schedulesStore.getAll();
items.push({
id: scheduleId,
@@ -223,13 +226,7 @@ export function updateScheduleTask(scheduleId, data, wsCallback) {
executeTask(agentId, taskDescription, null, wsCallback);
});
schedulesStore.update(scheduleId, {
agentId,
agentName: agent.agent_name,
taskDescription,
cronExpression,
});
schedulesStore.update(scheduleId, { agentId, agentName: agent.agent_name, taskDescription, cronExpression });
return schedulesStore.getById(scheduleId);
}
@@ -241,23 +238,9 @@ export function getActiveExecutions() {
return executor.getActiveExecutions();
}
export function getRecentExecutions(limit = 20) {
const agents = agentsStore.getAll();
const all = agents.flatMap((a) =>
(a.executions || []).map((e) => ({
...e,
agentName: a.agent_name,
agentId: a.id,
}))
);
all.sort((a, b) => new Date(b.startedAt) - new Date(a.startedAt));
return all.slice(0, limit);
}
export function exportAgent(agentId) {
const agent = agentsStore.getById(agentId);
if (!agent) return null;
return {
agent_name: agent.agent_name,
description: agent.description,
@@ -270,11 +253,8 @@ export function exportAgent(agentId) {
}
export function importAgent(data) {
if (!data.agent_name) {
throw new Error('agent_name é obrigatório para importação');
}
const agentData = {
if (!data.agent_name) throw new Error('agent_name é obrigatório para importação');
return agentsStore.create({
agent_name: data.agent_name,
description: data.description || '',
tags: sanitizeTags(data.tags),
@@ -283,9 +263,7 @@ export function importAgent(data) {
status: data.status || 'active',
assigned_host: data.assigned_host || 'localhost',
executions: [],
};
return agentsStore.create(agentData);
});
}
export function restoreSchedules(wsCallback) {