UNPKG

agentfootprint

Version:

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

217 lines • 11.9 kB
"use strict"; /** * empty-lookup — the run itself produced this identifier, and the lookup for * it found nothing. Filed at the WRITE seam, as an advisory, never an * accusation. * * Pattern: pure function over (one finished call, the results its declared * producers served); the domain check, one seam later than * `unsupported-argument`. * Role: the decidable fragment of "did this tool answer, or did it only * appear to answer?". * * THE MEASURED FAILURE. A triage agent's reverse-lookup tool filtered a * column before a pivot, so the column did not exist yet and EVERY reverse * lookup returned an empty result — for every identifier, always. The tool * then answered SUCCESSFULLY with an empty list, and the agent reported in a * table, with confidence, that the device was "not currently logged in to any * port on the collected switches" and advised checking physical cabling. It * was logged in the whole time. Nothing in the framework noticed, because an * empty result from a broken filter is byte-identical to an empty result from * a genuine absence. * * WHAT THE LIBRARY ALREADY KNOWS, and this check is nothing more than the * join of the two: * (a) the identifier was GROUNDED — it came out of an earlier tool result * in THIS run, from a tool the consumer's author named in * `Tool.argumentsFrom`; * (b) the lookup keyed on it came back EMPTY. * When the run produced the identifier and the lookup for it finds nothing, * that is worth filing. It is not worth accusing anyone of. * * THE CEILING, and it is the whole reason this is an advisory: an empty * answer can be perfectly true. The device may exist and simply have no * logins right now. NOTHING IN THIS FILE CAN TELL THOSE APART, and nothing in * this file pretends to — the finding says "here is a pair worth a look", the * record carries the ceiling sentence verbatim, and the same finding is filed * for the true absence and the broken filter alike. A check that guessed * which one it was looking at would be inventing the certainty the field * failure was made of. * * DECLARED, NEVER INFERRED — the `argumentsFrom` precedent, THIRD use. A tool * is this check's subject only because its author said its arguments come * from another tool's results. One declaration now arms three seams: * `dangling-reference` asks whether the ground is still in reach when the * tool is OFFERED, `unsupported-argument` whether the value the model chose * came from that ground when the tool was CALLED, and this whether the ground * the run really did serve then found anything when the tool ANSWERED. * * WHAT COUNTS AS EMPTY — and what this refuses to judge. See * {@link readLookupResult}. A rowset is counted, never interpreted; an * absence is read from the marker its own helper mints. Every other shape a * tool has ever returned — a sentence, an object, a `null`, a claim ticket — * is BESPOKE: the library cannot see whether it holds rows, so the encounter * is `not-applicable` and says so in the ledger. That row is the point. A * check that silently skipped what it could not read would be the decoration * the disposition family exists to make impossible. * * Detection only. Nothing here blocks, retries, rewrites or delays anything; * the result the model reads is the result the tool returned. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.emptyLookupOf = exports.readLookupResult = exports.EMPTY_LOOKUP_CEILING = void 0; const argumentLeaves_js_1 = require("../argumentLeaves.js"); /** * THE CEILING, as one string with one owner. * * Quoted verbatim into every finding's message, into the check's README and * into its docs page, so the bound cannot drift out of one of them and leave * a reader thinking the library knows more than it does. */ exports.EMPTY_LOOKUP_CEILING = 'An empty result can be perfectly true — the thing may exist and simply have nothing to show ' + 'right now — so this is a place to look, never a verdict that anything is wrong.'; /** * READ a finished result's shape, or decline to. * * The two readable shapes, and why only these two: * * • a **rowset** — an array. Its length is COUNTED, not interpreted: zero * rows is zero rows, and no judgement about what the tool meant is * involved. This is the shape the recorded failure actually returned. * • an **absence** — the `absent()` envelope, whose author said in so many * words "the search ran and matched nothing". * * Everything else returns `undefined`, which the caller files as * `not-applicable`. A prose sentence, a bespoke `{ rows: [...] }` wrapper, a * `null`, a placement claim ticket — the library cannot see rows in any of * them, and guessing is how a checker starts lying. * * @param value the tool's own answer, as it returned it. * @param declaredAbsence whether `readAbsence` recognized the value. PASSED * IN rather than re-derived here on purpose: the `af_absent` marker has * exactly one owner (`semantics/coverage/absent.ts`), a second spelling of * a reserved word would eventually disagree with the first, and * `src/integrity/` stays a leaf that imports no agent code. */ function readLookupResult(value, declaredAbsence) { if (declaredAbsence) return { shape: 'absence', empty: true }; if (Array.isArray(value)) return { shape: 'rowset', empty: value.length === 0, rows: value.length }; return undefined; } exports.readLookupResult = readLookupResult; /** * Judge one finished call to an armed tool. * * @param call the call and what the library could read of its result. The * caller has already established the tool declared `argumentsFrom`; * nothing here re-decides who is a subject. * @param produced every tool result this run served BEFORE this call, with * the tool that produced each. Filtered to the declared producers here. * @param epoch the run iteration, stamped on every witness. */ function emptyLookupOf(call, produced, epoch) { // A shape the library cannot read is not a pass and not a fail. It is the // check looking at a subject that is out of its scope BY RULE, which is // exactly what `not-applicable` was minted for. if (call.reading === undefined) return { findings: [], disposition: 'not-applicable' }; // The lookup found something. Nothing to say — and a `checked-pass` rather // than silence, because a check that only speaks when it fires cannot be // told apart from one that was never wired. if (!call.reading.empty) return { findings: [], disposition: 'checked-pass' }; // Only the DECLARED producers ground anything here. The choice seam asks // the broader question (did ANYTHING the run served carry this value); this // one is narrower on purpose — `argumentsFrom` says where the value was // supposed to come from, and "it came from exactly there, and the lookup // still found nothing" is the pair worth a person's attention. const grounds = produced .filter((p) => call.argumentsFrom.includes(p.toolName) && p.text.length > 0) .map((p) => ({ toolName: p.toolName, lowered: p.text.toLowerCase() })); // The declared ground served nothing at all in this run, so there is no // corpus to join against and no identity edge between the two tools. Not a // pass — the check could not run. `unreachable` is the family's word for // exactly that, and it is the falsification instrument: an `empty-lookup` // row dominated by `unreachable` means apps are calling consumers before // their declared producers, and this check is watching a seam that is not // where their identifiers come from. if (grounds.length === 0) return { findings: [], disposition: 'unreachable' }; const consumer = { kind: 'tool', id: call.toolName }; const emptyPhrase = call.reading.shape === 'absence' ? 'a declared absence — the tool said the search ran and matched nothing' : 'a rowset with zero rows'; const findings = []; for (const leaf of (0, argumentLeaves_js_1.stringLeaves)(call.args, '')) { const value = leaf.value.trim(); // The same fences as the choice seam, for the same reasons and from the // same file: non-strings are never identifier-shaped, and below four // characters substring matching is noise in both directions. if (value.length < argumentLeaves_js_1.MIN_CHECKED_LENGTH) continue; const needle = value.toLowerCase(); // Substring, case-insensitive — deliberately THE SAME rule the choice // seam grounds by. The two checks must agree about what "this value came // from that tool" means, or a value could fall between them and be seen // by neither. const ground = grounds.find((g) => g.lowered.includes(needle)); if (ground === undefined) continue; const producer = { kind: 'tool', id: ground.toolName }; const witnesses = [ { subject: consumer, predicate: leaf.path, value: leaf.value, epoch, stratum: 'asserted', provenance: `tool call ${call.toolCallId}: argument '${leaf.path}' as the model chose it`, }, { subject: producer, predicate: leaf.path, value: 'served this value earlier in this run', epoch, stratum: 'asserted', provenance: `'${ground.toolName}' is a declared ground of '${call.toolName}' ` + `(Tool.argumentsFrom) and one of its results in this run carries the value`, }, { subject: consumer, predicate: leaf.path, value: `found nothing — ${emptyPhrase}`, epoch, stratum: 'asserted', provenance: `tool call ${call.toolCallId}: the result, as the tool returned it`, }, ]; findings.push({ kind: 'empty-lookup', seam: 'write', subjects: [consumer, producer], // The dot-path IS the discriminator, exactly as at the choice seam: two // arguments of one call are two notices. The stated limit is the same // one — the SAME argument re-chosen with a different value stays one // finding, and the ledger's `findings` count is what says how often. predicate: leaf.path, witnesses, epoch, // Doubt, not contradiction. Counted apart from real defects everywhere // the family reports, because this check can never know it found one. advisory: true, message: `'${call.toolName}' was called with ${leaf.path} = "${(0, argumentLeaves_js_1.clipValue)(value)}" — a value ` + `this run itself produced, in a result from '${ground.toolName}', which ` + `'${call.toolName}' declares as where its arguments come from — and the lookup came ` + `back with ${emptyPhrase}. That pairing is what makes it worth a look: the run had ` + `the identifier, so an empty answer is either a true absence or a lookup that could ` + `never have matched, and those two are byte-identical from here. ` + `${exports.EMPTY_LOOKUP_CEILING} Call id ${call.toolCallId}. Nothing here blocked the call, ` + `changed the result, or retried anything.`, }); } return { findings, disposition: findings.length > 0 ? 'checked-fail' : 'checked-pass' }; } exports.emptyLookupOf = emptyLookupOf; //# sourceMappingURL=check.js.map