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

84 lines (83 loc) • 3.78 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/. /** * Freeze-time check of references into a child-workflow step's output: the * first path segment after `steps.<child>.output` must name one of the child's * declared `outputs:` (or `runId`/`status` when it declares none). Deeper * segments resolve at run time. */ import { UsageError } from "../../core/errors.js"; import { formatReference, parseReference } from "../program/expressions.js"; /** The frozen target a step's root unit (or map template) dispatches, if any. */ function stepFrozenTarget(step) { const root = step?.root; if (!root) return undefined; const unit = root.kind === "map" ? root.template : root; return unit.frozenTarget; } /** Every reference string a step's `inputs[]`, `map.over`, `route.input`, and reference-kind `inputBindings[].from` carry. */ function collectReferenceSites(step) { const sites = []; const push = (reference) => { sites.push({ stepId: step.stepId, reference }); }; if (step.route) push(step.route.input); const root = step.root; if (root) { const unit = root.kind === "map" ? root.template : root; if (root.kind === "map") push(root.over); for (const reference of unit.inputs ?? []) push(reference); const bindings = unit.frozenTarget.inputBindings ?? []; for (const binding of bindings) { if (binding.kind === "reference") push(binding.from); } } return sites; } /** The names a `child-workflow` target's own exported result carries at its first segment. */ function acceptedFirstSegments(target) { const declared = target.frozenPlan.outputs; return declared ? Object.keys(declared) : ["runId", "status"]; } function exportsDescription(target) { const declared = target.frozenPlan.outputs; return declared ? `outputs: ${Object.keys(declared).join(", ")}` : "only {runId, status} — it declares no `outputs:`"; } function checkSite(site, stepsById) { const parsed = parseReference(site.reference); if (!parsed.ok || parsed.expr.kind !== "stepOutput") return; const targetStep = stepsById.get(parsed.expr.stepId); const frozenTarget = stepFrozenTarget(targetStep); if (!frozenTarget || frozenTarget.kind !== "child-workflow") return; const first = parsed.expr.path[0]; if (first === undefined) return; // bare steps.<child>.output — the whole exported object, always accepted. const accepted = acceptedFirstSegments(frozenTarget); if (typeof first === "string" && accepted.includes(first)) return; throw new UsageError(`Workflow step ${site.stepId} reads "${formatReference(parsed.expr)}", but child workflow ${frozenTarget.ref} ` + `exports ${exportsDescription(frozenTarget)}. Declare the output in the child's \`outputs:\` frontmatter, or ` + `reference one of the names above.`, "COMPOSITION_INVALID", `Fix the reference to name one of: ${accepted.join(", ")} — or add the missing name to ${frozenTarget.ref}'s ` + "own `outputs:` frontmatter."); } /** * Assert every reference into a `child-workflow`-targeted step's output * names an output the child actually exports. Throws `UsageError` * (`COMPOSITION_INVALID`) on the first violation found. */ export function assertChildOutputReferences(steps) { const stepsById = new Map(steps.map((step) => [step.stepId, step])); for (const step of steps) { for (const site of collectReferenceSites(step)) { checkSite(site, stepsById); } } }