UNPKG

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 (335 loc) • 15.6 kB
/** * 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 {};