UNPKG

agentfootprint

Version:

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

217 lines • 12 kB
/** * answer — how a ledger survives into the final answer. * * Pattern: composed by the FRAMEWORK, not requested from the model. Pure * function of the run's declarations; the stage that calls it is a * two-line wrapper (see `stages/prepareFinal.ts`). * Role: core/ layer, pure. * Emits: N/A. * * ## Why this is an append and not a check * * A ledger the model can drop is worthless, and every mechanism that ASKS the * model to carry it can be dropped: a note in the tool result is advice, a * system-prompt rule is advice, and a judge that reads the answer back and * asks "did it state its limits?" needs a second model to decide what * counts — which would put a language judgement in the one place this library * refuses to put one (see `evidence/README.md`: the guard may not need a * bigger model than the thing it guards). * * So the limits are APPENDED. The model does not write them, so the model * cannot drop them, and the mechanism is a string concatenation over data the * tools declared — deterministic, auditable, and cheap. It changes the bytes * of the answer, which is why it is opt-in * (`.limitsTravelWithTheAnswer()`): an agent that never asks for it is * byte-identical. * * ## Why a TYPED answer gets the same fold as data, never the block * * An answer with an output schema is JSON, and JSON followed by prose is not * JSON — appending the block made `runTyped()` throw on every answer that had * limits. So when an output schema is configured the answer string stays the * model's own, and the same fold travels BESIDE it: `coverageOfAnswer` below, * committed as `AgentState.answerCoverage`. The model still cannot drop it — * the framework composes it from what the tools declared — and a person reads * it wherever the app draws it, not wherever the prose happened to end. * * ## Why an absence contributes too * * `absent()` and `coverage()` make the same kind of statement about the same * run. A search that found nothing in the fcns database really did cover the * fcns database, and a search that could not reach the archive really is a * limit on the answer. Folding both into one block is the only way the reader * gets ONE boundary instead of a boundary per tool — and duplicates are * dropped, so five tools naming the same missing collector say it once. */ import { type DeclaredPeriod } from './period.js'; import type { BoundPresentation } from '../../time/present.js'; import type { Coverage, DeclaredCoverage, InProgressItem } from './types.js'; /** * One declaring call's period, as the answer's limits carry it (honesty step * 7b) — the tool, the call, and the period AS DECLARED: the data twin of one * `Period:` line. * * @inline */ export interface AnswerPeriod extends DeclaredPeriod { readonly toolName: string; readonly toolCallId?: string; } /** * One declaring call's in-progress items, as the answer's limits carry them — * the tool, the call, and what its read found still running, as declared: the * data twin of that call's lines under "In progress (outcome not known yet)". * * @inline */ export interface AnswerInProgress { readonly toolName: string; readonly toolCallId?: string; readonly items: readonly InProgressItem[]; } /** * A TYPED answer's limits, as data — the three coverage lists the prose block * would print, plus the values a tool's `assume` rule filled this turn (the * inputs layer, honesty layer 2), which the prose answer prints as its * "Assumed" block. The value of `AgentState.answerCoverage`, * `turn_end.answerCoverage` and `agent.answerCoverage()`. * * `assumed` is the SAME reading of the SAME rows the block prints * (`arguments/serve.ts` · `assumedLinesFor`): one entry per distinct (tool, * argument, value), in the order the rows were filed, a row a before-tool * rewrite superseded left out. `value` is the tool's own argument view — * `'REDACTED'`, with `hidden: true`, when that view hides the argument. * Present only when a value was assumed. `periods` (honesty step 7b) is the * data twin of the block's `Period:` lines, present only when a call declared one. */ export interface AnswerCoverage extends Coverage { /** The periods the calls declared (honesty step 7b) — one per declaring * call, as declared; present only when one did. */ readonly periods?: readonly AnswerPeriod[]; /** What the calls found still running — its outcome not known yet — one * entry per declaring call, as declared; present only when one did. Never a * reason on the answer's standing: a label that travels with the limits. */ readonly inProgress?: readonly AnswerInProgress[]; readonly assumed?: readonly { readonly toolName: string; readonly argument: string; readonly value: string; readonly hidden: boolean; }[]; } /** One assumed value as `AnswerCoverage` carries it. */ type AssumedValue = NonNullable<AnswerCoverage['assumed']>[number]; /** The block's opening line. Stable — tests and readers match on it. */ export declare const COVERAGE_BLOCK_HEADING = "Coverage of this answer"; /** * The inputs layer's "Assumed" block's opening line (`arguments/serve.ts` · * `assumedBlock` composes the block). Owned here, beside the limits block's * heading: this module's `composeAnswerWithCoverage` is the one composer of * the section the framework appends after an answer, and a reader of the * answer's text (`answer-account/account.ts` · `readAnswer`) finds every * block of it through this one module. Stable — tests and readers match on it. */ export declare const ASSUMED_BLOCK_HEADING = "Assumed (a tool's rule, not your words):"; /** The section heading the periods print under (honesty step 7b). Stable — readers match on it. */ export declare const PERIOD_SECTION_LABEL = "Period"; /** The section heading the wall-clock sources print under (the time layer, step T8). Stable. */ export declare const CLOCKS_SECTION_LABEL = "Clocks"; /** * The time layer's limits lines (step T8), already composed from the record * (`coverage/period.ts` · `periodCheckLine`, `clockLines`): `period` — one per * call whose read differs from what was asked, shifted, or was older than the * source keeps, printed under `Period` after the declared periods; `clocks` — * the wall-clock sources, under `Clocks`. Passed only under `.time()`. */ export interface TimeLimitLines { readonly period: readonly string[]; readonly clocks: readonly string[]; /** * The MODEL's audience only (`coverage/timeLimits.ts` · `heldLine`): per call * whose source holds none or only part of the asked time, what it asked * about and what the source holds, in the person's zone. The person's block * never reads it — its `Period:` line already prints both spans. */ readonly held?: readonly string[]; } /** * The run's declarations folded into the ANSWER's coverage — the three lists * `composeAnswerWithCoverage` renders, as data. * * `.limitsTravelWithTheAnswer()` on a typed answer (`.outputSchema()`): the * answer string has to stay the model's JSON for `runTyped()` to parse it, so * the limits travel beside it instead of inside it — this value, committed as * `AgentState.answerCoverage`, projected onto `turn_end.answerCoverage` and * returned by `agent.answerCoverage()`. * * Folded by the block's own fold (`foldSections`, the one place each list is * merged — declaration order, duplicates dropped), so every entry is one the * block would print and none is added. It keeps EVERY entry: the block's cap * (twelve per section, then "… and N more") is a reading aid for prose, not a * limit on the data. * * Fresh plain objects, copied field by field (`what`, `why`, and the * record-only `short` / `kind` when declared), never a reference into the rows * it was folded from — so the value can be committed, emitted and handed to a * caller as detached data. `undefined` when nothing was declared: the identity * case, the one every run whose tools declare nothing takes. */ export declare function coverageOfAnswer(declared: readonly DeclaredCoverage[]): AnswerCoverage | undefined; /** * A typed answer's limits as ONE value: the folded coverage (or three empty * lists) and the values assumed this turn, `assumed` present only when one * was. `undefined` when there is neither — the identity case, so a run whose * tools declared nothing and assumed nothing commits exactly the keys it * always did. */ export declare function answerCoverageOf(folded: AnswerCoverage | undefined, assumed: readonly AssumedValue[]): AnswerCoverage | undefined; /** A coverage value as detached plain data — the same three lists, every item copied. */ export declare function copyCoverage(value: Coverage): Coverage; /** A typed answer's limits as detached plain data — the lists and any assumed values, copied. */ export declare function copyAnswerCoverage(value: AnswerCoverage): AnswerCoverage; /** * Fold the run's declarations into one block and append it to the answer. * * Returns the answer UNCHANGED when nothing was declared — the identity case * matters, because it is the one every agent that never returns a coverage * shape takes. * * `standing` (the answer layer, honesty layer 4, under * `.answerLayer({ standingLine: true })`) is the answer's standing as one * already-composed line (`assessment/compose.ts` · `standingLineOf`); it opens * the section, and while it is on the caller passes no `assumed` block — the * line names the assumed values itself (one composer for one fact). * * `assumed` (the inputs layer, honesty layer 2) is a second, already-composed * block — the values a tool's `assume` rule filled this turn * (`arguments/serve.ts` · `assumedBlock`) — appended after the coverage block * under the same separator. With neither declarations nor an assumed block the * answer is unchanged, byte for byte. * * `presentation` (the time layer, only under `.time()`) is the run's clock * zone: each `Period:` line renders its instants there, the zone named * (`period.ts` · `periodLine`). Absent → the lines are the declared instants * verbatim, as they always were. * * `displayNameOf` (a tool's `displayName`, the app's words — given only on an * agent where a tool can declare one) names each item's declaring calls the * way the standing line names a reason's calls (`lib/displayName.ts` · * `namedCalls`): `- PDF attachments (loading the attached data)`, the * declared names named and the rest counted; and a `Period:` or "In progress" * line names its call by the declared words instead of the registered name. * An item no declaring call names stays the bytes it always was. * * `scripts` (`.scriptTravelsWithTheAnswer()`) is a third, already-composed * block — the programs this turn's calls SUBMITTED to a declared code-running * tool, each in the library's own fence under a label line read from its * execution record (`core/agent/scripts/attachment.ts` · `scriptsBlock`) — * appended LAST, after the standing line and the limits block, under the same * separator. Absent → the bytes it always was. Given as a function, it is * handed the exact text it will follow — the model's answer, the separator and * every block above — so the attachment can set a rendering boundary first * (`core/agent/scripts/attachment.ts` · `scriptsBlockAfter`): whatever block * that text leaves open must not take the programs in. */ export declare function composeAnswerWithCoverage(answer: string, declared: readonly DeclaredCoverage[], assumed?: string, standing?: string, presentation?: BoundPresentation, time?: TimeLimitLines, displayNameOf?: (toolName: string) => string | undefined, scripts?: string | ((before: string) => string)): string; export {}; //# sourceMappingURL=answer.d.ts.map