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

173 lines (172 loc) • 8.15 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 a compiled workflow plan into the plan a run executes. Every step's * authored spec resolves through the shared command/task authorities into a * frozen `root` (engine, model, timeout, concurrency, dispatch target and * environment descriptors); gates get their judge; the compile-only fields are * dropped; the source file's sha256 is recorded. Nothing is published here. */ import { createHash } from "node:crypto"; import { ConfigError, UsageError } from "../../core/errors.js"; import { warn } from "../../core/warn.js"; import { NO_ENGINE_MESSAGE_SUFFIX } from "../../integrations/agent/engine-fallback.js"; import { resolveExecution } from "../../integrations/agent/execution.js"; import { checkWorkflowPlan } from "../compile.js"; import { defaultMapConcurrency, workflowMaxConcurrency } from "../concurrency-policy.js"; import { canonicalJson } from "../ir/plan-hash.js"; import { WORKFLOW_PLAN_VERSION, } from "../plan.js"; import { classifyWorkflowStepUses } from "../source-semantics.js"; import { assertChildOutputReferences } from "./child-output-references.js"; import { qualifyRef } from "./environment.js"; import { baseUnitOf } from "./step-values.js"; import { childWorkflowDispatch } from "./targets/child-workflow.js"; import { commandDispatch, commandResult } from "./targets/command.js"; import { directScript } from "./targets/script.js"; import { directShell } from "./targets/shell.js"; import { taskDispatch } from "./targets/task.js"; /** Resolve and freeze one workflow (and, recursively, every child it composes). */ export async function freezeWorkflow(asset, config, refPath = [asset.ref]) { const compiled = asset.plan; const checked = checkWorkflowPlan(compiled); if (!checked.ok) { throw new UsageError(checked.errors.map((error) => `${asset.path}:${error.line}: ${error.message}`).join("\n"), "WORKFLOW_SOURCE_INVALID"); } const context = { asset, config, plan: compiled, refPath, freezeChild: (child, childRefPath) => freezeWorkflow(child, config, childRefPath), }; let engineAnnouncement; const steps = []; for (const step of compiled.steps) { const { spec, ...frozenStep } = step; const source = { ...(spec ?? { source: { path: asset.path, start: 1, end: 1 } }), id: step.stepId }; let root; if (!step.route) { const resolved = await resolveStep(source, context); engineAnnouncement ??= resolved.engineAnnouncement; root = frozenRoot(step.stepId, source, resolved, compiled, config); } let frozenJudge = null; if (step.gate.criteria.length > 0) { const judge = resolveJudge(source, step.gate.criteria[0] ?? "", context); if (judge) { frozenJudge = judge.target; engineAnnouncement ??= judge.engineAnnouncement; } else { warn(`Workflow step "${step.stepId}" has completion criteria but no verification engine is available ` + "(set workflow.judgeEngine, defaults.engine, or install an opencode-sdk binary). The step will block " + "for `akm workflow resume` once it is reached."); } } steps.push({ ...frozenStep, ...(root ? { root } : {}), gate: { ...step.gate, frozenJudge } }); } assertChildOutputReferences(steps); const plan = { irVersion: WORKFLOW_PLAN_VERSION, title: asset.title, ...(compiled.params ? { params: compiled.params } : {}), ...(compiled.paramSchemas ? { paramSchemas: compiled.paramSchemas } : {}), ...(compiled.budget ? { budget: compiled.budget } : {}), ...(compiled.outputs ? { outputs: compiled.outputs } : {}), execution: { maxConcurrency: workflowMaxConcurrency(config.workflow?.maxConcurrency) }, sourceHash: asset.sourceHash, steps, }; return { plan, warnings: checked.warnings, ...(engineAnnouncement ? { engineAnnouncement } : {}) }; } async function resolveStep(source, context) { const baseUnit = baseUnitOf(source); if (source.exec) return directShell(source, baseUnit, context); if (!source.uses) throw new Error(`workflow step ${source.id} has neither exec nor uses`); const target = classifyWorkflowStepUses(source.uses); if (target.kind === "task") return taskDispatch(source, baseUnit, target.ref, context); if (target.kind === "workflow") { return childWorkflowDispatch({ source, baseUnit, childRefInput: target.ref, context, via: "direct", authoredInputs: { kind: "with", value: source.with }, }); } // `commands/<ref>` and `scripts/<ref>` are not binding surfaces; `akm/command`'s // `with:` is its own argument bag. if (target.kind !== "builtin-command" && source.with !== undefined) { const family = target.kind === "command" ? "commands" : "scripts"; throw new UsageError(`Workflow step ${source.id} cannot pass with: to ${family} target ${target.ref}; a ${target.kind} ref is not a binding surface.`, "COMPOSITION_INVALID"); } if (target.kind === "script") return directScript(source, baseUnit, target.ref, context); const action = target.kind === "builtin-command" ? source.with : { ref: qualifyRef(target.ref, "commands", context.asset, context.config) }; return commandDispatch(source, baseUnit, action, context); } /** The gate judge's frozen command target, or undefined when no engine is available and none is configured. */ function resolveJudge(source, rubric, context) { const configuredEngine = context.config.workflow?.judgeEngine; try { const prepared = resolveExecution({ content: rubric.trim() || "Judge workflow completion.", config: context.config, ...(configuredEngine ? { current: { engine: configuredEngine } } : {}), }); return commandResult(source, { onError: "fail", source: source.source }, prepared, context); } catch (err) { if (configuredEngine || !(err instanceof ConfigError && err.message.includes(NO_ENGINE_MESSAGE_SUFFIX))) throw err; return undefined; } } function frozenRoot(stepId, source, resolved, plan, config) { const unit = resolved.unit; const target = resolved.target.kind === "shell" ? { ...resolved.target, contentHash: createHash("sha256") .update("akm.workflow.shell.v1\0") .update(canonicalJson({ exec: resolved.target.exec, environment: resolved.environment, cwdIdentity: resolved.target.cwdIdentity, })) .digest("hex"), } : resolved.target; const node = { kind: "unit", id: source.map ? `${stepId}.unit` : stepId, instructions: resolved.instructions, ...(source.inputs && source.inputs.length > 0 ? { inputs: [...source.inputs] } : {}), ...(unit.output !== undefined ? { schema: unit.output } : {}), ...(unit.retry ? { retry: { max: unit.retry.max, on: [...unit.retry.on] } } : {}), onError: unit.onError ?? plan.defaults?.onError ?? "fail", ...(unit.env ? { env: [...unit.env] } : {}), isolation: unit.isolation ?? "none", source: { ...source.source }, frozenTarget: target, environment: [...resolved.environment], }; if (!source.map) return node; return { kind: "map", id: `${stepId}.map`, over: source.map.over, template: node, concurrency: source.map.concurrency ?? defaultMapConcurrency(config.workflow?.defaultMapConcurrency), reducer: source.map.reducer ?? "collect", source: { ...source.source }, }; }