agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
71 lines • 3.62 kB
JavaScript
;
/**
* IntentScorer — the pluggable PORT that judges a new user message against the
* graph's declared intents (SG-C tier 2).
*
* Custody split, deliberately: a scorer produces NUMBERS for EVERY candidate —
* never a bare winner — and the FRAMEWORK (`routingPolicy.decideTier2`)
* applies the floor and the near-tie margin, decides the verdict, and records
* it. That is what keeps scorers a tier and never a correctness dependency: a
* scorer cannot route by itself, and its near-ties fall through to the model.
*
* Three built-ins share this port:
* • `keywordScorer()` — widened in `entryScorer.ts` (no dependency;
* set-cosine over intent + example tokens; `floor: 0` — zero overlap IS
* honestly "did not match at all");
* • `embeddingScorer(e)` — widened in `entryScorer.ts` (semantic cosine;
* NO floor unless the consumer declares one — an absolute embedding
* threshold is a lie by default);
* • `llmClassifier(provider)` — `llmClassifier.ts` (one constrained-enum
* model call per turn, off the hot loop; `categorical`).
*
* `when` predicates remain the opaque escape hatch — tier 1, never scored.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateIntentScores = void 0;
/**
* The framework's half of the scorer contract: every candidate scored exactly
* once, no foreign ids. Returns the scores in CANDIDATE order (so a scorer
* that reordered them cannot smuggle a different ranking past the caller's
* declaration order). Throws a teaching error naming the scorer, what it
* omitted or invented, and the contract — the RouteTurn stage treats that
* throw exactly like a scorer failure (fall through to the recorded fallback),
* because a misbehaving scorer must never abort a run.
*/
function validateIntentScores(scorerName, candidates, result) {
const byId = new Map();
const foreign = [];
// A candidate scored twice is refused like a missing or foreign one: a Map
// would otherwise let the LAST row win silently, and which row that is
// depends on the scorer's iteration order (2026-09-22 review, S8).
const repeated = [];
for (const s of result) {
if (!candidates.some((c) => c.id === s.id))
foreign.push(s.id);
else if (byId.has(s.id)) {
if (!repeated.includes(s.id))
repeated.push(s.id);
}
else
byId.set(s.id, s);
}
const missing = candidates.filter((c) => !byId.has(c.id)).map((c) => c.id);
if (foreign.length > 0 || missing.length > 0 || repeated.length > 0) {
const clauses = [
...(missing.length > 0 ? [`omitted ${missing.map((m) => `"${m}"`).join(', ')}`] : []),
...(foreign.length > 0
? [`named ${foreign.map((f) => `"${f}"`).join(', ')} which it was not given`]
: []),
...(repeated.length > 0
? [`scored ${repeated.map((r) => `"${r}"`).join(', ')} more than once`]
: []),
];
throw new Error(`IntentScorer '${scorerName}' returned an unusable result: it ${clauses.join(' and ')}. ` +
`A scorer must return one score per candidate it was given — every candidate, no ` +
`foreign ids — and the framework decides the verdict from the numbers. Fix the ` +
`scorer's score() to map over its \`candidates\` argument.`);
}
return candidates.map((c) => byId.get(c.id));
}
exports.validateIntentScores = validateIntentScores;
//# sourceMappingURL=intentScorer.js.map