agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
79 lines • 3.34 kB
JavaScript
/**
* pairwiseSimilarity — pairwise cosine over a set of texts
* (RFC-002 C1's core: tool descriptions → matrix + ranked pairs).
*
* Pattern: pure async function, embedder-injected. No thresholds, no
* verdicts, no lint rules — those are C1's `analyzeToolCatalog`
* policy layer ON TOP of this geometry. The core stays
* reusable for any "how confusable are these texts" question.
* Role: `src/influence-core/` leaf. No agent/runtime imports.
*
* Honest claim: similarity is embedding geometry over the DESCRIPTIONS
* — a confusability HEURISTIC, not a measurement of the model's actual
* selection function (RFC-002 §2; tier 3 validates the proxy via
* choice-entropy sampling).
*/
import { cosineSimilarity } from '../memory/embedding/cosine.js';
/**
* Embed every item once (deduplicated batch) and compute the full
* cosine matrix plus ranked upper-triangle pairs (descending; ties
* keep input pair order).
*
* Invariants (pinned by property tests):
* - `matrix[i][j] === matrix[j][i]` — computed once, mirrored.
* - `matrix[i][i] === 1` EXACTLY — set by definition, so
* self-similarity is an invariant rather than a float artifact
* (and duplicate texts at different ids still compare via cosine).
* - N items → N·(N−1)/2 pairs.
*/
export async function pairwiseSimilarity(args) {
const { items, embedder } = args;
assertUniqueIds(items);
const ids = items.map((item) => item.id);
if (items.length === 0)
return { ids, matrix: [], pairs: [] };
// Deduplicated embedding pass (identical descriptions embed once).
const distinct = [...new Set(items.map((item) => item.text))];
const vectors = embedder.embedBatch
? await embedder.embedBatch({
texts: distinct,
...(args.signal ? { signal: args.signal } : {}),
})
: await sequentialEmbed(embedder, distinct, args.signal);
const vectorByText = new Map();
for (let i = 0; i < distinct.length; i++)
vectorByText.set(distinct[i], vectors[i]);
const itemVecs = items.map((item) => vectorByText.get(item.text));
// Upper triangle once, mirrored; diagonal exactly 1 by definition.
const matrix = items.map(() => new Array(items.length).fill(0));
const pairs = [];
for (let i = 0; i < items.length; i++) {
matrix[i][i] = 1;
for (let j = i + 1; j < items.length; j++) {
const similarity = cosineSimilarity(itemVecs[i], itemVecs[j]);
matrix[i][j] = similarity;
matrix[j][i] = similarity;
pairs.push({ a: ids[i], b: ids[j], similarity });
}
}
// Stable sort — ties keep (i, j) input order.
pairs.sort((p, q) => q.similarity - p.similarity);
return { ids, matrix, pairs };
}
async function sequentialEmbed(embedder, texts, signal) {
const out = [];
for (const text of texts) {
out.push(await embedder.embed({ text, ...(signal ? { signal } : {}) }));
}
return out;
}
function assertUniqueIds(items) {
const seen = new Set();
for (const item of items) {
if (seen.has(item.id)) {
throw new Error(`pairwiseSimilarity: duplicate item id '${item.id}' — ids must be unique`);
}
seen.add(item.id);
}
}
//# sourceMappingURL=similarity.js.map