UNPKG

agentfootprint

Version:

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

101 lines (100 loc) • 5.36 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 type { Coverage, CoverageLedgerDeclaration, CoveredResult, DeclaredCoveredResult, DeclaredLedgerDeclaration } from './types.js'; 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 declare const COVERAGE_NOTE: string; /** * Return a verdict with its own boundary attached. * * The model reads `{ af_coverage: {…}, result: <your value> }` — boundary * first, deliberately: a limit placed after a long result is a limit that gets * skimmed past. The framework records the ledger and, with * `.limitsTravelWithTheAnswer()` configured, appends it to the run's final * answer where the model cannot drop it. * * `period` (honesty step 7b) says what time the read behind the value * covered — `{ queried, held, readAt? }`, ISO 8601 instants with a zone; it is * a boundary on its own, served inside `af_coverage` before `result`, and the * results layer judges it. `coverage()` takes no `provenance`. * * `inProgress` says what the read found still RUNNING — its outcome not known * yet (a backup in progress, a replication session still synchronizing). The * tool decides what is in flight; this library never reads a vendor's state * name. Served as `in_progress` (the dispatch door adds one static clause after * the note), recorded, and printed under `.limitsTravelWithTheAnswer()`; it * never changes the answer's standing (`inProgress.ts`). It needs `checked` * beside it — an item in progress is ground the call read. * * Refuses (throws, where it is called) a boundary that declares nothing, a * malformed item or period, and any key the boundary does not have — naming the * spelling meant when the key is a casing slip (`not_checked` → * `notChecked`), so a list declared from plain JavaScript or JSON cannot * vanish without a word. Every refusal starts `refused: `: inside `execute` * it becomes the call's error result, which the model reads. * * @example the highest-stakes tool in a triage agent * defineTool({ * name: 'replication_health', * description: 'Replication health across the estate', * inputSchema: { type: 'object', properties: {} }, * execute: async () => { * const { verdict, ndmTimedOut } = await checkReplication(); * return coverage(verdict, { * checked: ['SRDF pair state on all 4 arrays (live query)'], * notChecked: ndmTimedOut * ? [{ what: 'NDM migration sessions', why: 'the API timed out — ask again' }] * : [], * cannotCover: [ * { what: 'host-side multipathing', why: 'no collector runs on the ESX hosts' }, * ], * }); * }, * }); */ export declare function coverage<T>(content: T, decl: DeclaredLedgerDeclaration): DeclaredCoveredResult<T>; export declare function coverage<T>(content: T, decl: CoverageLedgerDeclaration): CoveredResult<T>; /** The ledger's coverage, in the normalized three-list shape. */ export declare function coverageOfLedger(covered: CoveredResult): Coverage;