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
TypeScript
/**
* 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[];