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
TypeScript
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>>;