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
JavaScript
// 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 });
}