UNPKG

agentfootprint

Version:

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

418 lines (417 loc) • 20.3 kB
/** * coverage/period — the ONE period shape a result declares, the ONE rule set * every door asks, and the ONE verdict. * * Pattern: one rule set, every door (the `items.ts` law for `short` and * `kind`): the three mints — `absent()`, `coverage()`, * `describedResult()` — REFUSE a malformed period at the line the * author wrote, and the recognizers READ a period an envelope minted * elsewhere (a Python helper, an older or newer process) without * repairing it. Both ask {@link periodProblem}, so a mint and a read * cannot disagree about what a well-formed period is. * Role: core/ layer leaf of the result doors (honesty layer 3). Imports only * `refusal.ts`, the dev-mode flag and the time layer's one instant * parser (`time/instant.ts` · `instantOf`, lenient profile) and its * person-facing renderer (`time/present.ts`). The results layer * (`../../core/agent/results/subflow.ts`) files one verdict per call from * {@link periodVerdict}; the limits block prints one line per * declaring call from {@link periodLine}. * Emits: N/A. * * ## The shape — anchored on the READ * * ```ts * period: { * queried: { from: '2026-09-26T09:00:00Z', to: '2026-09-26T10:00:00Z' }, // what the read asked for * held: { from: '2026-08-27T02:00:00Z', to: '2026-09-26T02:00:00Z' }, // what the store holds — or 'unknown' * readAt: '2026-09-26T10:00:03Z', // when the read ran (optional) * } * // on the wire: "period": { "queried": {…}, "held": {…} | "unknown", "read_at"? } * ``` * * Every value is an ISO 8601 instant WITH a zone (`Z` or `±HH:MM`) — the * library compares instants, and an instant without a zone is ambiguous, so it * is refused rather than guessed. `held: 'unknown'` is the store saying out * loud that it cannot vouch for what it holds. Nothing here ever parses a * duration: the library compares instants the tool declared and never reads * "2h" (the inputs layer's `ToolPeriod` says which ARGUMENT set the period; * this shape says what the READ covered). * * ## Why not the word `window`, and why not a coverage item * * `window` names the context window (`core/agent/window/`, `.window()`). A * coverage item's `kind` is a closed, record-only word: it cannot hold two * instants to compare. */ import type { BoundPresentation } from '../../time/present.js'; import { type PeriodDiffers, type SourceClock } from '../../time/checkRecord.js'; /** * The period a result declares — what its READ covered — in the camelCase * spelling an author writes on `absent()`, `coverage()` and * `describedResult()`, and the record keeps (the `coverageDeclared` row, the * `tools.absent` / `tools.coverage_declared` events). The wire spells it * snake_case (`read_at`). * * @example * ```ts * absent({ * what: 'failed backup runs for host-103', * checked: ['every job in the 02:00 export'], * period: { * queried: { from: '2026-09-26T09:00:00Z', to: '2026-09-26T10:00:00Z' }, * held: { from: '2026-08-27T02:00:00Z', to: '2026-09-26T02:00:00Z' }, * }, * }); * ``` */ export interface DeclaredPeriod { /** The instants of the READ that produced this result — not of "this call". * ISO 8601 instants WITH a zone; `from` is not after `to`. */ readonly queried: { readonly from: string; readonly to: string; }; /** What the store holds at the time of the read — the same shape — or * `'unknown'`, said out loud when the tool cannot vouch for it. */ readonly held: { readonly from: string; readonly to: string; } | 'unknown'; /** When the read ran — a cached answer is served minutes after the read it * describes. An ISO 8601 instant with a zone. */ readonly readAt?: string; } /** * {@link DeclaredPeriod} as the wire spells it — `read_at` is the one key that * differs. What a model reads inside `af_absent`, `af_coverage` and * `af_semantics`, and what a foreign minter writes (`canonical-notes.json` * publishes every key, {@link PERIOD_WIRE}). * * @inline */ export interface PeriodOnWire { readonly queried: { readonly from: string; readonly to: string; }; readonly held: { readonly from: string; readonly to: string; } | 'unknown'; readonly read_at?: string; } /** * The period's wire spelling as DATA — every reserved key and the one literal * (`'unknown'`) — so a tool written in another language mints it byte for * byte. `canonical-notes.json` publishes it, generated from this constant * (`scripts/gen-canonical-notes.mjs`): the JSON cannot disagree with the code. * * @example * ```ts * PERIOD_WIRE.readAt; // 'read_at' — the one key whose wire spelling differs * ``` */ export declare const PERIOD_WIRE: Readonly<{ /** The key a period sits under on `af_absent`, `af_coverage` and `af_semantics`. */ readonly key: "period"; readonly queried: "queried"; readonly held: "held"; readonly from: "from"; readonly to: "to"; readonly readAt: "read_at"; /** The literal `held` carries when the store cannot vouch for what it holds. */ readonly heldUnknown: "unknown"; }>; /** * How far a result's READ covered what it asked for — one pure rule over the * instants the tool declared (bounds are inclusive): * * - `covered` — the store holds every instant the read asked for; * - `partly-held` — it holds some of them; * - `not-held` — it holds none of them; * - `unknown` — the tool said it cannot vouch for what the store holds. * * @inline */ export type PeriodVerdict = 'covered' | 'partly-held' | 'not-held' | 'unknown'; /** Which door's words a problem is named in: the author's (camelCase) or the wire's. */ export type KeySpelling = 'camel' | 'wire'; /** One fault in a period, naming its field the way the reader spelled it. */ export interface PeriodProblem { /** Dot-pathed, from `period` down (`period.queried.from`, `period.readAt`). */ readonly field: string; readonly message: string; } /** * THE rule set: the first fault in a period, or `undefined` when it is well * formed — asked by every mint (which refuses it) and every recognizer (which * drops it from the record and serves the envelope as the tool wrote it). * * In order: a plain object; no key it does not have (a spelling slip names the * key meant — `read_at` at a camelCase door names `readAt`); `queried` a * `{ from, to }` span of instants with `from` not after `to`; `held` the * literal `'unknown'` or a span by the same rule; `readAt` an instant. * * @example * ```ts * periodProblem({ queried: { from: 'yesterday', to: '2026-09-26T10:00:00Z' }, held: 'unknown' }, 'camel'); * // → { field: 'period.queried.from', message: '`period.queried.from` must be an ISO 8601 instant with a zone — …' } * ``` */ export declare function periodProblem(value: unknown, spelling: KeySpelling): PeriodProblem | undefined; /** * Mint the wire form from an author's (camelCase) declaration — THROWS a * refusal (`refused: …`) on the first fault, at the line the author wrote (the * `absent()` law). `undefined` or `null` → `undefined`: not declared. A fresh * object, keys in wire order; nothing of the author's is held. */ export declare function mintPeriod(declared: unknown): PeriodOnWire | undefined; /** What a recognizer read off an envelope's `period`. */ export interface PeriodReading { /** The record's (camelCase) form — present when the envelope declared a well-formed one. */ readonly period?: DeclaredPeriod; /** Why a declared period was NOT read — present when it was malformed. */ readonly problem?: PeriodProblem; } /** * Read a wire period off a recognized envelope — never repaired. `{}` when * the envelope declares none (`undefined` or `null`, a JSON producer's * `None`); `{ period }` in the record's camelCase form when it is well formed; * `{ problem }` when it is not, so the caller can drop it from the record and * say so — the model still reads what the tool wrote. */ export declare function readPeriod(wire: unknown): PeriodReading; /** A declared period as detached plain data — the record's copy. */ export declare function copyPeriod(period: DeclaredPeriod): DeclaredPeriod; /** * Say, once per tool per process and in dev mode only, that a recognized * envelope's `period` — or an absence's `provenance` — could not be read and * was left off the record. The envelope itself is still recognized and served * as the tool wrote it — read, never repaired (the `items.ts` · * `readItemExtras` precedent). */ export declare function warnDroppedDeclaration(toolName: string | undefined, declaration: 'period' | 'provenance', problem: { readonly message: string; }): void; /** * Forget every dropped-declaration warning issued so far. * @internal test seam — the ledger is process-wide and warn-once. */ export declare function _resetPeriodWarnings(): void; /** * How far a declared period's READ covered what it asked for — ONE pure rule * over the instants the tool declared; bounds are inclusive: * * ```text * held === 'unknown' → 'unknown' * held.from ≤ queried.from and queried.to ≤ held.to → 'covered' * queried.to < held.from or queried.from > held.to → 'not-held' * otherwise → 'partly-held' * ``` * * No clock is read and no duration is parsed: the tool declared both spans. * Throws a `TypeError` naming the fault when handed a period that is not well * formed — a caller error; every period the library files was checked first. * * @example * ```ts * periodVerdict({ * queried: { from: '2026-09-26T09:00:00Z', to: '2026-09-26T10:00:00Z' }, * held: { from: '2026-08-27T02:00:00Z', to: '2026-09-26T02:00:00Z' }, * }); // 'not-held' — the data ends at 02:00; the hour asked about is after it * ``` */ export declare function periodVerdict(period: DeclaredPeriod): PeriodVerdict; /** * The verdict a CALL gets when its result declared more than one period (a * `coverage()` around an `absent()`, each with its own): the LEAST held of * them, so a reader is never told more than the weakest declaration covers — * `not-held`, then `partly-held`, then `unknown`, then `covered`. */ export declare function leastHeld(verdicts: readonly PeriodVerdict[]): PeriodVerdict | undefined; /** * The verdicts the MODEL is served — every verdict but `covered`, which is * served as nothing at all. * * @inline */ export type ServedPeriodVerdict = Exclude<PeriodVerdict, 'covered'>; /** The key the served verdict word sits under, inside the served `period`. */ export declare const SERVED_VERDICT_KEY = "verdict"; /** * The ONE note clause per served verdict (honesty step 7b, bench round 1). * Static library text: no instant, no tool value, nothing interpolated — the * `ABSENCE_NOTE` law. Each states the CONSEQUENCE of its word in the terms the * notes already teach ("ground this result does NOT cover"), because the bench * showed a model repeating `period.queried` as the ground a result covered * without ever comparing it with `period.held`. */ export declare const PERIOD_VERDICT_CLAUSES: Readonly<Record<ServedPeriodVerdict, string>>; /** A wire period as served: the tool's own keys, then the verdict word. */ export interface ServedPeriod { /** The wire period, copied, with {@link SERVED_VERDICT_KEY} last. */ readonly period: Readonly<Record<string, unknown>>; readonly verdict: ServedPeriodVerdict; /** {@link PERIOD_VERDICT_CLAUSES}`[verdict]`. */ readonly clause: string; } /** * What the model is served for one declared wire period — `undefined` when * there is nothing to add: no period, a period the rule set refuses (served * as the tool wrote it — read, never repaired), or a `covered` one. * * The verdict is {@link periodVerdict} over the instants the tool declared — * the one rule the record's `period` row is filed by, so the word the model * reads and the row the fold reads cannot disagree. Computed at the SERVE * door, never minted: the tool's own output keeps the bytes it declared, so a * helper in another language mints nothing new. * * @example * ```ts * servedPeriod({ * queried: { from: '2026-09-26T09:00:00Z', to: '2026-09-26T10:00:00Z' }, * held: { from: '2026-08-27T02:00:00Z', to: '2026-09-26T02:00:00Z' }, * })?.period; // { queried: {…}, held: {…}, verdict: 'not-held' } * ``` */ export declare function servedPeriod(wire: unknown): ServedPeriod | undefined; /** * The inverse, for a reader of what the model was SERVED: a wire period with * its served verdict word removed — when that word is exactly the one * {@link servedPeriod} computes from the rest. Anything else (no word, a word * that disagrees with the instants, a period the rule set refuses) comes back * as the same reference, so the rule set still names it: a verdict is derived * by the library, never declared by a tool. */ export declare function unservedPeriod(wire: unknown): unknown; /** * A served note with its period clause removed (the inverse of appending * {@link PERIOD_VERDICT_CLAUSES}`[verdict]`), or the note as found. */ export declare function noteWithoutClause(note: unknown, verdict: ServedPeriodVerdict): unknown; /** A served note: the note as found, then the period clause. */ export declare function noteWithClause(note: unknown, clause: string): string; /** * The `Period:` line the limits block prints for one declaring call — the * period AS THE TOOL DECLARED IT, instants verbatim (no reformatting, no zone * conversion, no verdict word: the answer's standing owns the verdict's * sentence). Static words around the tool's own values. With a * `presentation` (the run's clock zone, only under `.time()`), the instants * are rendered in that zone instead — see {@link periodLineInZone}. * * @example * ```ts * periodLine('backup_runs', { * queried: { from: '2026-09-26T09:00:00Z', to: '2026-09-26T10:00:00Z' }, * held: { from: '2026-08-27T02:00:00Z', to: '2026-09-26T02:00:00Z' }, * }); * // 'backup_runs queried 2026-09-26T09:00:00Z to 2026-09-26T10:00:00Z; the store holds * // 2026-08-27T02:00:00Z to 2026-09-26T02:00:00Z' * ``` */ export declare function periodLine(toolName: string, period: DeclaredPeriod, presentation?: BoundPresentation): string; /** * The results layer's verdict on ONE call's period — a row on the one honesty * ledger (`AgentState.findingsLedger`, `kind: 'period'`), filed by the results * subflow at the loop head (`../../core/agent/results/subflow.ts`), never by the model. * * - `verdict` — {@link periodVerdict} over the period the call's result * declared (the least held, when it declared more than one), or * `'undeclared'`: the tool declares a `ToolPeriod` and this result said * nothing about what its read covered — declared silence, recorded as * silence. * - `argument` — the argument the tool's `ToolPeriod` names: the join key to * the inputs layer's `argument` row for the same call (`period: true`), which * says WHO chose the period, while this row says WHAT the read covered. * * The row references the call's coverage row by `toolCallId` and never copies * the instants: the declaration stays in its own key (`coverageDeclared`). * Readers that switch over every row kind must skip one they do not know. */ export interface PeriodRow { readonly kind: 'period'; /** `AgentState.turnNumber` when the row was filed — the conversation turn. */ readonly turn: number; readonly toolCallId: string; readonly toolName: string; /** The iteration of the batch the call ran in. */ readonly iteration: number; readonly verdict: PeriodVerdict | 'undeclared'; /** The argument the tool's `ToolPeriod` names — present only when it declares one. */ readonly argument?: string; /** `period-differs-from-asked { missing, extra }` — the read differs from the asked range (or, for a window the model chose, from the person's). */ readonly differs?: PeriodDiffers; /** `period-shifted` — the call's look-back ran as sent, `byMs` after the turn's `now`. */ readonly shifted?: { readonly byMs: number; }; /** `period-beyond-retention` — refused as, or read as, wholly older than the tool's source keeps. */ readonly beyondRetention?: true; /** `partly-beyond-retention` — the read crosses the edge of what the source keeps; `verdict` decides. */ readonly partlyBeyondRetention?: true; /** * The time the source holds, when it holds none or only part of what the * call asked about (`verdict` `not-held` / `partly-held`) — the `queried` and * `held` spans of the declared period that verdict came from, as the tool * declared them. Filed only under `.time()`, where the served time line * hands the model this conclusion in the person's zone (`timeLimits.ts` · * `heldLine`) and the evidence gate spells it (`time/forms.ts` · * `timeFormsOf`, its `held` source): the model never converts a source's * UTC instants itself. */ readonly held?: { readonly queried: { readonly from: string; readonly to: string; }; readonly held: { readonly from: string; readonly to: string; }; }; } /** A source's held span beside what the call asked about — {@link PeriodRow}'s `held`. */ export type HeldSpan = NonNullable<PeriodRow['held']>; /** Every verdict a period row may carry, in the order the fold reports them. */ export declare const PERIOD_ROW_VERDICTS: readonly PeriodRow['verdict'][]; /** * The checkpoint door's test for a `period` row (`core/runCheckpoint.ts` · * `ledgerRowIsWellFormed`) — the one owner of the row's shape, so the door * refuses exactly what this module never files. */ export declare function periodRowIsWellFormed(row: Readonly<Record<string, unknown>>): boolean; /** * The one limits line for a call whose `period` row carries a time check * (time design § 10.2, step T8) — `undefined` when it carries none. Static * words around the two ranges, rendered in the presentation zone by the time * layer's one renderer, HANDED in bound (`time/present.ts` · * `bindPresentation` → `range`, to the second, the zone named — coverage never * imports the renderer); the raw instants stay on the row. * * @example * ```ts * periodCheckLine(row, bindPresentation({ zone: 'America/Los_Angeles' })); // time/present.ts * // 'search_logs read more than was asked — asked: 2026-10-08 00:00:00–23:59:59 America/Los_Angeles * // (UTC-07:00); read: 2026-10-08 00:00:00–2026-10-09 08:40:00 America/Los_Angeles (UTC-07:00)' * ``` * * `audience` names who reads the line: the PERSON (the limits block, the * default — their window is "your window") or the MODEL (the served time line, * `agent/arguments/serve.ts` · `timeLimitsSentence` — the same window is "the * person's window"). That reference differs, and a SHIFTED read: the model is * handed the library's conclusion ({@link shiftedConclusion}) — which part of * the window the result does not cover, in the person's zone, and that the * answer says so — not the two ranges to compare (T8 bench: after a pause the * two-range line did not stop "1 error in the last 30 minutes", 11/20). The * person's line keeps its bytes. */ export declare function periodCheckLine(row: PeriodRow, presentation: BoundPresentation, audience?: 'person' | 'model'): string | undefined; /** * The `Clocks` lines (time design § 9.6, § 10.2, step T8): one per wall-clock * source — a dataset whose declared axis names a zone — and, when two sources * declare different zones, the label that they differ. Every comparison is * made on instants; the lines only name the clocks. */ export declare function clockLines(sources: readonly SourceClock[], differ: { readonly zones: readonly string[]; } | undefined): string[];