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

534 lines (533 loc) • 23.8 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/. import { deepMergeConfig } from "../../core/config/deep-merge.js"; import { ConfigError } from "../../core/errors.js"; import { DURATION_UNITS, parseDuration } from "../../core/time.js"; import { EXECUTION_MAX_TIMEOUT_MS } from "../../execution/limits.js"; import { createInlineResolvedCommand, createResolvedExecutionRequest, decodeResolvedExecutionRequest, } from "../../execution/resolved-request.js"; import { cloneToolSelection, isPortableExecutionAgentSelector, MODEL_WORK_POLICY_ID, } from "../../execution/source.js"; import { getHarness } from "../harnesses/index.js"; import { DEFAULT_LLM_TIMEOUT_MS } from "./config.js"; import { FALLBACK_ENGINE_NAME, fallbackEngineConfig, NO_ENGINE_MESSAGE_SUFFIX, NO_ENGINE_REMEDY, } from "./engine-fallback.js"; import { configuredEngine, resolveEngine } from "./engine-resolution.js"; import { engineModelAndInference, loadModelMap, resolveModelMapAlias } from "./model-map.js"; import { extensionFields, hasToolSelection, untranslated } from "./request-lowering.js"; import { decodeFrozenRunnerSpec } from "./runner.js"; const LLM_INFERENCE_FIELDS = [ "temperature", "maxTokens", "supportsJsonSchema", "extraParams", "contextLength", "enableThinking", "reasoningEffort", ]; function has(values, key) { return (values !== null && values !== undefined && Object.hasOwn(values, key) && values[key] !== undefined); } function nearest(layers, key) { for (let index = layers.length - 1; index >= 0; index -= 1) { const layer = layers[index]; if (layer && has(layer.values, key)) return layer; } return undefined; } function explicit(layer, via = "explicit") { return { layer: layer.id, kind: layer.kind, via }; } function inferenceOf(source) { const out = {}; for (const key of LLM_INFERENCE_FIELDS) if (source[key] !== undefined) out[key] = source[key]; return Object.keys(out).length > 0 ? out : undefined; } /** A configured engine's model, inference, timeout and workspace, and where its aliases resolve. */ function engineDefaults(name, engine, config) { const own = engineModelAndInference(engine); const values = { ...(own.model !== undefined ? { model: own.model } : {}), ...(own.inference !== undefined ? { inference: own.inference } : {}), ...(Object.hasOwn(engine, "timeoutMs") ? { timeout: engine.timeoutMs ?? null } : {}), }; if (engine.kind === "llm") return { kind: "llm", platform: engine.provider ?? name, modelMapKey: name, values }; if (engine.workspace !== undefined) values.workspace = engine.workspace; if (engine.platform !== "opencode-sdk") { return { kind: "agent", platform: engine.platform, modelMapKey: engine.platform, values }; } // An SDK engine runs its own `llmEngine`'s model/inference/timeout unless it sets its own. With no // `llmEngine` it has no fallback, and opencode picks the model: `defaults.llmEngine` is not borrowed. const fallbackName = engine.llmEngine; const fallback = fallbackName && config.engines && Object.hasOwn(config.engines, fallbackName) ? config.engines[fallbackName] : undefined; if (fallback?.kind !== "llm" || !fallbackName) { return { kind: "sdk", platform: "opencode-sdk", modelMapKey: "opencode-sdk", values }; } const inherited = engineModelAndInference(fallback); return { kind: "sdk", platform: "opencode-sdk", modelMapKey: own.model === undefined ? fallbackName : "opencode-sdk", values: { ...(inherited.model !== undefined ? { model: inherited.model } : {}), ...(inherited.inference !== undefined ? { inference: inherited.inference } : {}), ...values, timeout: Object.hasOwn(engine, "timeoutMs") ? (engine.timeoutMs ?? null) : Object.hasOwn(fallback, "timeoutMs") ? (fallback.timeoutMs ?? null) : DEFAULT_LLM_TIMEOUT_MS, }, }; } /** The same defaults read back from an already-resolved runner. */ function runnerDefaults(runner) { const timeout = Object.hasOwn(runner, "timeoutMs") ? { timeout: runner.timeoutMs ?? null } : {}; if (runner.kind === "llm") { const inference = inferenceOf(runner.connection); return { kind: "llm", platform: runner.connection.provider ?? runner.engine, modelMapKey: runner.engine, values: { ...(runner.connection.model !== undefined ? { model: runner.connection.model } : {}), ...(inference ? { inference } : {}), ...timeout, }, }; } const platform = runner.profile.platform ?? runner.profile.name; const fallback = runner.kind === "sdk" ? runner.fallbackConnection : undefined; const model = runner.profile.model ?? fallback?.model; const fallbackInference = fallback ? inferenceOf(fallback) : undefined; const inference = fallbackInference !== undefined || runner.profile.inference !== undefined ? { ...fallbackInference, ...runner.profile.inference } : undefined; return { kind: runner.kind, platform, modelMapKey: platform, values: { ...(model !== undefined ? { model } : {}), ...(inference ? { inference } : {}), ...timeout, ...(runner.profile.workspace !== undefined ? { workspace: runner.profile.workspace } : {}), }, }; } /** * The engine, from an ordered list: the nearest layer that names one, then * `defaults.engine`, then the implicit opencode-sdk fallback when its binary * is present. A named engine that is not configured is an error, never rescued. */ function selectEngine(config, layers) { for (let index = layers.length - 1; index >= 0; index -= 1) { const layer = layers[index]; const name = layer?.values.engine; if (layer && typeof name === "string" && name.length > 0) { return { name, engine: configuredEngine(name, config), provenance: explicit(layer), fallback: false }; } } const installed = config.defaults?.engine; if (installed) { return { name: installed, engine: configuredEngine(installed, config), provenance: { layer: "installation-defaults", kind: "installation", via: "explicit" }, fallback: false, }; } const fallback = fallbackEngineConfig(config); if (fallback) { return { name: FALLBACK_ENGINE_NAME, engine: fallback, provenance: { layer: FALLBACK_ENGINE_NAME, kind: "fallback", via: "fallback" }, fallback: true, }; } throw new ConfigError(`Execution ${NO_ENGINE_MESSAGE_SUFFIX}`, "INVALID_CONFIG_FILE", NO_ENGINE_REMEDY); } function parseTimeout(value) { if (value === null) return null; const milliseconds = typeof value === "string" ? parseDuration(value, DURATION_UNITS) : value; if (typeof milliseconds !== "number" || !Number.isInteger(milliseconds) || milliseconds < 0 || milliseconds > EXECUTION_MAX_TIMEOUT_MS) { throw new ConfigError("Execution timeout must be null, a whole number of milliseconds, or a duration such as 20m.", "INVALID_CONFIG_FILE"); } return milliseconds; } /** Tool names a selection asks for; `undefined` when a structured policy cannot be reduced to names. */ function requestedToolNames(tools) { if (typeof tools === "string") { return tools .split(",") .map((tool) => tool.trim()) .filter(Boolean); } if (Array.isArray(tools)) return tools.map((tool) => tool.trim()).filter(Boolean); const policy = tools; if (Object.values(policy).some((value) => typeof value !== "boolean")) return undefined; return Object.keys(policy).filter((tool) => policy[tool] === true); } /** * Assets may only narrow the host's `execution.allowedTools`; without a config * nothing is allowed. The model-work policy is akm's own and always allowed: * it confines an engine more tightly than leaving tools unset does. */ function authorizeTools(tools, config, modelWork) { if (modelWork) { return { status: "allowed", reason: "The model-work tool policy is akm's own.", policy: { id: MODEL_WORK_POLICY_ID }, }; } if (!hasToolSelection(tools)) return { status: "not-required" }; if (!config) { return { status: "denied", reason: "Selected tools require an explicit machine/user authorization policy.", policy: { id: "unconfigured" }, }; } const allowed = new Set(config.execution?.allowedTools ?? []); const names = requestedToolNames(tools); const permitted = allowed.has("*") || names?.every((tool) => allowed.has(tool)) === true; return permitted ? { status: "allowed", reason: "Selected tools were authorized by operator policy.", policy: { id: "config-execution-allowed-tools" }, } : { status: "denied", reason: "Selected tools are not authorized by operator policy.", policy: { id: "config-execution-allowed-tools" }, }; } function withInference(connection, inference) { const out = { ...connection }; for (const key of LLM_INFERENCE_FIELDS) delete out[key]; if (inference) for (const key of LLM_INFERENCE_FIELDS) if (inference[key] !== undefined) out[key] = inference[key]; return out; } /** Apply a request's model, inference, timeout and workspace to its runner. Idempotent. */ function applyRequest(base, request) { const timeout = Object.hasOwn(request.runtime, "timeoutMs") ? { timeoutMs: request.runtime.timeoutMs ?? null } : Object.hasOwn(base, "timeoutMs") ? { timeoutMs: base.timeoutMs } : {}; const model = request.model?.resolved; if (base.kind === "llm") { const { model: _model, ...connection } = base.connection; return { ...base, connection: withInference({ ...connection, ...(model !== undefined ? { model } : {}) }, request.inference), ...timeout, }; } const { model: _model, workspace: _workspace, inference: ownInference, ...profile } = base.profile; const workspace = request.runtime.workspace; const inference = Object.hasOwn(request, "inference") ? request.inference : ownInference; const next = { ...base, profile: { ...profile, ...(model !== undefined ? { model } : {}), ...(typeof workspace === "string" ? { workspace } : {}), ...(inference ? { inference } : {}), }, ...timeout, }; if (next.kind === "sdk" && next.fallbackConnection) { let fallback = { ...next.fallbackConnection }; if (request.model === null) delete fallback.model; if (Object.hasOwn(request, "inference")) fallback = withInference(fallback, request.inference); next.fallbackConnection = fallback; } return next; } /** * Reasoning effort has one word in a request: `reasoningEffort`, which is what * engines, opencode and the LLM request body call it. `effort` is the same * setting as a `models.json` alias or an asset's `effort:` frontmatter spells * it, and becomes `reasoningEffort` here, in the one place every layer's * inference is merged, so the nearest layer wins whichever word it used. When * one inference object has both, `reasoningEffort` wins. */ function withReasoningEffort(inference) { if (!Object.hasOwn(inference, "effort")) return inference; const { effort, ...rest } = inference; return Object.hasOwn(rest, "reasoningEffort") ? rest : { ...rest, reasoningEffort: effort }; } function mergeInference(current, layerInference, source, provenance) { provenance["/inference"] = source; if (layerInference === null) { for (const key of Object.keys(provenance)) if (key.startsWith("/inference/")) delete provenance[key]; return null; } const next = withReasoningEffort(layerInference); for (const key of Object.keys(next)) { provenance[`/inference/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`] = source; } return deepMergeConfig(current ?? {}, next); } /** * Resolve one execution: select the engine, merge its defaults with the * caller's layers (nearest wins), expand a model alias, authorize tools, and * apply the result to the engine's runner. */ export function resolveExecution(input) { const command = input.command ?? createInlineResolvedCommand({ template: input.content ?? "", ...(input.argumentInput !== undefined ? { argumentInput: input.argumentInput } : {}), content: input.content ?? "", }); const callerLayers = [ ...(input.agentLayer ? [{ ...input.agentLayer, kind: "agent" }] : []), { id: input.commandLayer?.id ?? "inline-command", kind: "command", values: input.commandLayer?.values ?? {}, }, ...(input.invocationDefaults ? [{ id: "invocation-defaults", kind: "invocation-defaults", values: input.invocationDefaults }] : []), ...(input.current ? [{ id: "current-invocation", kind: "current", values: input.current }] : []), ]; const provenance = {}; let name; let defaults; let base; let fallback = false; if (input.runner) { name = input.runner.engine; defaults = runnerDefaults(input.runner); base = input.runner; provenance.engine = { layer: "installation-defaults", kind: "installation", via: "explicit" }; } else { if (!input.config) throw new TypeError("resolveExecution needs a config or a runner"); const selected = selectEngine(input.config, callerLayers); name = selected.name; fallback = selected.fallback; defaults = engineDefaults(name, selected.engine, input.config); base = resolveEngine(name, input.config, selected.engine); provenance.engine = selected.provenance; } const layers = [{ id: name, kind: "engine", values: defaults.values }, ...callerLayers]; provenance.command = { layer: command.source?.ref ?? "inline", kind: "command", via: "source" }; let persona = input.persona; const agentLayer = nearest(layers, "agent"); const agent = agentLayer?.values.agent; if (agentLayer) { provenance.agent = explicit(agentLayer); // A native selector (or an explicit null) replaces any persona. if (typeof agent !== "string" || !isPortableExecutionAgentSelector(agent)) persona = null; } if (persona) provenance.persona = { layer: persona.source.ref, kind: "agent", via: "source" }; const modelLayer = nearest(layers, "model"); let model; let aliasInference; if (modelLayer) { provenance.model = explicit(modelLayer); const value = modelLayer.values.model; if (value === null) model = null; else if (typeof value === "string" && value.length > 0) { const map = input.modelMap ?? (input.config ? loadModelMap({ engines: input.config.engines }).map : undefined); const selection = map ? resolveModelMapAlias(value, defaults.modelMapKey, map) : { input: value, interpretation: "exact", model: value }; model = { input: selection.input, interpretation: selection.interpretation, resolved: selection.model }; if ("inference" in selection && selection.inference !== undefined) aliasInference = selection.inference; } else { throw new ConfigError("Resolved model must be null or a non-empty string.", "INVALID_CONFIG_FILE"); } } // An alias's inference applies at the layer that chose the alias, under that layer's own inference. let inference; for (const layer of layers) { if (layer === modelLayer && aliasInference !== undefined) { inference = mergeInference(inference, aliasInference, explicit(layer, "model-alias"), provenance); } if (has(layer.values, "inference")) { inference = mergeInference(inference, layer.values.inference ?? null, explicit(layer), provenance); } } const select = (key, field) => { const layer = nearest(layers, key); if (layer) provenance[field] = explicit(layer); return layer; }; const schemaLayer = select("outputSchema", "outputSchema"); const modelWork = input.modelWork === true; const toolsLayer = modelWork ? undefined : select("tools", "tools"); const timeoutLayer = select("timeout", "runtime.timeoutMs"); const workspaceLayer = select("workspace", "runtime.workspace"); const environmentLayer = select("environment", "runtime.environment"); const settingsLayer = select("runtime", "runtime.settings"); const tools = toolsLayer ? cloneToolSelection(toolsLayer.values.tools ?? null, "tools") : undefined; const authorization = authorizeTools(tools, input.runner ? undefined : input.config, modelWork); provenance.authorization = { layer: typeof authorization.policy?.id === "string" ? authorization.policy.id : "not-required", kind: "authorization", via: "policy", }; const request = createResolvedExecutionRequest({ command, ...(input.conversation !== undefined ? { conversation: input.conversation } : {}), ...(agentLayer ? { agent: agent } : {}), ...(persona !== undefined ? { persona } : {}), engine: { name, kind: defaults.kind, platform: defaults.platform }, ...(modelLayer ? { model: model ?? null } : {}), ...(inference !== undefined ? { inference } : {}), ...(schemaLayer ? { outputSchema: schemaLayer.values.outputSchema ?? null } : {}), ...(toolsLayer ? { tools } : {}), authorization, runtime: { ...(timeoutLayer ? { timeoutMs: parseTimeout(timeoutLayer.values.timeout ?? null) } : {}), ...(workspaceLayer ? { workspace: workspaceLayer.values.workspace ?? null } : {}), ...(environmentLayer ? { environment: environmentLayer.values.environment ?? null } : {}), ...(settingsLayer ? { settings: settingsLayer.values.runtime ?? null } : {}), }, notices: [], }); const sorted = Object.fromEntries(Object.entries(provenance).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))); return { request, runner: applyRequest(base, request), provenance: sorted, ...(fallback ? { fallbackEngineName: name } : {}), }; } function nonEmpty(value) { return value !== null && value !== undefined && Object.keys(value).length > 0; } function buildLlm(request, runner, options) { if (typeof request.agent === "string" && request.persona === null) { throw new ConfigError(`The direct LLM transport cannot consume native agent selector ${JSON.stringify(request.agent)}.`, "INVALID_CONFIG_FILE"); } if (hasToolSelection(request.tools)) { throw new ConfigError("The direct LLM transport cannot enforce the resolved tool policy.", "INVALID_CONFIG_FILE"); } const notices = [...request.notices]; const skip = (field) => { notices.push(untranslated("llm", field)); }; for (const key of Object.keys(request.inference ?? {}).sort()) { if (!LLM_INFERENCE_FIELDS.includes(key)) skip(`inference.${key}`); } const chatOptions = {}; // Sent unless the engine opts out; chatCompletion drops it once if the provider rejects it. if (request.outputSchema) { if (runner.connection.supportsJsonSchema !== false) chatOptions.responseSchema = request.outputSchema; else skip("outputSchema"); } if (request.runtime.workspace) skip("runtime.workspace"); if (nonEmpty(request.runtime.environment)) skip("runtime.environment"); if (nonEmpty(request.runtime.settings)) skip("runtime.settings"); for (const field of extensionFields(request)) skip(field); const messages = [ ...(request.persona ? [{ role: "system", content: request.persona.content }] : []), ...(request.conversation ?? []).map((message) => ({ role: message.role, content: message.content })), { role: "user", content: request.command.content }, ]; return { request, runner, prompt: request.command.content, options, messages, chatOptions, notices }; } /** * Build a request for its runner's transport: the harness's own builder for * agent/SDK engines, chat messages for a direct LLM. A request whose tools * were denied by `execution.allowedTools` stops here. */ export function buildExecution(request, runner) { if (request.authorization.status === "denied") { throw new ConfigError(request.authorization.reason ?? "Resolved execution is not authorized by operator policy.", "EXECUTION_NOT_AUTHORIZED"); } const applied = applyRequest(runner, request); const environment = request.runtime.environment; const options = { stdio: "captured", parseOutput: "text", ...(applied.timeoutMs !== undefined ? { timeoutMs: applied.timeoutMs } : {}), ...(applied.kind !== "llm" && applied.profile.workspace ? { cwd: applied.profile.workspace } : {}), ...(environment ? { env: { ...environment } } : {}), }; if (applied.kind === "llm") return buildLlm(request, applied, options); const platform = applied.profile.platform ?? applied.profile.name; const harness = getHarness(platform); const lower = harness?.agentBuilder?.lower ?? harness?.executionLowerer?.lower; if (!lower) { throw new ConfigError(`Agent platform ${JSON.stringify(platform)} has no registered request builder.`, "INVALID_CONFIG_FILE"); } const built = lower(applied.profile, request); return { request, runner: applied, prompt: built.prompt, options: { ...options, dispatch: built.dispatch }, notices: [...request.notices, ...built.notices], }; } /** * Resume: build a journaled `{ request, runner }` exactly as frozen. Never * reads config, models.json or credentials, so a config edit after the freeze * cannot change what a resumed unit runs. */ export function buildExecutionFromWire(wire) { return buildExecution(decodeResolvedExecutionRequest(wire.request), decodeFrozenRunnerSpec(wire.runner)); } /** Each configured engine's own model selection and models.json column, for `akm health`. */ export function executionEngineDefinitionsFromConfig(config) { return Object.fromEntries(Object.entries(config.engines ?? {}).map(([name, engine]) => { const defaults = engineDefaults(name, engine, config); return [ name, { selection: { name, kind: defaults.kind, platform: defaults.platform }, defaults: defaults.values, modelMapKey: defaults.modelMapKey, }, ]; })); }