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