UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

114 lines • 4.78 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.scoreMargin = void 0; /** * scoreMargin — choice-margin scoring over a candidate set * (RFC-002 C4's core: (candidates, contextText, chosen) → scores + * margin + flags). * * Pattern: pure async function, embedder-injected. NO recorder wiring * — `toolChoiceRecorder` (C5) owns event ingestion and a * KeyedStore, and calls this for the math. * Role: `src/influence-core/` leaf. No agent/runtime imports. * * The competition model (RFC-002 §4): embed the choice context (what * the model saw — user message + latest reasoning), embed each offered * candidate's text, rank candidates by similarity to the context. * margin = score(best chosen) − score(best non-chosen) * Small margin = fragile choice (`narrow`); top-scored candidate not * among the chosen = `proxyDisagreement` (always worth surfacing: * either a proxy miss or a genuinely surprising model choice). * * Honest claim: the scores are embedding geometry between context and * descriptions — a proxy for the model's selection function, never * "the model chose because". Margin is EVIDENCE of decisiveness, not * proof; tier 3 (choice-entropy sampling) validates the proxy. */ const cosine_js_1 = require("../memory/embedding/cosine.js"); const types_js_1 = require("./types.js"); /** * Rank candidates by cosine similarity to the choice context and * measure how decisively the chosen one(s) won. * * Returns ranked `scores` (descending; ties keep candidate input * order), the `topScored` name, the `margin` (undefined when every * candidate was chosen — no competition to measure; `narrow` is false * in that case), and the two flags. * * Fail-loud validation: empty candidates/chosen, duplicate candidate * names, or a chosen name missing from the candidates throw — those * are wiring bugs in the caller, not runtime conditions. */ async function scoreMargin(args) { const { candidates, chosen, embedder } = args; const marginThreshold = args.marginThreshold ?? types_js_1.DEFAULT_MARGIN_THRESHOLD; validate(candidates, chosen); // One deduplicated embedding pass: context + distinct candidate texts. const distinct = [...new Set([args.contextText, ...candidates.map((c) => c.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 contextVec = vectorByText.get(args.contextText); const scores = candidates.map((candidate) => ({ name: candidate.name, score: (0, cosine_js_1.cosineSimilarity)(contextVec, vectorByText.get(candidate.text)), })); // Stable sort — ties keep candidate input order. scores.sort((a, b) => b.score - a.score); const chosenSet = new Set(chosen); let bestChosen = -Infinity; let bestOther = -Infinity; for (const { name, score } of scores) { if (chosenSet.has(name)) bestChosen = Math.max(bestChosen, score); else bestOther = Math.max(bestOther, score); } const margin = bestOther === -Infinity ? undefined : bestChosen - bestOther; const topScored = scores[0].name; return { scores, chosen: [...chosen], topScored, margin, flags: { narrow: margin !== undefined && margin < marginThreshold, proxyDisagreement: !chosenSet.has(topScored), }, }; } exports.scoreMargin = scoreMargin; async function sequentialEmbed(embedder, texts, signal) { const out = []; for (const text of texts) { out.push(await embedder.embed({ text, ...(signal ? { signal } : {}) })); } return out; } function validate(candidates, chosen) { if (candidates.length === 0) { throw new Error('scoreMargin: candidates must be non-empty'); } const names = new Set(); for (const candidate of candidates) { if (names.has(candidate.name)) { throw new Error(`scoreMargin: duplicate candidate name '${candidate.name}' — names must be unique`); } names.add(candidate.name); } if (chosen.length === 0) { throw new Error('scoreMargin: chosen must be non-empty — calls that chose nothing have no margin to score'); } for (const name of chosen) { if (!names.has(name)) { throw new Error(`scoreMargin: chosen '${name}' is not among the candidates`); } } } //# sourceMappingURL=margin.js.map