agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
119 lines (118 loc) • 6.56 kB
TypeScript
/**
* semantics/described — `describedResult()`, the declaration door spelled the
* way code is written.
*
* Pattern: one core, one authoring door (`envelope.ts` · `mintSemantics`). This file
* is the camelCase door: it says how its author spells the
* declaration and respells each object to the wire. It judges
* nothing itself — every rule lives in the core, once.
* Role: lib/ layer, pure.
* Emits: N/A (the caller emits `agentfootprint.tools.semantics_declared`).
*
* ## One authoring spelling, one unchanged wire
*
* `semantic()` copied the wire's snake_case names into its declaration
* (`measured_at`, `is_counter`, `filter_note`) beside camelCase ones
* (`notChecked`, `cannotCover`), so an author wrote two spellings in one
* object — and "semantic" already means vector retrieval everywhere else in
* this library. `describedResult()` takes the same fields in camelCase and
* mints the same wire, byte for byte. The wire does not change: the model and
* any foreign process keep reading snake_case.
*
* The authoring door takes ONE spelling and refuses snake_case, quoting the key its
* author wrote and naming the one meant (`provenance.measured_at` → "did you
* mean `measuredAt`?"). A migration that swaps only the function name then
* fails loudly at the line that needs the new spelling, instead of silently
* minting without it. The declaration never accepts both spellings in one object.
*/
import { type SemanticIssue } from './envelope.js';
import type { DescribedResultDeclaration, SemanticProvenance, ToolSemantics } from './types.js';
/** A provenance declaration as an author writes it (camelCase) — and as the record keeps it. */
export type DeclaredProvenance = NonNullable<DescribedResultDeclaration['provenance']>;
/**
* Mint an author's (camelCase) `provenance` into the wire's spelling by THIS
* door's rules — `absent()` asks it (honesty step 7b), so an absence says its
* source and time in the shape, the spelling and the rule set a described
* result does: `measuredAt` and `source` required, `ageSeconds` a finite
* number ≥ 0, `sourceExportDate` a non-empty string, a snake_case key refused
* naming the camelCase one. THROWS the refusal (`refused: …`) at the line the
* author wrote. A fresh object, keys in the author's order.
*/
export declare function mintProvenance(declared: unknown): SemanticProvenance;
/** What a recognizer read off an envelope's `provenance`. */
export interface ProvenanceReading {
/** The record's camelCase form — present when the envelope declared a well-formed one. */
readonly provenance?: DeclaredProvenance;
/** The first fault, when it declared one the record cannot carry. */
readonly problem?: SemanticIssue;
}
/**
* Read a WIRE provenance (an absence minted anywhere — a Python helper, an
* older process) into the record's camelCase form, by the same rule set —
* never repaired. `{}` when none is declared (`undefined` or `null`).
*/
export declare function readProvenance(wire: unknown): ProvenanceReading;
/**
* Return rows, a series or relationships from a system of record — WITH the
* caveats that make them honest — in a shape the framework recognizes, the
* record keeps whole, and a build gate can refuse.
*
* **Where it lives.** It is the tool's RESPONSE: `execute` returns it, and
* nothing is added to the system prompt or the tool's schema. The model reads
* a compact projection ({@link semanticsForModel}: the data, `grain`,
* `provenance`, the `not_covered` lines composed from `coverage`, a non-null
* `clarify` and a static note) — never the `checked` list, the `render` hints
* or the marker. The run record keeps the FULL envelope
* (`agentfootprint.tools.semantics_declared`), even when the result is over
* the tool's `resultCeiling` and refused. A declared `coverage` also flows
* through the channel `coverage()` uses, which is the only part that can reach
* the final answer (`.limitsTravelWithTheAnswer()`).
*
* **Values from the data.** `provenance.measuredAt` is when the WORLD was
* measured, taken from the data — the export's time, the moment of a live
* read, the newest sample of a series, the end of a window — never typed in.
* It is never parsed: the library passes your words through.
*
* **The period** (honesty step 7b). `period: { queried, held, readAt? }` says
* what time the READ covered — the instants it asked for, and what the store
* holds (or `'unknown'`) — as ISO 8601 instants with a zone. The model reads it
* as declared; the results layer compares the instants and files its verdict
* (covered · partly held · not held · unknown). A malformed period is refused
* here.
*
* Refuses (throws, at the call site — the `absent()` law) any declaration
* this vocabulary cannot honor: series without `grain`, series or facts
* without `provenance.measuredAt` and `provenance.source`, a counter-looking
* aggregation with `isCounter` unstated, and every malformed shape. Each
* refusal starts `refused: `, names the field as you spelled it, and — inside
* `execute` — is the call's error result, which the model reads in place of
* the data; the run continues. A snake_case key (`measured_at`,
* `not_checked`) is refused, naming the camelCase one.
*
* Use `absent()` when nothing matched and `coverage()` for
* any other value that has limits; never wrap one helper's result in another.
*
* @example rows from a nightly export — the time comes from the export
* ```ts
* const exportTime = snapshot.exportedAt; // when the WORLD was measured
* return describedResult({
* facts: rows.map((r) => ({ entity: r.vm, datastore: r.datastore, size_tb: r.sizeTb })),
* provenance: { measuredAt: exportTime, source: 'RVTools export' },
* coverage: {
* checked: [`every VM in the RVTools export of ${exportTime}`],
* cannotCover: [{ what: 'hosts that are not VMware', why: 'RVTools sees VMware only' }],
* },
* });
* ```
*
* @example a series — `measuredAt` is the newest sample
* ```ts
* const newest = rows.reduce((a, b) => (a.time > b.time ? a : b)).time;
* return describedResult({
* series: rows.map((r) => ({ t: r.time, entity: r.port, metric: 'avg_iops', value: r.iops })),
* grain: { interval: '30m', aggregation: 'avg', isCounter: false },
* provenance: { measuredAt: newest, source: 'InfluxDB SwitchPortStats' },
* });
* ```
*/
export declare function describedResult(decl: DescribedResultDeclaration): ToolSemantics;