UNPKG

@workflow-manager/runner

Version:

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

171 lines (170 loc) 7.79 kB
function asRecord(value) { return value && typeof value === "object" ? value : {}; } function chapterOutputs(previousOutput) { return Object.values(previousOutput) .map((entry) => asRecord(entry)) .filter((entry) => typeof entry.chapterMarkdown === "string"); } function asStringList(value) { if (!Array.isArray(value)) { return []; } return value.map((entry) => String(entry)); } async function emitStreamChunks(hooks, stream, chunks, delayMs) { if (chunks.length === 0) { return; } const emit = stream === "stdout" ? hooks?.onStdout : hooks?.onStderr; for (const chunk of chunks) { emit?.(chunk); if (delayMs > 0) { await new Promise((resolve) => setTimeout(resolve, delayMs)); } } } export async function executeMockStep(step, input, attempt, hooks) { const start = Date.now(); const payload = asRecord(step.taskSpec?.payload); const delayMs = Number(payload.delayMs ?? 0); const chunkDelayMs = Number(payload.chunkDelayMs ?? 0); const mockResult = String(payload.mockResult ?? "success").toLowerCase(); const feedback = String(payload.feedback ?? ""); const stdoutChunks = asStringList(payload.stdoutChunks); const stderrChunks = asStringList(payload.stderrChunks); hooks?.onStarted?.({ adapter: input.priming_configuration.adapter ?? "mock" }); await emitStreamChunks(hooks, "stdout", stdoutChunks, Number.isFinite(chunkDelayMs) ? Math.max(0, Math.floor(chunkDelayMs)) : 0); await emitStreamChunks(hooks, "stderr", stderrChunks, Number.isFinite(chunkDelayMs) ? Math.max(0, Math.floor(chunkDelayMs)) : 0); if (Number.isFinite(delayMs) && delayMs > 0) { await new Promise((resolve) => setTimeout(resolve, Math.floor(delayMs))); } const make = (status, action, extraPayload = {}, feedbackReason = feedback) => ({ step_id: step.key, execution_status: status, qa_routing: { action, feedback_reason: feedbackReason, }, mutated_payload: { stepKey: step.key, attempt, objective: input.step_context.step_objective, adapter: input.priming_configuration.adapter ?? "mock", priming: { skills: input.priming_configuration.required_skills, mcps: input.priming_configuration.mcp_endpoints, }, // Echoes back what this step actually received, so tests can assert on // global_state scoping (stateFrom) without a separate capture mechanism. // Shallow-copied: the live global_state object gets written back into // itself as this step's own output, so storing the reference directly // would create a self-referential cycle that crashes JSON.stringify. globalStateSeen: { ...input.global_context.global_state }, mockResult, ...extraPayload, }, metadata: { execution_time_ms: Date.now() - start, external_intervention_required: status === "YIELD_EXTERNAL", intervention_details: status === "YIELD_EXTERNAL" ? { reason: "Awaiting human/external validation" } : undefined, }, }); if (step.kind === "approval") { const autoApprove = step.approvalSpec?.autoApprove ?? false; const result = autoApprove ? make("SUCCESS", "PROCEED") : make("YIELD_EXTERNAL", "PROCEED"); hooks?.onFinished?.({ executionStatus: result.execution_status }); return result; } if (payload.mockJudgeVerdict && typeof payload.mockJudgeVerdict === "object") { const result = make("SUCCESS", "PROCEED", { judgeVerdict: payload.mockJudgeVerdict }); hooks?.onFinished?.({ executionStatus: result.execution_status }); return result; } if (typeof payload.storyChapter === "number") { const chapterNumber = payload.storyChapter; const prompt = String(input.global_context.global_state.storyRequest ?? input.global_context.primary_objective ?? "a bunny story"); const paragraph = chapterNumber === 1 ? `A curious bunny set out at sunrise to solve the puzzle in ${prompt}.` : `By sunset, the bunny used what it learned to finish ${prompt} with courage and kindness.`; const result = make("SUCCESS", "PROCEED", { chapterNumber, chapterTitle: `Chapter ${chapterNumber}`, paragraph, chapterMarkdown: `## Chapter ${chapterNumber}\n\n${paragraph}`, storyPrompt: prompt, }); hooks?.onFinished?.({ executionStatus: result.execution_status }); return result; } if (payload.validateStory === true) { const expectedChapters = Number(payload.requiredChapters ?? 2); const previousOutput = asRecord(input.step_context.previous_output); const chapters = chapterOutputs(previousOutput) .map((entry) => String(entry.chapterMarkdown)) .join("\n\n"); const chapterMatches = chapters.match(/^## Chapter\s+\d+/gm) ?? []; const hasBunnyTheme = /bunny/i.test(chapters); const isValid = chapterMatches.length === expectedChapters && hasBunnyTheme; if (!isValid) { const result = make("QA_REJECTED", "RETRY_CURRENT", { expectedChapters, foundChapters: chapterMatches.length, hasBunnyTheme, validationPassed: false, }, `Story must include exactly ${expectedChapters} chapters and bunny-themed content`); hooks?.onFinished?.({ executionStatus: result.execution_status }); return result; } const result = make("SUCCESS", "PROCEED", { expectedChapters, foundChapters: chapterMatches.length, hasBunnyTheme, validationPassed: true, }); hooks?.onFinished?.({ executionStatus: result.execution_status }); return result; } if (payload.renderStoryMarkdown === true) { const previousOutput = asRecord(input.step_context.previous_output); const chapters = chapterOutputs(previousOutput) .sort((a, b) => Number(a.chapterNumber ?? 0) - Number(b.chapterNumber ?? 0)) .map((entry) => String(entry.chapterMarkdown)); if (chapters.length === 0) { const result = make("QA_REJECTED", "RETRY_CURRENT", { chapterCount: 0 }, "No chapters available to render"); hooks?.onFinished?.({ executionStatus: result.execution_status }); return result; } const title = String(payload.storyTitle ?? input.global_context.global_state.storyRequest ?? "Bunny Story"); const storyMarkdown = `# ${title}\n\n${chapters.join("\n\n")}`; const result = make("SUCCESS", "PROCEED", { chapterCount: chapters.length, storyMarkdown, }); hooks?.onFinished?.({ executionStatus: result.execution_status }); return result; } let result; switch (mockResult) { case "retry": result = make("QA_REJECTED", "RETRY_CURRENT"); break; case "rollback": result = make("QA_REJECTED", "ROLLBACK_PREVIOUS"); break; case "restart": result = make("QA_REJECTED", "RESTART_ALL"); break; case "yield": result = make("YIELD_EXTERNAL", "PROCEED"); break; case "fail": result = make("FAILED", "PROCEED"); break; default: result = make("SUCCESS", "PROCEED"); break; } hooks?.onFinished?.({ executionStatus: result.execution_status }); return result; }