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

124 lines (123 loc) • 6.41 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 one child-workflow resolver. Both composition forms lower here: a direct * `uses: workflows/<ref>` step (`via: "direct"`) and a `uses: tasks/<ref>` * step whose task targets a workflow (`via: "task"`). It resolves the child, * refuses a composition cycle, freezes the child completely through the * injected `ResolutionContext.freezeChild`, binds the step's inputs against * the child's `params:`, and embeds the frozen child plan in the target. */ import { createHash } from "node:crypto"; import { parseBundleRef } from "../../../core/asset/asset-ref.js"; import { UsageError } from "../../../core/errors.js"; import { warn } from "../../../core/warn.js"; import { workflowParamContract } from "../../ir/params.js"; import { canonicalJson, canonicalPlanJson } from "../../ir/plan-hash.js"; import { loadWorkflowAsset } from "../../runtime/workflow-asset-loader.js"; import { resolveOwnedAsset } from "../environment.js"; import { declaredParamNames, earlierStepIds, } from "../step-values.js"; import { freezeTaskInputBindings, rebindTaskInputBindings } from "../task-bindings.js"; /** The child target's own content identity: its ref, embedded plan hash, route, and bindings. */ function childWorkflowContentHash(fields) { return createHash("sha256") .update("akm.workflow.child-workflow\0v1\0") .update(canonicalJson({ ref: fields.ref, planHash: fields.planHash, via: fields.via, taskRef: fields.taskRef ?? null, inputBindings: fields.inputBindings.length > 0 ? fields.inputBindings : null, })) .digest("hex"); } /** A composing step's own `env:` (literal or `unit.env`) cannot reach the child run, so say so. */ function warnIfStepEnvironment(stepId, childRef, source) { const hasLiteralEnv = Object.keys(source.env ?? {}).length > 0; const hasEnvRefs = (source.unit?.env ?? []).length > 0; if (!hasLiteralEnv && !hasEnvRefs) return; warn(`Workflow step ${stepId} declares env: while composing ${childRef}: a child run carries its own frozen ` + `environment inside its own plan, so this composing step's env: (or unit: env:) is not delivered into it ` + `and has no effect. Set those variables inside ${childRef}'s own source instead.`); } /** A workflow entry of a composition `refPath` — the only entries a cycle can close through. */ function isWorkflowRef(ref) { try { return parseBundleRef(ref).conceptId.startsWith("workflows/"); } catch { return false; } } function compositionPath(refPath, childRef) { return [...refPath, childRef].join(" -> "); } function assertNoCompositionCycle(stepId, childRef, refPath) { if (!refPath.filter(isWorkflowRef).includes(childRef)) return; throw new UsageError(`Workflow step ${stepId} cannot compose ${childRef}: that would create a composition cycle. ` + `Path: ${compositionPath(refPath, childRef)}.`, "COMPOSITION_INVALID", "Break the cycle: remove or redirect one of the compositions in the path above so no workflow ends up " + "composing itself, directly or through intermediates."); } export async function childWorkflowDispatch(input) { const { source, baseUnit, childRefInput, context, via, taskRef, authoredInputs } = input; // resolve + qualify. Resolution failures propagate unchanged, // in code and shape — the same authority every other // composition target (command/script/task) already resolves through. const owned = await resolveOwnedAsset(childRefInput, "workflow", context); const childAsset = await loadWorkflowAsset(owned.ref); const childRef = childAsset.ref; // Issue 10: an authored env: on the composing step has no path to reach // the child run — warn rather than vanish silently (see // warnIfStepEnvironment's doc comment). warnIfStepEnvironment(source.id, childRef, source); // the composition cycle check, before any child compilation. assertNoCompositionCycle(source.id, childRef, context.refPath); // Freeze the child completely; its plan is a pure function of its own source. const childRefPath = via === "task" && taskRef !== undefined ? [...context.refPath, taskRef, childRef] : [...context.refPath, childRef]; const child = await context.freezeChild(childAsset, childRefPath); // Embed the child plan as plain canonical JSON, exactly as `plan_json` stores it. const embeddedPlanJson = canonicalPlanJson(child.plan); const frozenPlan = JSON.parse(embeddedPlanJson); // Bind the step's effective inputs against the child's declared params. const inputBindings = authoredInputs.kind === "bindings" ? rebindTaskInputBindings({ stepId: source.id, targetRef: childRef, bindings: authoredInputs.value, contract: workflowParamContract(frozenPlan), }) : freezeTaskInputBindings({ stepId: source.id, targetRef: childRef, with: authoredInputs.value, contract: workflowParamContract(frozenPlan), earlierStepIds: earlierStepIds(context.plan, source.id), declaredParamNames: declaredParamNames(context.plan), }); // build the frozen target. const planHash = createHash("sha256").update(embeddedPlanJson).digest("hex"); const contentHash = childWorkflowContentHash({ ref: childRef, planHash, via, taskRef, inputBindings }); const target = Object.freeze({ kind: "child-workflow", ref: childRef, planHash, frozenPlan, contentHash, via, ...(taskRef !== undefined ? { taskRef } : {}), ...(inputBindings.length > 0 ? { inputBindings } : {}), }); return { target, // A child run carries its own frozen environment inside its own plan. // A composing step's own env: cannot reach it, so assertNoStepEnvironment // above rejects one instead of it silently vanishing here. environment: [], unit: baseUnit, instructions: source.instructions ?? (via === "task" && taskRef !== undefined ? `Run task ${taskRef}.` : `Run workflow ${childRef}.`), }; }