Continuação de conversa no terminal, histórico de agendamentos, webhooks e melhorias gerais

- Terminal com input de chat: após execução, permite continuar conversa com o agente
  via --resume do CLI, mantendo contexto da sessão (sessionId persistido)
- Nova rota POST /api/agents/:id/continue para retomar sessões
- Executor com função resume() para spawnar claude com --resume <sessionId>
- Histórico de agendamentos agora busca do executionsStore (persistente) com dados
  completos: agente, tarefa, status, duração, custo e link para detalhes no modal
- Execuções de agendamento tagueadas com source:'schedule' e scheduleId
- Correção da expressão cron duplicada na UI de agendamentos
- cronToHuman trata expressões com minuto específico (ex: 37 3 * * * → Todo dia às 03:37)
- Botão "Copiar cURL" nos cards de webhook com payload de exemplo contextual
- Webhooks component (webhooks.js) adicionado ao repositório
This commit is contained in:
Frederico Castro
2026-02-26 04:01:12 -03:00
parent 22a3ce9262
commit 93d9027e2c
18 changed files with 1609 additions and 75 deletions

View File

@@ -42,6 +42,7 @@ function buildSteps(steps) {
order: step.order !== undefined ? step.order : index,
inputTemplate: step.inputTemplate || null,
description: step.description || '',
requiresApproval: index === 0 ? false : !!step.requiresApproval,
}))
.sort((a, b) => a.order - b.order);
}
@@ -79,7 +80,12 @@ function executeStepAsPromise(agentConfig, prompt, pipelineState, wsCallback, pi
reject(new Error(result.stderr || `Processo encerrado com código ${result.exitCode}`));
return;
}
resolve(result.result || '');
resolve({
text: result.result || '',
costUsd: result.costUsd || 0,
durationMs: result.durationMs || 0,
numTurns: result.numTurns || 0,
});
},
}
);
@@ -93,11 +99,53 @@ function executeStepAsPromise(agentConfig, prompt, pipelineState, wsCallback, pi
});
}
export async function executePipeline(pipelineId, initialInput, wsCallback) {
function waitForApproval(pipelineId, stepIndex, previousOutput, agentName, wsCallback) {
return new Promise((resolve) => {
const state = activePipelines.get(pipelineId);
if (!state) { resolve(false); return; }
state.pendingApproval = {
stepIndex,
previousOutput: previousOutput.slice(0, 3000),
agentName,
resolve,
};
if (wsCallback) {
wsCallback({
type: 'pipeline_approval_required',
pipelineId,
stepIndex,
agentName,
previousOutput: previousOutput.slice(0, 3000),
});
}
});
}
export function approvePipelineStep(pipelineId) {
const state = activePipelines.get(pipelineId);
if (!state?.pendingApproval) return false;
const { resolve } = state.pendingApproval;
state.pendingApproval = null;
resolve(true);
return true;
}
export function rejectPipelineStep(pipelineId) {
const state = activePipelines.get(pipelineId);
if (!state?.pendingApproval) return false;
const { resolve } = state.pendingApproval;
state.pendingApproval = null;
resolve(false);
return true;
}
export async function executePipeline(pipelineId, initialInput, wsCallback, options = {}) {
const pl = pipelinesStore.getById(pipelineId);
if (!pl) throw new Error(`Pipeline ${pipelineId} não encontrado`);
const pipelineState = { currentExecutionId: null, currentStep: 0, canceled: false };
const pipelineState = { currentExecutionId: null, currentStep: 0, canceled: false, pendingApproval: null };
activePipelines.set(pipelineId, pipelineState);
const historyRecord = executionsStore.create({
@@ -108,11 +156,13 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
status: 'running',
startedAt: new Date().toISOString(),
steps: [],
totalCostUsd: 0,
});
const steps = buildSteps(pl.steps);
const results = [];
let currentInput = initialInput;
let totalCost = 0;
try {
for (let i = 0; i < steps.length; i++) {
@@ -121,10 +171,40 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
const step = steps[i];
pipelineState.currentStep = i;
if (step.requiresApproval && i > 0) {
const prevAgentName = results.length > 0 ? results[results.length - 1].agentName : '';
executionsStore.update(historyRecord.id, { status: 'awaiting_approval' });
if (wsCallback) {
wsCallback({ type: 'pipeline_status', pipelineId, status: 'awaiting_approval', stepIndex: i });
}
const approved = await waitForApproval(pipelineId, i, currentInput, prevAgentName, wsCallback);
if (!approved) {
pipelineState.canceled = true;
executionsStore.update(historyRecord.id, { status: 'rejected', endedAt: new Date().toISOString(), totalCostUsd: totalCost });
if (wsCallback) {
wsCallback({ type: 'pipeline_rejected', pipelineId, stepIndex: i });
}
break;
}
executionsStore.update(historyRecord.id, { status: 'running' });
}
if (pipelineState.canceled) break;
const agent = agentsStore.getById(step.agentId);
if (!agent) throw new Error(`Agente ${step.agentId} não encontrado no passo ${i}`);
if (agent.status !== 'active') throw new Error(`Agente ${agent.agent_name} está inativo`);
const stepConfig = { ...agent.config };
if (options.workingDirectory) {
stepConfig.workingDirectory = options.workingDirectory;
}
const prompt = applyTemplate(step.inputTemplate, currentInput);
const stepStart = new Date().toISOString();
@@ -139,12 +219,13 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
});
}
const result = await executeStepAsPromise(agent.config, prompt, pipelineState, wsCallback, pipelineId, i);
const stepResult = await executeStepAsPromise(stepConfig, prompt, pipelineState, wsCallback, pipelineId, i);
if (pipelineState.canceled) break;
currentInput = result;
results.push({ stepId: step.id, agentName: agent.agent_name, result });
totalCost += stepResult.costUsd;
currentInput = stepResult.text;
results.push({ stepId: step.id, agentName: agent.agent_name, result: stepResult.text });
const current = executionsStore.getById(historyRecord.id);
const savedSteps = current ? (current.steps || []) : [];
@@ -153,12 +234,15 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
agentId: step.agentId,
agentName: agent.agent_name,
prompt: prompt.slice(0, 5000),
result,
result: stepResult.text,
startedAt: stepStart,
endedAt: new Date().toISOString(),
status: 'completed',
costUsd: stepResult.costUsd,
durationMs: stepResult.durationMs,
numTurns: stepResult.numTurns,
});
executionsStore.update(historyRecord.id, { steps: savedSteps });
executionsStore.update(historyRecord.id, { steps: savedSteps, totalCostUsd: totalCost });
if (wsCallback) {
wsCallback({
@@ -166,19 +250,23 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
pipelineId,
stepIndex: i,
stepId: step.id,
result: result.slice(0, 500),
result: stepResult.text.slice(0, 500),
costUsd: stepResult.costUsd,
});
}
}
activePipelines.delete(pipelineId);
const finalStatus = pipelineState.canceled ? 'canceled' : 'completed';
executionsStore.update(historyRecord.id, {
status: pipelineState.canceled ? 'canceled' : 'completed',
status: finalStatus,
endedAt: new Date().toISOString(),
totalCostUsd: totalCost,
});
if (!pipelineState.canceled && wsCallback) {
wsCallback({ type: 'pipeline_complete', pipelineId, results });
wsCallback({ type: 'pipeline_complete', pipelineId, results, totalCostUsd: totalCost });
}
return results;
@@ -188,6 +276,7 @@ export async function executePipeline(pipelineId, initialInput, wsCallback) {
status: 'error',
error: err.message,
endedAt: new Date().toISOString(),
totalCostUsd: totalCost,
});
if (wsCallback) {
wsCallback({
@@ -205,6 +294,10 @@ export function cancelPipeline(pipelineId) {
const state = activePipelines.get(pipelineId);
if (!state) return false;
state.canceled = true;
if (state.pendingApproval) {
state.pendingApproval.resolve(false);
state.pendingApproval = null;
}
if (state.currentExecutionId) executor.cancel(state.currentExecutionId);
activePipelines.delete(pipelineId);
return true;
@@ -215,6 +308,7 @@ export function getActivePipelines() {
pipelineId: id,
currentStep: state.currentStep,
currentExecutionId: state.currentExecutionId,
pendingApproval: !!state.pendingApproval,
}));
}