agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
134 lines (133 loc) • 6.56 kB
TypeScript
import type { Embedder, EvidenceInput, InfluenceScore, InfluenceWeights, SignalScores } from './types.js';
/**
* FA — Final Answer Similarity (paper Eq. 1).
*
* `FA(d) = sim(e_d, e_f)` — cosine between the evidence embedding and
* the final-answer embedding. The strongest prior: verbatim or
* paraphrased reuse of a tool result scores high. Proxy: semantic
* overlap, not provenance.
*/
export declare function finalAnswerSimilarity(evidenceVec: readonly number[], finalAnswerVec: readonly number[]): number;
/**
* AVG — Average Relevancy (paper Eq. 2).
*
* Mean cosine between the evidence and each LLM reasoning ancestor;
* 0 when there are no ancestors (structurally zero — see
* `adaptWeights`). Proxy: consistent semantic closeness across the
* chain, not actual consultation.
*/
export declare function averageRelevancy(evidenceVec: readonly number[], ancestorVecs: ReadonlyArray<readonly number[]>): number;
/**
* PERSIST — Persistence (paper Eq. 3).
*
* Fraction of ancestors whose similarity to the evidence EXCEEDS the
* threshold T (strict `>`, default 0.30); 0 when there are no
* ancestors. Unlike AVG it measures BREADTH: referenced in 4 of 5
* steps (0.8) beats referenced intensely in 1. Proxy: similarity
* above a tunable bar, not counted citations.
*/
export declare function persistence(evidenceVec: readonly number[], ancestorVecs: ReadonlyArray<readonly number[]>, threshold?: number): number;
/**
* DEPTH — Structural Proximity (paper Eq. 4).
*
* `DEPTH(d) = 1 / (1 + n)` where n counts LLM reasoning ancestors
* ONLY (not pipeline plumbing — callers decide what counts as an
* ancestor when building `EvidenceInput.ancestorTexts`). Direct
* evidence with no intermediaries gets exactly 1.0. The only
* content-blind signal: pure trace structure.
*/
export declare function structuralProximity(ancestorCount: number): number;
/**
* Adaptive weight redistribution (paper Eq. 6, §5.3).
*
* When an item has NO LLM ancestors, AVG and PERSIST are structurally
* zero — not because the evidence was uninfluential, but because there
* is nothing to measure against. Without adaptation its score is
* capped at α+δ (≈0.50 under defaults). Eq. 6 moves the β+γ mass onto
* FA and DEPTH preserving their ratio:
*
* α′ = α + (β+γ)·α/(α+δ), δ′ = δ + (β+γ)·δ/(α+δ), β′ = γ′ = 0
*
* Defaults → α′=0.80, δ′=0.20 (the 4:1 FA:DEPTH ratio kept).
* Per-evidence-item: in a multi-tool pipeline some items adapt while
* others keep standard weights; `adapted` says which (surface it — the
* paper's UI marks adapted items).
*
* Degenerate guard: if α+δ = 0 there is no defined ratio to preserve —
* weights return unchanged with `adapted: false`, and the composite is
* honestly 0 for a no-ancestor item.
*/
export declare function adaptWeights(weights: InfluenceWeights, ancestorCount: number): {
weights: InfluenceWeights;
adapted: boolean;
};
/**
* Composite score S(d) (paper Eq. 5).
*
* `S = α·FA + β·AVG + γ·PERSIST + δ·DEPTH` under the given weights —
* pass the EFFECTIVE weights from `adaptWeights` for no-ancestor
* items. With weights summing to 1, S ∈ [−(α+β), 1] (FA/AVG are
* cosines and may go negative; PERSIST/DEPTH are non-negative).
*/
export declare function compositeScore(signals: SignalScores, weights: InfluenceWeights): number;
export interface ScoreInfluenceArgs {
/** Evidence items (tool results / context sources) with ancestors. */
readonly evidence: readonly EvidenceInput[];
/** The final answer text the evidence is scored against. */
readonly finalAnswerText: string;
/**
* Injected embedder. Wrap in an `EmbeddingCache` to share embeddings
* with the catalog lint / margin scorer (RFC-002 §3 — one cache
* serves all three consumers).
*/
readonly embedder: Embedder;
/** Composite weights. Default: paper priors 0.40/0.30/0.20/0.10. */
readonly weights?: InfluenceWeights;
/** PERSIST threshold T. Default 0.30. */
readonly persistenceThreshold?: number;
/** Abort signal threaded to the embedder (network backends). */
readonly signal?: AbortSignal;
}
/**
* Score every evidence item on the four FDL signals and rank by
* composite, descending (paper pipeline stages 4–6 in one call:
* embed → score → rank). Ties keep input order (stable sort).
*
* Deterministic for a deterministic embedder: same inputs → same
* scores. All texts are embedded in ONE deduplicated batch — with an
* `EmbeddingCache` injected, repeat calls embed nothing.
*
* Honest claim: ranked semantic-alignment proxies. NOT causal
* attribution — see module docs.
*/
export declare function scoreInfluence(args: ScoreInfluenceArgs): Promise<InfluenceScore[]>;
/**
* A pluggable influence scorer — the RANK stage's extension point.
*
* It takes the same `ScoreInfluenceArgs` the localizer assembles for a
* slice (the evidence items, the wrong-output text, an embedder) and
* returns one `InfluenceScore` per item, ranked descending. The shipped
* default is `scoreInfluence` (the FDL four-signal composite); pass your
* own to `localizeContextBug({ scorer })` to change the ranking ORDER —
* e.g. `scoreContrastiveInfluence` (wrap it to supply a `referenceText`),
* or a non-embedding scorer of your own that ignores `args.embedder`.
*
* Claim-ladder guarantee: a scorer only reorders suspects (how FAST
* ablation finds a culprit), never whether a claim counts as causal —
* ablation alone convicts. So any scorer is safe to swap in; the worst a
* bad one does is make confirmation slower, never wrong.
*/
export type InfluenceScorer = (args: ScoreInfluenceArgs) => Promise<InfluenceScore[]>;
/** Embed distinct texts via batch API when available, else sequentially. */
export declare function embedAll(embedder: Embedder, texts: readonly string[], signal?: AbortSignal): Promise<Map<string, readonly number[]>>;
/**
* Validate composite weights: every weight finite & non-negative, and not all
* zero. Shared by `scoreInfluence` and `scoreContrastiveInfluence` — `fnName`
* attributes the error to the actual caller.
*/
export declare function assertValidWeights(weights: InfluenceWeights, fnName?: string): void;
/**
* Validate that every evidence id is unique. Shared by `scoreInfluence` and
* `scoreLexicalInfluence` — `fnName` attributes the error to the actual caller.
*/
export declare function assertUniqueIds(evidence: readonly EvidenceInput[], fnName?: string): void;