UNPKG

@workflow-manager/runner

Version:

CLI runner for in-memory and markdown workflow orchestration using ATEP-like envelopes

249 lines (248 loc) 11.6 kB
import { describeAgentStatus, elapsedFrom, formatCount, isTerminalStatus, splitLines, stepLabel, } from "./runFormat.js"; export class CliRunRenderer { verbose; stream; heartbeatMs; steps = new Map(); lineBuffers = new Map(); lastSnapshot = null; lastStepDetails = new Map(); heartbeat = null; promptPauseCount = 0; started = false; closed = false; constructor(options) { this.verbose = options.verbose; this.stream = options.stream ?? process.stderr; this.heartbeatMs = options.heartbeatMs ?? 1000; options.workflow.steps.forEach((step, index) => { this.steps.set(step.key, { index, label: stepLabel(step) }); }); } onSnapshot(snapshot, stepDetails) { if (this.closed) { return; } const previous = this.lastSnapshot; this.lastSnapshot = snapshot; this.lastStepDetails = new Map(stepDetails.map((detail) => [detail.stepKey, detail])); if ((snapshot.status === "running" || snapshot.status === "waiting_for_approval") && !this.started) { this.started = true; this.write(`▶ Running workflow "${snapshot.workflowTitle}" (${formatCount(snapshot.steps.length, "step", "steps")})`); } this.syncHeartbeat(snapshot.status); for (const step of snapshot.steps) { const prevStatus = previous?.steps.find((entry) => entry.stepKey === step.stepKey)?.status; if (prevStatus !== step.status) { this.renderStepStatus(snapshot, step.stepKey, step.status); } } if (previous?.status !== snapshot.status && snapshot.status === "waiting_for_approval" && snapshot.waitingForApproval) { const waiting = snapshot.waitingForApproval; const stepSummary = this.stepSummary(snapshot, waiting.stepKey); const validation = waiting.validation ? ` (${waiting.validation})` : ""; this.write(`◌ ${stepSummary} waiting for approval${validation} - ${this.remainingText(snapshot)}`); if (waiting.preview) { this.write(` Approval summary: ${waiting.preview.summary}`); for (const item of waiting.preview.items) { const status = item.status ? ` [${item.status}]` : ""; this.write(` - ${item.title}${status}: ${item.summary}`); } } this.write(" Resolve it in the terminal prompt or with `wfm approve` / `wfm cancel` using the attach API shown above."); } if (isTerminalStatus(snapshot.status)) { this.stopHeartbeat(); this.flushBuffers(); this.closed = true; } } onEvent(event) { if (this.closed || !this.verbose) { return; } if (event.type === "agent.started") { const stepSummary = this.stepSummary(this.lastSnapshot, event.stepRunId); const command = typeof event.payload.command === "string" ? event.payload.command : "agent"; const args = Array.isArray(event.payload.args) ? event.payload.args.map((value) => String(value)).join(" ") : ""; const model = typeof event.payload.model === "string" ? ` model=${event.payload.model}` : ""; this.write(` ${stepSummary} agent started: ${command}${args ? ` ${args}` : ""}${model}`); return; } if (event.type === "agent.finished") { const stepSummary = this.stepSummary(this.lastSnapshot, event.stepRunId); const executionStatus = typeof event.payload.executionStatus === "string" ? ` ${event.payload.executionStatus}` : ""; const exitStatus = typeof event.payload.exitStatus === "number" || event.payload.exitStatus === null ? ` exit=${String(event.payload.exitStatus)}` : ""; const timedOut = event.payload.timedOut === true ? " timedOut=true" : ""; this.write(` ${stepSummary} agent finished:${executionStatus}${exitStatus}${timedOut}`); return; } if (event.type === "step.execution_finished") { const stepSummary = this.stepSummary(this.lastSnapshot, event.stepRunId); const status = typeof event.payload.status === "string" ? event.payload.status : "unknown"; const action = typeof event.payload.action === "string" ? ` action=${event.payload.action}` : ""; const feedbackReason = typeof event.payload.feedbackReason === "string" && event.payload.feedbackReason.length > 0 ? ` reason=${event.payload.feedbackReason}` : ""; this.write(` ${stepSummary} execution finished: ${status}${action}${feedbackReason}`); return; } if (event.type === "step.validation_started") { const stepSummary = this.stepSummary(this.lastSnapshot, event.stepRunId); const adapter = typeof event.payload.adapter === "string" ? ` (${event.payload.adapter})` : ""; const criteria = typeof event.payload.criteria === "string" ? ` criteria=${event.payload.criteria}` : ""; this.write(` ${stepSummary} agent validation started${adapter}${criteria}`); return; } if (event.type === "step.validation_finished") { const stepSummary = this.stepSummary(this.lastSnapshot, event.stepRunId); const status = typeof event.payload.status === "string" ? event.payload.status : "unknown"; const action = typeof event.payload.action === "string" ? ` action=${event.payload.action}` : ""; const feedbackReason = typeof event.payload.feedbackReason === "string" && event.payload.feedbackReason.length > 0 ? ` reason=${event.payload.feedbackReason}` : ""; this.write(` ${stepSummary} agent validation: ${status}${action}${feedbackReason}`); return; } if (event.type === "step.retried") { const stepSummary = this.stepSummary(this.lastSnapshot, event.stepRunId); const attempt = typeof event.payload.attempt === "number" ? ` next attempt ${event.payload.attempt}` : " retry queued"; this.write(` ${stepSummary}${attempt}`); } } onLog(log) { if (this.closed || !this.verbose) { return; } const stepKey = log.stepKey ?? "workflow"; const bufferKey = `${stepKey}:${log.stream}`; const existing = this.lineBuffers.get(bufferKey) ?? ""; const { lines, remainder } = splitLines(existing + log.text); this.lineBuffers.set(bufferKey, remainder); for (const line of lines) { this.write(` [${stepKey} ${log.stream}] ${line}`); } } close() { if (this.closed) { return; } this.stopHeartbeat(); this.flushBuffers(); this.closed = true; } pauseHeartbeat() { this.promptPauseCount += 1; this.stopHeartbeat(); } resumeHeartbeat() { this.promptPauseCount = Math.max(0, this.promptPauseCount - 1); if (this.promptPauseCount === 0 && this.lastSnapshot) { this.syncHeartbeat(this.lastSnapshot.status); } } renderStepStatus(snapshot, stepKey, status) { if (!this.verbose && status === "runnable") { return; } if (!this.verbose && status === "pending") { return; } const summary = this.stepSummary(snapshot, stepKey); const detail = this.lastStepDetails.get(stepKey); if (status === "running") { const adapter = detail?.adapter ? ` (${detail.adapter})` : ""; this.write(`→ ${summary} ${describeAgentStatus(status)}${adapter} - ${this.remainingText(snapshot)}`); return; } if (status === "succeeded") { const duration = detail?.startedAt ? ` in ${elapsedFrom(detail.startedAt)}` : ""; this.write(`✓ ${summary} ${describeAgentStatus(status)}${duration} - ${this.remainingText(snapshot)}`); return; } if (status === "failed") { const duration = detail?.startedAt ? ` after ${elapsedFrom(detail.startedAt)}` : ""; this.write(`✗ ${summary} ${describeAgentStatus(status)}${duration} - ${this.remainingText(snapshot)}`); return; } if (status === "cancelled") { this.write(`✗ ${summary} ${describeAgentStatus(status)} - ${this.remainingText(snapshot)}`); return; } if (status === "waiting_for_approval") { this.write(`◌ ${summary} ${describeAgentStatus(status)} - ${this.remainingText(snapshot)}`); return; } this.write(`… ${summary} ${describeAgentStatus(status)} - ${this.remainingText(snapshot)}`); } stepSummary(snapshot, stepKey) { if (!stepKey) { return "[workflow]"; } const descriptor = this.steps.get(stepKey); const index = descriptor?.index ?? Math.max(0, snapshot?.steps.findIndex((step) => step.stepKey === stepKey) ?? 0); const total = snapshot?.steps.length ?? this.steps.size; const label = descriptor?.label ?? stepKey; return `[${index + 1}/${Math.max(total, 1)}] ${stepKey}${label === stepKey ? "" : ` - ${label}`}`; } remainingText(snapshot) { const completed = snapshot.steps.filter((step) => step.status === "succeeded").length; const remaining = Math.max(snapshot.steps.length - completed, 0); return `${formatCount(remaining, "step remaining", "steps remaining")} - workflow ${elapsedFrom(snapshot.startedAt)}`; } syncHeartbeat(status) { if (this.promptPauseCount > 0) { this.stopHeartbeat(); return; } if (status === "running" || status === "waiting_for_approval") { if (!this.heartbeat) { this.heartbeat = setInterval(() => { this.renderHeartbeat(); }, this.heartbeatMs); } return; } this.stopHeartbeat(); } stopHeartbeat() { if (!this.heartbeat) { return; } clearInterval(this.heartbeat); this.heartbeat = null; } renderHeartbeat() { if (!this.lastSnapshot || this.closed) { return; } const snapshot = this.lastSnapshot; if (snapshot.status === "running" && snapshot.currentStepKey) { this.write(`.. ${this.stepSummary(snapshot, snapshot.currentStepKey)} running - ${this.remainingText(snapshot)}`); return; } if (snapshot.status === "waiting_for_approval" && snapshot.waitingForApproval) { const waiting = snapshot.waitingForApproval; this.write(`.. ${this.stepSummary(snapshot, waiting.stepKey)} waiting - ${this.remainingText(snapshot)}`); return; } if (snapshot.status === "running") { this.write(`.. workflow running - ${this.remainingText(snapshot)}`); } } flushBuffers() { for (const [bufferKey, text] of this.lineBuffers.entries()) { if (!text) { continue; } const [stepKey, stream] = bufferKey.split(":"); this.write(` [${stepKey} ${stream}] ${text}`); } this.lineBuffers.clear(); } write(line) { this.stream.write(`${line}\n`); } }