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
220 lines (219 loc) • 10.8 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 { embedCacheKey, getCachedEmbedding, setCachedEmbedding } from "./embedders/cache.js";
import { DETERMINISTIC_EMBED_MODEL_ID, deterministicEmbed, isDeterministicEmbedEnabled, } from "./embedders/deterministic.js";
import { DEFAULT_LOCAL_MODEL, isTransformersAvailable as isTransformersAvailableReal, LocalEmbedder, } from "./embedders/local.js";
import { hasRemoteEndpoint, RemoteEmbedder } from "./embedders/remote.js";
// ── Shared exports ──────────────────────────────────────────────────────────
export { clearEmbeddingCache } from "./embedders/cache.js";
export { _setTransformersLoaderForTests, DEFAULT_LOCAL_MODEL } from "./embedders/local.js";
let embedderOverrides;
/** TEST-ONLY. Swap embedder implementations; pass undefined to restore. */
export function _setEmbedderForTests(fakes) {
embedderOverrides = fakes;
}
/**
* Check whether the external Transformers dependency is available.
* Delegating wrapper around `./embedders/local`'s probe so tests can swap it
* via {@link _setEmbedderForTests}.
*/
export function isTransformersAvailable() {
if (embedderOverrides?.isTransformersAvailable)
return embedderOverrides.isTransformersAvailable();
return isTransformersAvailableReal();
}
// ── Singleton local embedder ────────────────────────────────────────────────
// `_localEmbedder` is an intentional module-level singleton but constructed
// lazily on first use. The underlying Transformers.js pipeline is
// expensive to initialise (model download + WASM compilation) and is safe to
// share across calls because it is stateless once created. Deferring
// construction to first call keeps the module side-effect-free at import time,
// which matters for the test suite (single Bun process, ~120 test files).
let _localEmbedder;
function getLocalEmbedder() {
if (!_localEmbedder) {
_localEmbedder = new LocalEmbedder();
}
return _localEmbedder;
}
/**
* Reset the cached local embedder pipeline. Used by tests that want a fresh
* pipeline construction (e.g. to assert the dtype-fallback retry logic).
*/
export function resetLocalEmbedder() {
getLocalEmbedder().reset();
}
// ── Public API ──────────────────────────────────────────────────────────────
/**
* Generate an embedding for the given text.
* If embeddingConfig has a remote endpoint, uses the configured OpenAI-compatible endpoint.
* Otherwise falls back to local Transformers.js using the model from
* `embeddingConfig.localModel` or `DEFAULT_LOCAL_MODEL`.
*
* Results are cached in an LRU cache (max ~100 entries) keyed by query text
* and embedding config. Repeated identical queries return the cached vector.
*/
export async function embed(text, embeddingConfig, signal) {
if (embedderOverrides?.embed)
return embedderOverrides.embed(text, embeddingConfig, signal);
// Deterministic mode (env-gated, test/bench only): model-free, stable.
if (isDeterministicEmbedEnabled()) {
return deterministicEmbed(text);
}
const key = embedCacheKey(text, embeddingConfig);
const cached = getCachedEmbedding(key);
if (cached)
return cached;
const result = await embedOnce(text, embeddingConfig, signal);
setCachedEmbedding(key, result);
return result;
}
/**
* Resolve a single embedding through the configured provider.
*
* The local branch must honour `localModel` exactly as {@link embedBatch}
* does. The singleton is constructed with no default model, so routing through
* `getLocalEmbedder().embed()` silently used DEFAULT_LOCAL_MODEL: queries were
* embedded with a different model than the index was built with. Nothing
* detected it, because the provider fingerprint keys on `localModel`, so no
* purge or "pending" status ever fired — a dimension mismatch made semantic
* ranking contribute nothing, and a same-dimension override silently produced
* meaningless cross-model scores.
*/
async function embedOnce(text, embeddingConfig, signal) {
if (embeddingConfig && hasRemoteEndpoint(embeddingConfig)) {
return new RemoteEmbedder(embeddingConfig).embed(text, signal);
}
const localModel = embeddingConfig?.localModel;
if (localModel) {
return getLocalEmbedder().embedWithModel(text, localModel);
}
return getLocalEmbedder().embed(text, signal);
}
/**
* Generate embeddings for multiple texts in batch.
* Uses the OpenAI-compatible batch API for remote endpoints, batched by an
* estimated token budget (not a fixed document count, #874). A remote
* sub-batch or oversized document that fails is skipped rather than
* aborting the whole call — pass `onSkip` to learn which indices were
* skipped and why; the result array holds `undefined` at those indices.
* Uses the LocalEmbedder.embedBatch path for the local transformer pipeline,
* which processes texts in chunks of 32 for genuine batched inference.
*
* `onBatch`, when given, fires once per provider/local batch as it completes
* (#954) so a caller can commit each batch's rows durably as they land
* rather than buffering the whole call — see `EmbeddingBatchCommit`.
*/
export async function embedBatch(texts, embeddingConfig, signal, onSkip, onBatch) {
if (embedderOverrides?.embedBatch) {
return embedderOverrides.embedBatch(texts, embeddingConfig, signal, onSkip, onBatch);
}
if (texts.length === 0)
return [];
// Deterministic mode (env-gated, test/bench only): model-free, stable.
if (isDeterministicEmbedEnabled()) {
const embeddings = texts.map((t) => deterministicEmbed(t));
// One onBatch commit for the whole call, like the local/remote batched
// paths — firing once per text opened one materializer transaction per
// entry in deterministic (test/bench) mode.
onBatch?.(embeddings.map((_embedding, i) => i), embeddings);
return embeddings;
}
if (embeddingConfig && hasRemoteEndpoint(embeddingConfig)) {
return new RemoteEmbedder(embeddingConfig).embedBatch(texts, signal, onSkip, onBatch);
}
// Local transformer: use the batched path (chunks of 32 via LocalEmbedder).
// When a localModel override is set we cannot share the singleton (which uses
// the default model), so fall back to per-text embedWithModel in that case.
const localModel = embeddingConfig?.localModel;
if (!localModel) {
return getLocalEmbedder().embedBatch(texts, signal, onBatch);
}
const results = [];
for (const [i, text] of texts.entries()) {
if (signal?.aborted) {
throw signal.reason instanceof Error ? signal.reason : new Error("embedding interrupted");
}
const embedding = await getLocalEmbedder().embedWithModel(text, localModel);
results.push(embedding);
onBatch?.([i], [embedding]);
}
return results;
}
// ── Similarity ──────────────────────────────────────────────────────────────
// `cosineSimilarity` was moved to `./embedders/types.ts` so importers
// (notably `db.ts`) can pull the math function without dragging in this
// module and its Transformers.js import chain.
export { cosineSimilarity } from "./embedders/types.js";
// ── Model ID resolution ─────────────────────────────────────────────────────
/**
* Derive a stable string identifier for the embedding model in use.
* This is the `model_id` stored in `body_embeddings` (and used for the
* drop-all-on-mismatch purge when the model changes).
*
* Rules:
* - Remote endpoint: use `config.model` (the API-level model name).
* - Local transformers: use `config.localModel ?? DEFAULT_LOCAL_MODEL`.
* - No config: use `DEFAULT_LOCAL_MODEL` (the shared singleton model).
*/
export function resolveEmbeddingModelId(embeddingConfig) {
if (embedderOverrides?.resolveEmbeddingModelId)
return embedderOverrides.resolveEmbeddingModelId(embeddingConfig);
if (isDeterministicEmbedEnabled())
return DETERMINISTIC_EMBED_MODEL_ID;
if (!embeddingConfig)
return DEFAULT_LOCAL_MODEL;
if (hasRemoteEndpoint(embeddingConfig))
return embeddingConfig.model ?? "remote";
return embeddingConfig.localModel ?? DEFAULT_LOCAL_MODEL;
}
// ── Availability check ──────────────────────────────────────────────────────
/**
* Check whether embedding is available with a detailed reason on failure.
*/
export async function checkEmbeddingAvailability(embeddingConfig) {
if (embedderOverrides?.checkEmbeddingAvailability) {
return embedderOverrides.checkEmbeddingAvailability(embeddingConfig);
}
// Deterministic mode (env-gated): always available — no model, no network.
if (isDeterministicEmbedEnabled()) {
return { available: true };
}
if (embeddingConfig && hasRemoteEndpoint(embeddingConfig)) {
try {
await new RemoteEmbedder(embeddingConfig).embed("test");
return { available: true };
}
catch (err) {
return {
available: false,
reason: "remote-unreachable",
message: err instanceof Error ? err.message : String(err),
};
}
}
// Check if the package is importable before attempting the model download.
if (!isTransformersAvailable()) {
return {
available: false,
reason: "missing-package",
message: "The @huggingface/transformers dependency is unavailable.",
};
}
try {
await getLocalEmbedder().getPipeline(embeddingConfig?.localModel);
return { available: true };
}
catch (err) {
return {
available: false,
reason: "model-download-failed",
message: err instanceof Error ? err.message : String(err),
};
}
}
export async function isEmbeddingAvailable(embeddingConfig) {
const result = await checkEmbeddingAvailability(embeddingConfig);
return result.available;
}