/** * EntryScorer — the pluggable STRATEGY for ranking a skill graph's entry * candidates by relevance to the user's message. * * `skillGraph().entryBy(scorer)` selects a strategy; the agent's PickEntry stage * runs it ONCE per turn (off the hot loop) and starts the cursor at the winner. * Two built-ins ship: * • `keywordScorer()` — no dependency, no model call: word overlap between * the message and each skill's `description`. The zero-config way to route. * • `embeddingScorer(e)` — semantic: cosine similarity of embeddings (needs an * Embedder). `.entryByRelevance(embedder)` is sugar for this. * Bring your own by implementing `EntryScorer`. * * The scorer OWNS both the surfaced `relevance` % AND the `chosen` winner, so the * explanation and the decision can never disagree (softmax is order-preserving, so * argmax-score == argmax-relevance). */ import type { Embedder } from '../../memory/embedding/types.js'; /** One entry candidate's relevance to the user's message. */ export interface EntryScore { /** The entry skill id. */ readonly id: string; /** Raw, strategy-specific score — cosine for `embedding`, word-overlap for * `keyword`. Higher = more relevant. Not normalized across strategies. */ readonly score: number; /** Softmax share across candidates, 0..1 — the surfaced "Why this skill?" %. */ readonly relevance: number; } /** Result of scoring the entries — the picked entry, the full ranking, and which * scorer produced it. */ export interface EntryScoring { /** The scorer's `name` (e.g. `'keyword'`, `'embedding'`) — surfaced so a lens / * the "Why this skill?" panel can say HOW the entry was chosen. */ readonly scorer: string; /** Winning entry id (highest `score`), or undefined if no candidate. */ readonly chosen: string | undefined; /** Every scored candidate, in declaration order. */ readonly ranked: readonly EntryScore[]; } /** A candidate the scorer ranks — its id + the text it's matched on (the skill's * `description`). */ export interface EntryCandidate { readonly id: string; readonly description: string; } /** What a scorer receives: the user's message + the candidates to rank. */ export interface EntryScorerInput { readonly userMessage: string; readonly candidates: readonly EntryCandidate[]; } /** * A strategy for ranking entry candidates. Pure given its inputs; may be async (an * embedder makes network calls). Runs ONCE per turn off the hot loop, so cost here * never touches the ReAct inner loop. */ export interface EntryScorer { /** Short, stable name — shown in the lens / "Why this skill?" panel + any * strategy picker. */ readonly name: string; score(input: EntryScorerInput, signal?: AbortSignal): Promise | EntryScoring; } /** * Shared finisher: raw scores → `EntryScoring`. Softmax turns the (sanitized) raw * scores into a `relevance` share (sums to 1); the winner is the argmax with * declaration order breaking ties. Because softmax is order-preserving over the * sanitized scores, the WINNER is always the argmax `relevance` — the surfaced % * and the pick can never disagree. * * `EntryScorer` is a public, consumer-implementable interface, so a third-party * scorer might return `NaN` / `±Infinity`. We sanitize those to `-Infinity` for BOTH * the softmax input AND the winner pick, so a non-finite score can never silently win * (`NaN > x` is always false — a leading `NaN` would otherwise seed-and-keep a naive * reduce) and the softmax stays well-defined. The raw (possibly non-finite) score is * still surfaced on `EntryScore.score` for honest debugging. */ export declare function rankEntries(scorerName: string, candidates: readonly EntryCandidate[], rawScores: readonly number[]): EntryScoring; /** * keywordScorer — rank by word overlap between the message and each description. * No embedder, no model call, deterministic. Scores the set-cosine of lowercased * word tokens (length-normalized so a long description can't win on sheer size), * minus a small stop-word list. The zero-config router: good enough when skill * descriptions use the words a user would. */ export declare function keywordScorer(options?: { readonly stopWords?: readonly string[]; }): EntryScorer; /** * embeddingScorer — rank by SEMANTIC similarity. Embeds the message + each * description and cosine-scores them. Needs an `Embedder` (a model call per text); * runs once per turn off the hot loop. `.entryByRelevance(embedder)` is sugar for * `.entryBy(embeddingScorer(embedder))`. */ export declare function embeddingScorer(embedder: Embedder): EntryScorer;