UNPKG

agentfootprint

Version:

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

129 lines (128 loc) • 8.21 kB
/** * findings/unsettled — "nothing was found" is not "ruled out". * * Pattern: one pure rule over (a standing the model just declared, the * result the model was served for it, the coverage the dispatch * door recorded); the sibling of `contingent.ts`. That one asks * "did the model build on a result it had set aside?"; this one * asks "did the model set something aside on a result that held * nothing?". * Role: core/ layer leaf. The two moments that file standings — * `stages/toolCalls.ts` (a call's `_findings.previous`) and * `stages/route.ts` (the answer's) — pass their standing rows * through `withUnsettledRows` on the way to `ledger.ts · * recordFindings`, the one writer, so a derived row lands in the * same write, immediately after the standing it is beside. * Emits: N/A — and the writer emits nothing for the row either (the * conflict row's precedent; see `ledger.ts · recordFindings`). * * ## The owner's ask, and the rule that answers it * * A model rules out an HBA path because an HBA lookup keyed on a storage * cluster's name found nothing — and the lookup's own envelope said it never * checked whether that name is a host at all. The model's `ruled-out` is its * word and stays its word: the library never writes a standing (round 1 of * the design re-filed it as `open`, which made the library a second writer * of the model's word — docs/design/2026-09-honest-answer-ledger.md § 14, * R12). What the library adds is a row BESIDE it: * * a `ruled-out` standing whose ONLY witness is an absence gets an * `unsettled-by-absence` row, keyed to the standing's `toolCallId`, * carrying what the envelope says was not checked and its `try_instead` * as printed. * * The fences, because they are the rule's honesty: * - TWO FACTS, TWO OWNERS, BOTH REQUIRED. "Did the tool return an absence?" * has one owner: the dispatch door (`stages/toolCalls.ts · * declareCoverage`), which reads the value the tool RETURNED, or the * `absence` a `requestInput` it raised declared (9.114.0, * `stages/toolCalls.ts · declareRaisedAbsence`), through the one * recognizer (`semantics/coverage/read.ts · readCoverageResult`), delivers the * status `'absent'` (a returned one only — a raised one is not settled), * emits `tools.absent` and files the call's rows on * `AgentState.coverageDeclared`. This rule never re-reads a result to * decide that: an envelope a tool returned as TEXT (an MCP server in its * default text mode) was no absence at the door (no status, no event, no * row), so it is none here either, and the record holds one answer, not * two. "Was the model SERVED an absence?" has another owner: the result as * the model was served it (`offer.ts · knownResults`, the identity source * the standing itself resolved against). The door records the return * BEFORE the after-tool chain, the tool's own ceiling and placement act, * so an after-tool `deny` ("the model does not get to read this"), a * ceiling's refusal, a placement ticket, a summary or an agent cap's cut * leave the door's rows saying absence while the model read something * else. A ruling-out rests on what the model READ: the row is filed only * when both hold, and it never quotes a word the model was not served. * - THE ROW'S WORDS ARE WHAT THE MODEL WAS SERVED. The served string's * LEADING JSON object — the dispatch loop serves the value's * serialization and joins any framework note AFTER it (a step * boundary's, a refused effect's, the repeated-call note) — read by the * one recognizer, bare or bounded by `coverage()`. `notChecked` / * `cannotCover` are that object's lists merged in the recognizer's order * (a bounding ledger first, then the absence; repeats dropped), so a * scrub the chain applied is honored; `tryInstead` is its absence's * `try_instead`, carried byte for byte when it is a non-blank string — * never parsed and never read for a tool name (a tool name found inside * prose is an inference). Every item is read, never trusted: copied as * `{ what, why? }`, and one that names no ground left out — a row the * checkpoint door would refuse is never filed. The door's own copies are * the witness that the tool returned an absence, and nothing more: they * are never quoted. * - RULED-OUT ONLY. `open` is already unsettled in the model's own word, and * `noise` says the result held nothing worth reading — which an absence * is. Only a ruling-out claims more than its witness can hold. * - A RECORD OF A CHECK. The row is written once, at the standing's moment, * and never recomputed (`types.ts · UnsettledByAbsenceRow`); which rows are * CURRENT is the ledger's one fold (`ledger.ts · foldLedger`). * - TOTAL. Every reader here takes what other code wrote and refuses a shape * it cannot read by filing nothing, never by throwing: a derived row must * never fail the run. * * Not read, and named in the README so nobody reads it as covered: a zero-row * array (no door records a zero-row reading of the value a tool returned, and * counting the SERVED string would count a text `'[]'` that * `integrity/empty-lookup/check.ts · readLookupResult` declines — a second * answer), a result whose door rows are not on this run's state — an * earlier turn of a continued conversation, or a run before `resumeOnError` * (`coverageDeclared` is per run and not on the `AgentRunCheckpoint`) — a * lookup that PAUSED on its miss (`requestInput`'s `absence`, 9.114.0): the * door's rows hold its miss, but on resume the model is served the person's * `InputResponseResult` for that call, which is no absence, so a ruling-out * on it files nothing — and * the envelope's TYPED suggestion, `try_instead_tool`: the row carries the * `try_instead` string alone, because nothing in this release reads a typed * tool off the row (the join that would, `source-not-consulted`, is not * built). * * `.findings()` must be armed for any of this to run — both call sites sit * inside the ledger's gate — and the door's key is read only for a * `ruled-out` standing whose result was SERVED as an absence, so a run whose * model rules out no such result records the bytes it always did. */ import type { DeclaredCoverage } from '../../../semantics/coverage/types.js'; import type { PreviousResult } from './ledger.js'; import type { FindingsRow, StandingRow, UnsettledByAbsenceRow } from './types.js'; /** * The rule for ONE standing: the derived row beside it, or nothing. Only a * `ruled-out` standing on a result the run identified (the same `known` list * `ledger.ts · standingRowsFrom` resolved it against) whose served string * reads as an absence AND whose call the door recorded as one (`declared` is * `AgentState.coverageDeclared`). */ export declare function unsettledRowOf(standing: StandingRow, known: readonly PreviousResult[], declared: readonly DeclaredCoverage[]): UnsettledByAbsenceRow | undefined; /** * The standings in declaration order, each followed immediately by its * derived row when the rule files one — what both moments hand to * `recordFindings`. The standing rows are the caller's objects, pushed * as they are: a batch the rule files nothing beside comes back row for row. * * `readDeclared` hands over `AgentState.coverageDeclared` and is called at * most once, and only for a standing the rule reads (a `ruled-out` on an * identified result) whose served result reads as an absence — a pure * reading of strings the moment already holds, taken first. A tracked read * is visible to every recorder (a narrative line, a `readKeys` entry under * `writeProvenance`), so an armed run whose model rules out only results * that held something reads nothing it did not read before (the * `contingent.ts · hasSetAsideStanding` gate's precedent). */ export declare function withUnsettledRows(standings: readonly StandingRow[], known: readonly PreviousResult[], readDeclared: () => readonly DeclaredCoverage[]): FindingsRow[];