agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
172 lines (171 loc) • 8.85 kB
TypeScript
/**
* variable-recall — footprintjs's VARIABLE slices, joined to agent vocabulary.
*
* footprintjs answers "what happened to this variable?" in the address space it
* owns: commit indices, runtimeStageIds, state keys (`keyTimeline` /
* `forwardSliceForKey`, `foottrace`). An agent debugger asks the same
* question in a DIFFERENT vocabulary: which LOOP was that, which injected fact
* or tool result is that write, and what would REMOVING it cost. This module is
* the join between the two — and nothing more:
*
* - **Pure assembly.** Every fact here was already recorded. No new capture,
* no new scorer, no embedder, no LLM. `joinVariableSlice` re-labels; it
* never measures.
* - **Loop lifting.** Each moment gets the `loopIndex` of the frame that
* contains its `runtimeStageId` (the walk's own `buildWriterFrameIndex` —
* one resolver, not two).
* - **Agent identity.** A WRITE whose value the trajectory carries is run
* through the SAME `defaultSuspectClassifier` the localizer uses, so a
* write joins 1:1 with a `Suspect` (`injectionId` / `toolName`) and gets
* the `AblationSpec` that would remove it. Writers the classifier cannot
* name get NO hook — an honest absence, never a fabricated one.
* - **Honesty rides along verbatim.** footprintjs's `HonestyNote`s, the
* missing-reason, and `readsCoverage` are copied, not re-worded.
*
* The one JUDGEMENT this module makes is {@link DataflowCoverage} — see the
* long comment on `dataflowCoverage()`. It is what lets `walkToRoot` replace a
* text-similarity guess with a recorded dataflow edge, and it is deliberately
* strict.
*
* @beta Beta feature (RFC-003 Part B). The API works and is tested, but may
* change before GA.
*/
import type { ForwardSlice, HonestyNote, KeyMoment, KeyTimeline, MissingSliceReason, ReadsCoverage, StateKey } from 'foottrace';
import { type SuspectClassifier } from './localize.js';
import { type Trajectory } from './trajectory.js';
import type { AblationSpec, ContextBugArtifacts, SuspectKind } from './types.js';
/**
* How exactly this log records what a key's value FED — the one judgement in
* this module, and the gate on `walkToRoot`'s deterministic hop.
*
* - `'exact'` — every recorded `fed` edge for this key carries per-write
* provenance (`FedBasis: 'per-write'`, the `writeProvenance: 'reads-prefix'`
* dial): a downstream write is linked to this value BECAUSE that write's
* recorded read-prefix names the key, and a write whose prefix omits it is
* excluded exactly.
* - `'conservative'` — at least one edge is stage-level co-occurrence
* (`'conservative-fed-edges'`), or the log carries no recorded reads at all.
* Sound over-approximation; NEVER presented as exact.
* - `'unknown'` — no forward slice was supplied, OR the key has no recorded
* `fed` edge at all. The second case is the trap: with zero edges there is
* also zero conservative edge, so "no conservative edges" would be VACUOUSLY
* true and a key nothing ever read would masquerade as the most exact key in
* the run. It is demoted here instead.
*/
export type DataflowCoverage = 'exact' | 'conservative' | 'unknown';
/**
* One moment in a key's life ({@link KeyMoment}), lifted into agent vocabulary.
* The footprintjs fields are carried through UNCHANGED — this type only ADDS.
*/
export interface AgentKeyMoment {
readonly kind: 'write' | 'read';
/** Commit ARRAY position — the write's own, or the READING stage's. */
readonly commitIdx: number;
readonly runtimeStageId: string;
readonly stageId: string;
readonly stageName: string;
/** `'write'` moments only: the trace verb. */
readonly verb?: KeyMoment['verb'];
/** `'read'` moments only: the commitIdx of the write whose value it saw. */
readonly fromWriteIdx?: number;
/**
* The ReAct loop this moment happened in. ABSENT when the step is not in any
* frame (run prelude, root-seeded setup) — absence is information, not a gap.
*/
readonly loopIndex?: number;
/**
* `'write'` moments the classifier recognized: what KIND of agent source this
* write produced. When one write produced several sources (an injection slot
* commits an array of records), this is the FIRST one's kind and
* {@link AgentVariableSlice.ablations} carries them all.
*/
readonly suspectKind?: SuspectKind;
/** The suspect identity (`injectionId` / `toolName`) — joins a `Suspect` 1:1. */
readonly suspectId?: string;
}
/**
* The counterfactual attached to one classifiable WRITE: "to test this write,
* re-run without this source". A HOOK, not a verdict — running it is the
* consumer's `AblationRunner` (§B2: only ablation verdicts are causal claims).
*/
export interface VariableAblationHook {
/** runtimeStageId of the write that introduced the source. */
readonly writerId: string;
/** `injectionId` / `toolName` — the localizer's suspect identity. */
readonly suspectId: string;
readonly kind: SuspectKind;
/** What to remove for the counterfactual re-run. */
readonly spec: AblationSpec;
}
/**
* One variable's recorded life, in agent vocabulary — the artifact BOTH a human
* board ({@link variableToBacktrackTrace}) and the walk read.
*
* JSON-safe by construction (flat moments, no graph, no live references): the
* same object can be persisted, sent over the wire, or handed to an LLM tool.
*/
export interface AgentVariableSlice {
/** The state key asked about (footprintjs's normalised string form). */
readonly key: string;
/** Writes and reads in commit order. EMPTY when `missing`. */
readonly moments: readonly AgentKeyMoment[];
/** Present ONLY when there are no moments — footprintjs's honest absence. */
readonly missing?: MissingSliceReason;
/** How exactly this log records what the value fed — see {@link DataflowCoverage}. */
readonly coverage: DataflowCoverage;
/** One per classifiable write — see {@link VariableAblationHook}. */
readonly ablations: readonly VariableAblationHook[];
/** footprintjs's honesty notes, VERBATIM (both doors' notes, de-duplicated). */
readonly notes: readonly HonestyNote[];
/** Which `KeysReadSource` strategy resolved reads (honesty/debug). */
readonly keysReadKind: string;
/** Reads telemetry — `stepsWithReads === 0` means reads were not recorded. */
readonly readsCoverage?: ReadsCoverage;
}
export interface JoinVariableSliceOptions {
/**
* The FORWARD slice of the same key (`forwardSliceForKey`) — the ONLY carrier
* of per-edge exactness (`ForwardEdge.basis`). Without it `coverage` is
* `'unknown'` and the walk will not take a dataflow hop.
*/
readonly forward?: ForwardSlice;
/** Override the suspect classifier (default `defaultSuspectClassifier`). */
readonly classify?: SuspectClassifier;
}
export interface TraceVariableOptions extends JoinVariableSliceOptions {
/** Reuse an already-assembled trajectory (avoids re-slicing the run). */
readonly trajectory?: Trajectory;
/** Exclusive commit-array-index bound — the variable as it stood BEFORE this idx. */
readonly before?: number;
}
/**
* Join a footprintjs variable slice to the agent's loops and sources.
*
* @param slice A `keyTimeline(...)` result — the flat, JSON-safe life of one
* key. (Chosen over the backward `sliceForKey` deliberately: the walk needs
* moments in commit order, and a timeline cannot be mistaken for a DAG.)
* @param trajectory `assembleTrajectory(artifacts)` — supplies loop frames AND
* the committed values used for classification.
* @param opts `forward` (the exactness carrier) and `classify`.
*
* Nothing here is measured: every field is a re-label of something already
* recorded. See the module doc.
*/
export declare function joinVariableSlice(slice: KeyTimeline, trajectory: Trajectory, opts?: JoinVariableSliceOptions): AgentVariableSlice;
/**
* One call, from a recorded run to a variable's joined life: `keyTimeline` +
* `forwardSliceForKey` + `assembleTrajectory` + {@link joinVariableSlice}.
*
* Reads come from the snapshot's execution tree (zero setup, `readTracking` ≠
* `'off'`); exactness comes from the run's `writeProvenance` dial. Both facts
* ride out on the result (`readsCoverage`, `coverage`) rather than being
* assumed.
*
* @example
* ```ts
* const life = traceVariable({ snapshot }, 'systemPromptInjections');
* life.coverage; // 'exact' | 'conservative' | 'unknown'
* life.moments.filter((m) => m.kind === 'write').map((m) => m.loopIndex);
* ```
*/
export declare function traceVariable(artifacts: ContextBugArtifacts, key: StateKey, opts?: TraceVariableOptions): AgentVariableSlice;