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
TypeScript
/**
* 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;