UNPKG

akm-cli

Version:

akm (Agent Knowledge Manager) — a portable, local-first capability library for AI agents. Discover, load, share, and improve reusable skills, scripts, workflows, and knowledge across any shell-capable coding agent, including Claude Code, OpenCode, and Cur

261 lines (260 loc) • 11.5 kB
// This Source Code Form is subject to the terms of the Mozilla Public // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. /** * The child workflow executor. `driveChildWorkflowUnit` is the one place a * `child-workflow`-targeted unit is published (idempotently) and driven, * reached from `native-executor.ts`'s dispatch seam: (1) validate the resolved * `with:` bindings against the child's `params:`; (2) derive the deterministic * invocation key; (3) publish the child run idempotently; (4) drive it with * the same engine as a top-level run unless it is already `blocked`/`failed`; * (5) map the child's final status onto this unit's outcome. * * `runWorkflowSteps` is reached through a lazy dynamic import: a static one * would close the cycle native-executor -> child-workflow -> run-workflow -> * native-executor, and most runs compose no child at all. */ import { randomUUID } from "node:crypto"; import { TransientError } from "../../core/errors.js"; import { withWorkflowRunsRepo } from "../../storage/repositories/workflow-runs-repository.js"; import { validateWorkflowParams } from "../ir/params.js"; import { canonicalPlanJson } from "../ir/plan-hash.js"; import { workflowRunExportedResult } from "../runtime/run-outputs.js"; import { frozenStepRows } from "../runtime/run-plan.js"; import { computeChildInvocationKey } from "./child-invocation.js"; /** The real engine, via a dynamic import (see the module doc). The caller re-reads the child row afterward. */ async function driveWithRealEngine(options) { const { runWorkflowSteps } = await import("./run-workflow.js"); await runWorkflowSteps(options); } function errorMessage(err) { return err instanceof Error ? err.message : String(err); } /** Another live process holds the child run's lock file (run-workflow.ts). */ function isLeaseBusyError(err) { return err instanceof TransientError && err.code === "RUN_LEASE_HELD"; } /** The `child_workflow_failed` message. */ function childWorkflowFailedMessage(input) { return (`Child workflow run ${input.childRunId} (${input.childRef}) failed at step "${input.childStepId}". ` + `Inspect it with \`akm workflow status ${input.childRunId}\`; the parent run's step ` + `"${input.parentStepId}" cannot advance until it succeeds.`); } /** Param validation and the deterministic invocation key, or a `child_workflow_publish_failed` outcome. */ function precheckAndDeriveInvocationKey(input) { const { request, target, ctx, childParams, inputHash } = input; const paramErrors = validateWorkflowParams(target.frozenPlan, childParams); if (paramErrors.length > 0) { return { ok: false, outcome: { unitId: request.unitId, ok: false, failureReason: "child_workflow_publish_failed", error: `Workflow step "${request.stepId}" composes child workflow ${target.ref}, but the resolved params do not ` + `satisfy its declared param schemas:\n${paramErrors.map((e) => ` - ${e}`).join("\n")}`, }, }; } return { ok: true, invocationKey: computeChildInvocationKey({ parentRunId: ctx.runId, parentUnitId: request.unitId, unitInputHash: inputHash, }), }; } /** * Publish the child run idempotently and return the pre-drive row. Runs with * no transaction open on this connection (reached from the dispatch seam). */ async function publishChildRun(input, invocationKey) { const { request, target, ctx, childParams } = input; try { const parentRow = await withWorkflowRunsRepo((repo) => repo.getRunById(ctx.runId)); if (!parentRow) { throw new Error(`parent run ${ctx.runId} was not found`); } const now = new Date().toISOString(); const childRunId = randomUUID(); const childRow = await withWorkflowRunsRepo((repo) => repo.publishChildWorkflowRun({ parentRunId: ctx.runId, spawnedByUnitId: request.unitId, invocationKey, run: { id: childRunId, workflowRef: target.ref, scopeKey: parentRow.scope_key, workflowEntryId: null, workflowTitle: target.frozenPlan.title, paramsJson: JSON.stringify(childParams), currentStepId: target.frozenPlan.steps[0]?.stepId ?? null, createdAt: now, updatedAt: now, agentHarness: parentRow.agent_harness, agentSessionId: parentRow.agent_session_id, }, steps: frozenStepRows(target.frozenPlan).map((row) => ({ ...row, runId: childRunId })), planJson: canonicalPlanJson(target.frozenPlan), planHash: target.planHash, })); return { ok: true, childRow }; } catch (err) { return { ok: false, outcome: { unitId: request.unitId, ok: false, failureReason: "child_workflow_publish_failed", error: `Workflow step "${request.stepId}" could not publish child workflow run for ${target.ref}: ${errorMessage(err)}`, }, }; } } /** * Drive the published child run with the real engine unless it is already * `blocked`/`failed` (never re-driven). Returns the final re-read row, or a * `child_workflow_busy` / `child_workflow_drive_failed` outcome. */ async function driveChildRun(input, childRow) { const { request, target, ctx } = input; const shouldDrive = childRow.status !== "blocked" && childRow.status !== "failed"; if (!shouldDrive) { return { ok: true, finalRow: childRow }; } const driveOptions = { target: childRow.id, ...(ctx.signal ? { signal: ctx.signal } : {}), ...(ctx.dispatcher ? { dispatcher: ctx.dispatcher } : {}), ...(ctx.maxConcurrency !== undefined ? { maxConcurrency: ctx.maxConcurrency } : {}), ...(ctx.eventSource !== undefined ? { eventSource: ctx.eventSource } : {}), // The parent's own `finally` owns the process-lifecycle drain; no maxSteps/maxRetries. disposeDispatchResources: () => { }, }; try { // The re-read stays inside this try: an escaped throw would skip the // parent attempt's finish and leave its row `running` with a false // "not dispatched" diagnostic. await driveWithRealEngine(driveOptions); const finalRow = (await withWorkflowRunsRepo((repo) => repo.getRunById(childRow.id))) ?? childRow; return { ok: true, finalRow }; } catch (err) { if (isLeaseBusyError(err)) { return { ok: false, outcome: { unitId: request.unitId, ok: false, failureReason: "child_workflow_busy", error: errorMessage(err), childRun: { runId: childRow.id, ref: childRow.workflow_ref, status: childRow.status, currentStepId: childRow.current_step_id, }, }, }; } // Every other throw (a repository error mid-drive, a status race) maps // to child_workflow_drive_failed — never rethrown into the scheduler. return { ok: false, outcome: { unitId: request.unitId, ok: false, failureReason: "child_workflow_drive_failed", error: `Workflow step "${request.stepId}" composes child workflow run ${childRow.id} (${target.ref}), ` + `but driving it failed: ${errorMessage(err)}`, childRun: { runId: childRow.id, ref: childRow.workflow_ref, status: childRow.status, currentStepId: childRow.current_step_id, }, }, }; } } /** The one child drive. Every failure before the drive produces `child_workflow_publish_failed`. */ export async function driveChildWorkflowUnit(input) { const { request, ctx } = input; const precheck = precheckAndDeriveInvocationKey(input); if (!precheck.ok) { return precheck.outcome; } const published = await publishChildRun(input, precheck.invocationKey); if (!published.ok) { return published.outcome; } const { childRow } = published; const driven = await driveChildRun(input, childRow); if (!driven.ok) { return driven.outcome; } const { finalRow } = driven; const childRunSummary = { runId: finalRow.id, ref: finalRow.workflow_ref, status: finalRow.status, currentStepId: finalRow.current_step_id, }; // Checked against the re-read status, so an already-terminal child is never misreported as aborted. if (finalRow.status === "active" && ctx.signal?.aborted) { return { unitId: request.unitId, ok: false, failureReason: "aborted", error: `Child workflow run ${finalRow.id} (${finalRow.workflow_ref}) was not driven to completion: ` + "the parent workflow invocation was interrupted.", childRun: childRunSummary, }; } switch (finalRow.status) { case "completed": return { unitId: request.unitId, ok: true, result: workflowRunExportedResult(finalRow), childRun: childRunSummary, }; case "failed": { const childStepId = finalRow.current_step_id ?? "(unknown)"; return { unitId: request.unitId, ok: false, failureReason: "child_workflow_failed", error: childWorkflowFailedMessage({ childRunId: finalRow.id, childRef: finalRow.workflow_ref, childStepId, parentStepId: request.stepId, }), childRun: childRunSummary, }; } case "blocked": return { unitId: request.unitId, ok: false, failureReason: "child_workflow_blocked", error: `Child workflow run ${finalRow.id} (${finalRow.workflow_ref}) is blocked at its own step ` + `"${finalRow.current_step_id ?? "(unknown)"}". Inspect it with \`akm workflow status ${finalRow.id}\`.`, childRun: childRunSummary, }; default: // A driven child left non-terminal (its own gate loop exhausted) fails // the unit, so the parent never advances on an unresolved child. return { unitId: request.unitId, ok: false, failureReason: "child_workflow_failed", error: `Child workflow run ${finalRow.id} (${finalRow.workflow_ref}) did not reach a terminal state ` + `(status: ${finalRow.status}). Inspect it with \`akm workflow status ${finalRow.id}\`.`, childRun: childRunSummary, }; } }