UNPKG

agentfootprint

Version:

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

142 lines (141 loc) • 7.4 kB
/** * extract — which tokens in an answer are DATA, and therefore have to be * grounded in a tool result. * * Pattern: a pure classifier over normalized tokens and the small shared * range/precision leaves, so it can be unit-tested on strings with no agent, no * chart and no model. * Role: core/ layer. The hard half of `namesAndNumbersFromEvidence`. * Emits: N/A. * * ## The rule, and why every part of it is there * * A naive "flag every number" extractor makes the feature worse than useless. * It flags `24 hours`, `3 issues` and `first`, and under `guard` it then spends * a real turn asking the model to justify the word "three" — which is the * retry loop this library exists to remove. So the default is CONSERVATIVE: it * would rather miss a fabricated value than accuse a correct answer. * * A token is a candidate only if: * * 1. **It contains a digit.** English prose is alphabetic. Requiring a digit * is the single cheapest separator between "data someone read off a * screen" and "a word". It is also the rule's biggest blind spot, stated * plainly: a fabricated all-letters name (`esxi-host-alpha`) is invisible * to the default and needs a declared shape. * 2. **It is distinctive.** Either * • an IDENTIFIER — digits mixed with letters or with structural * punctuation (`:` `_` `-` `/` `.`), at least 4 characters: * `0xef0101`, `fc1/3`, `21:00:00:24:ff:4a:12:03`, `UCSB-B200-M5`; or * • a NUMBER with at least `minDigits` (default 4) digits: `41,200`, * `18450`, `786432`. * 3. **It is not prose wearing a number.** Three exclusions, each from a * real sentence in the material: * • a number with a short unit or word glued to it — `32G`, `100MB`, * `47th`, `2h`, `48-port`, `$20/month` — is judged on its NUMBER * alone, so `48-port switch` never trips while `41200iops` still * does; * • a bare `N/M` of one or two digits each — `24/7`, `1/2` — is a ratio * in prose. A two-number port id is spelled the same way, so the * conservative reading wins and a domain that needs it declares a * shape; * • anything the caller declared exempt. * * Declared shapes are tested FIRST and win: an app that says "this is what my * identifiers look like" has better information than these heuristics. */ import type { ResolvedEvidenceGate, UnsupportedValue } from './types.js'; /** * A value the answer asserts, and the rule that made it one. `token` is the * glued-unit spelling the value was read off (`1007us` for the value * `1007`), present only when there was one — the lookup meets the result * under that spelling too (`candidateForms`), and the index is never * widened to do it (9.110.0). */ export type Candidate = UnsupportedValue & { readonly token?: string; /** The precision the answer actually wrote, only under the decimal dial. */ readonly decimalSpelling?: string; /** An intact range can ground or exempt both of the endpoints it spelled. */ readonly rangeToken?: string; }; /** True when the caller declared this token exempt. */ export declare function isDeclaredExempt(token: string, gate: ResolvedEvidenceGate): boolean; /** * Classify ONE normalized token. Returns the candidate it produces, or * `undefined` when the token is prose. * * Exported for the unit tests, which is the whole reason the classifier is a * function over a string rather than a loop body. */ export declare function classifyToken(token: string, gate: ResolvedEvidenceGate, /** Under the figures dial: the numbers the answer writes wearing a unit * (`figures.ts` · `figureNumbersOf`). A number in it is a candidate * whatever its digit count — `53.2%` and `6.3 TB` are readings. */ figures?: ReadonlySet<string>): Candidate | undefined; /** * The spellings a candidate is looked up under: its value's forms * (`lookupForms` — the `0x` rule), plus the glued-unit token's when the * value was read off one. The LOOKUP side widens, the index never does: a * result carrying `1007us` grounds an answer's `1007us`, while a result * carrying `latency 2024ms` does not ground an answer's prose year `2024` * — that candidate came from a bare token and asks for `2024` alone. The * one place both the gate (`gate.ts · checkAnswer`) and the contingent * check (`findings/contingent.ts`) take a candidate's forms from. */ export declare function candidateForms(candidate: Candidate): readonly string[]; /** How one answer is read. */ export interface AnswerReadOptions { /** Under the figures dial: the numbers the answer writes wearing a unit * (`figures.ts` · `figureNumbersOf`, computed by the caller that loaded * it — the module stays off a plain agent's graph). */ readonly figures?: ReadonlySet<string>; /** A read-only exact-token view supplied by the caller's corpus. Whole * carried values win before a numeric range is interpreted as endpoints. */ readonly exactTokens?: { readonly has: (value: string) => boolean; }; /** * The answer is TYPED (`.outputSchema()`): one JSON object or array is read * by its VALUES only. Its object keys are the schema's field names * (`p95LatencyMs`, `sha256`, `ipv4`), not values the answer asserts — read * as values, a digit-bearing name was an identifier no tool result * carried. The price, stated: an answer keyed BY data (an id as a key) is * not judged on its keys. Absent, keys are read too. */ readonly typed?: boolean; } /** One reading of an answer: the values it asserts, and how much of it was never read. */ export interface AnswerReading { /** Every distinct value the answer asserts, in first-appearance order. */ readonly candidates: readonly Candidate[]; /** * Words and numbers past `MAX_ANSWER_TOKENS` that were never classified or * looked up — 0 when the whole answer was read. A value among them could be * invented and would go unfound, so a reading with any is not a clean one. */ readonly unread: number; } /** * Every distinct value the answer asserts, in first-appearance order. * * De-duplicated by value: a port named six times is one claim to ground, and * a correction that lists it six times reads like noise. A typed answer is * read value by value (`answerTexts`). */ export declare function extractCandidates(answer: string, gate: ResolvedEvidenceGate, /** Under the figures dial: the numbers the answer writes wearing a unit * (`figures.ts` · `figureNumbersOf`, computed by the caller that loaded * it — the module stays off a plain agent's graph). */ figures?: ReadonlySet<string>, /** A read-only exact-token view supplied by the caller's corpus. Whole * carried values win before a numeric range is interpreted as endpoints. */ exactTokens?: { readonly has: (value: string) => boolean; }): readonly Candidate[]; /** * Read one answer: the values it asserts (`extractCandidates`), and how many * of its words and numbers lay past the ceiling, never read. The reading * stops CLASSIFYING at `MAX_ANSWER_TOKENS`; it never stops counting. */ export declare function readAnswer(answer: string, gate: ResolvedEvidenceGate, options?: AnswerReadOptions): AnswerReading;