UNPKG

agentfootprint

Version:

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

109 lines (108 loc) • 6.23 kB
/** * runbook/verdicts — the OPTIONAL verdict/rowset projection. * * Pattern: pure selection + rendering over the run's final state, plus one * tiny recorder that harvests decide() evidence for generated * meanings. Selected by `resultKind: 'verdict/*'`; a runbook of any * other kind never sees this module. * Role: core/runbook. * Emits: N/A. * * THE ONE-NUMBER LAW: the rendered table and the structured `verdicts` list * show the SAME rows under the SAME cap. A rendered table beside a longer * structured list is an invitation that gets accepted — rows come back * retyped with subtly wrong identifiers. * * TWO SURFACES, TWO LAWS: a rowset does not always reach its reader through * the model's prose. Where it does, the table ships pre-rendered and the model * is told to output it verbatim (`VERDICT_RENDER_NOTE`); where the HOST draws * the rowset itself, no table ships and the model is told the opposite * (`PANEL_RENDER_NOTE`). The bridge cannot tell which client it is in, so the * caller says — `presentation` on the options bag. * * GENERATED MEANINGS, never hand-restated: `verdict_meanings` is composed * from (a) the named decider's declared branches in the chart's own structure * (branch description, falling back to branch name), overlaid by (b) the rule * LABELS this run's decide() evidence carried — the rule speaking for itself — * and (c) the DEFAULT branch's declared label, carried by the same evidence. * A decider inside a dynamically generated fan-out branch is invisible to (a) * by construction (the branch chart does not exist at build time); (b) still * covers every verdict an executed rule produced. * * (c) exists because the default branch is chosen by NO rule — it fires * exactly when every rule failed, so it appears in no `rules[]` entry and (b) * can never name it. In a generated fan-out branch, where (a) is blind too, it * was the one verdict the rowset could show and the meanings map could not * explain. `decide(scope, rules, { branch, label })` (footprintjs ≥ 9.16.1) puts * that label on the decision evidence, so it arrives here the way every other * label does: harvested from the run, never declared at this boundary. There is * deliberately no caller-supplied meanings map — a map a caller can write is a * map that can describe rules that never ran. */ import type { CombinedRecorder } from 'footprintjs'; import type { FlowChart } from 'footprintjs'; import type { VerdictRow } from './types.js'; /** Default cap on `verdicts` rows and the rendered table. */ export declare const DEFAULT_MAX_ROWS = 50; /** The render law when PROSE is the rowset's only surface (`presentation: * 'prose'`, the default) — stated to the model beside the table. */ export declare const VERDICT_RENDER_NOTE: string; /** * The render law when the HOST renders the rowset (`presentation: 'panel'`) — * stated to the model INSTEAD of a table, because there is none to ship. * * Same failure, opposite instruction. A rowset the reader can already see does * not need retyping into prose; retyping it is how an identifier arrives * subtly wrong beside a correct one on screen. */ export declare const PANEL_RENDER_NOTE: string; /** The reserved verdict word for "no classification was reached" — rows * carrying it are counted into the coverage ledger as not-checked ground * (the three-outcome honesty: reached, reached-and-clear, DECLINED). */ export declare const DECLINED_VERDICT = "declined"; /** Read the rowset off the final state's `verdicts` key — an array of bags * each carrying a string `verdict`. Anything else reads as "no rowset". */ export declare function verdictRowsOf(state: Readonly<Record<string, unknown>>): VerdictRow[]; /** * Render the shown rows as one markdown table. Columns are the FIRST row's * own keys in declaration order — the chart writes its rows, so the chart * owns the column vocabulary; the bridge only renders it. */ export declare function renderVerdictTable(rows: readonly VerdictRow[]): string; /** The two spellings of the configured decider, once the static walk has * (maybe) resolved it — flow events report the decider by name. */ export interface DeciderIdentity { readonly spellings: ReadonlySet<string>; /** branch → declared meaning, from the chart's own structure. */ readonly declared: ReadonlyMap<string, string>; } /** * Find the named decider in the chart's static structure (root graph plus * statically declared subflows) and read its branches. Loop-ref stubs are * skipped FIRST — they deliberately violate node-id uniqueness. */ export declare function resolveDecider(chart: FlowChart, decider: string): DeciderIdentity; /** The evidence harvest: rule labels observed in this run, branch → label. */ export interface MeaningsHarvest { readonly recorder: CombinedRecorder; readonly observed: ReadonlyMap<string, string>; } /** * A tiny flow recorder capturing the named decider's decide() evidence as it * fires — collected DURING the traversal, never reconstructed after. Rule * labels are harvested for every rule the evidence lists (matched or not): * a rule that was evaluated has spoken its label, whichever branch won. The * DEFAULT branch's label rides the same evidence and is harvested with them. * * A decider inside a subflow (including a generated fan-out branch) reports * itself PATH-PREFIXED (`per-subject~0/Protection posture`), so the match is * on the LAST `/`-segment — the same last-delimiter reading every upstream * parser of these paths uses, which is what keeps the generated-branch * marker opaque here. */ export declare function meaningsRecorder(identity: DeciderIdentity): MeaningsHarvest; /** Compose the final meanings: declared branches first, the labels this run * observed (rule labels, and the default branch's own) winning where both * speak — the declaration made beside the rules is the sharper sentence. * Undefined when neither source produced anything — absent, never `{}`. */ export declare function composeMeanings(identity: DeciderIdentity, observed: ReadonlyMap<string, string>): Record<string, string> | undefined;