UNPKG

agentfootprint

Version:

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

67 lines • 3.46 kB
/** * 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. */ /** * 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. */ export 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)); } //# sourceMappingURL=intentScorer.js.map