agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
173 lines (172 loc) • 9.28 kB
TypeScript
/**
* arguments/subflow — the four stages of the inputs layer's subflow
* (`sf-inputs`): Declare → Verify → Record → Resolve.
*
* Pattern: thin stage bodies over the pure steps in `resolve.ts`. Each reads
* what the stage before it staged on the subflow's own (isolated)
* scope and stages its own output; the mount's output mapper
* (`honesty/mounts.ts`) carries the rows and the resolutions out.
* Role: core/ layer, the inputs layer (honesty layer 2). Loaded through
* `import()` by the mount's stage wrappers — never on a plain agent's
* graph. Everything that is not this folder's arrives as `deps`
* (the dispatch resolver, Route's predicate, the ledger's emit half),
* so `arguments/` stays a leaf that never imports `findings/` or
* `stages/`.
* Emits: `agentfootprint.findings.argument` — one per row, from the Record
* stage, through the ledger's emit half (`deps.emitRows`).
*
* ## Only a batch that will dispatch
*
* Declare asks Route's own predicate (`deps.willDispatch`) first: a reply with
* no tool call, a run out of iterations, a halting cost budget — the batch is
* going to the final branch, so nothing is planned and every later stage finds
* an empty plan. The layer never files a row about a call Route will not run.
*
* ## Declared sources (`.inputsLayer({ argumentSources: true })`, `.findings({ argumentSources: true })`)
*
* Present only under the arm: `deps.sources` reads each call's `from` entries
* through the ONE reader of `_findings` (`findings/reserved.ts` ·
* `readDeclaration`, handed in as a closure, so this folder still never
* imports `findings/`), and Verify builds the corpora the checks read from the
* raw pieces the mount handed in (`sourceInputs`) — loaded on first use, never
* on a plain agent's graph. Every stage re-reads the entries rather than
* staging a second copy of the model's quotes.
*/
import type { TypedScope } from 'footprintjs';
import type { CallWindowRow, ClockRow, TimeAnswerRow, TimeReadingRow } from '../../../time/rows.js';
import type { ZoneName } from '../../../time/zone.js';
import type { SourceCorpus } from './checks.js';
import type { KeptAnswer } from './kept.js';
import type { ArgumentRow } from './rows.js';
import { type ArgumentResolution, type BatchCall, type CheckedArgument, type PlannedCall, type ToolOf } from './resolve.js';
import type { CallSources } from './sources.js';
/**
* The raw pieces the declared-sources corpora are built from — what the mount
* hands the layer under the arm (`honesty/mounts.ts` · `mountInputsLayer`),
* read by `honesty/sourceCorpus.ts` · `sourceCorpusOf`. Never the whole
* ledger: only the rows the checks read.
*/
export interface SourceInputs {
/** The history the model was served for this batch — the person's, the results', the app's words. */
readonly history: readonly unknown[];
/** The composed system prompt's records — the app's own text. */
readonly systemPromptInjections?: readonly unknown[];
/** The ledger's standing rows and `answered` argument rows — never the whole ledger. */
readonly ledger?: readonly unknown[];
/** The previous batch's results, by id and tool name (`AgentState.toolResults`). */
readonly previousBatch?: readonly {
readonly toolCallId: string;
readonly toolName?: string;
}[];
/** The run's message came from a composition, not a person (`AgentInput.messageFrom`). */
readonly composed?: true;
}
/**
* Under `.time()`: the rows of THIS turn the time layer reads — the turn's
* `clock` row, its `time-reading` rows and its `time-answer` rows (the
* windows the person settled in the time ask — `time/bind.ts` ·
* `turnWindowsOf`). Handed by the mount only under the arm, for a batch with
* calls, and only once the turn has a clock.
*/
export interface TimeInputs {
readonly clock: ClockRow;
readonly readings: readonly TimeReadingRow[];
/** Absent on a checkpoint an earlier version paused: no answer was filed then. */
readonly answers?: readonly TimeAnswerRow[];
}
/** The subflow's own state — inputs frozen by the mount, then one key per stage. */
export interface InputsLayerState {
readonly calls: readonly BatchCall[];
readonly iteration: number;
readonly maxIterations: number;
readonly costBudgetHit?: boolean;
readonly costBudgetOnExceed?: 'warn' | 'halt';
/** Handed in only when the run's tools are off (`AgentState.toolsOffAfterRevision`). */
readonly toolsOff?: true;
readonly turnNumber: number;
/**
* This turn's answers KEPT for a call the batch that asked could not finish
* (`kept.ts`) — handed in only when there are some, so a run that never
* keeps one never carries the key.
*/
readonly argumentAnswersKept?: readonly KeptAnswer[];
/** Under declared sources only: the raw pieces the corpora are built from. */
readonly sourceInputs?: SourceInputs;
/** Under `.time()` only: this turn's clock and readings. */
readonly timeInputs?: TimeInputs;
argumentPlan?: readonly PlannedCall[];
argumentChecks?: readonly CheckedArgument[];
argumentRows?: readonly ArgumentRow[];
/** Under `.time()` only: one `call-window` row per call to a tool whose period declares forms. */
argumentWindowRows?: readonly CallWindowRow[];
argumentResolutions?: readonly ArgumentResolution[];
}
/** What the stages are handed from outside the folder — closures, never scope. */
export interface InputsLayerDeps {
/** The implementation that will answer a name — the shared dispatch resolver's. */
readonly toolOf: ToolOf;
/** The evidence gate's revision turns the tools off — present only then;
* only then is the `toolsOff` input read. */
readonly toolsOffAfterRevision?: true;
/** Route's dispatch test (`stages/route.ts` · `willDispatch`). */
readonly willDispatch: (values: {
readonly callCount: number;
readonly iteration: number;
readonly maxIterations: number;
readonly costBudgetHit?: boolean;
readonly costBudgetOnExceed?: 'warn' | 'halt';
readonly toolsOff?: boolean;
}) => boolean;
/** The ledger's emit half — one `findings.argument` event per row. */
readonly emitRows: (scope: TypedScope<InputsLayerState>, rows: readonly ArgumentRow[]) => void;
/**
* Present exactly under `.time()`: the stages read `timeInputs` and a tool
* whose period declares forms is filled, bound or recorded against the
* turn's windows (`resolve.ts` · `timeDecisionsOf`).
*/
readonly time?: {
/** The app's `.time({ zone })` — the zone of a form declaring `wallZone: 'app'`. */
readonly appZone?: ZoneName;
};
/** Present exactly under declared sources (either door: `.inputsLayer({ argumentSources: true })`
* or `.findings({ argumentSources: true })`). */
readonly sources?: {
/**
* A tool the agent registers carries an arguments view — present only
* then: no quote is shown on a row or an ask (`resolve.ts` · `quotesMayShow`).
*/
readonly argumentViews?: true;
/** Each call's `from` entries, through the one reader of `_findings`. */
readonly declaredOf: (calls: readonly BatchCall[]) => readonly CallSources[];
/** The corpora the checks read, from the pieces the mount handed in (loaded on first use). */
readonly corpusOf: (inputs: SourceInputs, calls: readonly BatchCall[], turn: number) => Promise<SourceCorpus>;
};
}
/**
* DECLARE — which calls of a DISPATCHING batch carry rules, read off the
* implementation that will answer each name, and which ruled values are
* missing. Stages `argumentPlan` (identities and a flag — no value); an empty
* plan when the batch will not dispatch or no call is ruled.
*/
export declare function declareArgumentsStage(scope: TypedScope<InputsLayerState>, deps: InputsLayerDeps): void;
/**
* VERIFY — where each planned ruled value came from (`resolve.ts` ·
* `verifyPlan`); under declared sources, each present value checked against
* the source the model declared (`checks.ts` · `checkSource`) over corpora
* built from the mount's `sourceInputs`. Stages `argumentChecks` (identities,
* enums and the checks' verdicts — no value, no quote).
*/
export declare function verifyArgumentsStage(scope: TypedScope<InputsLayerState>, deps: InputsLayerDeps): Promise<void>;
/**
* RECORD — one row per checked argument, in the tool's own argument view,
* stamped with the conversation turn; one event per row, fired HERE (inside the
* subflow, before the call dispatches). Stages `argumentRows`; the mount's
* output mapper merges them into the ledger in ONE write.
*/
export declare function recordArgumentsStage(scope: TypedScope<InputsLayerState>, deps: InputsLayerDeps): void;
/**
* RESOLVE — the fills and refusals ToolCalls applies (`resolve.ts` ·
* `resolutionsOf`). Stages `argumentResolutions`, each entry stamped with the
* batch's iteration.
*/
export declare function resolveArgumentsStage(scope: TypedScope<InputsLayerState>, deps: InputsLayerDeps): void;