UNPKG

agentfootprint

Version:

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

195 lines • 9.68 kB
"use strict"; /** * findings/contingent — no towers on unverified lemmas. * * Pattern: one pure rule over (the values a moment used, the corpus's * carriers, the ledger's fold); the sibling of * `integrity/unsupported-claim` (a value against the settled facts, * at the answer) and `integrity/unsupported-argument` (a value * against what was served, at dispatch). Those two ask "did this * value come from a result at all?"; this asks "did it come ONLY * from results the model itself set aside?". * Role: core/ layer leaf. Two moments call it: `stages/route.ts · * judgeEvidence` over the values the extractor found in the answer * (the gate's `EvidenceVerdict.grounded`), and `stages/toolCalls.ts` * over the DATA values of a call's arguments (`groundedArgumentValues` * below). Both hand the rows to `ledger.ts · recordFindings`, the one * writer. * Emits: N/A — the writer emits `agentfootprint.findings.contingent`. * * ## The owner's ask, and the rule that answers it * * Agents build theorem towers on unverified lemmas — a value read in step * two, judged noise in step three, and used in the answer anyway, where it * reads exactly like a fact — and the staircase comes down as circular. The * ledger already has the standings the model DECLARED (`fact` / `open` / * `noise` / `ruled-out`); the evidence module already knows which result * carried which value. The rule joins the two and infers nothing: * * a value the model USES — in its final answer, or as an argument of a * later call — that came from a result the model itself declared `open`, * `noise` or `ruled-out` is recorded as CONTINGENT. * * The fences, because they are the rule's honesty: * - DECLARED STANDINGS ONLY. A carrier with no standing row is undeclared — * not "unverified" — and the rule says nothing about it: an undeclared * carrier means the value stands. * - THE LAST STANDING PER RESULT IS CURRENT. Read off `foldLedger(...) * .standingOf`, the ledger's own fold — never a second fold here. * - EVERY CARRIER NON-FACT. A value several results carried is contingent * only when every one of them holds `open`, `noise` or `ruled-out`; one * `fact` carrier and the value stands. A value the corpus lists more * carriers for than it keeps (`ValueCarriers.truncated`) is NOT judged — * "every carrier" cannot be read off a prefix — and NOTHING is judged from * a corpus whose token ceiling was hit (`EvidenceCorpus.truncated`): the * results after the cut carried nothing into the index, so a `fact` * carrier past it is invisible and a row read off the prefix would be * false. The gate downgrades itself to record-only under the same flag; * this check files nothing. * - THE SAME QUESTION THE GATE ASKED. A grounded value carries the exact * spellings the gate looked it up under (`EvidenceVerdict.grounded[].forms`, * `extract.ts · candidateForms`); the carriers are read under those and * no wider set. One row per VALUE: two spellings of one value in one * answer (`0xef0101`, `ef0101`) share a canonical form * (`normalize.ts · canonicalForm`) and file once. * - THE EXTRACTOR DECIDES WHAT IS A VALUE. Both moments read * `evidence/extract.ts`'s DATA rule (a digit and a distinctive shape, or a * declared shape), so a word is never a tower and an all-letters name * needs the shape the gate would need. Exempt values — the person's * message, the app's prompt — are never contingent: the model had them * from a source it never judged. * - ONE ROW PER VALUE PER MOMENT, never per carrier; the carriers ride the * row, in wire order, each with the standing that made it one. At * dispatch, every standing in the batch's `_findings.previous` is filed * BEFORE the check, so a standing declared on a sibling call of the same * batch governs this call's arguments too. * - THIS TURN ONLY. The carriers are the current turn's results * (`EvidenceCorpus.carriers`); a result of an earlier turn of a continued * conversation, whatever its standing, carries nothing here — a value * taken from it is not judged and gets no mark. * * Both doors must be armed for any of this to run — `.findings()` (the * standings) and `.namesAndNumbersFromEvidence()` (the corpus) — and an * agent with one or neither records the bytes it always did. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.contingentRowsOf = exports.groundedArgumentValues = exports.hasSetAsideStanding = void 0; const argumentLeaves_js_1 = require("../../../integrity/argumentLeaves.js"); const extract_js_1 = require("../evidence/extract.js"); const normalize_js_1 = require("../../../lib/normalize.js"); const types_js_1 = require("./types.js"); /** The standings the rule reads: everything a model can declare short of standing on a result. */ const SET_ASIDE = ['open', 'noise', 'ruled-out']; /** * Whether any result's CURRENT standing is one the rule reads. The cheap * question both moments ask first: a ledger of facts alone — or of bases * alone — can produce no contingent row, so no corpus need be walked. */ function hasSetAsideStanding(standingOf) { for (const row of standingOf.values()) if (SET_ASIDE.includes(row.standing)) return true; return false; } exports.hasSetAsideStanding = hasSetAsideStanding; /** * The DATA values of a call's arguments that the corpus holds, each with * the spellings it was looked up under, distinct by value, in argument * order, exempt values left out — the dispatch moment's twin of * `EvidenceVerdict.grounded`, asked with the gate's own `candidateForms`. * Every string leaf (`integrity/argumentLeaves.ts · stringLeaves`, the one * walk both argument checks share) is read by the extractor's rule, so a * number typed as a number, a word, and a short identifier are not * candidates here any more than they are in an answer. */ function groundedArgumentValues(args, gate, corpus, exempt) { const out = []; const seen = new Set(); for (const leaf of (0, argumentLeaves_js_1.stringLeaves)(args, '')) { for (const candidate of (0, extract_js_1.extractCandidates)(leaf.value, gate, undefined, corpus.values)) { if (seen.has(candidate.value)) continue; seen.add(candidate.value); const forms = (0, extract_js_1.candidateForms)(candidate); if (forms.some((f) => exempt.has(f))) continue; if (forms.some((f) => corpus.values.has(f))) out.push({ value: candidate.value, forms }); } } return out; } exports.groundedArgumentValues = groundedArgumentValues; /** * The rule: one row per value of `values` whose every carrier holds a * set-aside standing. `values` are the grounded values with the spellings * they were looked up under (`EvidenceVerdict.grounded`, or * `groundedArgumentValues`); the carriers are read under exactly those * spellings, each result once, in wire order. Two spellings of one value * share a canonical form and file one row. A corpus whose token ceiling was * hit (`truncated`) files nothing — see the header. */ function contingentRowsOf(values, corpus, standingOf, declaredOn, iteration) { if (corpus.truncated) return []; const on = declaredOn === 'answer' ? 'answer' : { toolCallId: declaredOn.toolCallId }; const rows = []; const seen = new Set(); for (const { value, forms } of values) { const canonical = (0, normalize_js_1.canonicalForm)(value); if (seen.has(canonical)) continue; seen.add(canonical); const carriers = carriersOf(forms, corpus.carriers); if (carriers === undefined || carriers.length === 0) continue; const judged = []; let stands = false; for (const toolCallId of carriers) { const row = standingOf.get(toolCallId); // Undeclared, or stood on: the value stands, whatever the others say. if (row === undefined || !SET_ASIDE.includes(row.standing)) { stands = true; break; } judged.push({ toolCallId, standing: row.standing }); } if (stands) continue; rows.push({ kind: 'contingent', declaredOn: on, value: clipValue(value), carriers: judged, iteration, }); } return rows; } exports.contingentRowsOf = contingentRowsOf; /** * Every result that carried any of the spellings looked up, each once, in * the order the corpus listed them; `undefined` when any spelling's list * was cut — the rule needs the whole list or none. */ function carriersOf(forms, carriers) { const out = []; for (const form of forms) { const entry = carriers.get(form); if (entry === undefined) continue; if (entry.truncated === true) return undefined; for (const id of entry.toolCallIds) if (!out.includes(id)) out.push(id); } return out; } /** The one cut a contingent value gets — the `ledger.ts · clipText` law at `CONTINGENT_VALUE_CHARS`. */ function clipValue(value) { if (value.length <= types_js_1.CONTINGENT_VALUE_CHARS) return value; return `${value.slice(0, types_js_1.CONTINGENT_VALUE_CHARS)} …[clipped ${value.length - types_js_1.CONTINGENT_VALUE_CHARS} chars]`; } //# sourceMappingURL=contingent.js.map