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
JavaScript
// 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);
}
}