agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
139 lines • 8.5 kB
TypeScript
/**
* walk-to-root — L4: the influence-guided backtracking debugger (proposal 007).
*
* `walkToRoot` walks BACKWARD from the symptom across loops to find the ROOT context source of a
* DECISION bug (root ≠ proximate). Per hop: NARROW with per-loop influence (L3) → HOP along
* `writerId` provenance to the loop that produced the culprit → ISOLATE with run-wide ablation (L2).
* Pure orchestration over shipped tiers — no new scorer, no new ablation.
*
* Home: `context-bisect` (consumes Trajectory + shortlist + the localizer's ablation).
*
* HONESTY (the proxy CAN misdirect the hop — folded in from review):
* - The per-hop narrow is a correlational PROXY (FA-dominated) — it points at a neighborhood and
* CANNOT separate a planted instruction from an innocent same-topic sibling. Three guards: (1) BEAM
* not top-1 (keep siblings in play); (2) ablation is the ONLY discriminator (a wrong-branch hop that
* doesn't flip is never `root`); (3) the narrow reorders, never drops. Remaining caveat: if the narrow
* never surfaces the true root into the beam, ablation never tests it and the walk stops shallow — a
* recall blind spot, amplified in a single chain.
* - `root` is CAUSAL (ablation-only): set only when run-wide ablation flips on a stable baseline.
* Without a `rerun`, the walk is correlational and `root` is absent.
* - FLAT charts only for the cross-loop hop — grouped loop frames are scope-isolated (degraded + flagged).
* - Three first-class honest stops (never silent): `unseparated-siblings`, `overdetermined-or-incomplete`,
* `untracked-origin`.
* - THE PROXY IS NOT ALWAYS NEEDED (`variables`, 7.12): where the recording carries per-write
* provenance, a suspect's state key has an EXACT recorded life — the value this loop read and the
* write that produced it — so the descent is a commit-log fact, not a similarity guess, and the hop
* says `narrowedBy: 'dataflow'`. The proxy still picks WHO; dataflow picks WHERE. Anything less than
* exact (dial off, stage-level edges, no dataflow, scope-isolated frames) falls back to the beam.
*
* VALIDATION (the descent edge is now populated on real agents — proposal 008): `assembleTrajectory`
* surfaces each loop's proximate `lastToolResult` on the WALK-ONLY `LoopFrame.proximateToolSource`
* field (writer = the PRODUCING loop's tool-calls stage — a cross-loop edge). `writtenByOf` reads it,
* so on a real flat agent with tool calls the cross-loop DESCENT fires (proven at the component level:
* the enrichment populates the edge on a real trajectory + the algorithm descends on it). It is
* WALK-ONLY — not in `contextSources` — so L3's narrow + its measured recall are untouched.
* END-TO-END VALIDATED (ctxbug/harness/eval-l4-walk.mjs): on a REAL agentfootprint misdirect agent
* with a realistic embedder (bge) for the narrow and a REAL causal ablation (rebuild the agent WITHOUT
* the planted fact → the outcome flips), the walk BURIES the plant at the symptom, DESCENDS via the
* proximate tool edge to the wrong-decision loop, and ablation convicts `root = the planted
* instruction` — where flat single-trigger localize does not. FLAT only — grouped's run-level
* `lastToolResult` lives outside the per-scope inner logs (deferred, degrade-flagged below).
*/
import type { Embedder } from '../influence-core/index.js';
import { type SuspectClassifier } from './localize.js';
import type { Trajectory } from './trajectory.js';
import type { AblationRerun, AblationVerdict, ContextBugArtifacts, HonestyFlag, SuspectKind } from './types.js';
import type { AgentVariableSlice } from './variable-recall.js';
/** One honest stop reason when the walk cannot cleanly descend/convict. */
export type RootCauseNote = 'unseparated-siblings' | 'overdetermined-or-incomplete' | 'untracked-origin';
/**
* How this hop's DESCENT TARGET was chosen:
*
* - `'text-similarity'` — the embedding beam picked the suspect and the hop
* followed that suspect's scraped provenance writer. A PROXY (the default,
* and the only value when no `variables` are supplied).
* - `'dataflow'` — the descent came from the RECORDED life of the suspect's
* state key: the value this loop read, and the write that produced it. No
* embedding took part in choosing where to go. Only ever stamped when that
* key's dataflow is per-write EXACT (see `DataflowCoverage`) — a conservative
* stage-level edge always falls back to the beam.
*/
export type HopNarrowedBy = 'text-similarity' | 'dataflow';
/** One hop of the symptom→root walk. */
export interface RootCauseHop {
/** The loop this hop examined. */
readonly loopIndex: number;
/** The narrowed culprit at this hop (joins a localizer Suspect 1:1). */
readonly suspectId: string;
readonly kind: SuspectKind;
/** How the descent target was chosen — see {@link HopNarrowedBy}. */
readonly narrowedBy: HopNarrowedBy;
/** The causal convict — present only when a `rerun` ablated this hop's suspect. */
readonly verdict?: AblationVerdict;
/** The provenance writer this hop's culprit came from (runtimeStageId). */
readonly writtenBy?: string;
/** The loopIndex the walk descended to next (the writer's frame); absent if it stopped here. */
readonly cameFrom?: number;
/** A first-class honest stop, when applicable. */
readonly note?: RootCauseNote;
}
export interface RootCausePath {
/** Symptom → … in walk order. */
readonly hops: readonly RootCauseHop[];
/** The DEEPEST ablation-convicted hop (CAUSAL). Absent without a flip / without a rerun. */
readonly root?: RootCauseHop;
readonly honestyFlags: readonly HonestyFlag[];
readonly truncated?: {
readonly byHops: boolean;
readonly byAblations: boolean;
};
}
export interface WalkToRootOptions {
readonly embedder: Embedder;
/** The convict tier. Without it the walk is correlational and `root` is absent. */
readonly rerun?: AblationRerun;
/** Top-k writers ablated per hop (NOT top-1 — the proxy can't separate same-topic siblings). Default 2. */
readonly beamK?: number;
/** Forwarded to the per-loop narrow. */
readonly recencyDecay?: number;
readonly k?: number;
/** Walk-depth budget. Default 8. */
readonly maxHops?: number;
/** Total ablation-probe budget across the whole walk. Default 24. */
readonly maxAblations?: number;
readonly classifier?: SuspectClassifier;
readonly signal?: AbortSignal;
/**
* Joined variable lives (`traceVariable` / `joinVariableSlice`), one per state
* key you want the walk to be able to descend DETERMINISTICALLY along.
*
* When the narrowed suspect carries a state key whose {@link DataflowCoverage}
* is `'exact'`, the hop's descent target is taken from that key's RECORDED
* ancestry — the write that produced the value this loop read — instead of
* from the proxy's provenance scrape, and the hop is stamped
* `narrowedBy: 'dataflow'`. Anything less than exact (dial off, stage-level
* edges, no dataflow at all, a grouped/scope-isolated frame) falls back to the
* beam, unchanged.
*
* OMIT IT and the walk behaves EXACTLY as before — same hops, same order, same
* verdicts, every hop stamped `'text-similarity'`.
*/
readonly variables?: readonly AgentVariableSlice[];
}
/**
* Resolve every `writerId` to the index of the frame whose `bodyIds` contains it (NOT via
* `parseRuntimeStageId` — that yields executionIndex, not loopIndex). A writer not in any frame
* (run prelude / root-seeded) maps to `undefined`. Invariant: each id lands in exactly one frame.
*/
export declare function buildWriterFrameIndex(trajectory: Trajectory): Map<string, number>;
/**
* Walk backward from the symptom to the root context source — from a recorded run.
* Thin wrapper: `assembleTrajectory` then {@link walkTrajectory}. See module docs for the honesty model.
*/
export declare function walkToRoot(artifacts: ContextBugArtifacts, opts: WalkToRootOptions): Promise<RootCausePath>;
/**
* The walk itself, over an already-assembled {@link Trajectory} (composable + directly testable).
* FLAT charts only for the cross-loop hop; grouped charts degrade (within-loop) with a flag.
*/
export declare function walkTrajectory(trajectory: Trajectory, opts: WalkToRootOptions): Promise<RootCausePath>;
//# sourceMappingURL=walk-to-root.d.ts.map