agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
231 lines • 10.7 kB
JavaScript
/**
* reasons — the closed table of why an answer does not stand.
*
* Pattern: one frozen table, the ONE owner of each reason's layer, class and
* the committed row it is read from. `assess.ts` fires reasons in
* this table's order; a reader renders them from it.
* Role: core/ layer leaf, pure data.
*
* THE CLASS. `ask` is a reason the person can settle by answering a question
* the run already put to them — the turn ENDED in a pause waiting on a
* person's reply (a typed input, a question, a consent gate). Every other
* reason is `not-sure`: the library cannot ask at the answer (the call that
* could have used an answer has already run, and the library never replaces
* the model's reply with a question), so an absence renders "not sure — what
* was searched", never "ask".
*
* THE SOURCE. What each reason rests on is declared here too, once: `calls`
* when its rows are about specific tool calls (a result, a coverage row, an
* argument or period row, a conflict between readings); `answer` when its row
* is a verdict on the answer itself (a value no result carried, a claim in it,
* the app's checks on its bytes); `run` when it is about how the run went (a
* pause still waiting, a limit, unfinished steps, a check that could not reach
* a verdict). The fold never decides it per run, so "the run" is never an
* empty list of calls in disguise.
*
* WHAT IS NOT HERE, AND WHY. A reason is read from a COMMITTED row, never from
* an event, so the running agent and a later reader fold the same bytes. Two
* verdicts the honesty design names are events only in this version and so
* cannot be read yet: the `.claims()` dispositions (`claim-contradicted`, and
* the `checked-pass` rows that would SUPPORT "known"), and the integrity
* dispositions (`integrity.disposition`). The step that needs each one adds
* one committed row, and the reason joins this table in the same change — as
* the answer layer (honesty layer 4) did for two: the evidence gate's clean
* pass (a `grounded` witness row, read as the names-and-numbers check having
* run — never as a reason, never as support) and an answer given before its
* declared steps finished (`steps-unfinished`, below). Both rows are filed only
* while the answer layer is armed (`assessment/witness.ts`).
*/
/** Every reason, in the order the fold reports them: the ask first, then by layer. */
export const REASONS = Object.freeze([
{
reason: 'asked',
class: 'ask',
source: 'run',
reads: 'pausedToolCallId: the call a pause is still waiting on (requestInput, askHuman / pauseHere, a checkIn, a middleware ask, a credential consent)',
},
{
reason: 'argument-asked',
layer: 2,
class: 'ask',
source: 'run',
reads: "argumentAsk: the inputs layer's batch ask with a question still out (`waiting`) — nothing in that batch has run; the findingsLedger's current `asked` rows of this turn name the values",
},
{
reason: 'argument-assumed',
layer: 2,
class: 'not-sure',
source: 'calls',
reads: "findingsLedger: an argument row of this turn with source 'default' (the tool's rule assumed the value); or middlewareDecisions: a before-tool rewrite of a ruled argument (changedKeys) with no declared origin",
},
{
reason: 'argument-unverified',
layer: 2,
class: 'not-sure',
source: 'calls',
reads: "findingsLedger: an argument row of this turn with source 'model' on a ruled or period argument, or with a failed declared-source check (on any argument)",
},
{
reason: 'argument-read',
layer: 2,
class: 'not-sure',
source: 'calls',
reads: "findingsLedger: an argument row of this turn with source 'said' and reading — the person's words were found, the value is the model's reading of them (under an ask rule it asked instead)",
},
{
reason: 'value-contingent',
layer: 2,
class: 'not-sure',
source: 'calls',
reads: "findingsLedger: an argument row of this turn with source 'result' and setAside (the model named a result it had set aside); or a contingent row of this turn",
},
{
reason: 'coverage-gap',
layer: 3,
class: 'not-sure',
source: 'calls',
reads: "coverageDeclared: a notChecked or cannotCover item on a call of this turn; or history: the result's own envelope lists one, when its call has no coverage row",
},
{
reason: 'declared-absent',
layer: 3,
class: 'not-sure',
source: 'calls',
reads: "coverageDeclared: an absence on a call of this turn; or history: an empty rowset inside a declared coverage() boundary, or an absence in the result's own envelope when its call has no coverage row (the one emptiness reader)",
},
{
reason: 'empty-undeclared',
layer: 3,
class: 'not-sure',
source: 'calls',
reads: 'history: an empty rowset (a top-level array, the app’s rowsAt key, or the dataset ticket left where those rows were, counting 0) whose call has no coverage row',
},
{
reason: 'period-not-held',
layer: 3,
class: 'not-sure',
source: 'calls',
reads: "findingsLedger: a period row of this turn with verdict 'not-held' — the store holds none of what the read asked for (and, when the tool declares a ToolPeriod, the inputs layer's argument row for the same call: who chose the period)",
},
{
reason: 'period-partly-held',
layer: 3,
class: 'not-sure',
source: 'calls',
reads: "findingsLedger: a period row of this turn with verdict 'partly-held'",
},
{
reason: 'period-unknown',
layer: 3,
class: 'not-sure',
source: 'calls',
reads: "findingsLedger: a period row of this turn with verdict 'unknown' — the tool declared held: 'unknown'",
},
{
reason: 'period-undeclared',
layer: 3,
class: 'not-sure',
source: 'calls',
reads: "findingsLedger: a period row of this turn with verdict 'undeclared' — the tool declares a ToolPeriod and the result declared no period",
},
{
reason: 'period-differs-from-asked',
layer: 3,
class: 'not-sure',
source: 'calls',
reads: "findingsLedger: a period row of this turn with differs (the time layer, step T8) — what the call read is not the range it asked for (missing: asked, not read; extra: read, not asked — TQ8: extra alone is 'not sure' too), or, for a window the model chose, not the person's window",
},
{
reason: 'period-beyond-retention',
layer: 3,
class: 'not-sure',
source: 'calls',
reads: 'findingsLedger: a period row of this turn with beyondRetention (the time layer, step T8) — the window was wholly older than the tool declares its source keeps: refused before dispatch, or read that way',
},
{
reason: 'sources-conflict',
layer: 3,
class: 'not-sure',
source: 'calls',
reads: 'findingsLedger: a conflict row of this turn — its witnesses name a call of this turn, or it is stamped with this turn and the record does not place every witness in an earlier turn (assess.ts · conflictOfThisTurn)',
},
{
reason: 'value-unsupported',
layer: 4,
class: 'not-sure',
source: 'answer',
reads: 'unsupportedValues (revised: false)',
},
{
reason: 'value-survived-revision',
layer: 4,
class: 'not-sure',
source: 'answer',
reads: 'unsupportedValues (revised: true)',
},
{
reason: 'derived-from-reading',
layer: 4,
class: 'not-sure',
source: 'answer',
reads: "findingsLedger: a time-derived row of this turn — the answer states a time value no tool result carried that the library itself spelled from a reading of the person's words (an implied year, an offset, the end-of-grain minute, the served time line)",
},
{
reason: 'script-unmatched',
layer: 4,
class: 'not-sure',
source: 'answer',
reads: "findingsLedger: a script-shown witness row of this turn with label 'unmatched' — a fenced block the answer showed equals no program this turn's calls submitted to a declared code-running tool (filed under code: 'match' while the answer layer is armed)",
},
{ reason: 'stopped-early', layer: 4, class: 'not-sure', source: 'run', reads: 'stoppedEarly' },
{
reason: 'steps-unfinished',
layer: 4,
class: 'not-sure',
source: 'run',
reads: "findingsLedger: a steps-unfinished witness row of this turn (the answer came before the active skill's declared steps finished — filed while the answer layer is armed)",
},
{
reason: 'negative-prose-unverified',
layer: 4,
class: 'not-sure',
source: 'answer',
reads: 'findingsLedger: a negative-claim or negative-claim-note row of this turn; model prose never establishes a negative claim',
},
{
reason: 'claim-exceeds-search',
layer: 4,
class: 'not-sure',
source: 'answer',
reads: 'answerValidation.checks: a typed negative exceeded the admitted search',
},
{
reason: 'declaration-overclaimed',
layer: 3,
class: 'not-sure',
source: 'calls',
reads: "answerValidation.checks: an agent:negative-claims: row with reason 'declaration-overclaimed' — a tool's refusal said more than it read; the row's tools name the tool (negativeClaims/delivery.ts · reportWithNegatives keeps both)",
},
{
reason: 'answer-check-failed',
layer: 4,
class: 'not-sure',
source: 'answer',
reads: "answerValidation: status 'failed'",
},
{
reason: 'check-unreachable',
class: 'not-sure',
source: 'run',
reads: "answerValidation: status 'unverified' (an armed check that could not reach a verdict), or a failed report that also counts unreachable checks or names a reason (a validator that did not finish)",
},
]);
/** The table entry for one reason. */
export function reasonEntry(reason) {
const entry = REASONS.find((r) => r.reason === reason);
// Unreachable while `REASONS` covers the union — pinned by `test/core/agent/assessment`.
if (entry === undefined)
throw new Error(`assessment: no table entry for reason "${reason}"`);
return entry;
}
//# sourceMappingURL=reasons.js.map