agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
110 lines (109 loc) • 5.97 kB
TypeScript
/**
* assess — the answer's standing, folded from the committed record.
*
* Pattern: one pure Fold over the Trace. It reads COMMITTED state only — the
* run's `sharedState` (the snapshot's, or a paused run's checkpoint's)
* — and never an event, so the running agent and every later reader
* fold the same bytes. No clock, network, model or randomness; the
* same inputs give the same bytes; it never writes into the run.
* Role: core/ layer, pure. The one owner of the answer's standing: the
* `/observe` door publishes it (`assessAnswer`), `Agent.assessment`
* reads the last run through it, and the answer account's "How sure"
* row renders it (`answer-account/facts/howSure.ts` ·
* `readHowSure`).
* Emits: N/A.
*
* ## The law
*
* The standing comes from what the record can check, never from how sure the
* model sounds — and never "known" from silence. A reason that fires makes it
* `unknown`; only a TIE check (a passed enforce `.answerValidation()` report for
* these bytes) SUPPORTS `known`; checks that ran and fired nothing make it
* `unrefuted` ("consistent with the record"); a record on which nothing could be
* checked is `not-applicable` ("not assessed"). A membership pass — a value
* found, a result that came back non-empty — keeps a reason from firing and
* never supports anything.
*
* ## This turn only
*
* The turn begins after the last message a person said
* (`lib/saidByPerson.ts` · `isSaidByPerson`, the one owner of that question): a
* continued conversation carries earlier turns' results in `history`, and a
* library frame (a correction, a nudge) is not a person's turn. The fold reads
* the tool results after that message, every coverage row of the run
* (`coverageDeclared` is never carried into a next turn), the conflict rows of
* this turn — one that names one of those calls, or one stamped with this turn
* whose readings the record does not all place in earlier turns
* (`conflictOfThisTurn`; the ledger IS carried, so rows about earlier turns'
* calls are left out), and the
* answer's own rows (`unsupportedValues`,
* `stoppedEarly`, `answerValidation`). "Rests on" is every call of the turn: it
* may over-report, it never hides.
*
* ## How the turn ended
*
* A turn that ended in a PAUSE has no answer yet: a person's reply is what it
* waits on. The fold reads that from the committed state every pause leaves —
* `pausedToolCallId`, written by each pause the dispatch loop raises and
* cleared by each resume — so a snapshot, a checkpoint and a saved recording
* all say it, and every pause kind reads `ask`. A turn that ended in an ERROR
* has no answer either, and no committed row says so in this version: the
* readers settle that before they fold (`Agent.assessment` returns
* `undefined`; the answer account says the record shows no answer).
*/
import type { AnswerAssessment, AssessmentDeclarations, AssessmentRecord } from './types.js';
/**
* Fold one answer's standing from its committed record.
*
* The record of a turn that ended in an ANSWER or in a PAUSE (a pause reads
* `ask`, from the committed state the pause leaves — no checkpoint needed). A
* turn that ended in an error has no answer, and its committed state does not
* say so in this version: do not fold it (`agent.assessment()` returns
* `undefined` for one; the answer account says the record shows no answer).
*
* @param record a recording (`recordRun`), `{ snapshot }`, or a paused
* run's `{ checkpoint }` — its `sharedState` is read when
* no snapshot is given.
* @param declarations what the APP declares — any `AnswerAccountDeclarations`
* is accepted as it is: the fold reads only
* `tools[name].rowsAt`, where an object result keeps its
* rows.
* @throws TypeError only on a caller error (a record that is not an object, a
* malformed `rowsAt`); a record it cannot read yields
* `not-applicable`, never a throw.
*
* A REDACTED record (an agent's `redact`): when the fold would read a state key
* the record keeps out, it gives no verdict — `standing: 'not-assessed'`, with
* the keys it needed in `keptOut`. A verdict over the placeholder would read a
* kept-out pause as a pause and a kept-out history as an empty one.
*
* @example
* ```ts
* import { assessAnswer, recordRun } from 'agentfootprint/observe';
*
* const recorder = recordRun(agent);
* await agent.run({ message: 'Which ports on switch A are down?' });
* const a = assessAnswer(recorder.toRecording());
* a.standing; // 'not-sure'
* a.reasons; // [{ reason: 'empty-undeclared', layer: 3, witness: [...] }]
* ```
*/
export declare function assessAnswer(record: AssessmentRecord, declarations?: AssessmentDeclarations): AnswerAssessment;
/**
* `assessAnswer` over a runner's LIVE snapshot (`Agent.assessment()`): the
* state is the run's own heap, never served, so a placeholder string in it is
* a value. The snapshot's `recorders` row says a policy covered the run — true
* of its commit log, never of its live state — so it is not consulted here.
*
* @internal — `Agent · assessment`'s.
*/
export declare function assessLive(snapshot: unknown, declarations?: AssessmentDeclarations): AnswerAssessment;
/**
* The one fold, over a committed state already in hand. `served`: the state
* comes from a record served under a redaction policy, so a key holding the
* placeholder is a value the record keeps out (`keptOutGuard`).
*
* @internal — `assessAnswer`'s, and the answer account's, which holds the
* state and the marker apart.
*/
export declare function foldState(recorded: Readonly<Record<string, unknown>>, declarations: AssessmentDeclarations | undefined, served: boolean): AnswerAssessment;