UNPKG

agentfootprint

Version:

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

361 lines • 20 kB
"use strict"; /** * absent — an absence that names its own coverage. * * Pattern: minted by a helper, RECOGNIZED by the framework (the tool-effects * envelope precedent). A return shape the framework does not * understand is a convention, and a convention cannot set a status, * keep a value out of the evidence corpus, or stop a retry loop. * Role: core/ layer, pure. The dispatch loop calls `readAbsence` at the * execute boundary; `absent()` is what a tool author writes. * Emits: N/A (the caller emits `agentfootprint.tools.absent`). * * ## The direction-of-error argument — the whole point of this file * * A tool that finds nothing returns *something*: an empty array, a `null`, a * sentence. From any of those a model cannot tell **"I looked and there is * nothing"** from **"I could not look"**. That confusion is not symmetric, * and the asymmetry is what makes it worth a primitive: * * • a *nothing-found* misread as an *outage* sends someone to investigate a * collector that is working perfectly — expensive, and self-correcting. * • an *outage* misread as *nothing-found* declares a system healthy that * was never checked — cheap, silent, and wrong in the direction that * hurts. * * So the two must not share a shape. An error is a `role: 'tool'` result with * `error: true`, a message, and no coverage. An absence is this: a result * that RAN, carries the ground it covered, and says out loud that asking * again changes nothing. * * ## Why the note is never interpolated * * Tool results are the evidence gate's corpus, so an absence that echoed the * model's own arguments into that corpus would GROUND every identifier a model * invented, as long as it handed the invention to one tool that found nothing. * That is laundering an invention through a failed lookup — the same bug * `evidence/frames.ts` exists to stop on the other side of the conversation. * * The gate therefore withholds `looked_for` (see `coverage/evidence.ts`): it * is the one field whose job is to quote the request. The record-only item * keys `short` and `kind` are withheld with it — a short form restates * `what`, so nothing is lost, and it is the field an author is most tempted to * personalise. Everything else the absence carries — the coverage lists, the * author's `tryInstead` and `tryInsteadTool`, any extra key the tool attached * to the envelope — is the TOOL speaking about the world and does ground, * because an answer that follows the absence's own advice must not be called * ungrounded for doing so. * The note is kept free of interpolation for the same reason the exclusion * exists: static library text has nothing of the caller's to leak. * * ## A suggestion to try another tool is typed, never parsed (9.113.0) * * `tryInstead` is ONE sentence, for the model; nothing in this library reads * a tool name out of it. When the sentence points at another tool, the author * names that tool beside it — `tryInsteadTool: { tool, why? }` — and the * envelope carries it under its OWN key, `try_instead_tool`. Its own key, not * `try_instead`: that field has been a string since it shipped and every * reader typed against `ToolAbsence` reads it as one, so a second shape there * would be dropped by each of them without a word. `tools.absent` carries * both, as declared. ONE rule set decides what each is ({@link readSentence}, * {@link readToolSuggestion}): `absent()` refuses by it and the recognizer * reads by it, so a mint and a read cannot disagree. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.tryInsteadToolOfAbsence = exports.tryInsteadOfAbsence = exports.coverageOfAbsence = exports.absent = exports.ABSENCE_NOTE_HELD_ONLY = exports.ABSENCE_NOTE_COMPOSED = exports.ABSENCE_NOTE = exports.readAbsence = exports.ABSENCE_MARKER = void 0; const described_js_1 = require("../described.js"); const toolName_js_1 = require("../../lib/toolName.js"); const among_js_1 = require("./among.js"); const items_js_1 = require("./items.js"); const period_js_1 = require("./period.js"); const refusal_js_1 = require("./refusal.js"); const resultFacts_js_1 = require("./search/resultFacts.js"); // The recognizer lives in the leaf `recognize.ts` (a post-hoc reader asks it // without loading this mint); re-exported here so every import keeps working. const recognize_js_1 = require("./recognize.js"); Object.defineProperty(exports, "ABSENCE_MARKER", { enumerable: true, get: function () { return recognize_js_1.ABSENCE_MARKER; } }); Object.defineProperty(exports, "readAbsence", { enumerable: true, get: function () { return recognize_js_1.readAbsence; } }); /** * The static sentence every absence carries. Says the three things the field * implementation proved a model needs: that the call SUCCEEDED, that nothing * was substituted, and that a retry is futile. The third clause is the one * that ends the loop. */ // One literal on purpose: its TYPE is the sentence, so a redacted record keeps // it as the library's own words (`redaction/knownStrings.ts`). exports.ABSENCE_NOTE = 'The search ran and matched nothing. This is an ANSWER, not an error: nothing failed, nothing was substituted for what was asked, and calling this tool again with the same arguments returns this same result. `checked` is the ground this answer covers; anything under `not_checked` or `cannot_cover` is ground it does NOT cover, and reaching that needs a different question, not a retry.'; /** * The note a COMPOSED absence is served (`Tool.search`): `checked` there is * what the library composed from the call's arguments and the tool's declared * defaults, so the note cannot say "nothing was substituted for what was * asked" — a declared default, or an argument the call left out, may have * narrowed the search. It keeps what stays true (the call ran, a retry * returns the same) and points at the gaps. Static, never interpolated. */ // One literal on purpose, as `ABSENCE_NOTE`: its TYPE is the sentence, so a // redacted record keeps it as the library's own words. exports.ABSENCE_NOTE_COMPOSED = 'The search ran and matched nothing — the call did not fail, and calling this tool again with the same arguments returns this same result. `checked` is the ground this answer covers, as the arguments of the call and the defaults the tool declares set it: where an argument was not sent, a default the tool applies may have narrowed the search. Anything under `not_checked` or `cannot_cover` is ground it does NOT cover, and reaching that needs a different question, not a retry.'; /** * The note the MODEL is served in place of {@link ABSENCE_NOTE} when the * absence's own period is `partly-held` or `not-held` (honesty step 7b, bench * round 1) — `coverage/read.ts` · `strip` swaps it in and appends the period's * one clause. It keeps what stays true (the call ran, a retry returns the same * result) and drops the two completeness claims that do not: "nothing was * substituted for what was asked" and `checked` as the whole ground. Static, * never interpolated, never minted — the tool's own output keeps * {@link ABSENCE_NOTE}. */ exports.ABSENCE_NOTE_HELD_ONLY = 'The search ran and matched nothing — the call did not fail, and calling this tool again ' + 'with the same arguments returns this same result. `checked` is the ground this answer ' + 'covers inside `period.held` only; anything under `not_checked` or `cannot_cover` is ground ' + 'it does NOT cover, and reaching that needs a different question, not a retry.'; /** * The typed tool's keys, tied to {@link TryInsteadTool} in BOTH directions: a * key the interface gains, or one listed here that it does not have, fails to * compile. Any other key on a value is refused, so a misspelt `why` cannot * vanish without a word. */ const TOOL_SUGGESTION_KEYS = Object.keys({ tool: true, why: true, }); /** * The declaration's keys, tied to {@link AbsenceDeclaration} in BOTH * directions, the {@link TOOL_SUGGESTION_KEYS} way. Any other key is refused, * naming the spelling meant when it is a slip of one of these: a declaration * fed from plain JavaScript or JSON with `not_checked` used to mint without * that list, and the limit it declared vanished without a word. */ const ABSENCE_DECLARATION_KEYS = Object.keys({ what: true, checked: true, notChecked: true, cannotCover: true, tryInstead: true, tryInsteadTool: true, provenance: true, period: true, resolved: true, paging: true, among: true, }); /** * `undefined` and `null` both mean "not given" for the typed tool and its * `why`. A JSON producer writes a missing optional value as `null`, and * `tryInstead: null` read as no suggestion before 9.113.0 — refusing it would * turn a nothing-found into a tool error, the direction of error this file * exists to prevent. */ const notGiven = (raw) => raw === undefined || raw === null; /** An object literal's shape — `{…}` or `Object.create(null)`; never an * array, a Date, a boxed string or any other class instance. */ const isPlainObject = (raw) => { if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return false; const prototype = Object.getPrototypeOf(raw); return prototype === Object.prototype || prototype === null; }; /** * The rule for the SENTENCE (`tryInstead`, `try_instead` on the wire): * trimmed, and one that trims to nothing is no suggestion, as it always was. * * ONE non-string is refused: a plain object. That is the typed form * `{ tool, why? }` written into the sentence's slot, and the refusal points it * at `tryInsteadTool` rather than drop the author's tool without a word. Any * OTHER value that is not a string — `null`, `false` from * `tryInstead: cond && '…'`, a number, a list, a Date — reads as no * suggestion, exactly as it did before 9.113.0: `absent()` runs inside a * tool's `execute`, so refusing it would turn a nothing-found the 9.112.2 * loop delivered as an absence into that call's error result. */ function readSentence(raw) { if (typeof raw === 'string') { const sentence = raw.trim(); return { value: sentence === '' ? undefined : sentence }; } if (isPlainObject(raw)) { return { problem: '`tryInstead` is the sentence the model reads, and only that. To name the other ' + 'tool as data, give { tool, why? } as `tryInsteadTool`, beside the sentence.', }; } return { value: undefined }; } /** * The rule for the TYPED TOOL (`tryInsteadTool`, `try_instead_tool` on the * wire): `{ tool, why? }`, where `tool` is a non-empty name and `why`, when * given, says something. Both are trimmed; `why: null` reads as omitted. The * name is recorded as declared: never looked up (a provider may serve it on a * later iteration), and held to no charset here — which names a provider * accepts is `lib/toolName.ts` · `assertValidToolName`'s question, and * `absent` asks the dev-mode warning that owns it. * * Why ONE tool and not a list: every suggestion on record names at most one * other tool, and where a sentence joins that tool to "or widen the window", * the rest of the advice stays in the sentence. A list would be a second * shape of this field for every reader to narrow — the cost this key exists * to spare `try_instead`. */ function readToolSuggestion(raw) { if (notGiven(raw)) return { value: undefined }; if (Array.isArray(raw)) { return { problem: '`tryInsteadTool` names ONE tool — { tool, why? }, never a list. Say the rest of the ' + 'advice in the `tryInstead` sentence.', }; } if (typeof raw !== 'object') { return { problem: `\`tryInsteadTool\` must be { tool, why? } — got ${typeof raw}. For a sentence, use ` + '`tryInstead`; to suggest nothing, omit the field.', }; } const value = raw; const unknownKey = Object.keys(value).find((key) => !TOOL_SUGGESTION_KEYS.includes(key)); if (unknownKey !== undefined) { return { problem: `\`tryInsteadTool\` has an unknown key '${unknownKey}' — the form is { tool, why? }, ` + 'and a misspelt `why` would otherwise vanish without a word.', }; } const { tool, why } = value; if (typeof tool !== 'string' || tool.trim() === '') { return { problem: '`tryInsteadTool.tool` must name the tool to try — its registered name, e.g. ' + "{ tool: 'cluster_inventory', why: 'it lists the collected names' }. The name is " + 'recorded as declared and never looked up (a provider may serve it).', }; } const name = tool.trim(); if (notGiven(why)) return { value: { tool: name } }; if (typeof why !== 'string' || why.trim() === '') { return { problem: `\`tryInsteadTool.why\` (for '${name}') must be a sentence — say why that tool, or ` + 'omit the field.', }; } return { value: { tool: name, why: why.trim() } }; } function absent(decl) { const fn = 'absent'; if (typeof decl !== 'object' || decl === null) { throw (0, refusal_js_1.refusal)(`${fn}() takes a declaration — { what, checked, notChecked?, cannotCover?, tryInstead?, ` + `tryInsteadTool?, provenance?, period? }, or { what, among, … }.`); } (0, refusal_js_1.refuseUnknownKeys)(decl, ABSENCE_DECLARATION_KEYS); const what = typeof decl.what === 'string' ? decl.what.trim() : ''; if (what === '') { throw (0, refusal_js_1.refusal)(`\`what\` must say what was looked for (e.g. 'FLOGI entries on fc1/3'). An ` + `absence that cannot name what it did not find is indistinguishable from a tool that ` + `returned nothing by accident.`); } // `checked: 'declared'` — the tool's `search` declares the ground, and the // dispatch funnel composes it from the arguments the call ran with. The // envelope carries the literal until then; `readAbsence` does not read it. const declared = decl.checked === recognize_js_1.DECLARED_CHECKED; // `among` (the c6 fix): the list a missed name was looked up in. `null` // reads as omitted, as for every optional field. Its facet leads `checked` // and its gap leads `notChecked`, so the author can add ground, never // remove the gap. const raw = decl; const among = raw.among == null ? undefined : (0, among_js_1.mintAmong)(raw.among); if (among !== undefined && declared) { throw (0, refusal_js_1.refusal)("`among` and `checked: 'declared'` each compose an absence's ground — `among` for a " + "name looked up in a known list, `'declared'` for a tool whose `search` declares it. " + 'Give one.'); } if (among !== undefined && raw.provenance != null) { throw (0, refusal_js_1.refusal)('`provenance` and `among` both name a source and a time — `among` composes the ' + "absence's `provenance` from its own `source` and `measuredAt`, so give `provenance` " + 'only without `among`.'); } const authored = declared ? [] : (0, items_js_1.normalizeCoverageList)('checked', decl.checked, false); const checked = among === undefined ? authored : [among.facet, ...authored]; if (!declared && checked.length === 0) { throw (0, refusal_js_1.refusal)(`'${what}' — \`checked\` must name at least one source, window or population ` + `that WAS searched. An absence with no coverage is a null with extra steps: the reader ` + `still cannot tell "I looked and there is nothing" from "I could not look", which is ` + `the confusion this function exists to remove. If you genuinely could not look, that ` + `is an error — throw, or return a failure the model can retry.`); } const notCheckedAuthored = (0, items_js_1.normalizeCoverageList)('notChecked', decl.notChecked, false); const notChecked = among === undefined ? notCheckedAuthored : [...among.gaps, ...notCheckedAuthored]; const cannotCover = (0, items_js_1.normalizeCoverageList)('cannotCover', decl.cannotCover, true); const sentence = readSentence(decl.tryInstead); if (sentence.problem !== undefined) throw (0, refusal_js_1.refusal)(`'${what}' — ${sentence.problem}`); const tool = readToolSuggestion(decl.tryInsteadTool); if (tool.problem !== undefined) throw (0, refusal_js_1.refusal)(`'${what}' — ${tool.problem}`); const tryInstead = sentence.value; const tryInsteadTool = tool.value; // The name's charset is the tool registry's question: the owner warns in // dev mode and never throws — the verdict a `defineTool` of it would get. if (tryInsteadTool !== undefined) (0, toolName_js_1.warnIfInvalidToolName)(tryInsteadTool.tool); // Honesty step 7b — the source and time of the search, by the ONE provenance // rule `describedResult()` mints with (`semantics/described.ts` · // `mintProvenance`), and the period its read covered, by the ONE period rule // (`period.ts` · `mintPeriod`). Each refuses at this line, in camelCase. // `null` reads as omitted for either — a JSON producer's missing value. const provenance = among !== undefined ? (0, described_js_1.mintProvenance)(among.provenance) : raw.provenance == null ? undefined : (0, described_js_1.mintProvenance)(raw.provenance); const period = (0, period_js_1.mintPeriod)(decl.period); // The two facts a search tool's result states (`search/resultFacts.ts`). const resolved = (0, resultFacts_js_1.mintResolved)(decl.resolved); const paging = (0, resultFacts_js_1.mintPaging)(decl.paging); return { af_absent: true, outcome: 'nothing_found', looked_for: what, checked: declared ? recognize_js_1.DECLARED_CHECKED : checked, ...(notChecked.length > 0 && { not_checked: notChecked }), ...(cannotCover.length > 0 && { cannot_cover: cannotCover }), ...(provenance !== undefined && { provenance }), ...(period !== undefined && { period }), ...(resolved !== undefined && { resolved }), ...(paging !== undefined && { paging }), retry_returns_the_same: true, ...(tryInstead !== undefined && { try_instead: tryInstead }), ...(tryInsteadTool !== undefined && { try_instead_tool: tryInsteadTool }), note: exports.ABSENCE_NOTE, }; } exports.absent = absent; /** The absence's coverage, in the normalized three-list shape everything * downstream (the event, the answer block) reads. */ function coverageOfAbsence(absence) { return { checked: absence.checked ?? [], notChecked: absence.not_checked ?? [], cannotCover: absence.cannot_cover ?? [], }; } exports.coverageOfAbsence = coverageOfAbsence; /** * The absence's sentence, read by the rules `absent()` mints by. `undefined` * when it declared none, AND when it declared one those rules refuse: an * envelope minted elsewhere (a non-JS tool, a hand-built value) is read, * never repaired, and the absence itself is still an absence either way. */ function tryInsteadOfAbsence(absence) { return readSentence(absence.try_instead).value; } exports.tryInsteadOfAbsence = tryInsteadOfAbsence; /** * The absence's typed tool, read by the same rules — a copy, never the * object the envelope holds. `undefined` when it declared none, or one those * rules refuse (see {@link tryInsteadOfAbsence}). */ function tryInsteadToolOfAbsence(absence) { return readToolSuggestion(absence.try_instead_tool).value; } exports.tryInsteadToolOfAbsence = tryInsteadToolOfAbsence; //# sourceMappingURL=absent.js.map