UNPKG

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
/** * 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 {};