agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
145 lines • 7.87 kB
JavaScript
;
/**
* 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.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.coverageOfLedger = exports.coverage = exports.COVERAGE_NOTE = exports.readCoverageLedger = exports.COVERAGE_MARKER = void 0;
const inProgress_js_1 = require("./inProgress.js");
const items_js_1 = require("./items.js");
const period_js_1 = require("./period.js");
const refusal_js_1 = require("./refusal.js");
const recognize_js_1 = require("./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.
const recognize_js_2 = require("./recognize.js");
Object.defineProperty(exports, "COVERAGE_MARKER", { enumerable: true, get: function () { return recognize_js_2.COVERAGE_MARKER; } });
Object.defineProperty(exports, "readCoverageLedger", { enumerable: true, get: function () { return recognize_js_2.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.
*/
exports.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.';
function coverage(content, decl) {
const fn = 'coverage';
if (typeof decl !== 'object' || decl === null) {
throw (0, refusal_js_1.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.`);
}
(0, refusal_js_1.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 === recognize_js_1.DECLARED_CHECKED;
const checked = declared
? []
: (0, items_js_1.normalizeCoverageList)('checked', decl.checked, false);
const notChecked = (0, items_js_1.normalizeCoverageList)('notChecked', decl.notChecked, false);
const cannotCover = (0, items_js_1.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 = (0, period_js_1.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 = (0, inProgress_js_1.mintInProgress)(decl.inProgress);
if (decl.inProgress != null && checked.length === 0 && !declared) {
throw (0, refusal_js_1.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 (0, refusal_js_1.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: recognize_js_1.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: exports.COVERAGE_NOTE,
},
result: content,
};
return minted;
}
exports.coverage = coverage;
/** The ledger's coverage, in the normalized three-list shape. */
function coverageOfLedger(covered) {
const m = covered.af_coverage;
return {
checked: m.checked ?? [],
notChecked: m.not_checked ?? [],
cannotCover: m.cannot_cover ?? [],
};
}
exports.coverageOfLedger = coverageOfLedger;
//# sourceMappingURL=ledger.js.map