/** * 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; /** * 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; /** * 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;