/** * llmEdgeWeigher — influence-scored `EdgeWeigher` for LLM-call slice edges * (RFC-003 Part B, block D7). * * The gap this fills: footprintjs's causal slice treats every parent of an * LLM call equally — a 12-parent hairball (system prompt, history, tool * results, cache markers, counters …) gives a debugger 12 equally-plausible * leads. D7 turns the hairball into a RANKED shortlist by weighing each * parent edge with influence-core's composite (D6): the parent's WRITTEN * content vs the LLM call's OUTPUT. * * Pattern: two-pass adapter over footprintjs's synchronous `EdgeWeigher` * hook (A3). Embedding is async, the hook is sync — so the handle * PRIMES first (walk an unweighted slice, embed every needed text * in deduplicated batches, memoize composites), and `weigh` then * answers synchronously from the primed map. `localizeContextBug` * drives the two passes; standalone consumers call * `prime(dag)` themselves and re-run `causalChain({ weigh })`. * Role: `src/lib/context-bisect/` leaf. The engine stays zero-dep — the * weigher is consumer-injected exactly as A3 intended. * * ## Honest claim (§B2) * * Weights are CORRELATIONAL proxies: deterministic embedding geometry * between texts the run already committed — never model internals, never * causal attribution (ablation is the causal tier). A weight of 0.93 means * "this parent's content is semantically close to what the LLM produced", * not "the LLM used it". * * ## Determinism * * Same artifacts + same (deterministic) embedder → same weights and same * ranking, run after run: texts are built from the commit log in commit * order, embedded via influence-core's deduplicated batch, and ties in * `rankedParents` keep first-seen (slice BFS) order. Wrap the embedder in * `embeddingCache(...)` to also make repeat localizations embed nothing. * * ## What is weighed * * DATA edges whose CHILD is an LLM call (`llmCallIds`). Everything else — * non-LLM children, control edges (a routing decision's influence is not a * semantic-content question) — returns `undefined`, which footprintjs * stamps as the default 1.0. * * ## Redaction posture * * Texts come exclusively from the COMMIT LOG, which footprintjs scrubs at * commit time — a redacted key's committed value IS the placeholder, so * the embedder never sees the raw secret. */ import type { CommitBundle } from 'footprintjs/advanced'; import type { CausalNode, EdgeWeigher } from 'footprintjs/trace'; import { type Embedder, type InfluenceWeights } from '../influence-core/index.js'; export interface LlmEdgeWeigherOptions { /** * Injected embedder (D6 contract). Wrap in `embeddingCache(...)` so the * weigher, the suspect refinement, and any margin/lint consumer share * one embedding spend. */ readonly embedder: Embedder; /** * runtimeStageIds of LLM-call executions — the children whose parent * edges get weighed. Provide explicitly, or extract from captured * events with `llmCallIdsFromEvents` (the `stream.llm_start` ids). */ readonly llmCallIds: Iterable; /** The run's commit log — where edge texts are read from. */ readonly commitLog: readonly CommitBundle[]; /** Char cap per embedded text. Default 2000. */ readonly maxTextChars?: number; /** Composite weights forwarded to influence-core. Default: paper priors. */ readonly weights?: InfluenceWeights; /** * Override the CHILD text (the LLM call's output). Default: the values * the child committed, serialized `key=value` in trace order, capped. */ readonly childTextOf?: (runtimeStageId: string) => string | undefined; /** * Override the PARENT text for one edge. Default: the value the parent * committed for the edge's key, serialized + capped. */ readonly parentTextOf?: (runtimeStageId: string, key: string) => string | undefined; } /** One ranked parent edge of an LLM call (descending weight). */ export interface RankedParentEdge { readonly parentId: string; readonly stageName: string; readonly key: string; /** Influence composite clamped to [0, 1] — a correlational proxy (§B2). */ readonly weight: number; } export interface LlmEdgeWeigherHandle { /** * Pass 1 — walk an (unweighted) causal DAG, embed every LLM-edge text in * one deduplicated batch, and memoize composite weights. Idempotent; * call again with a different DAG to extend the map. */ prime(root: CausalNode): Promise; /** * Pass 2 — the synchronous footprintjs `EdgeWeigher`. Returns the primed * weight for (LLM child, parent, data key); `undefined` (→ engine * default 1.0) for control edges, non-LLM children, and unprimed pairs. */ readonly weigh: EdgeWeigher; /** * The D7 acceptance view: an LLM call's parents ranked by weight, * descending; ties keep first-seen (slice BFS) order. Empty until * `prime` ran over a DAG containing the call. */ rankedParents(llmCallId: string): readonly RankedParentEdge[]; /** Texts the embedder was given — exposed for security audits/tests. */ embeddedTexts(): readonly string[]; } /** * Default child text: everything the step committed, `key=value` in trace * order. For an agent's LLM call this carries the assistant content + * tool-call intents — the step's observable OUTPUT. */ export declare function stepOutputText(commitLog: readonly CommitBundle[], lastIdxOf: Map, runtimeStageId: string, maxChars: number): string | undefined; /** * Build the D7 weigher. See module docs for the two-pass contract, the * determinism guarantee, and the §B2 honest claim (weights = proxies). */ export declare function llmEdgeWeigher(options: LlmEdgeWeigherOptions): LlmEdgeWeigherHandle;