UNPKG

agentfootprint

Version:

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

139 lines • 7.22 kB
/** * ledger — what a clean result does NOT rule out. * * Pattern: a wrapper the framework recognizes, minted by a helper (the * `absent()` shape one level up: same vocabulary, different claim). * Role: core/ layer, pure. `coverage()` is what a tool author writes; * `readCoverageLedger` is what the dispatch loop calls. * Emits: N/A (the caller emits `agentfootprint.tools.coverage_declared`). * * ## The problem, stated as narrowly as it deserves * * An agent answers confidently from partial coverage. "Everything looks fine" * is produced from four checks that passed, and arrives with no way to tell * whether *fine* means **verified** or **unexamined** — whether the fifth * thing was healthy or was never looked at. The evidence gate (9.35.0) is the * sibling of this: the gate catches invented VALUES, this catches unstated * LIMITS. Both are ways an answer can be false while every token in it is * real. * * A ledger is not a caveat and not a disclaimer. It is three lists the TOOL * knows and the model does not: what it checked, what it did not check, and * what it can never cover. Only the tool knows the third one, which is why * this cannot be prompt engineering. * * ## Why the wrapper, and not a field on the result * * The alternative — teaching the framework to read a `coverage` key off any * returned object — would make every domain object that happens to have one * change behavior. The wrapper is opt-in by construction: `coverage(x, …)` * produces a shape that did not exist before, so nothing that ever ran can * become a ledger by accident. Same argument as the effects envelope's strict * recognizer, same guarantee. */ import { mintInProgress } from './inProgress.js'; import { normalizeCoverageList } from './items.js'; import { mintPeriod } from './period.js'; import { refusal, refuseUnknownKeys } from './refusal.js'; import { DECLARED_CHECKED } from './recognize.js'; /** * The keys `coverage()`'s declaration has — the three lists and, since honesty * step 7b, the period the value's read covered — tied to the type in BOTH * directions, the `COVERAGE_DECLARATION_KEYS` way. That list stays the three * lists alone: a described result's `coverage` shares it, and takes no period * (its period is top-level). */ const LEDGER_DECLARATION_KEYS = Object.keys({ checked: true, notChecked: true, cannotCover: true, period: true, inProgress: true, }); // The recognizer lives in the leaf `recognize.ts` (a post-hoc reader — the one // emptiness reader, the standing fold — asks it without loading this mint); // re-exported here so every import keeps working. import { COVERAGE_MARKER, readCoverageLedger } from './recognize.js'; export { COVERAGE_MARKER, readCoverageLedger }; /** * The static sentence every ledger carries. The last clause is the OFFER half * of survival — the model is told to carry the limits into its answer. The * ENFORCEMENT half (`.limitsTravelWithTheAnswer()`) does not depend on the * model obeying it; see `answer.ts` for why both exist. * * "the call this result answers", not "this call" (9.86.1): the note rides a * tool result, which is re-read on every later call of the turn, and a bare * `this call` there denotes whichever call is reading it. The container * deictic's own anchor names the call instead. */ export const COVERAGE_NOTE = 'This result covers only what `checked` lists. `not_checked` is ground the call this result ' + 'answers did not look at, and `cannot_cover` is ground this tool can never see — a clean ' + 'result here is ' + 'NOT evidence about either, and no retry changes `cannot_cover`. Carry these limits into ' + 'any answer you build on this result.'; export function coverage(content, decl) { const fn = 'coverage'; if (typeof decl !== 'object' || decl === null) { throw refusal(`${fn}() takes the result and its boundary — coverage(result, { checked?, notChecked?, ` + `cannotCover?, period?, inProgress? }). To return a result with no declared boundary, ` + `return it bare.`); } refuseUnknownKeys(decl, LEDGER_DECLARATION_KEYS); // `checked: 'declared'` — the tool's `search` declares the ground; the // dispatch funnel composes it (the `absent()` twin). It is a boundary, so a // ledger declaring only it is not the empty ledger refused below. const declared = decl.checked === DECLARED_CHECKED; const checked = declared ? [] : normalizeCoverageList('checked', decl.checked, false); const notChecked = normalizeCoverageList('notChecked', decl.notChecked, false); const cannotCover = normalizeCoverageList('cannotCover', decl.cannotCover, true); // Honesty step 7b: the period the value's read covered, by the ONE period // rule (`period.ts` · `mintPeriod`) — refused here, in camelCase, when // malformed. A period IS a declared boundary (in time), so a ledger that // declares only a period is not the empty ledger refused below. const period = mintPeriod(decl.period); // What the read found still running (`inProgress.ts` — the ONE rule set). // Refused without `checked` whether or not the list is empty: a rule that // fired only when something happened to be running would pass every test // and refuse on the first busy night in production (the 7a′ lesson). const inProgress = mintInProgress(decl.inProgress); if (decl.inProgress != null && checked.length === 0 && !declared) { throw refusal('`inProgress` needs `checked` beside it — an item still running is ground the call ' + 'read, so say what the call read.'); } if (!declared && checked.length === 0 && notChecked.length === 0 && cannotCover.length === 0 && period === undefined) { throw refusal(`all three lists are empty, so this ledger declares no boundary at all — it ` + `would tell a reader nothing while looking like it did, which is worse than saying ` + `nothing. Name what you checked, what you skipped, or what you can never see; or ` + `return the result bare.`); } // Built untyped, then given the type its `checked` decides (the overloads). const minted = { af_coverage: { ...(declared ? { checked: DECLARED_CHECKED } : checked.length > 0 && { checked }), ...(notChecked.length > 0 && { not_checked: notChecked }), ...(cannotCover.length > 0 && { cannot_cover: cannotCover }), ...(inProgress.length > 0 && { in_progress: inProgress }), // Serialized BEFORE `result`, with the lists, so a truncated view keeps it. ...(period !== undefined && { period }), note: COVERAGE_NOTE, }, result: content, }; return minted; } /** The ledger's coverage, in the normalized three-list shape. */ export function coverageOfLedger(covered) { const m = covered.af_coverage; return { checked: m.checked ?? [], notChecked: m.not_checked ?? [], cannotCover: m.cannot_cover ?? [], }; } //# sourceMappingURL=ledger.js.map