/** * The four arms, behind one interface, so the benchmark can compare them. * * V1 frontier compress only what arrives after the last cache * breakpoint; elisions name a readable path * V2 speculative V1, plus re-inflate a span the model's last reply shows it * is about to need -- on a request that was happening anyway * V3 history V1's markers, applied behind the frontier too * ccr a faithful imitation of HeadRoom: opaque hash markers, an * injected retrieval tool, an injected system message, and * history compressed. The control we have to beat. * * The ccr arm exists so the comparison is against their DESIGN rather than * against their marketing. It is implemented honestly -- if it wins, it wins, * and the plan says to stop and report rather than ship the proxy. */ import type { Tuning } from './options.js'; import type { EmbeddingCache } from './embedding.js'; import { type Finding } from './knowledge.js'; import { type AnchorDecision, type AnchorStore } from './anchor.js'; import { type ProviderRequest } from './frontier.js'; import type { Elision } from './types.js'; export type StrategyName = 'v1-frontier' | 'v2-speculative' | 'v3-history' | 'v4-substitute' | 'ccr'; export interface StrategyOptions { /** Writes content with no file of its own somewhere readable. */ readonly spill?: (content: string, hint: string) => string; /** * V2 only: spans the model's previous reply suggests it is about to need. * Supplied by the caller so this stays a pure function of its inputs. */ readonly wanted?: readonly string[]; /** * Memory of which conversations we have already re-anchored. * * Supplied by the caller rather than held here, so this module keeps no * state of its own and two concurrent requests cannot observe each * other's -- HeadRoom's #3486 is exactly that bug. Absent means V1 behaves * as it always has and never touches the cached prefix. */ readonly anchors?: AnchorStore; /** * What this project already established, for the cached prefix. * * Supplied by the caller rather than read here, because reaching for a * graph on disk would make a pure function of (request, options) into * something that depends on the filesystem. Absent means nothing is * injected, which is the default: this is payload, and it has to be * asked for. */ readonly findings?: readonly Finding[]; /** True when `findings` came from a graph shared across projects. */ readonly sharedGraph?: boolean; /** Characters of findings allowed in the prefix. */ readonly knowledgeBudget?: number; /** * Vectors a request-level pre-pass already computed. * * Supplied by the caller, never built here: embedding is async and these * strategies are synchronous all the way down. See `embedding.ts`. */ readonly embeddings?: EmbeddingCache; /** * The resolved dials. * * Resolved by the CALLER and held fixed for the life of a proxy, because * changing a dial mid-session changes how the cached prefix compresses -- * and a prefix that changes is a cache miss on everything. */ readonly tuning?: Tuning; } export interface StrategyResult { readonly request: ProviderRequest; readonly elisions: readonly Elision[]; /** Tokens of preamble this strategy ADDED to the request. */ readonly injectedChars: number; /** * What to remember about this conversation IF this request is the one sent. * * Uncommitted on purpose: a caller that decides not to use the rewritten body * must not leave a record saying it did. Commit it with * `anchors.remember(anchor.key, anchor.record)` once the body is accepted. */ readonly anchor?: AnchorDecision; } /** * What the agent is asking, read off the request itself. * * Only SHORT blocks count. The last message in an agentic conversation is * usually a tool result -- tens of kilobytes of the very content being * compressed -- and tokenising that as the question would make every block * maximally relevant to itself. That failure would look like it was working, * which is the worst kind. */ export declare function questionIn(request: ProviderRequest): string; /** * The steering text for TOOL deferral, which must not move during a session. * * WHY THIS IS NOT `questionIn`. Both pick what is relevant, but they rewrite * different halves of the request and only one of them is cached. Content * compression works AFTER the cache frontier, so it may follow the live * question and change every turn at no cost. Tool deferral rewrites the tools * array, which sits at the FRONT of the prefix -- so if its steering text * changes, the chosen tools change, the prefix changes, and every cached token * behind it is invalidated. * * MEASURED, and it is not a small effect. Steering deferral with `questionIn` * kept a stable COUNT of 14 deferred tools while producing 11 distinct * `deferredToolChars` values across 41 requests -- the same number of tools, * but a different set each turn. Cache creation went from 2,188 tokens per * request to 7,048 while cache reads fell from 30,307 to 14,181: weighting * writes at 1.25x and reads at 0.1x, that is 5,766 -> 10,228, so the feature * that removes 38,322 characters per request made the bill 1.77x WORSE. * * The first user turn is the task, and the task does not change while it is * being worked on. Relevance to it is what tool selection actually wants. */ export declare function taskIn(request: ProviderRequest): string; /** * The share a rewrite must remove to repay itself, for this proxy's prior. * * FIXED FOR THE LIFE OF A PROXY, like every other dial, and that is the whole * point: a threshold that moved with the conversation would decline a rewrite * on one turn and accept it on the next, re-sending the entire prefix at 1.25x * instead of re-reading it at 0.1x. */ export declare function minRewriteShare(tuning?: Tuning): number; export declare function v1Frontier(request: ProviderRequest, options?: StrategyOptions): StrategyResult; /** * V2: V1, plus re-inflation of spans the model is about to need. * * MEASURED, NOT ASSUMED. This guesses, and a wrong guess spends tokens * re-inflating something unwanted. It is compared against V1 on the same * fixtures; if it loses, V1 ships. */ export declare function v2Speculative(request: ProviderRequest, options?: StrategyOptions): StrategyResult; /** V3: V1's markers with no frontier restriction. */ export declare function v3History(request: ProviderRequest, options?: StrategyOptions): StrategyResult; /** * The CCR-style control: what HeadRoom does, as faithfully as we can state it. * * Opaque markers, history compressed, and the two injections that make the * markers redeemable. `injectedChars` is what those cost -- the number their * published reduction figures do not appear to include. */ export declare function ccrStyle(request: ProviderRequest, options?: StrategyOptions): StrategyResult; /** * V1, plus the history substitution -- the one region nothing else touches. * * COMPOSED RATHER THAN FORKED. V1 attacks the fresh tail and the tool * definitions; this attacks the reasoning in history, which is 52% of it and * which every other arm steps over because `messageIsSigned` forbids rewriting * it. They are disjoint, so the substitution runs first and V1 then does * exactly what it always did to what is left. Anything V1 learns about * anchoring, knowledge injection and the saving floor is inherited rather than * reimplemented, which is the difference between a fifth arm and a second * codebase. * * ORDER IS NOT ARBITRARY. Substitution must happen BEFORE the anchor decision, * because the anchor records what the cached prefix looks like and it has to * record the prefix we actually send. Running it afterwards would anchor one * body and transmit another -- the same class of defect as recording * `anchored: true` for a rewrite the proxy then discarded. * * OFF BY DEFAULT AT THE CALLER. Registered here so it is measurable; the proxy * gates it on `TOKEN_OPTIMIZER_PROXY_SUBSTITUTE`. Deferral shipped default-off * and was therefore never measured for months, so the switch is deliberate and * so is the instrumentation behind it. */ export declare function v4Substitute(request: ProviderRequest, options?: StrategyOptions): StrategyResult; /** Every arm, by name. */ export declare const STRATEGIES: Record StrategyResult>; //# sourceMappingURL=strategy.d.ts.map