UNPKG

agentfootprint

Version:

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

98 lines • 4.26 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.attributeChoice = void 0; /** * attributeChoice — which context UNIT best explains one tool choice * (the transpose of `scoreMargin`). * * `scoreMargin` fixes the context and ranks candidate tools against it — * so a CONSTANT part of the context (the system prompt, identical on every * call) is excluded, because it adds no power to tell one call apart from * another. But that exclusion also makes procedural picks unexplainable: * the agent calls `whats_here` because a RULE in the system prompt says * "orient first", and margin scoring can never see that rule. * * `attributeChoice` fixes the CHOSEN TOOL and ranks the context units * against it. Here a constant-but-load-bearing rule is exactly what we * want surfaced: cosine(tool, "call whats_here first") is high and * specific, so the answer becomes a citation — "picked because rule-1". * The units are tagged by channel (`'system' | 'task' | 'result' | …`) so * the result also reports HOW procedural vs topical the pick was. * * Pattern: pure async function, embedder-injected. No agent/runtime * imports — `src/influence-core/` leaf, same as `scoreMargin`. * * Honest claim (RFC-002 §2): `score` is embedding geometry between the * tool text and each instruction unit — a PROXY for which instruction the * pick ALIGNS with, never proof the model used that rule. This is Tier 1 * (similarity); Tier 3 (counterfactual ablation of each unit) is the * ground truth that would validate it. */ const cosine_js_1 = require("../memory/embedding/cosine.js"); /** * Embed the tool text and every unit once (deduplicated batch), rank the * units by cosine to the tool, and report the top unit plus the per-channel * share of positive similarity mass. * * Fail-loud validation: empty units or duplicate unit ids throw — those are * caller wiring bugs, not runtime conditions. */ async function attributeChoice(args) { const { tool, units, embedder } = args; validate(units); // One deduplicated embedding pass: the tool text + each distinct unit text. const distinct = [...new Set([tool.text, ...units.map((u) => u.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 toolVec = vectorByText.get(tool.text); const scored = units.map((u) => ({ id: u.id, channel: u.channel, score: (0, cosine_js_1.cosineSimilarity)(toolVec, vectorByText.get(u.text)), })); // Stable sort — ties keep input order. scored.sort((a, b) => b.score - a.score); // Per-channel share of POSITIVE mass (negatives are anti-alignment, not // evidence FOR a channel — clamp to 0 so they neither add nor subtract). const positiveByChannel = {}; let total = 0; for (const s of scored) { const w = Math.max(0, s.score); if (w === 0) continue; positiveByChannel[s.channel] = (positiveByChannel[s.channel] ?? 0) + w; total += w; } const byChannel = {}; if (total > 0) for (const [ch, w] of Object.entries(positiveByChannel)) byChannel[ch] = w / total; return { tool: tool.name, units: scored, top: scored[0], byChannel }; } exports.attributeChoice = attributeChoice; 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(units) { if (units.length === 0) { throw new Error('attributeChoice: units must be non-empty — nothing to attribute to'); } const ids = new Set(); for (const u of units) { if (ids.has(u.id)) { throw new Error(`attributeChoice: duplicate unit id '${u.id}' — ids must be unique`); } ids.add(u.id); } } //# sourceMappingURL=attribute.js.map