UNPKG

agentfootprint

Version:

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

484 lines (483 loc) • 23.6 kB
/** * coverage/types — the shared vocabulary of the two coverage primitives. * * Pattern: one value type, two consumers — `absent()` says "I looked HERE and * found nothing", `coverage()` says "my answer covers THIS and not * THAT". Both are statements about the same thing: the ground a * result stands on. Spelling them with one vocabulary is what lets * the answer-level block merge an absence's boundary with a * verdict's without translating between two grammars. * Role: core/ layer, pure data. No behavior; TYPE-only imports (they erase * at runtime): the one period shape (`period.ts`) and the one * provenance shape `describedResult()` already declares * (`semantics/types.ts`), so an absence says its source and time * in the same words a described result does. * Emits: N/A. */ import type { DescribedResultDeclaration, SemanticProvenance } from '../types.js'; import type { DeclaredPeriod, PeriodOnWire } from './period.js'; import type { PagingDeclaration, ResolvedDeclaration } from './search/resultFacts.js'; /** * One piece of ground, and (optionally) why it is where it is. * * `what` is prose the TOOL AUTHOR wrote — a source, a window, a filter, a * fleet ("the fcns database on shq-fab-a", "the last 24h", "all four * arrays"). It is never composed from the model's arguments by this library, * because the library does not know which of them are real. */ export interface CoverageItem { /** The source, window, filter or population. Non-empty. */ readonly what: string; /** * Why it sits where it does. REQUIRED on `cannotCover` (a permanent blind * spot is a claim about capability, and a claim with no reason cannot be * acted on or disproved); optional on `checked` and `notChecked`, where * "we did" and "we did not need to" are often the whole story. */ readonly why?: string; /** * A short plain form of `what`, for a person reading a report — "every VM * disk in the RVTools export of 2026-09-19" for a `what` that names the two * tables it read. Optional; at most 80 characters, one line, and never * longer than `what`. * * RECORD-ONLY: it rides the events (`tools.absent`, * `tools.coverage_declared`), the tracked `coverageDeclared` rows and the * answer account, and is REMOVED from what the model is served * (`read.ts` · `servedToModel`). It is also withheld from the evidence * corpus (`evidence.ts`), because it is the field an author is most tempted * to personalise: NEVER interpolate the caller's arguments into it — a true * short form restates `what`, so no value should live only here. */ readonly short?: string; /** * What kind of ground a `notChecked` or `cannotCover` item is — a CLOSED * set, read by the answer account's existence check: * * • `'existence'` — whether the thing asked about exists at all, or is * the kind of thing the question assumes ("whether that name is a * storage array"). * • `'scope'` — a population, family or source outside what this tool * reaches ("hosts that are not VMware"). * * Refused on `checked`. RECORD-ONLY, like `short`. Absent = not declared: * a reader never infers a kind from `what`. Library code never switches * EXHAUSTIVELY over this union (every switch has a `default` arm), so a * later member is an additive change for producers. */ readonly kind?: 'existence' | 'scope'; } /** What an author may write in a coverage list: bare prose, or prose + why. */ export type CoverageInput = string | CoverageItem; /** * One thing the tool READ and found still RUNNING — a backup in progress, a * replication session still synchronizing — whose outcome is therefore not * known yet: neither a success nor a failure (`coverage()`'s `inProgress`). * * The TOOL decides which of its vendor's states are in flight — this library * never reads a state name, and the vendor's own verdict field, when there is * one, is the tool's to weigh. `what` is the author's prose, as for every * coverage item; `short` is the same RECORD-ONLY short form. `count` says how * many things this one entry stands for ("96 sessions still synchronizing" is * one entry with `count: 96`); omitted, the entry is one thing. It is served — * the model reads the number as the tool's own. * * No `kind`: that names ground a call did NOT reach, and an item in progress * is ground it reached. */ export interface InProgressItem { /** What is still running. Non-empty. */ readonly what: string; /** Since when, or what the vendor reported — optional. */ readonly why?: string; /** RECORD-ONLY short form, the {@link CoverageItem.short} rules. */ readonly short?: string; /** How many things this entry stands for — a positive whole number; omitted = one. */ readonly count?: number; } /** What an author may write in `inProgress`: bare prose, or an {@link InProgressItem}. */ export type InProgressInput = string | InProgressItem; /** * The three lists, normalized. This is the shape everything downstream reads — * the renderer, the event payload, the answer-level block. * * The three are NOT interchangeable, and the difference is the whole point: * * • `checked` — ground this result actually stands on. A clean verdict here * means VERIFIED. * • `notChecked` — ground this run could have covered and did not (budget, * a timeout, a scope the caller chose). A clean verdict says nothing * about it. Re-asking with a wider scope can move it to `checked`. * • `cannotCover` — ground no call to this tool will ever reach (no * collector, no permission, no such telemetry). A clean verdict says * nothing about it, and no retry ever will. This is the list that turns * "everything looks fine" into "everything I can see looks fine". */ export interface Coverage { readonly checked: readonly CoverageItem[]; readonly notChecked: readonly CoverageItem[]; readonly cannotCover: readonly CoverageItem[]; } /** What a tool author passes to {@link import('./ledger.js').coverage}. */ export interface CoverageDeclaration { readonly checked?: readonly CoverageInput[]; readonly notChecked?: readonly CoverageInput[]; readonly cannotCover?: readonly CoverageInput[]; } /** * The other TOOL a suggestion points at, typed (9.113.0) — `tryInsteadTool` * on the declaration, `try_instead_tool` on the envelope. * * The `tryInstead` sentence puts a tool's name inside prose ("…or check * cluster_inventory for the collected names."), and a reader that wants to * know WHICH tool was suggested could only find out by parsing that sentence. * This library never parses a tool name out of prose, so the name rides here * as data — BESIDE the sentence, never in its place: `try_instead` stays a * string, so every reader of it reads what it always read. * * `tool` is NOT looked up. The tool may be served by a provider that resolves * per iteration, or be offered on a later turn; `absent()` requires a * non-empty name and records it as declared. Which names a provider accepts is * `lib/toolName.ts` · `assertValidToolName`'s question, and the library only * warns about it (dev mode), here as at `defineTool`. */ export interface TryInsteadTool { /** The tool's registered name, as a call would name it. */ readonly tool: string; /** Why that tool, in the author's words ("it lists the collected names"). */ readonly why?: string; } /** What a tool author passes to {@link import('./absent.js').absent}. */ export interface AbsenceDeclaration { /** * What was looked for, in the author's own words ("FLOGI entries on * fc1/3"). Required: an absence that cannot say what it did not find is * indistinguishable from a tool that returned nothing by accident. */ readonly what: string; /** * The coverage of the search — REQUIRED and non-empty. An absence that * names no coverage is a `null` with extra steps: the reader still cannot * tell "I looked and there is nothing" from "I could not look", which is * the entire failure this primitive exists to prevent. */ readonly checked: readonly CoverageInput[]; /** Ground the search did not reach this time — an absence here proves * nothing about it. */ readonly notChecked?: readonly CoverageInput[]; /** Ground no search by this tool can reach. Each needs a `why`. */ readonly cannotCover?: readonly CoverageInput[]; /** * Where to go INSTEAD, in one sentence ("widen the window with * `window: '7d'`, or ask for a different interface"). Optional, and the * highest-value optional field in the shape: the loop this primitive stops * is a model with nowhere else to go. * * Prose for the model. Nothing in this library reads a tool name out of it: * when the sentence points at another tool, name that tool in * {@link AbsenceDeclaration.tryInsteadTool} as well. */ readonly tryInstead?: string; /** * The other TOOL the suggestion points at, as data (9.113.0) — * `{ tool, why? }`. Beside the sentence, not instead of it: the sentence is * what the model is told, this is what a reader reads without parsing * prose. Either may be given without the other; when both are given they * must name the same tool, and the library cannot check that they do — it * would have to read a tool name out of the sentence. ONE tool; a list is * refused (see `absent.ts` · `readToolSuggestion` for why). */ readonly tryInsteadTool?: TryInsteadTool; /** * Where the search looked and how old that source is — the SAME shape * `describedResult()` declares (`measuredAt`, `source`, and optionally * `ageSeconds`, `sourceExportDate`), respelled to the same snake_case wire * (honesty step 7b). `measuredAt` and `source` are both required once it is * present. So the found branch and the not-found branch of one `execute` * carry their source and time in one shape: "searched the 02:00 export — * nothing" as data, not prose in `checked`. `measuredAt` is the tool's own * words, never parsed. */ readonly provenance?: NonNullable<DescribedResultDeclaration['provenance']>; /** * What the search's READ covered in time — the instants it asked for, and * what the store holds (or `'unknown'`, said out loud) — honesty step 7b. * Every value is an ISO 8601 instant with a zone; a malformed period is * refused here. The results layer files its verdict (covered · partly held · * not held · unknown), and `.limitsTravelWithTheAnswer()` prints it. */ readonly period?: DeclaredPeriod; /** * The entity each population value RESOLVED to — `[{ argument, id, name?, * aliases? }]` — the tool's own claim, said out loud. Read by a tool that * declares its `search`: it binds the value the call was SENT for and no * other entity. Refused here when malformed. */ readonly resolved?: readonly ResolvedDeclaration[]; /** * Whether there is a next page — `{ next: null }` says there is none. A tool * with a `page` argument whose result declares no `paging` is never complete. */ readonly paging?: PagingDeclaration; } /** * An absence whose coverage the tool's `search` DECLARES — * `absent({ what, checked: 'declared' })`. The author writes the search once; * the dispatch funnel composes `checked` (and the gaps) from the declaration * and the arguments the call ran with. A tool with no `search` that returns * one has its call refused, naming the field to add. */ export type DeclaredAbsenceDeclaration = Omit<AbsenceDeclaration, 'checked'> & { readonly checked: 'declared'; }; /** * The list a missed name was looked up in — `absent({ what, among })`. * * A lookup that checks a name against a known list IS a search, and its miss * says only that the name is not on THAT list. `among` declares the list, and * the mint composes the rest: the names facet the tool searched, the gap the * author cannot remove ("names outside <source>" — and, for a `partial` read, * "not every name in <source> was read"), and the absence's `provenance` from * `source` and `measuredAt`. The gap holds where `among` is declared; a plain * `absent({ what })` or a thrown refusal never passes through it. * * @example a name lookup that found nothing in the inventory export * if (!array) return absent({ * what: array_id, * among: { argument: 'array_id', source: inventory.source, measuredAt: inventory.readAt, * count: inventory.names.length, names: inventory.names }, * }); */ export interface AmongDeclaration { /** The input argument whose value was looked up ('array_id'). */ readonly argument: string; /** The list the names were read from, in the author's words ('the PowerStore inventory export'). One line. */ readonly source: string; /** * When that list was read — the tool's own words (the `provenance` rule). * When it is an ISO 8601 instant this library can read, the names facet * also carries a dated note, "names as of <measuredAt>" — no judgement of * whether that is old. */ readonly measuredAt: string; /** How many names the list holds — a whole number. The tool's claim; served on the facet. */ readonly count: number; /** * The names the list holds, read ONLY to check `count`: when given, * `names.length` must equal it (`names-count-mismatch`). No name is served * to the model or written to an event or a record. */ readonly names?: readonly string[]; /** * `true` when the read did not see every name in the source — a truncated * or paged inventory. The mint then composes a completeness gap, "not every * name in <source> was read" (`bears: 'completeness'`), beside "names * outside <source>", and `authoritative` never removes it. */ readonly partial?: boolean; /** * The ONE opt-out of the "names outside <source>" gap: the author says the * source is every name there is, and why — the tool's claim, not verified. * Every use is listed for review by `check:semantics` (`authoritative-names`). */ readonly authoritative?: { readonly why: string; }; } /** * `absent()` given an {@link AmongDeclaration}: `checked` becomes optional (the * mint composes the names facet first) and `provenance` is refused — `among` * composes it from its own `source` and `measuredAt`. */ export interface AbsenceAmongDeclaration extends Omit<AbsenceDeclaration, 'checked' | 'provenance'> { /** The list the missed name was looked up in. */ readonly among: AmongDeclaration; /** Further ground the search covered, after the composed names facet. */ readonly checked?: readonly CoverageInput[]; } /** * The names facet an `among` miss composes — the first `checked` item. A * {@link CoverageItem} whose `what` the library writes from the declaration * (never from the looked-for value). */ export interface NamesFacetItem extends CoverageItem { readonly facet: 'names'; /** The input argument whose value was looked up. */ readonly argument: string; /** How many names the list holds. */ readonly count: number; /** The list, as declared. */ readonly source: string; /** The names came from the tool's own read. */ readonly from: 'result'; /** Present only when the author declared the list authoritative. */ readonly authoritative?: { readonly why: string; }; } /** * The gap an `among` miss adds and its author cannot remove except by * `authoritative` — the first `not_checked` item: names outside the source * the tool read. It bears on the source facet: a claim that leaves the source * open (a flat "X does not exist") rests on ground this search never covered. * `kind` is record-only, like every item's. */ export interface SourceGapItem extends CoverageItem { readonly kind: 'scope'; readonly relevance: 'bears'; readonly bears: 'source'; } /** * The gap a `partial` among read adds, which nothing removes: the names in * the source the read did not see. It bears on completeness — a claim that * the name is not in the source rests on a read that did not see all of it. */ export interface CompletenessGapItem extends CoverageItem { readonly kind: 'scope'; readonly relevance: 'bears'; readonly bears: 'completeness'; } /** * What a tool author passes to {@link import('./ledger.js').coverage}: the * three lists, and the period the verdict's read covered (honesty step 7b). * `coverage()` takes no `provenance` — a value that comes as rows from a * system of record has `describedResult()`. * * @inline */ export type CoverageLedgerDeclaration = CoverageDeclaration & { /** * What the read behind this value covered in time — the instants it asked * for, and what the store holds (or `'unknown'`). Refused when malformed. */ readonly period?: DeclaredPeriod; /** * What the read found still RUNNING — its outcome not known yet (a backup in * progress, a session still synchronizing). The tool decides what is in * flight; the library never reads a vendor's state name. Needs `checked` * beside it (an item in progress is ground the call read). An empty list * says nothing and is omitted, like every coverage list. Served as * `in_progress`, with one static clause after the note; recorded on the * `coverageDeclared` row and `tools.coverage_declared`; printed under * `.limitsTravelWithTheAnswer()`. It never changes the answer's standing. */ readonly inProgress?: readonly InProgressInput[]; }; /** * The rendered absence — the exact object a tool hands back and the model * reads. Field names are snake_case and English on purpose: this value is * read by a language model far more often than by code, and `af_absent` is * the only field here that exists for the machine. * * The `af_absent` key is RESERVED vocabulary on the tool-result wire (the * `propose-transition` / `require-instruction` precedent): a plain object * carrying it is an absence, and nothing else in this library will treat any * other shape as one. */ export interface ToolAbsence { readonly af_absent: true; /** The plain-English handle. Present so a model that skims one key still * reads the outcome rather than inferring it from a missing field. */ readonly outcome: 'nothing_found'; readonly looked_for: string; readonly checked: readonly CoverageItem[]; readonly not_checked?: readonly CoverageItem[]; readonly cannot_cover?: readonly CoverageItem[]; /** Stated as data as well as prose — the note can be skimmed past, a * `true` in a field named for the question cannot. */ readonly retry_returns_the_same: true; /** The author's sentence. A string, always — the typed tool rides its own * key, so a reader of this one never has to narrow it. */ readonly try_instead?: string; /** The typed tool the suggestion points at (9.113.0), as declared — the * author's words, `{ tool, why? }`. The model reads it as written. */ readonly try_instead_tool?: TryInsteadTool; /** Where the search looked and when that source was measured — the wire * `describedResult()` mints (`measured_at`, `source`, …), honesty step 7b. */ readonly provenance?: SemanticProvenance; /** What the search's read covered in time — `queried`, `held` (or * `'unknown'`), `read_at` — honesty step 7b. */ readonly period?: PeriodOnWire; /** The entity each population value resolved to, as declared. */ readonly resolved?: readonly ResolvedDeclaration[]; /** Whether there is a next page, as declared. */ readonly paging?: PagingDeclaration; /** The static sentence. Never interpolated — see `absent.ts`. */ readonly note: string; } /** * The envelope `absent({ checked: 'declared' })` mints, before the dispatch * funnel composes its coverage. `readAbsence` does not read it (its `checked` * is not a list), so every reader that predates it sees data; * `recognize.ts` · `readDeclaredAbsence` is its one recognizer. */ export type DeclaredToolAbsence = Omit<ToolAbsence, 'checked'> & { readonly checked: 'declared'; }; /** * A ledger whose `checked` the tool's `search` DECLARES — * `coverage(rows, { checked: 'declared', … })`, the twin of * {@link DeclaredAbsenceDeclaration} for a value that came back with rows. */ export type DeclaredLedgerDeclaration = Omit<CoverageLedgerDeclaration, 'checked'> & { readonly checked: 'declared'; }; /** * The envelope `coverage(rows, { checked: 'declared' })` mints, before the * dispatch funnel composes it. `readCoverageLedger` does not read it, so a * reader that predates it sees data; `recognize.ts` · `readDeclaredLedger` is * its one recognizer. */ export interface DeclaredCoveredResult<T = unknown> { readonly af_coverage: Omit<CoveredResult['af_coverage'], 'checked'> & { readonly checked: 'declared'; }; readonly result: T; } /** The rendered coverage ledger, wrapped around the result it bounds. */ export interface CoveredResult<T = unknown> { readonly af_coverage: { readonly checked?: readonly CoverageItem[]; readonly not_checked?: readonly CoverageItem[]; readonly cannot_cover?: readonly CoverageItem[]; /** What the read found still running — its outcome not known yet. * Serialized before `result`, like the lists. */ readonly in_progress?: readonly InProgressItem[]; /** What the read behind the value covered in time — honesty step 7b. * Serialized before `result`, like the lists. */ readonly period?: PeriodOnWire; readonly note: string; }; /** The tool's own answer, untouched. */ readonly result: T; } /** * One coverage statement as the RUN recorded it — what the event carries and * what accumulates in `AgentState.coverageDeclared`. * * `kind` is kept because the two read differently at the answer boundary: a * `'ledger'` bounds a verdict the answer is probably built on, an * `'absence'` bounds a search that found nothing. */ export interface DeclaredCoverage extends Coverage { readonly kind: 'absence' | 'ledger'; readonly toolName: string; readonly toolCallId?: string; readonly iteration: number; /** Present for `'absence'` only — what the search was for. */ readonly lookedFor?: string; /** * The period the declaration said its read covered, in the record's * camelCase form (honesty step 7b) — present only when the envelope declared * a well-formed one. A described result with a period and no coverage lists * files a `'ledger'` row whose three lists are empty. */ readonly period?: DeclaredPeriod; /** * What the call found still running — its outcome not known yet — as the * `coverage()` declared it (copied item by item; `short` and `count` when * valid). Present only on a `'ledger'` row whose envelope declared a * well-formed, non-empty `in_progress`. */ readonly inProgress?: readonly InProgressItem[]; }