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

317 lines (316 loc) • 14.3 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/. /** * Reading a run row's frozen plan: the ONE plan decoder and its tolerant * row-level wrapper. * * {@link decodeWorkflowPlan} accepts every plan shape a release has stored — * irVersion 4 and 5 (with their `sourceReadSet` and host-identity fields) as * well as the current one — and keeps keys it does not know rather than * refusing them. It rejects only a plan it cannot run: no steps, a unit with * no frozen target, a request that does not decode. `plan_ir_version` and * `plan_hash` are informational columns: neither gates execution. A plan that * cannot be decoded comes back from {@link readRunPlan} as a `problem` * sentence and the caller decides the remedy (the engine abandons such a run * instead of throwing) — except for a plan a NEWER akm froze, which is `newer` * and whose problem names upgrading akm as the remedy. */ import { UsageError } from "../../core/errors.js"; import { warnOnce } from "../../core/warn.js"; import { decodeResolvedExecutionRequest } from "../../execution/resolved-request.js"; import { decodeFrozenRunnerSpec } from "../../integrations/agent/runner.js"; import { WORKFLOW_PLAN_VERSION, } from "../plan.js"; export function readRunPlan(row) { if (!row.plan_json) return { ok: false, newer: false, problem: `Workflow run ${row.id} has no frozen workflow plan.` }; const version = row.plan_ir_version ?? null; try { const plan = decodeWorkflowPlan(JSON.parse(row.plan_json), { ...(row.workflow_ref ? { workflowRef: row.workflow_ref } : {}), }); if (version !== null && version !== WORKFLOW_PLAN_VERSION) { warnOnce(`workflow-plan-version:${row.id}`, `Workflow run ${row.id} was frozen as plan irVersion ${version}; running it with the current engine ` + `(irVersion ${WORKFLOW_PLAN_VERSION}).`); } return { ok: true, plan }; } catch (cause) { const detail = cause instanceof Error ? cause.message : String(cause); if (version !== null && version > WORKFLOW_PLAN_VERSION) { return { ok: false, newer: true, problem: `Workflow run ${row.id} was frozen by a newer akm (plan irVersion ${version}) and this akm cannot decode it: ` + `${detail}. Upgrade akm to continue this run.`, }; } const frozenAs = version === null ? "" : ` (frozen as plan irVersion ${version})`; return { ok: false, newer: false, problem: `Workflow run ${row.id} has a frozen plan this akm cannot decode${frozenAs}: ${detail}.`, }; } } /** Project persisted spine rows from the plan at publication time. */ export function frozenStepRows(plan) { return plan.steps.map((step) => ({ stepId: step.stepId, stepTitle: step.title, instructions: step.root ? step.root.kind === "map" ? step.root.template.instructions : step.root.instructions : routeInstructions(step.route), completionJson: step.gate.criteria.length > 0 ? JSON.stringify(step.gate.criteria) : null, sequenceIndex: step.sequenceIndex, })); } function routeInstructions(route) { const branches = Object.entries(route.when) .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) .map(([match, stepId]) => `"${match}" -> ${stepId}`); if (route.defaultStepId !== undefined) branches.push(`default -> ${route.defaultStepId}`); return `Route on ${route.input}: ${branches.join(", ")}.`; } /** Decode a stored plan (any release's) into the one plan type. */ export function decodeWorkflowPlan(input, options = {}) { const raw = record(input, "plan"); if (!Array.isArray(raw.steps) || raw.steps.length === 0) fail("steps must be a non-empty array"); const steps = raw.steps.map((value, index) => decodeStep(value, index)); if (raw.outputs !== undefined) { for (const [name, entry] of Object.entries(record(raw.outputs, "outputs"))) { string(record(entry, `outputs.${name}`).from, `outputs.${name}.from`); } } const sourceHash = typeof raw.sourceHash === "string" ? raw.sourceHash : legacySourceHash(raw.sourceReadSet, options.workflowRef); return { ...raw, irVersion: typeof raw.irVersion === "number" ? raw.irVersion : WORKFLOW_PLAN_VERSION, title: typeof raw.title === "string" ? raw.title : "", ...(sourceHash ? { sourceHash } : {}), steps, }; } /** Plans frozen before irVersion 6 recorded the source hash inside `sourceReadSet`. */ function legacySourceHash(readSet, workflowRef) { if (!workflowRef || !Array.isArray(readSet)) return undefined; for (const entry of readSet) { const identity = entry?.identity; if (identity?.ref === workflowRef && typeof identity.hash === "string") return identity.hash; } return undefined; } function decodeStep(value, index) { const step = record(value, `step ${index}`); const stepId = string(step.stepId, `step ${index} stepId`); const route = step.route === undefined ? undefined : decodeRoute(step.route, stepId); const root = step.root === undefined ? undefined : decodeNode(step.root, stepId); if (!root && !route) fail(`step ${stepId} has neither a frozen root nor a route`); return { ...step, stepId, title: typeof step.title === "string" ? step.title : stepId, sequenceIndex: index, ...(root ? { root } : {}), ...(route ? { route } : {}), gate: decodeGate(step.gate, stepId), }; } function decodeRoute(value, stepId) { const route = record(value, `step ${stepId} route`); string(route.input, `step ${stepId} route.input`); const when = record(route.when, `step ${stepId} route.when`); for (const target of Object.values(when)) string(target, `step ${stepId} route target`); return route; } function decodeNode(value, stepId) { const node = record(value, `step ${stepId} root`); if (node.kind !== "map") return decodeUnit(node, stepId); string(node.over, `step ${stepId} map.over`); return { ...node, concurrency: positiveInteger(node.concurrency) ?? 1, reducer: node.reducer === "vote" ? "vote" : "collect", template: decodeUnit(record(node.template, `step ${stepId} map template`), stepId), }; } function decodeUnit(node, stepId) { const id = string(node.id, `step ${stepId} unit id`); const label = `unit ${id}`; if (node.frozenTarget === undefined) fail(`${label} has no frozen target`); return { ...node, kind: "unit", id, instructions: typeof node.instructions === "string" ? node.instructions : "", onError: node.onError === "continue" ? "continue" : "fail", isolation: node.isolation === "worktree" ? "worktree" : "none", frozenTarget: decodeTarget(node.frozenTarget, label), environment: Array.isArray(node.environment) ? node.environment.map((binding, index) => decodeEnvironmentBinding(binding, `${label} environment[${index}]`)) : [], }; } function decodeGate(value, stepId) { const gate = value === undefined ? {} : record(value, `step ${stepId} gate`); const criteria = Array.isArray(gate.criteria) ? gate.criteria.filter((item) => typeof item === "string" && item.length > 0) : []; return { kind: "gate", id: typeof gate.id === "string" ? gate.id : `${stepId}.gate`, stepId, criteria, maxLoops: positiveInteger(gate.maxLoops) ?? 1, frozenJudge: criteria.length > 0 && gate.frozenJudge !== null && gate.frozenJudge !== undefined ? decodeCommandTarget(record(gate.frozenJudge, `gate ${stepId} judge`), `gate ${stepId} judge`) : null, }; } function decodeTarget(value, label) { const target = record(value, `${label} frozenTarget`); const inputBindings = decodeInputBindings(target.inputBindings, label); const gitCommitOid = typeof target.gitCommitOid === "string" ? target.gitCommitOid : undefined; const optional = { ...(gitCommitOid ? { gitCommitOid } : {}), ...(inputBindings ? { inputBindings } : {}), }; switch (target.kind) { case "command": return decodeCommandTarget(target, label); case "shell": return { kind: "shell", contentHash: string(target.contentHash, `${label} contentHash`), exec: decodeExec(target.exec, label), cwdIdentity: record(target.cwdIdentity, `${label} cwdIdentity`), ...optional, }; case "script": return { kind: "script", ref: string(target.ref, `${label} script ref`), contentHash: string(target.contentHash, `${label} contentHash`), exec: decodeExec(target.exec, label), interpreter: string(target.interpreter, `${label} interpreter`), extension: string(target.extension, `${label} extension`), bytesBase64: typeof target.bytesBase64 === "string" ? target.bytesBase64 : fail(`${label} has no script bytes`), byteLength: typeof target.byteLength === "number" ? target.byteLength : 0, cwdIdentity: record(target.cwdIdentity, `${label} cwdIdentity`), materialization: "ephemeral-0700-delete", ...optional, }; case "child-workflow": return { kind: "child-workflow", ref: string(target.ref, `${label} child ref`), planHash: string(target.planHash, `${label} planHash`), frozenPlan: decodeWorkflowPlan(target.frozenPlan), contentHash: string(target.contentHash, `${label} contentHash`), via: target.via === "task" ? "task" : "direct", ...(typeof target.taskRef === "string" ? { taskRef: target.taskRef } : {}), ...(inputBindings ? { inputBindings } : {}), }; default: return fail(`${label} has an unsupported frozen target kind ${JSON.stringify(target.kind)}`); } } function decodeCommandTarget(target, label) { let request; let runner; try { request = decodeResolvedExecutionRequest(target.request); runner = decodeFrozenRunnerSpec(target.runner); } catch (cause) { fail(`${label} request: ${cause instanceof Error ? cause.message : String(cause)}`); } const inputBindings = decodeInputBindings(target.inputBindings, label); const concurrency = positiveInteger(target.concurrency); return { kind: "command", ref: typeof target.ref === "string" ? target.ref : null, contentHash: typeof target.contentHash === "string" ? target.contentHash : "", request, runner, ...(concurrency !== undefined ? { concurrency } : {}), ...(target.cwdIdentity && typeof target.cwdIdentity === "object" ? { cwdIdentity: target.cwdIdentity } : {}), ...(typeof target.gitCommitOid === "string" ? { gitCommitOid: target.gitCommitOid } : {}), ...(inputBindings ? { inputBindings } : {}), }; } function decodeExec(value, label) { const exec = record(value, `${label} exec`); const command = exec.command; if (!Array.isArray(command) || command.length === 0 || !command.every((arg) => typeof arg === "string")) { fail(`${label} exec.command must be a non-empty argv`); } const timeoutMs = exec.timeoutMs; if (timeoutMs !== null && typeof timeoutMs !== "number") fail(`${label} exec.timeoutMs must be a number or null`); return exec; } function decodeInputBindings(value, label) { if (!Array.isArray(value) || value.length === 0) return undefined; return value.map((raw, index) => { const binding = record(raw, `${label} inputBindings[${index}]`); const name = string(binding.name, `${label} inputBindings[${index}].name`); if (binding.kind === "reference") { return { kind: "reference", name, from: string(binding.from, `${label} inputBindings[${index}].from`), schema: (binding.schema ?? {}), }; } return { kind: "literal", name, value: binding.value }; }); } function decodeEnvironmentBinding(value, label) { const binding = record(value, label); if (binding.kind === "literal") { return { kind: "literal", name: string(binding.name, label), value: String(binding.value ?? "") }; } if (binding.kind === "pass-through") return { kind: "pass-through", name: string(binding.name, label) }; if (binding.kind !== "env-ref") fail(`${label} has an unsupported kind`); const strings = (items) => Array.isArray(items) ? items.filter((item) => typeof item === "string") : []; return { kind: "env-ref", ref: string(binding.ref, `${label} ref`), owner: record(binding.owner, `${label} owner`), keys: strings(binding.keys), secretNames: strings(binding.secretNames), precedence: typeof binding.precedence === "number" ? binding.precedence : 0, }; } function positiveInteger(value) { return Number.isSafeInteger(value) && value >= 1 ? value : undefined; } function string(value, label) { if (typeof value !== "string" || value.length === 0) fail(`${label} must be a non-empty string`); return value; } function record(value, label) { if (!value || typeof value !== "object" || Array.isArray(value)) fail(`${label} must be an object`); return value; } function fail(message) { throw new UsageError(`Invalid frozen workflow plan: ${message}.`, "INVALID_JSON_ARGUMENT"); }