UNPKG

agentfootprint

Version:

The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.

114 lines • 5.1 kB
"use strict"; /** * context-ledger/gates.ts — L2: the ledger's rows feed what gets included. * * Three gates, one policy, each hooking an EXISTING seam (no new framework * surface): `gatedTools(inner, predicate)` for tools, `skillGraph().entryBy` * for skills, and a rule-trigger wrapper for injections. * * THE POLICY PRINCIPLE — demote, never starve: a piece the ledger judges * poorly is demoted, but every `refreshEvery`-th decision lets it through * anyway ("parole"), so it keeps generating fresh ledger data. Without * parole a piece demoted once could never earn again — a self-fulfilling * verdict. And the ledger never judges a piece it barely knows: below * `minOffers` everything passes. */ Object.defineProperty(exports, "__esModule", { value: true }); exports.ledgerGated = exports.ledgerEntryScorer = exports.ledgerToolGate = void 0; const entryScorer_js_1 = require("../injection-engine/entryScorer.js"); const DEFAULTS = { minOffers: 5, earnRateFloor: 0.05, refreshEvery: 10 }; /** * The shared verdict: should this piece be included right now? * Stateful per gate instance (the parole counter), pure otherwise. */ function makeVerdict(ledger, policy) { const p = { ...DEFAULTS, ...policy }; const demotions = new Map(); return (kind, id) => { const row = ledger.row(kind, id); if (!row) return true; // unknown to the ledger → always include if (row.offered < p.minOffers) return true; // not enough data to judge if (row.earnRate >= p.earnRateFloor) return true; // earning its tokens // Demoted — but parole every Nth decision keeps the data flowing. const key = `${kind}:${id}`; const count = (demotions.get(key) ?? 0) + 1; demotions.set(key, count); return p.refreshEvery !== Infinity && count % p.refreshEvery === 0; }; } /** * TOOL GATE — plug into the existing provider combinator: * * ```ts * import { gatedTools, staticTools } from 'agentfootprint/providers'; * builder.toolProvider(gatedTools(staticTools(tools), ledgerToolGate(ledger))); * ``` * * Unused tool schemas are usually the biggest silent token cost — this is * the gate to start with. */ function ledgerToolGate(ledger, policy) { const verdict = makeVerdict(ledger, policy); return (toolName) => verdict('tool', toolName); } exports.ledgerToolGate = ledgerToolGate; /** * SKILL GATE — an `EntryScorer` for `skillGraph().entryBy(...)` that wraps * another scorer (keyword, embedding, …) and demotes candidates the ledger * says never earn. Demotion multiplies the inner score toward the floor — * ranking pressure, not exclusion — and parole restores full weight * periodically, so a demoted skill can still win when nothing else fits. */ function ledgerEntryScorer(ledger, inner, policy) { const verdict = makeVerdict(ledger, policy); const DEMOTION_WEIGHT = 0.25; return { name: `ledger(${inner.name})`, async score(input, signal) { const base = await inner.score(input, signal); // Re-rank through rankEntries so `chosen`, `score` AND `relevance` are // recomputed TOGETHER from the demoted scores — preserving the // entryScorer contract (argmax-score == argmax-relevance; the "Why // this skill?" panel and the pick can never disagree). const candidates = base.ranked.map((r) => ({ id: r.id, description: input.candidates.find((c) => c.id === r.id)?.description ?? '', })); const demotedScores = base.ranked.map((r) => verdict('skill', r.id) ? r.score : r.score * DEMOTION_WEIGHT); return (0, entryScorer_js_1.rankEntries)(`ledger(${base.scorer})`, candidates, demotedScores); }, }; } exports.ledgerEntryScorer = ledgerEntryScorer; /** * INJECTION GATE — wrap ONE injection so the ledger's verdict joins its * trigger. `always` becomes a ledger-backed rule; an existing `rule` is * AND-ed with the verdict. `on-tool-return` / `llm-activated` are returned * UNCHANGED (they are already demand-driven — the ledger has nothing to * add). `always` pieces are exempt unless you explicitly wrap them — the * safety-first default from the design. */ function ledgerGated(injection, ledger, policy) { const verdict = makeVerdict(ledger, policy); const kind = injection.flavor === 'skill' ? 'skill' : 'injection'; const trigger = injection.trigger; if (trigger.kind === 'always') { return { ...injection, trigger: { kind: 'rule', activeWhen: () => verdict(kind, injection.id) }, }; } if (trigger.kind === 'rule') { const original = trigger.activeWhen; return { ...injection, trigger: { kind: 'rule', activeWhen: (ctx) => original(ctx) && verdict(kind, injection.id) }, }; } return injection; // demand-driven triggers pass through untouched } exports.ledgerGated = ledgerGated; //# sourceMappingURL=gates.js.map