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
102 lines (101 loc) • 4.97 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 { getAllEntries } from "../../storage/repositories/index-entries-repository.js";
import { getUtilityScoresByIds } from "../../storage/repositories/index-utility-repository.js";
import { WARM_START_CAP } from "./outcome-loop.js";
const DAY_MS = 86_400_000;
const RECENCY_HALFLIFE_DAYS = 21;
/**
* The recency floor itself halves every 180 days, so an asset nobody uses keeps
* drifting down instead of parking at the floor.
*/
const RECENCY_FLOOR_HALFLIFE_DAYS = 180;
/** Keeps frequency ordinal for assets whose last use is unknown. */
const RECENCY_EPSILON = 0.01;
const SIZE_FLOOR_BYTES = 200;
/** Default weights: the outcome term is on (opt out with `improve.salience.outcomeWeightEnabled: false`). */
export const W_ENCODING = 0.25;
export const W_OUTCOME = 0.15;
export const W_RETRIEVAL = 0.6;
/** Weights with the outcome term off. */
export const W_ENCODING_PARITY = 0.3;
export const W_OUTCOME_PARITY = 0;
export const W_RETRIEVAL_PARITY = 0.7;
/** Encoding salience by type, for assets with no content-derived score yet. */
export const DEFAULT_TYPE_ENCODING_WEIGHTS = Object.freeze({
skill: 0.9,
agent: 0.9,
command: 0.8,
workflow: 0.8,
lesson: 0.75,
knowledge: 0.7,
script: 0.6,
memory: 0.5,
});
export const DEFAULT_ENCODING_SALIENCE = 0.5;
const clamp01 = (value) => Math.min(1, Math.max(0, value));
/** The salience vector for one asset (pure). */
export function computeSalience(inputs) {
const now = inputs.now ?? Date.now();
const encodingSource = inputs.encodingSalience !== undefined ? "content" : "type-stub";
const encoding = inputs.encodingSalience !== undefined
? clamp01(inputs.encodingSalience)
: (DEFAULT_TYPE_ENCODING_WEIGHTS[inputs.type] ?? DEFAULT_ENCODING_SALIENCE);
// Without an outcome row, utility (capped) seeds the outcome term — the same
// seed the first outcome row gets — so it is not zero at launch.
const outcome = inputs.outcomeSalience !== undefined
? clamp01(inputs.outcomeSalience)
: Math.min(WARM_START_CAP, Math.max(0, inputs.utilityScore ?? 0));
// log(1 + freq) × recency, where recency halves every 21 days above a floor
// that itself halves every 180 days; soft-capped to [0, 1).
const lastUseMs = inputs.lastUseMs ?? 0;
const useAgeDays = lastUseMs > 0 ? (now - lastUseMs) / DAY_MS : 9999;
const recencyDecay = Math.max(RECENCY_EPSILON, 0.1 * 0.5 ** (useAgeDays / RECENCY_FLOOR_HALFLIFE_DAYS) + 0.5 ** (useAgeDays / RECENCY_HALFLIFE_DAYS));
const rawRetrieval = Math.log(1 + inputs.retrievalFreq) * recencyDecay;
const retrieval = rawRetrieval / (rawRetrieval + 1);
// Larger assets rank slightly lower.
const sizePenalty = 1 / Math.log10(Math.max(SIZE_FLOOR_BYTES, inputs.sizeBytes ?? 0));
const [we, wo, wr] = inputs.outcomeWeightEnabled !== false
? [W_ENCODING, W_OUTCOME, W_RETRIEVAL]
: [W_ENCODING_PARITY, W_OUTCOME_PARITY, W_RETRIEVAL_PARITY];
const rankScore = clamp01((we * encoding + wo * outcome + wr * retrieval) * sizePenalty);
return { encoding, outcome, retrieval, rankScore, encodingSource };
}
export { getAssetSalience, getConsecutiveNoOps, recordNoOp, resetConsecutiveNoOps, upsertAssetSalience, } from "../../storage/repositories/salience-repository.js";
/** Whether a stored row's encoding salience is content-derived (unknown provenance is not). */
export function isContentEncodingRow(row) {
return row.encoding_source === "content";
}
/**
* Consolidation selection only: an asset whose last three runs were no-ops
* sorts at half its score. The stored rank is untouched.
*/
export const SALIENCE_NO_OP_DAMPEN_THRESHOLD = 3;
export const SALIENCE_NO_OP_DAMPEN_FACTOR = 0.5;
/** `ref → last retrieval (ms)` from the index's utility scores; absent means never retrieved. */
export function getLastUseMsByRef(indexDb, candidates) {
const result = new Map();
if (candidates.length === 0)
return result;
const refByItemRef = new Map(candidates.flatMap((candidate) => (candidate.itemRef ? [[candidate.itemRef, candidate.ref]] : [])));
const idToRef = new Map();
for (const indexed of getAllEntries(indexDb)) {
const ref = refByItemRef.get(indexed.itemRef);
if (ref)
idToRef.set(indexed.id, ref);
}
if (idToRef.size === 0)
return result;
const scores = getUtilityScoresByIds(indexDb, [...idToRef.keys()]);
for (const [id, row] of scores) {
const ref = idToRef.get(id);
const lastUsedAt = row.lastUsedAt;
if (!ref || !lastUsedAt)
continue;
const ms = typeof lastUsedAt === "number" ? lastUsedAt : Date.parse(lastUsedAt);
if (ms > 0)
result.set(ref, ms);
}
return result;
}