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

116 lines (115 loc) • 4.81 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 { DEFAULT_LLM_TIMEOUT_MS } from "../integrations/agent/config.js"; /** * For each feature key, return the effective enabled state by reading the * 0.9.0 config shape. */ const FEATURE_LOCATION = { memory_inference: (cfg) => cfg.index?.memory?.enabled ?? true, // #951: a real implementation of the dead `curate_rerank` key removed in // 0.8.0. Off by default — it requires a `search.curateRerank.endpoint` a // caller must explicitly configure. curate_rerank: (cfg) => Boolean(cfg.search?.curateRerank?.enabled), // Always on at the LLM-wrapper level. Enablement is decided ONCE at the // extract entry point (`akmExtract`): the `extract.enabled` process toggle // gates extract as a STAGE of `akm improve` (the active improve strategy, per // #593/#594), while an explicit `akm extract` command always runs. Gating the // inner LLM calls on `default.processes.extract.enabled` here was a footgun — // dropping extract from the daily improve strategy silently disabled the // standalone `akm extract` command. (cfg unused — kept for resolver signature.) session_extraction: (_cfg) => true, }; /** * Pure predicate: is the named feature gate enabled in `config`? * * Reads from the unified 0.9.0 config shape. */ export function isLlmFeatureEnabled(config, feature, improveEnabled) { if (!config) return false; // Improve resolves enablement from its selected strategy and passes the // decision in. This module does not own a second improve config lookup. if (improveEnabled !== undefined) return improveEnabled; const resolver = FEATURE_LOCATION[feature]; if (!resolver) return false; return resolver(config); } /** * Run `fn()` only if `isLlmFeatureEnabled(config, feature)` is `true`. On * disablement, throw, or timeout, return `fallback` (or — if it is a * thunk — the value produced by calling it). The timeout aborts the signal * `fn` receives, so the work it started stops too. */ export async function tryLlmFeature(feature, config, fn, fallback, opts) { const resolveFallback = async () => typeof fallback === "function" ? await fallback() : fallback; if (!isLlmFeatureEnabled(config, feature, opts?.enabled)) { opts?.onFallback?.({ feature, reason: "disabled" }); return resolveFallback(); } const timeoutMs = opts && Object.hasOwn(opts, "timeoutMs") ? (opts.timeoutMs ?? null) : DEFAULT_LLM_TIMEOUT_MS; try { if (timeoutMs === null || timeoutMs <= 0) { return await fn(new AbortController().signal); } return await runWithTimeout(fn, timeoutMs, feature); } catch (err) { const error = err instanceof Error ? err : new Error(String(err)); const reason = error instanceof LlmFeatureTimeoutError ? "timeout" : "error"; opts?.onFallback?.({ feature, reason, error }); return resolveFallback(); } } /** * Section-agnostic process gate that maps process identifiers to their current * config locations. * * For unknown (section, processName) pairs the result is `false`. */ export function isProcessEnabled(section, processName, config) { if (!config) return false; // Index passes are first-class 0.9 entries. if (section === "index") { if (processName === "memory_inference" || processName === "memoryInference") { return isLlmFeatureEnabled(config, "memory_inference"); } } return false; } /** Specific error class so call sites and the wrapper can tell timeouts apart from generic throws. */ export class LlmFeatureTimeoutError extends Error { feature; timeoutMs; constructor(feature, timeoutMs) { super(`LLM feature "${feature}" timed out after ${timeoutMs}ms.`); this.name = "LlmFeatureTimeoutError"; this.feature = feature; this.timeoutMs = timeoutMs; Object.setPrototypeOf(this, new.target.prototype); } } async function runWithTimeout(fn, timeoutMs, feature) { let timer; const controller = new AbortController(); try { return await new Promise((resolve, reject) => { timer = setTimeout(() => { const timedOut = new LlmFeatureTimeoutError(feature, timeoutMs); controller.abort(timedOut); reject(timedOut); }, timeoutMs); Promise.resolve() .then(() => fn(controller.signal)) .then(resolve, reject); }); } finally { if (timer) clearTimeout(timer); } }