agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
336 lines • 15.7 kB
TypeScript
/**
* semantics/types — the semantic tool-result vocabulary, as a PURE leaf
* (9.53.0, the `toolOutcome.ts` precedent).
*
* WHY IT LIVES HERE. Two sides read these words and neither may own the
* other: the agent loop's dispatch boundary (core/agent/stages/toolCalls.ts)
* recognizes the envelope at run time, and the `check:semantics` build gate
* (semantics/check.ts) judges the same shapes with no agent anywhere in
* sight. Declaring the vocabulary in a leaf both sides import keeps the
* dependency pointing the right way — the loop and the gate depend on the
* words, never on each other.
*
* The one exception to "zero imports" is TYPE-ONLY and deliberate: the
* envelope's `coverage` field ABSORBS the existing coverage vocabulary
* (`CoverageItem` / `CoverageInput`, semantics/coverage/types.ts — itself a
* pure-data leaf) instead of duplicating it. Two spellings of "ground this
* result does not cover" would eventually disagree; type-only imports erase
* at runtime, so this module stays a runtime leaf.
*
* ## What the envelope is
*
* A tool that answers "how many IOPS did fc1/3 do last hour?" today returns
* prose or an ad-hoc object, and every caveat that makes the number honest —
* the collection interval, whether the values are counters that must not be
* summed, when the world was actually measured, which clusters were not
* collected — is re-implemented by hand inside every tool and held in place
* by review. Culture scales to one disciplined author; it does not scale to
* a hundred tools. The semantic envelope makes those caveats TYPED DATA that
* travel with the values, so honesty is inherited, not re-authored — and so
* a build gate can refuse a triage tool that forgot its own limits.
*
* It is a SIBLING recognizer beside the effects envelope and the coverage
* primitives — its own reserved marker (`af_semantics`, the
* `af_absent`/`af_coverage` family), never new keys on the effects envelope.
* The three compose on one result: `{ content: describedResult({…}), effects:
* […], status }` is a tool that returns typed data AND proposes a transition.
*/
import type { CoverageDeclaration, CoverageItem } from './coverage/types.js';
import type { DeclaredPeriod, PeriodOnWire } from './coverage/period.js';
import type { PagingDeclaration, ResolvedDeclaration } from './coverage/search/resultFacts.js';
/**
* The reserved key that makes a semantic envelope recognizable. Reserved
* vocabulary on the tool-result wire (the `af_absent` / `af_coverage`
* precedent): a plain object carrying `af_semantics: true` that validates
* cleanly is an envelope; every other value any tool has ever returned keeps
* its bytes.
*/
export declare const SEMANTICS_MARKER = "af_semantics";
/**
* One measured point. `t` is the tool's own clock words (an ISO string or an
* epoch number — the library never reinterprets it), `entity` is what was
* measured, `metric` names the measurement, `value` is the reading.
*/
export interface SemanticSeriesPoint {
readonly t: string | number;
readonly entity: string;
readonly metric: string;
readonly value: number | string | boolean | null;
}
/**
* One row of typed facts about one entity. The columns are the tool's own —
* this library requires only that every row says WHAT it is about.
*/
export interface SemanticFact {
readonly entity: string;
readonly [field: string]: unknown;
}
/** One typed relationship — "this VM rides that datastore", "this zone
* contains that WWPN". `kind` names the relationship in the tool's words. */
export interface SemanticEdge {
readonly from: string;
readonly to: string;
readonly kind: string;
readonly [field: string]: unknown;
}
/**
* The grain — what one value MEANS, stated when it is not what a reader
* would assume. This is the field that stops a model from adding
* `jobs_local` to `jobs_replicated_in` and announcing a fleet size nobody
* has.
*/
export interface SemanticGrain {
/** The collection interval the values live on ('30m', '1h', 'daily'). */
readonly interval?: string;
/** How the values were folded ('avg', 'max', 'sum', 'count', …). */
readonly aggregation?: string;
/**
* Whether the values are counters. MUST be stated (true or false) whenever
* `aggregation` is counter-looking (see
* {@link COUNTER_AGGREGATION_WORDS}): summing two counters double-counts,
* and a reader cannot tell a counter from a gauge by looking at a number.
*/
readonly is_counter?: boolean;
/** What was folded away ('per-port rows collapsed to per-switch'). */
readonly collapsed?: string;
}
/**
* Where the values came from and how old they are. `measured_at` is when the
* WORLD was measured — not when the tool ran; a tool that reads a nightly
* export and answers in 4ms is serving yesterday.
*/
export interface SemanticProvenance {
/** When the world was measured (the tool's own clock words). Required
* whenever the envelope carries `series` or `facts`. */
readonly measured_at: string;
/** How stale the data was when the tool answered, in seconds. */
readonly age_seconds?: number;
/** The system of record the values were read from. Required with
* `measured_at`. */
readonly source: string;
/** For file-fed collectors: the export the values rode in on. */
readonly source_export_date?: string;
}
/**
* The ask-vs-answer decision, as data. A tool that matched three volumes for
* one WWN should not pick one silently — it should hand the question and the
* candidates back, typed, so the loop (or a UI) can ask.
*/
export interface SemanticClarify {
readonly question: string;
/** The candidates the question is choosing between. May be empty — an open
* question is still a question. */
readonly candidates: readonly unknown[];
}
/**
* Rendering HINTS — the tool never renders. A UI that understands them draws
* a better table; one that does not loses nothing, because everything load-
* bearing is in the data fields. Dropped from the model's view entirely.
*/
export interface SemanticRender {
/** The default presentation ('table', 'chart', 'prose', …). A hint. */
readonly default: string;
/** Column order for a tabular view. */
readonly columns?: readonly string[];
/** Sort hint ('avg_iops desc'). */
readonly sort?: string;
/** A note about what filtering already happened ('replicas excluded'). */
readonly filter_note?: string;
/** Chart-shape hint ('line per entity'). */
readonly chart_hint?: string;
}
/**
* The envelope's coverage, normalized — the SAME three-list vocabulary the
* `coverage()` / `absent()` primitives speak (checked / not checked / cannot
* cover), in the snake_case spelling every rendered tool shape uses because
* a model reads it more often than code does. Declared through
* {@link DescribedResultDeclaration.coverage} with the exact `CoverageDeclaration`
* input the `coverage()` primitive takes; the dispatch loop declares it
* through the same channel (`tools.coverage_declared`, tracked state, the
* final-answer limits block) — absorbed, never duplicated.
*/
export interface SemanticCoverage {
readonly checked?: readonly CoverageItem[];
readonly not_checked?: readonly CoverageItem[];
readonly cannot_cover?: readonly CoverageItem[];
}
/**
* {@link SemanticGrain}, spelled as code: `isCounter` is `is_counter` on the
* wire.
*
* @inline
*/
interface DescribedGrain {
/** The collection interval the values live on ('30m', '1h', 'daily'). */
readonly interval?: string;
/** How the values were folded ('avg', 'max', 'sum', 'count', …). */
readonly aggregation?: string;
/**
* Whether the values are counters — cumulative readings a reader must never
* add together. MUST be stated (true or false) whenever `aggregation` is
* counter-looking (see {@link COUNTER_AGGREGATION_WORDS}).
*/
readonly isCounter?: boolean;
/** What was folded away ('per-port rows collapsed to per-switch'). */
readonly collapsed?: string;
}
/**
* {@link SemanticProvenance}, spelled as code: `measuredAt`, `ageSeconds` and
* `sourceExportDate` are `measured_at`, `age_seconds` and
* `source_export_date` on the wire.
*
* @inline
*/
interface DescribedProvenance {
/**
* When the WORLD was measured, in the tool's own clock words — never
* parsed. Take it from the data: the export's time for a file, the moment
* of the read for a live query, the newest sample for a series, and the END
* of the window for a value computed over one.
*/
readonly measuredAt: string;
/** How stale the data was when the tool answered, in seconds. */
readonly ageSeconds?: number;
/** The system of record the values were read from. */
readonly source: string;
/** For file-fed collectors: the export the values rode in on. */
readonly sourceExportDate?: string;
}
/**
* {@link SemanticRender}, spelled as code: `filterNote` and `chartHint` are
* `filter_note` and `chart_hint` on the wire.
*
* @inline
*/
interface DescribedRender {
/** The default presentation ('table', 'chart', 'prose', …). A hint. */
readonly default: string;
/** Column order for a tabular view. */
readonly columns?: readonly string[];
/** Sort hint ('avg_iops desc'). */
readonly sort?: string;
/** A note about what filtering already happened ('replicas excluded'). */
readonly filterNote?: string;
/** Chart-shape hint ('line per entity'). */
readonly chartHint?: string;
}
/**
* What a tool author passes to `describedResult()` — the
* result fields in ONE spelling, camelCase, respelled to
* the unchanged snake_case wire ({@link ToolSemantics}).
*
* `provenance` is REQUIRED whenever `series` or `facts` is present, and the
* compiler says so: a number with no source and no age is refused at run time
* anyway, inside the tool, where the model reads the refusal. A declaration
* with only `edges` or only a `clarify` question needs none.
*
* At least one of `series`, `facts`, `edges` or a non-null `clarify` must be
* present — an envelope with no data and no question declares nothing. There
* is no `notCovered`: the prose list the model reads is derived from
* `coverage`, so the two can never disagree.
*/
export type DescribedResultDeclaration = {
/** Measured points — `{ t, entity, metric, value }`. Needs `grain`. */
readonly series?: readonly SemanticSeriesPoint[];
/** Typed rows about entities — every row names its `entity`. */
readonly facts?: readonly SemanticFact[];
/** Typed relationships — `{ from, to, kind }`. */
readonly edges?: readonly SemanticEdge[];
/** What one value MEANS. Required with `series`. */
readonly grain?: DescribedGrain;
/** Where the values came from and how old they are. Required with
* `series` or `facts`. */
readonly provenance?: DescribedProvenance;
/**
* What the READ behind the data covered in time (honesty step 7b) — the
* instants it asked for (`queried`), what the store holds (`held`, or
* `'unknown'` said out loud) and when the read ran (`readAt`). Every value
* is an ISO 8601 instant with a zone; the model reads it as declared, and
* the results layer files its verdict.
*/
readonly period?: DeclaredPeriod;
/**
* The entity each population value RESOLVED to — `[{ argument, id, name?,
* aliases? }]` — the tool's own claim. Read by a tool that declares its
* `search`: it binds the value the call was SENT for, and no other entity.
*/
readonly resolved?: readonly ResolvedDeclaration[];
/** Whether there is a next page — `{ next: null }` says there is none. */
readonly paging?: PagingDeclaration;
/** The coverage()-vocabulary declaration this result absorbs. */
readonly coverage?: CoverageDeclaration;
/** A question the result hands back instead of picking silently. `null`
* states "ambiguity was considered; there is none". It does not pause the
* run — the model reads it and decides what to ask. */
readonly clarify?: SemanticClarify | null;
/** Hints for a UI. The model never reads them. */
readonly render?: DescribedRender;
} & ({
readonly provenance: DescribedProvenance;
} | {
readonly series?: undefined;
readonly facts?: undefined;
});
/**
* The rendered semantic envelope — the exact object a tool hands back.
* Field names are snake_case and English on purpose (the `ToolAbsence`
* precedent): this value is read by a language model far more often than by
* code, and `af_semantics` is the only field that exists for the machine.
*/
export interface ToolSemantics {
readonly af_semantics: true;
readonly series?: readonly SemanticSeriesPoint[];
readonly facts?: readonly SemanticFact[];
readonly edges?: readonly SemanticEdge[];
readonly grain?: SemanticGrain;
readonly provenance?: SemanticProvenance;
/** What the read behind the data covered in time — honesty step 7b; minted
* only by `describedResult()`. The model reads it as declared. */
readonly period?: PeriodOnWire;
/** The entity each population value resolved to, as declared. */
readonly resolved?: readonly ResolvedDeclaration[];
/** Whether there is a next page, as declared. */
readonly paging?: PagingDeclaration;
readonly coverage?: SemanticCoverage;
/** DERIVED from `coverage` (not checked + cannot cover), one prose line
* per item — never author-set, so the list and the lists cannot drift. */
readonly not_covered?: readonly string[];
readonly clarify?: SemanticClarify | null;
readonly render?: SemanticRender;
/** The static sentence. Never interpolated — see `envelope.ts`. */
readonly note: string;
}
/**
* The declared class of a tool's RESULTS — what kind of answer this tool
* gives, stated by whoever wrote it (`defineTool({ resultClass })`; the
* `capabilities` law: declared, never inferred). The `check:semantics` gate
* keys its per-class rules on it:
*
* • `'triage'` — a verdict about health or fault. Every sample result must
* declare coverage: a triage that cannot say what it did NOT check turns
* "everything looks fine" into a claim about ground it never stood on.
* • `'inventory'` — a population listing. Every sample result must declare
* coverage ("4 of 5 clusters"), and one with `facts` but no `render`
* hint is warned at.
*
* Two classes, deliberately closed: each carries a rule the gate can PROVE.
* A class with no rule would be dead vocabulary.
*/
export type ToolResultClass = 'triage' | 'inventory';
/** The closed set, as data — validators and docs read one list. */
export declare const RESULT_CLASSES: readonly ToolResultClass[];
/**
* Aggregation words that suggest the values are counters — the words that
* make `grain.is_counter` REQUIRED (stated true or false). Matched as whole
* tokens, singular or plural, case-insensitive.
*/
export declare const COUNTER_AGGREGATION_WORDS: readonly string[];
/**
* The static sentence every semantic envelope carries in the model's view.
* Never interpolated (the `ABSENCE_NOTE` law): everything else the model
* reads is the tool's own data, and a note that quoted any of it back would
* hand the evidence corpus a second copy of values it already indexes.
*/
export declare const SEMANTICS_NOTE: string;
export {};
//# sourceMappingURL=types.d.ts.map