UNPKG

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) • 11.4 kB
/** * results/subflow — the four stages of the results layer's subflow * (`sf-results`): Declare → Verify → Record → Resolve (honesty layer 3). * * Pattern: thin stage bodies over three pure steps (`planPeriods`, * `checkPeriods`, `periodRowsOf`). Each stage reads what the one * before it staged on the subflow's own (isolated) scope and stages * its own output; the mount's output mapping (`honesty/mounts.ts` · * `mountResultsLayer`) merges the rows into the ONE ledger in one * write. * Role: core/ layer, the results layer. Loaded through `import()` by the * mount's stage wrappers — never on a plain agent's graph. Its rules * stay with their data: the period's shape, its rule set and the * verdict are `coverage/period.ts`'s; the ledger's merge and emit * halves are `findings/ledger.ts`'s, handed in as `deps`. * Emits: `agentfootprint.findings.period` — one per row, from the Record * stage, through the ledger's emit half (`deps.emitRows`). * * ## Where it runs, and what it reads * * At the LOOP HEAD — ToolCalls' `loopTo` lands on it — so it runs right after * every batch and before the next model call, and before any window strategy * folds the batch away. It reads the batch ToolCalls just ran (`toolResults`, * which still holds it at the loop head: identities only, never a result's * bytes) and the periods those calls' results declared (their committed * `coverageDeclared` rows of the batch's iteration). * * ## Each batch once — told apart by its iteration, never by a call id * * The schema re-ask, the step nudge, the evidence recheck and the wrap-up run * no tool and loop back here with the batch still in place. The layer tells * the first visit from those by the iteration that DISPATCHED the batch, * which ToolCalls stamps beside it (`AgentState.toolResultsIteration`, under * the arm): ToolCalls advances the iteration by one and loops straight here, * and every other way back advances it again — so the batch is new exactly * when the loop head's iteration is the stamp plus one, and the mount hands * the batch only then (`honesty/mounts.ts` · `batchToJudge`). A * call id cannot say it: a provider's synthetic counter restarts with each * provider instance (a resumed leg repeats the ids of the leg that failed, * whose rows the conversation checkpoint carries), and nothing stops a * provider reusing an id across batches of one run. If that invariant ever * broke, a batch would be judged twice — an over-report, never a hidden one. * On the first iteration there is no batch, and every stage finds nothing to * do. * * ## One verdict per call * * A call gets a row when its result declared a period, or its tool declares a * `ToolPeriod` (the inputs layer's declaration: which argument sets the * period). The verdict is `coverage/period.ts` · `periodVerdict` over the * declared period — the LEAST held, when the result declared more than one * (`leastHeld`) — or `undeclared` when the tool declares a period argument and * this result said nothing about what its read covered: declared silence, * recorded as silence. The row names the `ToolPeriod`'s argument, the join * key to the inputs layer's row for the same call. */ import type { TypedScope } from 'footprintjs'; import { type DeclaredPeriod, type HeldSpan, type PeriodRow } from '../../../semantics/coverage/period.js'; import { type PeriodCheckInput, type PeriodTimeCheck } from '../../../time/check.js'; import type { PeriodFacts, PeriodForm } from '../../../time/convert.js'; /** One call of the batch ToolCalls just ran — identities only. */ export interface RanCall { readonly toolCallId: string; readonly toolName: string; } /** One period a call's result declared, read off its committed coverage row. */ export interface CallPeriod { readonly toolCallId: string; readonly period: DeclaredPeriod; } /** A call the layer will judge: the periods its result declared, and the argument its tool's `ToolPeriod` names. */ export interface PlannedPeriod { readonly toolCallId: string; readonly toolName: string; /** Every period the call's result declared — empty when it declared none. */ readonly declared: readonly DeclaredPeriod[]; /** The argument the tool's `ToolPeriod` names — absent when it declares none. */ readonly argument?: string; /** Under `.time()` (step T8): what the time layer's result checks read besides the declared periods. */ readonly time?: Omit<PeriodCheckInput, 'declared'>; } /** A judged call — the verdict and, under `.time()`, the time layer's checks that hold. */ export interface CheckedPeriod { readonly toolCallId: string; readonly toolName: string; readonly verdict: PeriodRow['verdict']; readonly argument?: string; readonly time?: PeriodTimeCheck; /** Under `.time()`: the source's held span, when it holds none or only part of the asked time. */ readonly held?: HeldSpan; } /** * One call's time-layer record, handed by the mount under `.time()` only * (step T8): its `call-window` row's fields the check reads and its `call` * row's `drift` — read off the ledger, never recomputed. */ export interface CallTime { readonly toolCallId: string; readonly window?: PeriodCheckInput['window']; readonly drift?: PeriodCheckInput['drift']; } /** A tool's declared period facts, by tool name — the inputs layer's declaration, read (step T8). */ export type PeriodFactsOf = (toolName: string) => PeriodFacts | undefined; /** A tool's declared period forms, by tool name — whether a call sent a look-back (§ 7.4, step T8). */ export type PeriodFormsOf = (toolName: string) => readonly PeriodForm[] | undefined; /** * The argument a tool's `ToolPeriod` names, by tool name — read off the * implementation the shared dispatch resolver says answered the name * (`stages/toolResolver.ts`), so the layer judges the tool that ran. At the * loop head the tools slot has not recomposed yet, so the resolver still * answers for the epoch that dispatched the batch. */ export type PeriodArgumentOf = (toolName: string) => string | undefined; /** The subflow's own state — inputs frozen by the mount, then one key per stage. */ export interface ResultsLayerState { /** The batch to judge — empty on a visit with none (the first iteration, or a re-entry). */ readonly calls: readonly RanCall[]; /** The periods the batch's results declared — handed only when there are some. */ readonly periods?: readonly CallPeriod[]; /** The iteration that dispatched the batch (`honesty/mounts.ts` · `batchToJudge`) — handed only with a batch. */ readonly batchIteration?: number; /** The conversation turn — handed only with a batch. */ readonly turnNumber?: number; /** Under `.time()` (step T8): the batch's calls' time rows — handed only when the turn has a clock. */ readonly times?: readonly CallTime[]; /** Under `.time()`: the turn's clock `now` — handed with `times`. */ readonly now?: string; periodPlan?: readonly PlannedPeriod[]; periodChecks?: readonly CheckedPeriod[]; periodRows?: readonly PeriodRow[]; } /** What the stages are handed from outside the folder — closures, never scope. */ export interface ResultsLayerDeps { readonly periodArgumentOf: PeriodArgumentOf; /** The tool's declared period facts (`retention`, `granularity`) — read only for a call with time rows. */ readonly periodFactsOf?: PeriodFactsOf; /** The tool's declared period forms — read only for a call with time rows. */ readonly periodFormsOf?: PeriodFormsOf; /** The ledger's emit half — one `findings.period` event per row. */ readonly emitRows: (scope: TypedScope<ResultsLayerState>, rows: readonly PeriodRow[]) => void; } /** * DECLARE, pure: the calls to judge, in batch order — each call whose result * declared a period or whose tool declares a `ToolPeriod`. A call id met twice * in ONE batch is judged once, over every period declared under it (the least * held wins, so merging can only over-report). */ export declare function planPeriods(calls: readonly RanCall[], periods: readonly CallPeriod[], periodArgumentOf: PeriodArgumentOf, time?: { readonly times: readonly CallTime[]; readonly now: string; readonly periodFactsOf?: PeriodFactsOf; readonly periodFormsOf?: PeriodFormsOf; }): PlannedPeriod[]; /** * Under `.time()` (take 3): the held span of the declared period a `not-held` / `partly-held` verdict * came from — the first declared period whose own verdict is the call's — * or `undefined` for any other verdict. Fresh plain data. * * Here, in the results layer that files it, and not in `coverage/period.ts`: * this file loads through `import()` under the arm, so a plain agent never * carries it (the docs site's demo budget). * * @example * ```ts * heldSpanOf([{ queried: { from: '2026-09-30T13:00:00Z', to: '2026-09-30T18:59:50Z' }, * held: { from: '2026-10-01T16:22:44Z', to: '2026-10-01T17:02:44Z' } }], 'not-held'); * // { queried: { from: '2026-09-30T13:00:00Z', … }, held: { from: '2026-10-01T16:22:44Z', … } } * ``` */ export declare function heldSpanOf(declared: readonly DeclaredPeriod[], verdict: PeriodRow['verdict']): HeldSpan | undefined; /** * VERIFY, pure: one verdict per planned call — the least held of its declared * periods, or `undeclared` when it declared none (its tool declares a * `ToolPeriod`, or it would not be planned). */ export declare function checkPeriods(plan: readonly PlannedPeriod[]): CheckedPeriod[]; /** RECORD, pure: one `period` row per judged call, stamped with the turn and the batch's iteration. */ export declare function periodRowsOf(checked: readonly CheckedPeriod[], stamp: { readonly turn: number; readonly iteration: number; }): PeriodRow[]; /** * DECLARE — which calls of the batch just run are the layer's: a declared * period, or a tool that declares a `ToolPeriod`. Stages `periodPlan` * (identities, the declared periods, the argument name). */ export declare function declareResultsStage(scope: TypedScope<ResultsLayerState>, deps: ResultsLayerDeps): void; /** VERIFY — the verdict on each planned call's period. Stages `periodChecks` (identities and a word). */ export declare function verifyResultsStage(scope: TypedScope<ResultsLayerState>): void; /** * RECORD — one `period` row per judged call, stamped with the conversation * turn and the batch's iteration; one event per row, fired HERE (inside the * subflow). Stages `periodRows`; the mount's output mapping merges them into * the ledger in ONE write. */ export declare function recordResultsStage(scope: TypedScope<ResultsLayerState>, deps: ResultsLayerDeps): void; /** * RESOLVE — FLAG, the one verb a result that has already run admits (the * layer contract's clause 4: an ask and an assumption need a call that has * not run yet, a refusal one that can still be stopped). The rows Record filed * ARE the flags: the answer's standing folds them, and the mount's output * mapping returns them. So in this version the stage decides nothing and * writes nothing — it is the seat the later reading checks and outcome rows * (honesty step 8) resolve in. */ export declare function resolveResultsStage(): void;