UNPKG

agentfootprint

Version:

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

209 lines • 10.6 kB
"use strict"; /** * skillIntent — the intent side of the turn-start routing cascade (SG-C): * candidate projection, duplicate-example normalization, the graph's * `TurnRoutingPlan` (what the agent's RouteTurn stage consumes), and the * leave-one-out `checkupIntents` audit. * * One module owns how a declared `{ intent, examples }` becomes a scoreable * {@link IntentCandidate}, so the router, the menu, the audit and the record * can never project the same declaration differently. Pure over the entry * declarations `skillGraph.ts` hands it; imports no engine machinery beyond * the injection-context type. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.runCheckupIntents = exports.findDuplicateIntentExamples = exports.normalizeExample = exports.buildTurnRoutingPlan = void 0; const types_js_1 = require("./types.js"); const intentScorer_js_1 = require("./intentScorer.js"); const devWarn_js_1 = require("./devWarn.js"); const routingPolicy_js_1 = require("./routingPolicy.js"); /** Build the plan. `describeFor` resolves a skill id to its description (the * incumbent's fallback intent text). */ function buildTurnRoutingPlan(input) { const { entries, describeFor, policy, scorer } = input; const intentEntries = entries.filter((e) => e.match?.kind === 'intent'); const ruleEntries = entries.filter((e) => e.when !== undefined && e.match?.kind !== 'intent'); const candidateOf = (e) => { const m = e.match; return { id: e.id, intent: m.intent, examples: m.examples }; }; return { ...(scorer && { scorer }), policy, entryIds: entries.map((e) => e.id), firstRuleMatch(ctx) { for (const e of ruleEntries) { if ((0, types_js_1.isHiddenSkill)(ctx, e.id)) continue; try { if (!e.when(ctx)) continue; // Evidence is extracted on the WINNER only — and a throwing extractor // costs the record a witness, never the turn its route. let witness; try { witness = e.witness?.(ctx); } catch { witness = undefined; } return { id: e.id, ...(witness !== undefined && { witness }) }; } catch (err) { (0, devWarn_js_1.devWarn)(`agentfootprint skillGraph: entry "${e.id}" predicate threw during the ` + `turn-start cascade — treated as no-match. Predicates must be pure + total. ` + `${err instanceof Error ? err.message : String(err)}`); } } return undefined; }, intentCandidates(exclude) { const list = intentEntries.map(candidateOf); return exclude === undefined ? list : list.filter((c) => !exclude.has(c.id)); }, incumbentCandidate(id) { const declared = intentEntries.find((e) => e.id === id); if (declared) return candidateOf(declared); return { id, intent: describeFor(id) ?? id, examples: [] }; }, }; } exports.buildTurnRoutingPlan = buildTurnRoutingPlan; /** Case/whitespace-normalize an example for the duplicate check — the SAME * string under two intents is scorer luck, whatever its casing. */ function normalizeExample(example) { return example.trim().toLowerCase().replace(/\s+/g, ' '); } exports.normalizeExample = normalizeExample; /** * The one PROVABLE intent-pair fact, decidable from the data alone: the same * (normalized) example declared under two intents. Whichever wins is scorer * luck, so it is named — both ids and the string. Everything else about two * example sets needs the configured scorer ({@link runCheckupIntents}). */ function findDuplicateIntentExamples(entries) { const seen = new Map(); const problems = []; for (const e of entries) { if (e.match?.kind !== 'intent') continue; for (const example of e.match.examples) { const key = normalizeExample(example); const first = seen.get(key); if (first === undefined) { seen.set(key, { id: e.id, example }); continue; } if (first.id === e.id) continue; // repeated inside ONE intent — harmless emphasis problems.push({ kind: 'warning', code: 'duplicate-intent-example', message: `Intents "${first.id}" and "${e.id}" both declare the example ` + `"${example}" (case/whitespace-normalized). A message matching it lands on ` + `whichever the scorer happens to rank first — scorer luck, not a declaration. ` + `Move the example to the one intent it belongs to, or merge the intents.`, skill: e.id, from: first.id, to: e.id, }); } } return problems; } exports.findDuplicateIntentExamples = findDuplicateIntentExamples; /** * The leave-one-out intent audit behind `graph.checkupIntents()` — run each * declared example through the CONFIGURED scorer (the router that will * actually run; auditing with a different scorer would prove nothing about * production) against all intent candidates, with the example's own intent * represented by its REMAINING examples. An example whose top-1 is a * different intent, or whose top-vs-own pairwise margin is inside * `nearTieMargin`, is an `overlapping-intents` warning naming the example, * both intents, both numbers, and the fix. * * Honesty boundaries, stated in the messages: only the configured scorer's * view is checked; `when` predicates are opaque and never claimed checked; * tier-1 regex/keyword rules that fire BEFORE tier 2 are named as unaudited * shadowers when present. On an `llmClassifier` graph this costs one model * call per example — the caller's docstring says so. */ async function runCheckupIntents(input) { const { entries, scorer, policy, signal } = input; const intentEntries = entries.filter((e) => e.match?.kind === 'intent'); const shadowers = entries .filter((e) => e.when !== undefined && e.match?.kind !== 'intent') .map((e) => e.id); const shadowClause = shadowers.length > 0 ? ` (Note: tier-1 rule entries ${shadowers .map((id) => `"${id}"`) .join(', ')} fire BEFORE the classifier in declaration order and were NOT audited — ` + `a message they match never reaches these intents.)` : ''; const problems = [...findDuplicateIntentExamples(intentEntries)]; for (const entry of intentEntries) { const m = entry.match; for (const [i, example] of m.examples.entries()) { const candidates = intentEntries.map((e) => { const em = e.match; return e.id === entry.id ? // Leave one out: the example under test must not match itself. { id: e.id, intent: em.intent, examples: em.examples.filter((_, j) => j !== i) } : { id: e.id, intent: em.intent, examples: em.examples }; }); const raw = await scorer.score({ message: example }, candidates, signal); const scores = (0, intentScorer_js_1.validateIntentScores)(scorer.name, candidates, raw); const verdict = (0, routingPolicy_js_1.decideTier2)(scores, undefined, policy, { ...(scorer.floor !== undefined && { floor: scorer.floor }), ...(scorer.categorical !== undefined && { categorical: scorer.categorical }), }); // An UNMATCHED example (nothing above the floor, its own intent // included) is not an overlap — under leave-one-out a two-example // intent whose examples share no tokens lands here routinely, and // reading the all-zero ranking's first row as a "winner" would name a // cross-match that never happened. if (verdict.kind === 'unmatched') continue; const top = verdict.ranked[0]; if (top === undefined) continue; const own = verdict.ranked.find((r) => r.id === entry.id); if (top.id !== entry.id) { problems.push({ kind: 'warning', code: 'overlapping-intents', message: `Example "${example}" is declared under intent "${entry.id}" but the configured ` + `'${scorer.name}' scorer ranks intent "${top.id}" first for it ` + `(${fmt(top.score)} vs own ${fmt(own?.score)}). Differentiate the examples, or ` + `merge the intents. (Only the configured scorer's view was checked; \`when\` ` + `predicates are opaque and were NOT.)${shadowClause}`, skill: entry.id, from: entry.id, to: top.id, }); } else if (verdict.runnerUp !== undefined && verdict.runnerUp.gap < policy.nearTieMargin) { problems.push({ kind: 'warning', code: 'overlapping-intents', message: `Example "${example}" (intent "${entry.id}") is a NEAR-TIE with intent ` + `"${verdict.runnerUp.id}" under the configured '${scorer.name}' scorer ` + `(pairwise gap ${fmt(verdict.runnerUp.gap)} < margin ${fmt(policy.nearTieMargin)}) ` + `— at run time this message would open a menu instead of routing. Differentiate ` + `the examples, or merge the intents. (Only the configured scorer's view was ` + `checked; \`when\` predicates are opaque and were NOT.)${shadowClause}`, skill: entry.id, from: entry.id, to: verdict.runnerUp.id, }); } } } return { ok: !problems.some((p) => p.kind === 'error'), problems }; } exports.runCheckupIntents = runCheckupIntents; function fmt(n) { return n === undefined ? '—' : Number.isFinite(n) ? n.toFixed(3) : String(n); } //# sourceMappingURL=skillIntent.js.map