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.7 kB
import type { FlowChart, FlowChartBuilder, StructureRecorder, TypedScope } from 'footprintjs'; import type { ExternalGround } from '../../../integrity/unsupported-argument/check.js'; import type { ToolOf } from '../arguments/resolve.js'; import type { ZoneName } from '../../../time/zone.js'; import type { RanCall, ResultsLayerState } from '../results/subflow.js'; import type { AgentState } from '../types.js'; /** What an armed agent hands the inputs layer's mount — closures, never scope. */ export interface InputsMountDeps { /** Filter refused references before input planning can consult host-supplied sources. */ readonly findings?: true; /** * The evidence gate's revision turns the tools off (an enforcing answer * validation) — present only then. The layer hands Route's dispatch test * the run's `toolsOffAfterRevision`, so it never asks about a call Route * will not run. */ readonly toolsOffAfterRevision?: true; /** The implementation that will answer a name — the shared dispatch resolver's. */ readonly toolOf: ToolOf; /** * A before-tool middleware chain is configured (`.toolMiddleware()`), so a * rewrite can supersede one of the layer's rows (the call then ran with the * rewrite's value). Read by the final branch's "Assumed" block * (`stages/prepareFinal.ts` · `prepareFinalWithLimitsAndAssumedStage`), * which then reads `middlewareDecisions`; absent, that key can hold no tool * rewrite and is never read. The mount itself does not read it. */ readonly rewrites?: true; /** * DECLARED SOURCES ARE ARMED (`.findings({ argumentSources: true })` or * `.inputsLayer({ argumentSources: true })`) — present only then. The mount hands the layer the pieces its checks read * and the closures that read them; `externalGrounds` is the app's own * vouched-for values (`AgentOptions.externalGrounds`), an `app` source with * its label. */ readonly sources?: { readonly externalGrounds?: () => readonly ExternalGround[]; /** * The findings ledger is armed beside declared sources (`.findings()`) — * present only then: a call that declares a basis files a BASIS row in * ToolCalls, and that row carries the count of dropped `from` entries. * Absent (declared sources without the ledger), no basis row is ever * filed, so the count rides the call's first argument row * (`sourceCorpus.ts` · `declaredSourcesOf`). */ readonly basisRows?: true; /** * A tool in reach may carry an arguments view — one the agent registers * carries one (`core/toolShownArgs.ts` · `carriesArgumentView`), or a * ToolProvider is wired (whatever it lists: its list is known only per * iteration) — present only then: no quote the model wrote is shown on a * row or an ask, since a quote may hold the value such a tool hides * (`arguments/resolve.ts` · `quotesMayShow`). */ readonly argumentViews?: true; }; /** * THE TIME LAYER IS ARMED (`.time()`) — present only then. The mount hands * the layer this turn's `clock` row and `time-reading` rows (`timeInputs`), * and the layer files one `call-window` row per call to a tool whose period * declares forms (`time/bind.ts`), merged into the ledger beside the * argument rows in the same ONE write. */ readonly time?: { /** The app's `.time({ zone })` — the zone of a form declaring `wallZone: 'app'`. */ readonly appZone?: ZoneName; }; } /** * The `sf-inputs` subflow: Declare → Verify → Record → Resolve, four thin * stages over the pure steps of `arguments/resolve.ts`. */ export declare function buildInputsSubflow(deps: InputsMountDeps): FlowChart; /** * Mount the inputs layer after the LLM call and before Route — or return the * builder untouched when the layer is not armed (`deps === undefined`), so an * agent without it builds the chart it always built. Both chart builders call * this, at the same place. */ export declare function mountInputsLayer<B extends FlowChartBuilder>(builder: B, deps: InputsMountDeps | undefined): B; /** What an armed agent hands the results layer's mount — closures, never scope. */ export interface ResultsMountDeps { /** The implementation that answered a name — the shared dispatch resolver's. */ readonly toolOf: ToolOf; } /** * The `sf-results` subflow: Declare → Verify → Record → Resolve, four thin * stages over the pure steps of `results/subflow.ts`. */ export declare function buildResultsSubflow(deps: ResultsMountDeps): FlowChart; /** * WHICH BATCH the loop head hands the results layer on this visit — the * iteration that dispatched it, or `undefined` when there is none to judge. * * `stamp` is the iteration ToolCalls wrote beside the batch it dispatched * (`AgentState.toolResultsIteration`, under the arm); `iteration` is the loop * head's. ToolCalls advances the iteration by exactly one and loops straight * here; every other way back — the schema re-ask, the step nudge, the evidence * recheck, the wrap-up — runs no tool, leaves the batch in place and advances * the iteration again. So the batch is new on this visit exactly when * `iteration === stamp + 1`: each batch judged ONCE per run. * * Never by call id. A provider's synthetic counter restarts with each provider * instance (`<prefix>-call-${++toolCallSeq}`), so 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. A stamp is * per run: no conversation checkpoint carries it, so a resumed leg starts with * none. Were the invariant above ever broken, a batch would be judged twice — * an over-report, never a hidden verdict. */ export declare function batchToJudge(iteration: number, stamp: unknown): number | undefined; /** * Under `.time()` (step T8): each call's time rows — its `call-window` row * and its `call` row's `drift` — and the turn's clock `now`, for the result * checks (`time/check.ts` · `periodTimeCheck`). Nothing when the turn has * no clock (an agent without `.time()`), so an unarmed run hands the layer the * input it always did. "This turn" is checked, not assumed: a ledger carried * forward (a checkpoint's `findingsLedger` from an agent that had `.time()`, * resumed and then run on by one that has not) holds an EARLIER turn's clock, * and that clock's `now` must not judge this turn's calls. Exported for that * test only. */ export declare function timeOfBatch(parent: Record<string, unknown>, calls: readonly RanCall[]): Pick<ResultsLayerState, 'times' | 'now'>; /** * Mount the results layer at the LOOP HEAD — or return the builder untouched * when the layer is not armed (`deps === undefined`), so an agent without it * builds the chart it always built. Both chart builders call this at the same * place — immediately before the window strategy's `Compact` stage (or the * loop target that stands there) — and make the mount the loop target * (`RESULTS_LOOP_TARGET`), the `Compact` precedent: the layer reads the batch * just run before any window strategy folds it away. * * Returned (the output mapping, `arrayMerge: Replace` — the loop-crossed mount * law): the rows, merged into the ledger in ONE write by the ledger's pure * half. Nothing to return → nothing is written. */ export declare function mountResultsLayer<B extends FlowChartBuilder>(builder: B, deps: ResultsMountDeps | undefined): B; /** The loop target the results layer's mount becomes when armed — `tool-calls` and every re-ask loop back to it. */ export declare const RESULTS_LOOP_TARGET: string; /** A committed key the one fold (`assessment/assess.ts` · `assessAnswer`) reads. */ export type AnswerFoldKey = 'history' | 'turnNumber' | 'pausedToolCallId' | 'findingsLedger' | 'coverageDeclared' | 'stoppedEarly' | 'unsupportedValues' | 'answerValidation' | 'argumentAsk' | 'middlewareDecisions'; /** What an agent with the answer layer armed hands the final branch — build-time facts, never scope. */ export interface AnswerMountDeps { /** The committed keys the fold reads — only those this agent's arms can write (`answerFoldReads`). */ readonly reads: readonly AnswerFoldKey[]; /** * `.answerLayer({ standingLine })` — compose one line for a prose answer, in * the answers its mode says it for (`true` is `'always'`). */ readonly standingLine?: import('../assessment/compose.js').StandingLineWhen; /** `.answerValidation()` is armed: an answer its report withholds is never assessed. */ readonly validated?: true; /** * A tool's `displayName` (the app's words), read through the agent's own * tools — present only where a tool can declare one; the line names a * reason's calls in those words (`assessment/stage.ts`). */ readonly displayNameOf?: (toolName: string) => string | undefined; } /** * THE READ LIST — which committed keys the answer layer's fold reads, from * the agent's arms: a key no arm of this agent can write is never read (a * tracked read of a key a run never writes is a phantom context source — * honesty law 9). Over-approximates on purpose: a key an arm CAN write is * read whether or not this run wrote it, so the in-run fold never misses a row * the read-after fold sees (the equality law). * * - always: `history`, `turnNumber`, `pausedToolCallId` (seed writes them), * `findingsLedger` (the witness rows, and any rows a continued conversation * restores — the restore is wired while a layer is armed) and * `stoppedEarly` — the Route decider writes it on EVERY agent * (`stages/route.ts` · `recordEarlyStop`): a limit cuts a turn short * whenever the model asked for calls, whether or not this agent registered * a tool that could answer them; * - a tool surface: `coverageDeclared` (only a tool that ran declares * coverage); * - the evidence gate: `unsupportedValues`; * - `.answerValidation()`: `answerValidation`; * - the inputs layer: `argumentAsk`, and `middlewareDecisions` when a * before-tool chain can rewrite a filled value. * * Every key's writers were checked against this list: a writer that no arm * gates puts its key in the always group — decide by the writer, never by the * usual path to it. * * @example * ```ts * answerFoldReads({ tools: true, evidenceGate: false, answerValidation: false, inputs: false, toolMiddleware: false }); * // ['history', 'turnNumber', 'pausedToolCallId', 'findingsLedger', 'stoppedEarly', 'coverageDeclared'] * ``` */ export declare function answerFoldReads(arms: { readonly tools: boolean; readonly evidenceGate: boolean; readonly answerValidation: boolean; readonly inputs: boolean; readonly toolMiddleware: boolean; }): readonly AnswerFoldKey[]; /** * Start the final branch: the answer layer's stage first when it is armed, * then PrepareFinal — or PrepareFinal alone when it is not, built exactly as * both chart builders always built it (same name, stage id, options and tags), * so an agent without the layer builds a byte-identical branch. Both builders * call this, so the twins cannot drift. */ export declare function startFinalBranch(answer: AnswerMountDeps | undefined, prepareFinal: (scope: TypedScope<AgentState>) => void | Promise<void>, structureRecorders: readonly StructureRecorder[] | undefined): FlowChartBuilder<any, TypedScope<AgentState>>;