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 • 20.3 kB
TypeScript
/**
* 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[];
//# sourceMappingURL=period.d.ts.map