agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
266 lines (265 loc) • 14.9 kB
TypeScript
/**
* findings/reserved — the reserved `_findings` argument: its schema, the one
* decorator that adds it to a served tool, and the two peels that take it
* back off before anything else reads the model's words.
*
* Pattern: pure functions over plain JSON; no scope, no I/O.
* Role: core/ layer leaf. `withFindingsArgument` runs at the ONE decoration
* site (the committed tool list in `buildToolsSlot`, and the seed
* fallback) — and so does its sources-only twin, `withSourcesArgument`
* (declared sources without the ledger: ruled tools only, `from`
* alone); `splitFindings` is the FIRST read of a tool call's args
* in the dispatch loop; `peelAnswerFindings` runs before the output
* schema judges an answer, reading the answer's text through
* `answerText.ts` — the scanner the stream shares, so the answer
* returned and the tokens shown are the same text. `ledger.ts` turns
* what they return into rows.
*
* THE LAWS THIS FILE KEEPS
* - Never mutate a schema or an args object: every changed value is a
* rebuilt copy, and an unchanged one is the SAME reference (so an unarmed
* path, or a schema the author already decorated, is byte-identical).
* - `_findings` is never added to a tool's `required` and never touches
* `additionalProperties`; the author's own `_findings` property wins.
* - Never coerce, never infer: a field that fails its enum check is dropped
* and COUNTED (`malformed`), never defaulted. An absent declaration stays
* absent — `splitFindings` returns no `findings` for it.
* - `FINDINGS_ARGUMENT_SCHEMA` is the frozen base and is never edited. The
* OFFER (`withFindingsArgument`'s second argument — the ids of served
* results the model can still read: no standing, `fact` or `open`,
* newest first; `offer.ts · offeredResultIds`) is a rebuilt copy per call
* whose `previous.items.properties.toolCallId` carries `enum: offer`; an
* empty offer serves the base by reference. The offer is what the model
* may COPY, never what the library resolves: an id outside it is still
* recorded as the model wrote it (`ledger.ts · standingRowsFrom`,
* `unknownId: true`), never mapped to a position — and every id inside
* it resolves, because the rows are identified against the same served
* history the offer was read from (`offer.ts · knownResults`).
*
* Both model-facing strings here (`FINDINGS_INSTRUCTION` and the schema's
* description) say what the model may DO and never what the library
* guarantees; `test/modelFacingSurfaces.test.ts` runs them through
* `unprovable` so a later call cannot falsify a sentence.
*/
import type { LLMToolSchema } from '../../../provider-contract/types.js';
import { type FindingsDeclaration } from './types.js';
type PlainObject = Readonly<Record<string, unknown>>;
/**
* How many ids an offer lists at most: the newest results the model may
* still name come first, and the late DATA reports omitted whole IDs
* in its counters.
*/
export declare const FINDINGS_OFFER_CAP = 32;
/**
* The JSON schema of the reserved property — frozen, served as-is, and NEVER
* listed in the parent's `required`. `basis` is required INSIDE it so a model
* that opens the object says why it made the call.
*/
export declare const FINDINGS_ARGUMENT_SCHEMA: PlainObject;
/**
* The `from` property (honesty layer 2, declared sources) — where each
* argument value of the call came from, planted ONLY under the sources arm
* (`.findings({ argumentSources: true })` or `.inputsLayer({ argumentSources:
* true })`) and ONLY on a tool that declares argument rules
* (`withFindingsArgument`'s `from` option, `withSourcesArgument`): planted
* everywhere it would cost every served tool its bytes before any benefit is
* measured.
*
* THE ONE PLACE `from` IS EXPLAINED (step 5, review round 1 of the bench). The
* sources' meanings live in `source`'s description, what the record keeps in
* the array's, and there is no second copy in the system prompt: the paid
* step-5 run served both (a 447-character instruction line beside this
* property) and the explanation cost its characters twice on every request.
* The wording is positive on purpose — the first draft opened "Optional:" and
* closed "Leave an argument out rather than guess", and the model read `from`
* as a place to cite RESULTS (34 of its 35 `from` arrays named a result; not
* one named the person's words unless it also named a result). A value with
* no entry is not refused; the sentence says what the record then holds.
*
* Says what the model may declare and what the record keeps, and nothing
* about what serving does; judged by `unprovable` at the strictest lifetime
* in `test/modelFacingSurfaces.test.ts`.
*/
export declare const FINDINGS_FROM_PROPERTY: PlainObject;
/**
* The reserved property DECLARED SOURCES plant WITHOUT the findings ledger
* (`.inputsLayer({ argumentSources: true })` on an agent that never called
* `.findings()`): the versioned marker as its whole description — so
* `withoutFindingsArgument` recognises it exactly as it recognises the
* ledger's decoration — and `from` alone, required. No `basis`, no
* `previous`, no offer: nothing here asks the model for a standing, so none of
* the ledger's ~2,500 characters per served tool are paid for an arm that
* only reads where argument values came from.
*/
export declare const FINDINGS_SOURCES_SCHEMA: PlainObject;
/**
* The answer-turn `_findings` property, for a provider's structured output
* (`.outputSchema(…, { strategy: 'native' })`, `nativeOutputFormat.ts`).
*
* What the served contract allows on an ANSWER is exactly what
* `FINDINGS_INSTRUCTION` says: a top-level `_findings.previous`. So this is
* that one property, taken from `FINDINGS_ARGUMENT_SCHEMA` itself (one source:
* a later edit to the standings shape reaches the answer too) — with ONE wire
* narrowing: an assertion's `value` is untyped there (any JSON), which a
* structured output cannot decode without recursion, so here it is a string,
* number, boolean or null. `readDeclaration` still reads whatever arrives.
* No offer `enum`: IDs are listed in late advisory DATA, and a format that
* changed with every offer would re-compile the grammar and invalidate the
* prompt cache on every call. Built per call, never edited.
*/
export declare function findingsAnswerSchema(): PlainObject;
/**
* The id of the always-on instruction `.findings()` registers (9.101.0) — the
* name a receipt's `system.pieces` carries and a test can look for; the
* declared-sources corpus leaves the library's own instruction out of the
* app's text by it.
*/
export declare const FINDINGS_INSTRUCTION_ID = "findings-ledger";
/**
* The always-on system instruction `.findings()` registers (the twin of
* `outputSchema()`'s piece). It asks; it promises nothing about serving.
*/
export declare const FINDINGS_INSTRUCTION: string;
/**
* The one line the instruction gains when the evidence gate is armed beside
* the ledger (9.110.0, `findings/contingent.ts`): what the record does with
* a value taken from a result the model set aside, and the two ways out —
* re-establish it, or say so. Never registered alone, and never on an agent
* with `.findings()` only: without `.namesAndNumbersFromEvidence()` there is
* no corpus, nothing is recorded as contingent, and a sentence saying it
* would be a promise the run cannot keep (`findingsInstructionFor`). It
* says what the record holds and what the model may DO; judged by
* `unprovable` at the strictest lifetime in `test/modelFacingSurfaces.test.ts`.
*/
export declare const FINDINGS_CONTINGENT_LINE: string;
/**
* The instruction an armed agent registers: `FINDINGS_INSTRUCTION` as it
* is, plus `FINDINGS_CONTINGENT_LINE` as one more line when the evidence
* gate is armed too. Composed at `AgentBuilder.build` (the two doors may be
* called in either order), and byte-identical to the constant when the gate
* is absent — so the `.findings()`-only references are the bytes they were.
* Declared sources add NO line here: `from` is explained once, in its own
* property (`FINDINGS_FROM_PROPERTY`), on the ruled tools that carry it.
*/
export declare function findingsInstructionFor(arms: {
readonly contingent: boolean;
}): string;
/**
* The answer-turn ask (9.103.0) — appended to the served ledger piece
* (`serve.ts · findingsLedgerPiece`) ONLY under
* `findings({ answerAsk: 'quote-facts' })`, a bench-gated dial whose default
* is `'none'`. It tells the model HOW to answer from the piece: the real-model
* page's fourth run measured one fact value in twelve restated when the
* piece is served with no such ask (`docs/design/2026-09-findings-ledger-real-model.md`
* § Fourth run), and this is the instruction variant that run asked for. It
* says what the model may DO with each kind of line and promises nothing
* about what the library serves; a constant, so the cache law of the piece
* holds with it (`serve.ts` · "The cache"). Judged by `unprovable` at the
* strictest lifetime in `test/modelFacingSurfaces.test.ts`.
*/
export declare const FINDINGS_ANSWER_ASK: string;
/**
* True when the schema's own `inputSchema.properties` carry `_findings` —
* the AUTHOR's property. The ONE predicate every reader of that fact asks:
* `withFindingsArgument` leaves such a schema by reference, the registry
* refusal (`buildToolRegistry · assertReservedArgument`) names such a tool at
* build, and the dispatch peel (`toolCalls · peelCall`) leaves such a call's
* value alone — it is the author's argument, not the model's declaration.
* `undefined` (a name nothing answers) owns nothing.
*/
export declare function ownsReservedArgument(schema: LLMToolSchema | undefined): boolean;
/** Plant the stable optional findings schema. The host's `from` capability is
* fixed for a tool; mutable result IDs live in the late request DATA, never
* here. Author-owned properties win and decorating twice is idempotent.
*/
export declare function withFindingsArgument(schema: LLMToolSchema, options?: {
readonly from?: boolean;
}): LLMToolSchema;
/**
* DECLARED SOURCES WITHOUT THE FINDINGS LEDGER (`.inputsLayer({
* argumentSources: true })`, no `.findings()`): a REBUILT copy of a RULED
* tool's schema with the reserved `_findings` argument carrying `from` alone
* (`FINDINGS_SOURCES_SCHEMA`, by reference) — the twin of
* `withFindingsArgument` for the one arm that plants the argument on ruled
* tools only. The SAME reference when the author's own schema already carries
* `_findings` (the author wins; `ownsReservedArgument`), and the caller plants
* nothing on a tool with no rules, so an unruled tool is served the bytes it
* always was. `required` and `additionalProperties` stay as the author wrote
* them.
*
* @example
* ```ts
* withSourcesArgument(searchLogs.schema).inputSchema.properties._findings; // FINDINGS_SOURCES_SCHEMA
* ```
*/
export declare function withSourcesArgument(schema: LLMToolSchema): LLMToolSchema;
/**
* The served `inputSchema` WITHOUT the library's decoration: when
* `properties._findings` is the decoration (`isFindingsDecoration` — the
* base, an offer copy, the sources-only decoration, or a committed clone of
* any of them), a rebuilt copy minus
* that property; otherwise the SAME reference — an author's own `_findings`
* is not the decoration and is read as written. Asked by readers that judge
* the model's ARGUMENTS against the schema (`callLLM` · the choice seam's
* enum fence), so the reserved words (`direct`, `fact`, `noise`, …) and the
* offered ids never excuse a value.
*/
export declare function withoutFindingsArgument(inputSchema: unknown): unknown;
/**
* Whether a SERVED `inputSchema` carries the library's `_findings` decoration
* (`isFindingsDecoration`) — so the model's `_findings` value on a call of
* that tool is its declaration, never the author's argument. The one
* predicate a reader of the model's arguments asks when only some served tools
* carry the decoration (declared sources without the findings ledger plant it
* on ruled tools only): `callLLM` · the choice seam peels such a call's
* arguments before judging them.
*/
export declare function carriesFindingsDecoration(inputSchema: unknown): boolean;
/** @internal — shared with `peel.ts`. */
export interface ReadDeclaration {
readonly declaration?: FindingsDeclaration;
/** Entries and fields dropped as malformed. */
readonly malformed: number;
/**
* Of `malformed`, the `from` entries dropped — set only when `from` was read
* (the declared-sources arm), so the inputs layer can carry the count on an
* argument row when the call files no basis row.
*/
readonly sourcesMalformed?: number;
}
/** Which optional parts of `_findings` a reader reads. Absent → none (the declaration as it always was). */
export interface DeclarationArms {
/** `_findings.from` — the declared-sources arm (honesty layer 2): `.findings({ argumentSources:
* true })` or `.inputsLayer({ argumentSources: true })`. */
readonly argumentSources?: boolean;
}
/**
* The one validator both peels share. Enum-checks every field; a field that
* fails is dropped and counted; nothing is defaulted. No `declaration` comes
* back when nothing readable survived.
*
* `arms.argumentSources` (honesty layer 2) also reads `from` — judged against
* the call's own arguments (`args`, `_findings` taken off) by
* `arguments/sources.ts` · `readSources` — and counts a `from`-only
* declaration as readable. Unarmed, `from` is ignored exactly as any unknown
* key is, so a `.findings()`-only agent reads the bytes it always read.
*/
/** @internal — shared with `peel.ts`. */
export declare function readDeclaration(raw: unknown, arms?: DeclarationArms, args?: PlainObject): ReadDeclaration;
export interface SplitFindings {
/** The SAME reference when the key was absent; a fresh object without it otherwise. */
readonly args: PlainObject;
readonly findings?: FindingsDeclaration;
/** Present only when something was dropped. */
readonly malformed?: number;
}
/**
* Take `_findings` off a tool call's args. The first read of `tc.args` in the
* dispatch loop when armed: what comes back as `args` is what the call runs
* with, and the model's declaration rides on `findings`. `arms` is the
* reader's (`readDeclaration`): under declared sources the peel reads `from`
* exactly as the inputs layer did, so both reads of one `_findings` agree —
* the same readable declaration, the same malformed count.
*/
export declare function splitFindings(args: PlainObject, arms?: DeclarationArms): SplitFindings;
export {};