agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
169 lines (168 loc) • 8.39 kB
TypeScript
/**
* assessment/witness — the answer layer's committed witness rows: two verdicts
* the Route decider computes that were EVENTS only until the answer layer
* (honesty layer 4) needed them on the record.
*
* Pattern: Map. Three row types, their builders and their well-formedness
* check — the one owner of the `grounded`, `steps-unfinished` and
* `script-shown` row kinds.
* `findings/types.ts` · `FindingsRow` imports the types; the rows
* are filed through the one writer (`findings/ledger.ts` ·
* `recordFindings`) by the Route decider, and ONLY while the answer
* layer is armed (`stages/route.ts` · `judgeEvidence`,
* `judgeUnfinishedSteps`).
* Role: core/ layer leaf of the answer layer. Static on purpose: the
* checkpoint door (`core/runCheckpoint.ts` · `validateCheckpoint`,
* a synchronous door) asks `witnessRowIsWellFormed`, so this module
* imports nothing but the script labels (`../scripts/label.ts`, a
* leaf) and stays small.
* Emits: N/A. A witness row emits nothing of its own — the verdict's event
* (`agentfootprint.agent.evidence_checked`,
* `agentfootprint.skill.steps_unfinished`) already fired, beside the
* row, from the same stage.
*
* ## Why a row, when the event already says it
*
* The answer's standing is folded from COMMITTED rows only
* (`assessment/assess.ts` · `assessAnswer`), never from events, so the running
* agent and every later reader fold the same bytes. A verdict that exists only
* as an event cannot be folded: the gate's clean pass could not say "the names
* and numbers check ran", and an answer given before its declared steps
* finished could not say so at all. Each row is the verdict, not a copy of the
* event: names, enums and counts — never a value from the answer, a quote, or
* a step's note.
*
* ## Always stamped with the turn
*
* The ledger crosses turns on a continued conversation and `iteration`
* restarts at 1 every run, so a witness row always carries `turn`
* (`AgentState.turnNumber`): the fold reads this turn's witness rows only.
*/
import type { ScriptBlockVerdict, ScriptLabel, UnmatchedReason } from '../scripts/types.js';
/** The evidence gate's posture — the `.namesAndNumbersFromEvidence()` vocabulary. */
export type WitnessPosture = 'assist' | 'guard' | 'rails';
/**
* The evidence gate's CLEAN verdict on the answer (`action: 'grounded'` on
* `agentfootprint.agent.evidence_checked`): every name and number the answer
* states was found in a tool result, or was exempt. Filed by the Route decider
* on the turn's answer while the answer layer is armed. A flagged or refused
* verdict needs no witness row — `AgentState.unsupportedValues` is already
* committed.
*
* The fold reads it as the names-and-numbers check having RUN on this answer —
* never as support: finding a value in a tool result is a membership pass, and
* a membership pass never makes an answer "known".
*/
export interface GroundedRow {
readonly kind: 'grounded';
/** `AgentState.turnNumber` when the row was filed — the conversation turn. */
readonly turn: number;
/** The iteration whose answer the gate judged. */
readonly iteration: number;
/** The posture in force. */
readonly posture: WitnessPosture;
/** How many distinct values the answer had to ground, exempt ones included. */
readonly candidates: number;
/** How many of `candidates` the gate looked up in the tool results (the rest were exempt). */
readonly lookedUp: number;
/** Set when the grounded answer was the one revision the gate asked for. */
readonly afterRevision?: true;
}
/** One declared step the answer came before — its position and its tool, never its note. */
export interface UnfinishedStep {
readonly index: number;
readonly tool: string;
}
/**
* The answer came before the active skill's declared steps finished
* (`agentfootprint.skill.steps_unfinished` with `action: 'accepted'` — the one
* teaching nudge was already spent — or `'cut-short'` — a limit forced the
* answer). Filed by the Route decider while the answer layer is armed, and
* only for the answer that STANDS: the step judge runs before the evidence
* gate, so a stop it accepted on a draft the gate then sends back files no row
* (the event still fires) — the revision is judged again, and at most one row
* per answer reaches the ledger. The `'nudged'` verdict needs no row: the turn
* went on.
*/
export interface StepsUnfinishedRow {
readonly kind: 'steps-unfinished';
/** `AgentState.turnNumber` when the row was filed — the conversation turn. */
readonly turn: number;
/** The iteration whose answer the step judge read. */
readonly iteration: number;
/** The skill whose procedure was in progress. */
readonly skillId: string;
/** The declared steps not reached, in order — position and tool only. */
readonly remaining: readonly UnfinishedStep[];
/** How many steps the procedure declares. */
readonly total: number;
readonly action: 'accepted' | 'cut-short';
}
/**
* One fenced block the answer SHOWED, and the script match's verdict on it
* (`.namesAndNumbersFromEvidence({ code: 'match' })`, `scripts/match.ts`) —
* filed by the Route decider while the answer layer is armed, one row per
* block read, and only for the answer that STANDS (a draft sent back for
* revision files none). Names, enums and counts — never the code.
*
* The fold reads `label: 'unmatched'` as the reason `script-unmatched` (a
* script shown in the answer matches no submission of this turn on the
* record), and every row as the `script-match` check having reached a
* verdict. A matched block fires nothing and supports nothing.
*/
export interface ScriptShownRow {
readonly kind: 'script-shown';
/** `AgentState.turnNumber` when the row was filed — the conversation turn. */
readonly turn: number;
/** The iteration whose answer the match read. */
readonly iteration: number;
/** 1-based position among the answer's fenced blocks that were read. */
readonly block: number;
/** The matched call's label, or `'unmatched'`. */
readonly label: ScriptLabel | 'unmatched';
/** On `'unmatched'`: why. */
readonly reason?: UnmatchedReason;
/** On a match: the call it equals, and the name the model called. */
readonly toolCallId?: string;
readonly tool?: string;
/** On a match: the call's dispatch within the turn (`ScriptRow.dispatch`) — its identity. */
readonly dispatch?: number;
/** On a match: exactly the runner's exit code, when it supplied one. */
readonly exitCode?: number;
}
/** Either witness row. */
export type AnswerWitnessRow = GroundedRow | StepsUnfinishedRow | ScriptShownRow;
/** The script match's verdicts as rows — one per block read, ids and labels only. */
export declare function scriptShownRowsFrom(blocks: readonly ScriptBlockVerdict[], at: {
readonly turn: number;
readonly iteration: number;
}): ScriptShownRow[];
/** The gate's clean verdict as a row — built from the event's own counts. */
export declare function groundedRowFrom(verdict: {
readonly turn: number;
readonly iteration: number;
readonly posture: WitnessPosture;
readonly candidates: number;
readonly lookedUp: number;
readonly afterRevision: boolean;
}): GroundedRow;
/** The step judge's accepted or cut-short verdict as a row — each step's position and tool only. */
export declare function stepsUnfinishedRowFrom(verdict: {
readonly turn: number;
readonly iteration: number;
readonly skillId: string;
readonly remaining: readonly {
readonly index: number;
readonly tool: string;
}[];
readonly total: number;
readonly action: 'accepted' | 'cut-short';
}): StepsUnfinishedRow;
/**
* Whether a value read from a stored checkpoint is a witness row this library
* could have written: the kind, a numeric `turn` and `iteration`, and every
* other field in its own shape. The checkpoint door's two arms
* (`core/runCheckpoint.ts`). An older runtime refuses a checkpoint that
* carries either kind.
*/
export declare function witnessRowIsWellFormed(r: Readonly<Record<string, unknown>>): boolean;