agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
164 lines (163 loc) • 8.11 kB
TypeScript
/**
* localizeContextBug — the contextual-bug LOCALIZER, "git bisect for
* context" (RFC-003 Part B, block D8).
*
* The five-stage pipeline (each stage is a shipped piece — this file only
* ASSEMBLES):
*
* 1. TRIGGER — an explicit `atStep`, a custom trigger strategy, or
* the QualityRecorder's lowest-scoring step.
* 2. SLICE — footprintjs `causalChain` over the commit log, WITH
* control-dependence edges (D3) and honesty markers
* (A2/A4) when the artifacts carry them.
* 3. WEIGH — D7's `llmEdgeWeigher` stamps influence weights on
* every LLM-call parent edge (two-pass: prime, re-slice).
* 4. RANK — suspects = slice nodes classified into ablatable
* context sources (tool / injection / memory / arg),
* scored by max-product path weight × per-item semantic
* refinement. CORRELATIONAL tier — and marked so.
* 5. ABLATE — optional: the consumer's `AblationRunner` re-runs the
* scenario without each top suspect, N seeded times.
* Verdicts (the ONLY causal claims, §B2) + variance.
*
* Without a runner the report stops at stage 4 with
* `mode: 'correlational'` — explicitly a ranking of proxies, no causal
* claim anywhere.
*
* Every `source` / `step` id in the report is a plain runtimeStageId —
* drill any of them with the trace-toolpack tools (`trace_node`,
* `trace_slice`, `get_value`) over the same artifacts bag.
*/
import type { CausalNode, ValueBasis } from 'foottrace';
import { type Embedder, type InfluenceScorer, type InfluenceStrategy } from '../influence-core/index.js';
import type { LoopRecallShortlist } from './loop-recall.js';
import { type ContextUnit } from './missingContext.js';
import { type RestorationRerun } from './restoration.js';
import type { AblationRerun, ContextBugArtifacts, ContextBugReport, Suspect, SuspectDetail, SuspectKind } from './types.js';
/**
* Extract LLM-call step ids from captured typed events: the
* `meta.runtimeStageId` of every `agentfootprint.stream.llm_start`
* envelope, deduplicated in event order. Collect events with
* `agent.on('*', (e) => events.push(e))`.
*/
export declare function llmCallIdsFromEvents(events: readonly {
readonly type: string;
readonly meta: {
readonly runtimeStageId: string;
};
}[]): string[];
/** A classified-but-unscored suspect produced by a classifier. */
export interface SuspectSeed {
readonly kind: SuspectKind;
readonly detail?: SuspectDetail;
}
/** What a classifier sees for one slice node. */
export interface ClassifyContext {
readonly node: CausalNode;
/** Keys this node committed. */
readonly keysWritten: readonly string[];
/** Verb-aware value of a key as of this node's last commit. */
readonly valueOf: (key: string) => unknown;
/**
* Why `valueOf(key)` is not exact, when it is not — footprintjs's
* `commitValueAtWithBasis` codes for the same fold (`HONESTY_CODES` on
* `foottrace` has each sentence). `[]` = exact. ABSENT = unknown (a hand-built
* context): no `valueBasis` is attached.
*/
readonly basisOf?: (key: string) => readonly ValueBasis[];
}
/**
* Pluggable classifier: map one slice node to its ablatable context
* sources. Return `undefined` to fall through to the default (which
* understands the agent chart's committed shapes); return `[]` to
* suppress the node entirely.
*/
export type SuspectClassifier = (ctx: ClassifyContext) => readonly SuspectSeed[] | undefined;
/**
* The default classifier — reads the node's COMMITTED values (already
* redaction-scrubbed) and recognizes the agent chart's shapes:
*
* - `systemPromptInjections` / `messagesInjections` / `toolsInjections`
* records with an engineered source → one suspect per `Injection.id`
* (kind `'memory'` for source `'memory'`, else `'injection'`).
* - `lastToolResult` → a `'tool'` suspect for the tool that ran.
* - footprintjs A2 honesty marker `args` → an `'arg'` suspect (the
* consumer's runner must override the input — nothing to filter).
* - anything else → the honest `'stage'` fallback (no ablation spec).
*/
export declare function defaultSuspectClassifier(ctx: ClassifyContext): readonly SuspectSeed[];
export interface LocalizeContextBugOptions {
readonly artifacts: ContextBugArtifacts;
/** Injected embedder (D6) — wrap in `embeddingCache(...)`. */
readonly embedder: Embedder;
/** Explicit trigger step (runtimeStageId). Wins over everything. */
readonly atStep?: string;
/** Custom trigger strategy — consulted when `atStep` is absent. */
readonly trigger?: (artifacts: ContextBugArtifacts) => string | undefined;
/**
* The counterfactual tier: supply a runner (+ the original output) and
* top suspects get ablation verdicts. Absent → the report stops at the
* ranking, marked `mode: 'correlational'`.
*/
readonly rerun?: AblationRerun;
/**
* Interface #3 — the MISSING-context tier. Supply what was `available` for
* the turn and what was `sent` to the model; the report's `dropped` lists the
* units that never reached the model (`available − sent`). Add a `rerun` and
* each dropped candidate gets a RESTORATION verdict (the mirror of ablation:
* restoring it flips the outcome → causal). Absent → no `dropped` section.
*/
readonly missingContext?: {
readonly available: readonly ContextUnit[];
readonly sent: readonly ContextUnit[];
readonly rerun?: RestorationRerun;
};
/** Slice depth budget. Default 12. */
readonly maxDepth?: number;
/** Slice node budget. Default 80. */
readonly maxNodes?: number;
/** Ranked suspects kept on the report. Default 12. */
readonly maxSuspects?: number;
/** Override / extend the suspect classifier. */
readonly classify?: SuspectClassifier;
/**
* The RANK stage's pluggable influence scorer (the suspect-ordering
* extension point). Default `scoreInfluence` — the FDL four-signal
* composite. Bring your own `InfluenceScorer` — e.g.
* `scoreContrastiveInfluence` wrapped with a `referenceText`, or a
* scorer of your own — to change the ranking ORDER, never causality:
* a scorer only changes how FAST §B2 ablation confirms a culprit;
* ablation alone makes the causal claim.
*
* Or pass a named `InfluenceStrategy` (`semanticAlignmentStrategy` /
* `lexicalOverlapStrategy` / your own) — the report's `rankedBy` echoes
* its name.
*/
readonly scorer?: InfluenceScorer | InfluenceStrategy;
/**
* L3 narrowing (proposal 006): a per-loop recall shortlist from `shortlistEarlyCulprits`.
* When supplied, suspects are REORDERED (never filtered) so high-recall candidates float to the
* top and survive the `maxSuspects` slice → ablation targets them first. Joined on the suspect
* identity (`detail.injectionId`/`detail.toolName`). Default off (back-compat).
*/
readonly shortlist?: LoopRecallShortlist;
}
/**
* Localize a contextual bug: trigger → causal slice → influence-weighted
* ranking → (optional) counterfactual ablation. See module docs for the
* pipeline and the §B2 claim tiers.
*
* @beta Beta feature — the API may change before GA.
*
* @throws when no trigger can be resolved (no `atStep`, no custom
* strategy hit, no `artifacts.quality`), or when the trigger step
* is not in the commit log.
*/
export declare function localizeContextBug(options: LocalizeContextBugOptions): Promise<ContextBugReport>;
export declare function suspectLabel(suspect: Suspect): string;
/**
* Human-readable report. The claim tiers are spelled out in the output
* itself (§B2): scores are proxies; verdict lines are the only causal
* claims; every ⚠ honesty flag prints.
*/
export declare function formatContextBugReport(report: ContextBugReport): string;