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

132 lines (131 loc) • 7.05 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/. /** * Pure freeze-time binding of a composing step's inputs. * {@link freezeTaskInputBindings} classifies an authored `with:` against a * contract; {@link rebindTaskInputBindings} re-binds already-classified * bindings by name against another contract (a task's effective inputs * against a child workflow's `params:`) without re-deriving their kind. A * reference's resolved value is validated before each attempt (`exec/step-work.ts`). * See docs/architecture/decisions/0008-task-binding-normalization.md. */ import { isRecord } from "../../core/common.js"; import { UsageError } from "../../core/errors.js"; import { validateInputs } from "../../execution/input-contract.js"; import { parseReference } from "../program/expressions.js"; function inputBindingInvalid(message) { return new UsageError(message, "INPUT_BINDING_INVALID"); } function unknownBindingNameError(stepId, targetRef, name, declaredNames) { return inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name}, which is not a declared input. ` + `Declared inputs: ${declaredNames.length > 0 ? declaredNames.join(", ") : "(none)"}.`); } /** * Normalize an authored `with:` into the sorted `TaskInputBinding[]` a frozen * target carries: one entry per declared input with an effective value * (literal, reference, or default). Throws `INPUT_BINDING_INVALID` on the first violation. */ export function freezeTaskInputBindings(input) { const { stepId, targetRef, contract, earlierStepIds, declaredParamNames } = input; const authored = input.with ?? {}; const declaredNames = Object.keys(contract).sort(); const byName = new Map(); for (const [name, value] of Object.entries(authored)) { if (!Object.hasOwn(contract, name)) throw unknownBindingNameError(stepId, targetRef, name, declaredNames); const declaration = contract[name]; if (!declaration) throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} is invalid.`); byName.set(name, normalizeOneEntry(stepId, targetRef, name, value, declaration.schema, earlierStepIds, declaredParamNames)); } return finalizeBindings(stepId, targetRef, contract, byName); } /** * Re-bind already-normalized bindings by name against a different contract, * keeping each entry's kind (a literal shaped like `{from}` stays a literal). * A literal is validated against the new contract; a reference keeps its * `from` and takes the new contract's schema; undeclared names are * `INPUT_BINDING_INVALID`; missing keys are defaulted or required as usual. */ export function rebindTaskInputBindings(input) { const { stepId, targetRef, contract } = input; const declaredNames = Object.keys(contract).sort(); const byName = new Map(); for (const binding of input.bindings ?? []) { if (!Object.hasOwn(contract, binding.name)) { throw unknownBindingNameError(stepId, targetRef, binding.name, declaredNames); } const declaration = contract[binding.name]; if (!declaration) throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${binding.name} is invalid.`); byName.set(binding.name, binding.kind === "literal" ? binding : Object.freeze({ kind: "reference", name: binding.name, from: binding.from, schema: declaration.schema })); } return finalizeBindings(stepId, targetRef, contract, byName); } /** Shared tail: apply defaults (or require), schema-validate every literal, and sort by name. */ function finalizeBindings(stepId, targetRef, contract, byName) { for (const [name, declaration] of Object.entries(contract)) { if (byName.has(name)) continue; if (Object.hasOwn(declaration, "default")) { byName.set(name, Object.freeze({ kind: "literal", name, value: declaration.default })); continue; } if (declaration.required) { throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef}, which declares required input "${name}" with no default; ` + `supply it with with.${name}.`); } } // Validate literals only: a reference's value is not known until the attempt. const literalContract = {}; const literalValues = {}; for (const binding of byName.values()) { if (binding.kind !== "literal") continue; const declaration = contract[binding.name]; if (declaration) literalContract[binding.name] = declaration; literalValues[binding.name] = binding.value; } const schemaErrors = validateInputs(literalContract, literalValues, { pathRoot: "with" }); if (schemaErrors.length > 0) { throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef}: ${schemaErrors.join("; ")}`); } const sorted = [...byName.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0)); return Object.freeze(sorted); } /** * Classify one authored `with:` entry: exactly `{ from: <valid reference> }` * is a reference; any other object with an own `from` key is * `INPUT_BINDING_INVALID`, never a literal. */ function normalizeOneEntry(stepId, targetRef, name, value, schema, earlierStepIds, declaredParamNames) { if (!isRecord(value) || !Object.hasOwn(value, "from")) { return Object.freeze({ kind: "literal", name, value }); } const keys = Object.keys(value); if (keys.length !== 1 || typeof value.from !== "string") { throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} looks like a reference binding ({from: ...}) ` + `but is not one: it must have exactly one key, "from", whose value is a reference string.`); } const parsed = parseReference(value.from); if (!parsed.ok) { throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} reference ${JSON.stringify(value.from)} is ` + `invalid: ${parsed.message}`); } if (parsed.expr.kind === "stepOutput") { if (!earlierStepIds.has(parsed.expr.stepId)) { throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} reference ${value.from} does not name an ` + `earlier step of this workflow.`); } } else if (!declaredParamNames.has(parsed.expr.name)) { const sortedParams = [...declaredParamNames].sort(); throw inputBindingInvalid(`Workflow step ${stepId} targets ${targetRef} with.${name} reference ${value.from} does not name a ` + `declared workflow param; declared params: ${sortedParams.length > 0 ? sortedParams.join(", ") : "(none)"}.`); } return Object.freeze({ kind: "reference", name, from: value.from, schema }); }