/** * History-image compression (Variant C). * * Collapses the largest closed-tool-sequence prefix into one synthetic user message * containing 1-N PNG image blocks. The live tail (keepTail turns + any open tool * sequence) stays as text. thinking blocks are dropped from the collapsed range — * only the most-recent assistant-with-tool_use must round-trip bit-perfect, and * that turn is in the live tail by construction. * * Synthesized message uses role:'user' because Anthropic forbids image blocks inside * role:'assistant'. cache_control placement is left to the caller (transform.ts). */ import type { CacheControl, ContentBlock, Message } from './types.js'; import { type RenderStyle } from './render.js'; /** * Banner text blocks that bracket the collapsed-history image(s) in the synthetic * user message. Exported as the SINGLE SOURCE OF TRUTH: transform.ts keys its * cache-anchor relocation off the intro text, so a literal copy there would * silently break relocation whenever this wording changes (it did exactly once — * the XML-framing reword left the matcher pointing at the old banner). Both the * emitter (here) and the matcher (transform.ts) must reference this constant. */ export declare const HISTORY_SYNTHETIC_INTRO = "[Earlier turns of THIS conversation, transcribed in the image(s) below. Each turn is wrapped in ... or ... tags, where N is an absolute turn index (larger N = more recent); attribute every turn strictly by its tag, and treat the highest-N turns as the most recent prior context, NOT the low-N opening turns. Earlier turns may contain questions or tasks that were already answered later in this same history; do not reopen low-N turns unless the live text after this block asks you to. For exact identifiers, hashes, version strings, and numbers from the transcript, rely on the exact-value factsheet or re-read the source; do not guess an exact value seen only in the image. This is prior context, NOT the current request.]"; export declare const HISTORY_SYNTHETIC_OUTRO = "[End of earlier conversation. The current request is the live text that follows below.]"; /** Break-even gate predicate. Injected by transform.ts to avoid a circular import. * IMPORTANT: pass the full string, not text.length — the row-aware path in * isCompressionProfitable must see actual newlines to budget images correctly. * History text is newline-heavy (headers, JSON args, labels); chars-only * under-predicts image count ~5-10× and lets net-losers through. */ export type ProfitableFn = (text: string, cols: number) => boolean; /** Configuration for history collapse. */ export interface HistoryCollapseOptions { /** Turns at the tail to keep as text. Default 4. */ keepTail: number; /** Minimum collapsible prefix turns — below this, cache-amortization math doesn't work. Default 10. */ minCollapsePrefix: number; /** Soft-wrap columns for the renderer; should match host cols. Default 100. */ cols: number; /** Advance the collapse boundary in steps of this many messages so the rendered PNG stays * byte-identical for collapseChunk turns and keeps hitting Anthropic's prompt cache. * Set to 0 for a per-turn moving boundary. Default 50. */ collapseChunk: number; /** Append-only freeze granularity, in messages. The collapse range is rendered * as independent image blocks on an ABSOLUTE grid anchored at protectedPrefix, * in steps of this many messages. Each completed chunk's bytes are fixed by its * message range alone, so old chunks stay byte-identical (cache_read forever) as * the conversation grows — only the newest partial chunk re-renders. Caller * cache_control marks force an extra split so a roaming breakpoint stays an * aligned, independently-cacheable image boundary. Set to 0 to render the whole * range as one paginated blob (legacy, non-append-only). Default 10. */ freezeChunk: number; /** Leading messages to never collapse. Protects the slab-bearing first user message * (system-prompt + tool-docs images) so its cache_control anchor stays at the front * and isn't swept into the history image as [image] placeholders. Default 0. */ protectedPrefix: number; /** Reflow the transcript before RENDERING: pack soft-wrapped lines and mark * every hard newline with the ↵ sentinel — same treatment as the static slab. * History text is newline-heavy (role headers, JSON args), so without this * each short line wastes a full render row, inflating image count and shrinking * the savings. Glyph size is unchanged (cols stays the same) so legibility is * identical — it just removes the blank-row waste. `collapsedChars` still * reports the ORIGINAL transcript length. Default true. */ reflow: boolean; /** Model-profile render style. */ style: RenderStyle; /** Model-profile page-height cap. */ maxHeightPx: number; /** Chars one rendered page holds. Only an ESTIMATOR input (the renderer paginates * on its own); it decides how many pages a candidate chunk grid will produce. * Default {@link DENSE_CONTENT_CHARS_PER_IMAGE}. */ pageChars: number; /** Hard cap on image blocks this collapse may emit. Anthropic rejects requests * with more than 100 images (opaque 500), and a 10-message freeze grid emits * ≈1 page per chunk regardless of how little text the chunk holds — a 3000-turn * session hit 317 history images at 43% page fill (#161). When the grid would * exceed the budget the freeze step is DOUBLED (chunks merge, pages fill) until * the estimate fits; if even a single chunk cannot fit, the collapse range is * trimmed from the tail and the remainder stays live text. 0 = unlimited. */ imageBudget: number; /** Fill-optimal repack. When true the freeze step is raised until the grid costs * at most one page more than a perfectly packed render — trading the append-only * cache freeze for ~2× fewer image tokens. Only correct when the upstream prefix * cache is dead anyway (cold session, see node.ts session store); on a warm * session it would re-key every frozen chunk. Default false. */ packFill: boolean; /** Sticky lower bound for the freeze step, in messages. Once a session has been * repacked at a coarser grid, every later turn must keep that grid or the * re-render re-keys the whole history. Rounded UP to a power-of-two multiple of * `freezeChunk` so chunk boundaries stay a subset of the base grid. Default 0. */ minFreezeStep: number; } /** Images Anthropic accepts per request. Exceeding it fails the WHOLE request with * an opaque `500` (observed 2026-07-31 at 387 images), not a typed 400 — so the * cap has to be enforced on our side, before the wire. */ export declare const ANTHROPIC_MAX_IMAGES = 100; /** Default history-image budget: the hard cap minus headroom for the slab, tool-doc * and tool_result images that share the same request. transform.ts narrows this * further with the count it has already emitted for this very request. */ export declare const ANTHROPIC_HISTORY_IMAGE_BUDGET = 80; export declare const HISTORY_DEFAULTS: HistoryCollapseOptions; /** Per-request telemetry surfaced back to TransformInfo. */ export interface HistoryCollapseInfo { /** Number of turns collapsed into the history image. */ collapsedTurns: number; /** Total chars of text that went into the history image. */ collapsedChars: number; /** Number of PNG image blocks emitted for the history (≥1 if collapsed). */ collapsedImages: number; /** Total PNG bytes emitted. */ collapsedImageBytes: number; /** Total pixel area (Σ width×height) — pairs with cache_create tokens for px/token regression. */ collapsedImagePixels: number; /** Raw PNG bytes of each emitted history image, in order. Lets the caller register * them into the dashboard image ring (info.imagePngs) so colored history frames are * visible, not merely counted — every other image path already feeds the ring. */ collapsedPngs: Uint8Array[]; /** Per-image pixel dims, parallel to collapsedPngs. The dashboard ring reads * info.imageDims in lockstep with info.imagePngs, so these must be pushed together. */ collapsedImageDims: { width: number; height: number; }[]; /** Ordinal (0-based, into the emitted history images) of the last byte-stable * history image — the carry-over cache anchor. The relocator pins the cache * breakpoint here so it survives window advances (#11). Undefined when history is * too short to have a fully grid-aligned chunk before collapseLen. */ carryOverImageOrdinal?: number; /** Why we didn't collapse — populated only when no collapse happened. */ reason?: 'no_history' | 'prefix_too_short' | 'no_closed_prefix' | 'not_profitable' | 'render_empty' | 'over_budget'; /** Freeze step actually used (messages per chunk). Larger than `o.freezeChunk` * when the image budget or fill-repack forced chunks to merge. The caller pins * it per session (`minFreezeStep`) so the coarser grid never falls back — a * fallback would re-key every frozen chunk it already paid to cache. */ freezeStep?: number; /** True when the collapse range had to be shortened to stay inside the image * budget; the dropped tail stays as live text. */ budgetTrimmed?: boolean; /** Dropped codepoints from the history render, merged into the * transform-wide map by the caller. */ droppedChars: number; droppedCodepoints: Map; } /** * Return the last index ≤ cutoffExclusive at which all tool_use_ids are matched * by tool_results in [0..i]. Returns -1 if no closed boundary exists. * Robust to interleaved/parallel tool calls via openSet tracking. Consecutive * assistant-tool/user-result pairs are treated as one tool round: some Anthropic * clients serialize a parallel batch that way, so the apparently-closed gap * between two pairs is not a safe collapse boundary. */ export declare function findClosedPrefixBoundary(messages: Message[], cutoffExclusive: number): number; export declare function staleFreshnessHints(text: string): string; /** * Linearise content blocks to a single string. Drops thinking blocks (only the * most-recent assistant turn needs bit-perfect thinking, and it's in the live tail). * Inline images collapse to [image] to avoid double-encoding. */ export declare function blocksToText(content: string | ContentBlock[]): string; /** Return the caller's cache_control marker on a message, if any block carries one. * Used to align freeze-chunk boundaries to roaming breakpoints so a marked segment * stays independently cacheable instead of being silently flattened into the image. */ export declare function messageCacheControl(m: Message): CacheControl | undefined; /** Serialize messages [fromInclusive..upToExclusive) to a text blob with * `` XML wrappers. Open+close tags bracket each turn so a misread * boundary self-corrects and the model attributes speakers reliably even off a * lossy image — bare `--- role ---` start-dividers let one role bleed into the * next when a divider is missed. */ export declare function messagesToHistoryText(messages: Message[], upToExclusive: number, fromInclusive?: number): string; /** Like {@link messagesToHistoryText} but also returns the parallel slot string for * colorByRole: a width-identical copy where each `` tag is replaced by its * role marker and the body is copied verbatim (slot 0). Role attribution is decided * HERE, where the message role is known — never re-parsed out of flattened text. * A tool_result block sits inside its user message and a tool_use block inside its * assistant message, so each is owned by the turn that carries it. */ export declare function messagesToHistorySegments(messages: Message[], upToExclusive: number, fromInclusive?: number): { text: string; slotText: string; }; /** * Collapse the closed-prefix run into one synthetic user message with 1+ history images. * Returns original messages unchanged on any no-collapse path (reason set in info). * Image blocks are returned with NO cache_control — caller decides placement. */ export declare function collapseHistory(messages: Message[], isProfitable: ProfitableFn, opts?: Partial): Promise<{ messages: Message[]; info: HistoryCollapseInfo; }>; //# sourceMappingURL=history.d.ts.map