agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
170 lines (169 loc) • 8.96 kB
TypeScript
/**
* arguments/checks — the declared-sources checks: where the model SAID a value
* came from, checked against the one place that source lives.
*
* Pattern: Walker, pure. `checkSource` is a function of one argument's value,
* its rule, the model's claim about it (`_findings.from`, read by
* `sources.ts` · `readSources`) and the corpora the layer is handed
* (`SourceCorpus`). The same inputs always give the same verdict; no
* clock, no model, no scope.
* Role: core/ layer, the inputs layer (honesty layer 2), step 5. Loaded with
* the layer's stage bodies (`resolve.ts` imports it; both through
* `import()`), never on a plain agent's graph. Imports nothing from
* `findings/` (the one-way law): the mount builds the corpus and hands
* it in.
* Emits: N/A.
*
* ## One claim, one corpus — and a pass is never support
*
* Every check is whole-token membership (`lib/normalize.ts` · `tokenize`
* and `canonicalForm`, the evidence gate's own "same value", through
* `evidence/resultCarries.ts` · `canonicalTokens`) in the ONE place the
* claimed source lives:
*
* | Claim | Checked against | Verified as |
* |-----------|-----------------------------------------------------------------------|------------------------------------|
* | `user` | the quote in the person's own messages; the value in the quote, or a phrase the author declared for it | `said` (`matched: 'quote' \| 'phrase'`), or a READING |
* | `result` | the named result's own bytes (`evidence/resultCarries.ts` · `resultReader`, read once per batch) | `result` (`setAside` when the model set it aside) |
* | `turn` | the person's earlier answers to the library's ask (the ledger's `answered` rows) | `answered` (`matched: 'spelling'` across `24h` ↔ `-24h`) |
* | `app` | the app's own text (system messages, the composed system prompt, `externalGrounds`) | `app` (`appSource` = the ground's label) |
* | `assumed`, nothing | nothing — the model's own | `model` |
*
* A failed check is FILED as written (`failed`), never repaired and never
* retried against another corpus: a false claim is itself a fact. For a failed
* claim and for the model's own value, the library still looks the value up
* (`coincides`) — a HINT for the lens and the bench, never a source, never
* support, never enough to skip an ask.
*
* Membership can REFUTE a claim ("those words are not in any message you
* sent"); a pass proves nothing — the words can be there by coincidence, in a
* negation, in another sentence. So no verdict here ever supports "known": a
* pass only keeps a reason from firing (`assessment/assess.ts` ·
* `readArgumentVerdicts`).
*
* ## The declared default beats the app and a result (V1)
*
* A value equal to the argument's own `assume` value that the checks did not
* verify as the PERSON's (their quoted words, or their answer) is filed
* `default`, the claim kept as written — a model that copies a default from a
* description chose nothing, and a default must never earn more standing
* through `app` or `result` than the same default declared as `assume`.
*/
import { type PeriodSpelling, type RuledArgument } from '../../../tools/declare.js';
import { type ArgumentCheckFailed, type ArgumentClaim } from './rows.js';
import type { DeclaredSource } from './sources.js';
/** One message a PERSON wrote (`lib/saidByPerson.ts` · `isSaidByPerson`), in the served window. */
export interface PersonWords {
readonly text: string;
/** Before the current request — an earlier turn. */
readonly earlier?: true;
/**
* The run's OWN message in a composed run (`AgentInput.messageFrom:
* 'composed'`): another runner's words handed on by a composition, never
* the person's. A quote found only here fails as `composed-message`.
*/
readonly composed?: true;
}
/** One tool result the model was served, by id. */
export interface ServedResult {
readonly toolCallId: string;
readonly toolName?: string;
/**
* The TOOL's own bytes as served — cut at the tool-bytes boundary
* (`lib/toolBytes.ts` · `toolBytesOf`), so a library note never speaks for
* the tool. Absent when the id is known only through the previous batch
* (its entry carries no cut): such a claim is `uncheckable`, never "found".
*/
readonly text?: string;
/** Served before the current request — an earlier turn. */
readonly earlier?: true;
/** The model's current standing on it — the ledger's fold plus this batch's own `previous[]`. */
readonly standing?: 'fact' | 'open' | 'noise' | 'ruled-out';
}
/** One piece of the app's own text. `label` names an `externalGrounds` entry's source. */
export interface AppWords {
readonly text: string;
readonly label?: string;
}
/** One answer the person gave the library's ask earlier (an `answered` row on the ledger). */
export interface EarlierAnswer {
readonly toolName: string;
readonly argument: string;
/** The row's value — the tool's own view, clipped; `'REDACTED'` where the view hides it. */
readonly value: string;
readonly turn: number;
readonly period?: true;
/** The answered tool's declared period spelling, on its period argument. */
readonly spelling?: PeriodSpelling;
}
/**
* What the checks read — built by the mount from the served history, the
* composed system prompt, the app's `externalGrounds` and the ledger's
* standings and answers (`honesty/sourceCorpus.ts` · `sourceCorpusOf`). Never
* the library's own frames, notes or served sentences; assistant text only to
* say a value was found nowhere else (`only-in-model-answer`).
*/
export interface SourceCorpus {
/** The conversation turn this batch belongs to (`AgentState.turnNumber`). */
readonly turn: number;
readonly person: readonly PersonWords[];
readonly results: readonly ServedResult[];
readonly assistant: readonly string[];
readonly app: readonly AppWords[];
readonly answers: readonly EarlierAnswer[];
}
/** One argument of one call, as the check is handed it. The value is RAW — never stored. */
export interface SourceSubject {
readonly toolName: string;
readonly argument: string;
readonly value: unknown;
/** The argument's rule; absent on a free argument a `from` entry named. */
readonly rule?: RuledArgument;
/** The tool's declared period spelling, when this is its period argument. */
readonly spelling?: PeriodSpelling;
/** The tool's `argumentsFrom` — the tools whose results ground its arguments. */
readonly argumentsFrom?: readonly string[];
/** The model's `from` entry for this argument; absent when it declared none. */
readonly claim?: DeclaredSource;
}
/** The check's verdict — enums, flags, one id, one label; never a value. */
export interface SourceCheck {
readonly source: 'said' | 'answered' | 'result' | 'app' | 'default' | 'model';
readonly claimed: ArgumentClaim;
readonly matched?: 'quote' | 'phrase' | 'spelling' | 'mention';
readonly reading?: true;
readonly earlier?: true;
readonly result?: string;
readonly setAside?: 'open' | 'noise' | 'ruled-out';
readonly argumentsFrom?: 'listed' | 'unlisted';
readonly appSource?: string;
readonly coincides?: 'person' | 'result' | 'app';
readonly failed?: ArgumentCheckFailed;
}
/**
* Whether a verdict traces the value to a source the record holds — the
* person's words (`said` via the quote or a declared phrase), the person's
* answer, a result, the app. A READING (`said` + `reading`), the model's own
* value, a hint and a failed claim do not. Under an `ask` rule with declared
* sources armed, a value this says no to is asked about (`resolve.ts`).
*/
export declare function isTraced(check: Pick<SourceCheck, 'source' | 'reading'>): boolean;
/**
* Where one argument value came from, as the record can check it — the
* model's claim (`subject.claim`) checked against the one corpus that source
* lives in, then the declared default applied last (V1). The value is read in
* memory and never kept: the verdict holds enums, flags, a result id and an
* app label only.
*
* @example
* ```ts
* checkSource(
* { toolName: 'search_logs', argument: 'window', value: '7d',
* claim: { argument: 'window', source: 'user', quote: 'over the last week' },
* rule: windowRule }, // choices: [{ value: '7d', said: ['last week'] }, …]
* corpus, // person: [{ text: 'errors on checkout over the last week?' }]
* );
* // { source: 'said', claimed: 'user', matched: 'phrase' }
* ```
*/
export declare function checkSource(subject: SourceSubject, corpus: SourceCorpus): SourceCheck;