agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
207 lines (206 loc) • 10.6 kB
TypeScript
/**
* assessment/compose — the answer's standing as ONE line for the person, under
* its own opt-in arm (`.answerLayer({ standingLine: true })`).
*
* Pattern: Lens. A pure composer over the standing as data
* (`AnswerAssessmentData`, the projection the answer layer files on
* `turn_end` and on `agentfootprint.answer.assessed`) and the values
* a tool's rule assumed this turn (the same `AssumedLine`s the
* "Assumed" block prints — `arguments/serve.ts` · `assumedLinesFor`).
* One closed, versioned table of words; nothing here reads the run.
* Role: core/ layer leaf of the answer layer (honesty layer 4). Loaded only
* by the layer's stage (`assessment/stage.ts`), which is itself
* loaded through `import()` under the arm — a plain agent's graph
* never carries it.
* Emits: N/A.
*
* ## What the line may say
*
* The owner's five words, in precedence order — ask > not sure > known >
* consistent > not assessed — each rendered from the fold's value and reasons
* alone (`assessment/assess.ts` · `assessAnswer`), never from how sure the
* model sounded. "Consistent with the run's record" is never "known" and never
* "verified". A reason is named by what the record holds, in the past tense of
* the run that holds it. The ONE place the line prints a value is an assumed
* argument, and then only in the tool's own argument view (a value that view
* hides is said to be hidden, never printed) — the "Assumed" block's rule, so
* "the standing line owns the assumed sentence" whenever both arms are on:
* one composer for one fact.
*
* ## What it never does
*
* It never edits the model's reply. The line is appended after it by the
* final branch's one composer (`semantics/coverage/answer.ts` ·
* `composeAnswerWithCoverage`), under the separator the limits block uses, and
* only on a PROSE answer: a typed answer (`.outputSchema()`) is JSON, and the
* builder refuses the line beside it — as it refuses the line beside
* `.answerValidation()`, which judges exact bytes.
*/
import type { AnswerAssessment, AssessmentCheck, AssessmentReason, AssessmentReasonSource } from './types.js';
/**
* Bumped whenever any word of the tables below changes. 2: the named clauses
* (`NAMED_REASON_WORDS`) and the assumed clause's named form — said only for a
* call whose tool declares a `displayName`; every version-1 line is unchanged.
* 3: the `script-unmatched` clause and the `script-match` check's word; every
* version-2 line is unchanged.
*/
export declare const STANDING_LINE_VERSION = 3;
/**
* WHEN the line is said — `.answerLayer({ standingLine: { when } })`:
*
* - `'always'` — on every prose answer (`standingLine: true` is this);
* - `'unless-consistent'` — every standing but `consistent`;
* - `'not-sure-or-ask'` — only `not-sure` and `ask`.
*
* `ask` is said in every mode. The mode decides only whether the LINE is
* appended: the standing travels as data in every mode
* (`turn_end.answerAssessment`, `agentfootprint.answer.assessed`), the limits
* block is appended whether or not the line is, and the answer the run
* returns, saves and emits is one string — so memory keeps exactly what was
* delivered. No line is never a verdict: under `'not-sure-or-ask'` it means
* no reason fired, which includes "nothing could be checked".
*/
export type StandingLineWhen = 'always' | 'unless-consistent' | 'not-sure-or-ask';
/**
* Whether `when` appends the line for an answer of `standing`. Never decides
* anything else: the standing, its data and the limits block are the same in
* every mode.
*
* @example
* ```ts
* standingLineSaid('not-sure-or-ask', 'consistent'); // false — no line; the data still travels
* standingLineSaid('not-sure-or-ask', 'ask'); // true — `ask` is said in every mode
* ```
*/
export declare function standingLineSaid(when: StandingLineWhen, standing: AnswerAssessment['standing']): boolean;
/**
* The line's first words, one per standing — the owner's words. They are
* plain words a model can write too, so a reader of the answer text never
* matches on them alone: the answer account finds the line by rebuilding the
* WHOLE line from the run's record (`answer-account/account.ts` ·
* `readAnswer`, through `standingLineOf`).
*/
export declare const STANDING_LINE_OPENINGS: Readonly<Record<AnswerAssessment['standing'], string>>;
/** The answer's standing as data — the projection filed on `turn_end` and on the event. */
export interface AnswerAssessmentData {
readonly assessment: AnswerAssessment['assessment'];
readonly standing: AnswerAssessment['standing'];
readonly reasons: readonly AssessmentReason[];
readonly checked: readonly {
readonly layer: AnswerAssessment['checked'][number]['layer'];
readonly check: AssessmentCheck;
readonly ran: number;
readonly of: number;
}[];
}
/** One value a tool's rule assumed this turn — the "Assumed" block's line, as data. */
export interface StandingAssumedValue {
readonly toolName: string;
readonly argument: string;
readonly value: string;
readonly hidden: boolean;
}
/**
* The projection of one fold — names, enums and counts only: the value, its
* rendering, the reason KINDS and the checks that ran. No witness pointer, no
* digest, no value.
*/
export declare function assessmentDataOf(a: AnswerAssessment): AnswerAssessmentData;
/**
* The standing as data read back off a record — `turn_end.answerAssessment`
* as a recording holds it — or `undefined` unless every field is in this
* version's vocabulary (a value, a standing with an opening, known reason
* kinds, checks with counts). A fresh copy, field by field.
*/
export declare function assessmentDataFrom(value: unknown): AnswerAssessmentData | undefined;
/**
* Whether a before-tool rewrite with no declared origin fired
* `argument-assumed` — the fold's witness that points at `middlewareDecisions`
* rather than at a row. The line then names it once, without a value.
*/
export declare function rewrittenBehind(assessed: AnswerAssessment): boolean;
/**
* Where this turn's assumed values are read from: the ledger, the turn, and
* the before-tool rewrites a row may have been superseded by — handed to the
* "Assumed" block's own reader (`arguments/serve.ts` · `assumedLinesFor`),
* which the caller loads: the answer layer's stage through `import()` (the
* optional-family law), the answer account statically.
*/
export interface AssumedValuesSource {
readonly ledger: readonly {
readonly kind: string;
}[];
readonly turn: number;
readonly readDecisions: (() => readonly unknown[]) | undefined;
}
/**
* The source of the line's assumed values in `record` (committed state) —
* `undefined` when the line names none: no `argument-assumed` reason, no
* turn, or no `argument` row. Rows that are not objects are passed over, so a
* damaged record is read, never thrown on.
*/
export declare function assumedValuesSourceOf(reasons: readonly AssessmentReason[], record: Readonly<Record<string, unknown>>): AssumedValuesSource | undefined;
/**
* What the line may NAME — given only when a tool the reasons rest on
* declares a `displayName`: each reason's source (`AnswerAssessment` ·
* `reasons[].source`, the fold's own projection) and the app's words for each
* tool (registered name → display name). Without it the line is the bytes it
* always was.
*/
export interface StandingNames {
readonly sources: ReadonlyMap<AssessmentReason, AssessmentReasonSource>;
readonly displayNames: ReadonlyMap<string, string>;
}
/**
* The words a record kept for the line — registered tool name → display name,
* as `turn_end.standingLineNames` holds them — or `undefined` unless every
* entry is one a person may read, exactly as the stage filed it (the rule
* `defineTool` holds a `displayName` to, trimmed). A fresh map, entry by
* entry.
*/
export declare function standingNamesFrom(value: unknown): ReadonlyMap<string, string> | undefined;
/** The fold's sources keyed by reason — what {@link StandingNames} carries. */
export declare function sourcesByReason(a: AnswerAssessment): ReadonlyMap<AssessmentReason, AssessmentReasonSource>;
/**
* The display names the line may use for `a`: for every tool a `not-sure`
* reason's calls name, and every tool whose rule assumed a value, the app's
* words when its tool declares them — read through `displayNameOf`, the
* agent's own tools. Empty unless the standing is `not-sure` (only that line
* names reasons) and a tool declared a name: the line then says what it always
* said.
*/
export declare function standingDisplayNames(a: AnswerAssessment, assumed: readonly StandingAssumedValue[], displayNameOf: (toolName: string) => string | undefined): ReadonlyMap<string, string>;
/**
* The standing line for one answer — `''` never: every standing has words.
*
* `assumed` is this turn's assumed values (empty unless the inputs layer filed
* `default` rows); `rewritten` says a before-tool middleware rewrote a ruled
* argument without declaring where the value came from (the fold's other
* `argument-assumed` witness). `names` lets a `not-sure` line name the calls a
* reason rests on in the app's words — only calls whose tool declares a
* `displayName`, the rest counted; never the registered name. Without it, or
* when no such call is behind a reason, the clause is today's, word for word.
* Naming never changes the standing: the opening is read from `data` alone.
*
* @example
* ```ts
* standingLineOf(
* { assessment: 'unknown', standing: 'not-sure', reasons: ['argument-assumed'], checked: [] },
* [{ toolName: 'search_logs', argument: 'window', value: '2h', hidden: false }],
* false,
* );
* // 'Not sure — window = "2h" was assumed by search_logs\'s rule, not given by you.'
*
* standingLineOf(
* { assessment: 'unknown', standing: 'not-sure', reasons: ['declared-absent'], checked: [] },
* [],
* false,
* {
* sources: new Map([['declared-absent', { kind: 'calls', calls: [{ toolCallId: 'c1', toolName: 'load_attachment' }] }]]),
* displayNames: new Map([['load_attachment', 'loading the attached data']]),
* },
* );
* // 'Not sure — loading the attached data reported no matching results.'
* ```
*/
export declare function standingLineOf(data: AnswerAssessmentData, assumed: readonly StandingAssumedValue[], rewritten: boolean, names?: StandingNames): string;