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