@oh-my-pi-zen/snapcompact 16.3.6-zen.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,586 @@
1
+ /**
2
+ * Snapcompact compaction: archive conversation history as dense bitmap images.
3
+ *
4
+ * Instead of asking an LLM to summarize discarded history, the serialized
5
+ * conversation is rendered into PNG frames of pixel-font text that vision
6
+ * models read back directly, like an archivist at a snapcompact frame
7
+ * reader. Frames are `frameSize` wide; their height hugs the text rows
8
+ * actually printed, so a partially filled frame never bills blank rows.
9
+ *
10
+ * The frame shape is provider-aware. Original choices came from the SQuAD
11
+ * prose evals (`packages/snapcompact`, 200k-token monolithic runs); the
12
+ * spacing choices below come from the tool-result legibility bench
13
+ * (`research/toolbench.py`, real search/read/find output with structure QA),
14
+ * which exposed that the prose-tuned dense cells erase the line numbers and
15
+ * indentation that code/search output depends on:
16
+ *
17
+ * - **Anthropic** (`11on16-bw`): 8x13 glyphs on an 11px advance (extra
18
+ * letter-spacing), black ink. On the tool-result bench, tracking the
19
+ * readable cell beat plain `8on16-bw` (opus-4.8 f1 .806 vs .755) and far
20
+ * beat the prior dense `6x12-dim` (.351, which fell below the OCR ~16px/char
21
+ * floor and abstained). Opus 4.7+/Fable/Mythos ingest high-res natively
22
+ * (2576px edge, 4,784 visual-token cap), so those lines get 1932px frames:
23
+ * same bill, fewer frames. Older Claude lines downscale past 1568px.
24
+ * - **Google** (`8on22-bw` @2048): 8x13 glyphs on a 22px pitch (extra line
25
+ * spacing), black ink. Leading lifted gemini-3.5-flash to f1 .934 vs .807
26
+ * for `8on16-bw` and .287 for the prior `doc-8on16-sent-dim`. Gemini 3.x
27
+ * bills a fixed `media_resolution` budget per image (default 1,120 tokens)
28
+ * regardless of pixels, so the 2048px frame carries more chars at the same
29
+ * bill.
30
+ * - **OpenAI** (`8on22-bw`): same leading win (gpt-5.5/gpt-5.4-mini). Patch
31
+ * billing (32px × 1.2, 10k-patch budget at `detail: "original"`) is
32
+ * area-proportional, so resolution cannot improve chars/$ — 1568 stays.
33
+ * `detail: "high"` would downgrade (2,500-patch cap); `original` is sent.
34
+ * - **Unknown providers** default to the Anthropic shape. `providerImageBudget`
35
+ * still caps per-request images per provider so inline imaging cannot flood a
36
+ * request with attachments, but the old OpenRouter-specific 8-image cap is
37
+ * gone; routers now use the same permissive budget as direct Anthropic/Claude
38
+ * lines unless configured otherwise upstream.
39
+ *
40
+ * The whole pass is local and deterministic — no LLM call, no API key, no
41
+ * latency beyond rendering. Rasterization and PNG encoding happen in native
42
+ * code (`renderSnapcompactPng` in `crates/pi-natives/src/snapcompact.rs`).
43
+ * Frames persist in the compaction entry's `preserveData` and are
44
+ * re-attached to the compaction summary message on every context rebuild.
45
+ */
46
+ import type { Api, ImageContent, Message, TextContent } from "@oh-my-pi-zen/pi-ai";
47
+ /** One eval-validated frame shape: font, cell, ink, repetition, and size. */
48
+ export interface Shape {
49
+ /** Bundled font in the native renderer. */
50
+ font: "5x8" | "8x8" | "6x12" | "8x13" | "silver";
51
+ /** Target cell advance in pixels; differing from the font's natural cell
52
+ * renders via Lanczos stretch (anti-aliased RGB frame). */
53
+ cellWidth: number;
54
+ /** Target cell pitch in pixels. */
55
+ cellHeight: number;
56
+ /** `false` → glyphs drawn at natural size on the cell pitch (8on16);
57
+ * `true`/`undefined` → legacy auto Lanczos stretch when cell ≠ natural. */
58
+ stretch?: boolean;
59
+ /** Ink: `sent` cycles six hues at sentence boundaries; `bw` is black. */
60
+ variant: "sent" | "bw";
61
+ /** Print stopwords in dim ink (research `dim`/`sent-dim` variants). */
62
+ stopwordDim?: boolean;
63
+ /** 1/undefined = row-major grid; 2 = two word-wrapped newspaper columns
64
+ * (research `doc`). */
65
+ columns?: number;
66
+ /** Each text line is printed this many times; copies after the first sit
67
+ * on a pale highlight band (redundancy coding). */
68
+ lineRepeat: number;
69
+ /** Frame edge in pixels. */
70
+ frameSize: number;
71
+ /** Per-frame billed-token estimate for the shape's target provider. */
72
+ frameTokenEstimate: number;
73
+ /** Resolution hint attached to frame images (OpenAI-only). */
74
+ imageDetail?: ImageContent["detail"];
75
+ }
76
+ /** Geometry half of a {@link Shape}: everything except provider billing. */
77
+ export type ShapeGeometry = Omit<Shape, "frameTokenEstimate" | "imageDetail">;
78
+ /**
79
+ * Frame variants exercised by the SQuAD evals in `research/` that the native
80
+ * renderer reproduces faithfully, keyed by their research names. Font codes:
81
+ * `8x8u` unscii square cell, `8x8r` unscii with every line printed twice
82
+ * (redundancy coding), `6x6u` unscii Lanczos-squeezed to 6x6 (densest
83
+ * readable cell), `5x8` the X.org legacy font on its 2576px frame, `6x12`
84
+ * and `8x13` the X.org misc fonts, `8on16` 8x13 glyphs on an 8x16 cell pitch
85
+ * (no stretch, extra leading), `8on22` the same glyphs on a 22px pitch (more
86
+ * leading), `11on16` the same glyphs on an 11px advance (more tracking),
87
+ * `silver16` the embedded Silver TrueType font on a 16px grid for CJK and
88
+ * other non-Latin text, and `doc-` prefixed shapes a two-column word-wrapped
89
+ * newspaper layout. Ink: `sent` cycles six hues at sentence boundaries, `bw`
90
+ * is plain black, `-dim` suffix prints stopwords in gray.
91
+ */
92
+ export declare const SHAPE_VARIANTS: {
93
+ readonly "8x8r-bw": {
94
+ readonly font: "8x8";
95
+ readonly cellWidth: 8;
96
+ readonly cellHeight: 8;
97
+ readonly variant: "bw";
98
+ readonly lineRepeat: 2;
99
+ readonly frameSize: 1568;
100
+ };
101
+ readonly "8x8r-sent": {
102
+ readonly font: "8x8";
103
+ readonly cellWidth: 8;
104
+ readonly cellHeight: 8;
105
+ readonly variant: "sent";
106
+ readonly lineRepeat: 2;
107
+ readonly frameSize: 1568;
108
+ };
109
+ readonly "8x8u-bw": {
110
+ readonly font: "8x8";
111
+ readonly cellWidth: 8;
112
+ readonly cellHeight: 8;
113
+ readonly variant: "bw";
114
+ readonly lineRepeat: 1;
115
+ readonly frameSize: 1568;
116
+ };
117
+ readonly "8x8u-sent": {
118
+ readonly font: "8x8";
119
+ readonly cellWidth: 8;
120
+ readonly cellHeight: 8;
121
+ readonly variant: "sent";
122
+ readonly lineRepeat: 1;
123
+ readonly frameSize: 1568;
124
+ };
125
+ readonly "6x6u-bw": {
126
+ readonly font: "8x8";
127
+ readonly cellWidth: 6;
128
+ readonly cellHeight: 6;
129
+ readonly variant: "bw";
130
+ readonly lineRepeat: 1;
131
+ readonly frameSize: 1568;
132
+ };
133
+ readonly "6x6u-sent": {
134
+ readonly font: "8x8";
135
+ readonly cellWidth: 6;
136
+ readonly cellHeight: 6;
137
+ readonly variant: "sent";
138
+ readonly lineRepeat: 1;
139
+ readonly frameSize: 1568;
140
+ };
141
+ readonly "5x8-bw": {
142
+ readonly font: "5x8";
143
+ readonly cellWidth: 5;
144
+ readonly cellHeight: 8;
145
+ readonly variant: "bw";
146
+ readonly lineRepeat: 1;
147
+ readonly frameSize: 2576;
148
+ };
149
+ readonly "5x8-sent": {
150
+ readonly font: "5x8";
151
+ readonly cellWidth: 5;
152
+ readonly cellHeight: 8;
153
+ readonly variant: "sent";
154
+ readonly lineRepeat: 1;
155
+ readonly frameSize: 2576;
156
+ };
157
+ readonly "6x12-dim": {
158
+ readonly font: "6x12";
159
+ readonly cellWidth: 6;
160
+ readonly cellHeight: 12;
161
+ readonly variant: "bw";
162
+ readonly stopwordDim: true;
163
+ readonly lineRepeat: 1;
164
+ readonly frameSize: 1568;
165
+ };
166
+ readonly "8x13-bw": {
167
+ readonly font: "8x13";
168
+ readonly cellWidth: 8;
169
+ readonly cellHeight: 13;
170
+ readonly variant: "bw";
171
+ readonly lineRepeat: 1;
172
+ readonly frameSize: 1568;
173
+ };
174
+ readonly "8on16-bw": {
175
+ readonly font: "8x13";
176
+ readonly cellWidth: 8;
177
+ readonly cellHeight: 16;
178
+ readonly stretch: false;
179
+ readonly variant: "bw";
180
+ readonly lineRepeat: 1;
181
+ readonly frameSize: 1568;
182
+ };
183
+ readonly "8on22-bw": {
184
+ readonly font: "8x13";
185
+ readonly cellWidth: 8;
186
+ readonly cellHeight: 22;
187
+ readonly stretch: false;
188
+ readonly variant: "bw";
189
+ readonly lineRepeat: 1;
190
+ readonly frameSize: 1568;
191
+ };
192
+ readonly "11on16-bw": {
193
+ readonly font: "8x13";
194
+ readonly cellWidth: 11;
195
+ readonly cellHeight: 16;
196
+ readonly stretch: false;
197
+ readonly variant: "bw";
198
+ readonly lineRepeat: 1;
199
+ readonly frameSize: 1568;
200
+ };
201
+ readonly "silver16-bw": {
202
+ readonly font: "silver";
203
+ readonly cellWidth: 16;
204
+ readonly cellHeight: 16;
205
+ readonly variant: "bw";
206
+ readonly lineRepeat: 1;
207
+ readonly frameSize: 1568;
208
+ };
209
+ readonly "doc-8on16-bw": {
210
+ readonly font: "8x13";
211
+ readonly cellWidth: 8;
212
+ readonly cellHeight: 16;
213
+ readonly stretch: false;
214
+ readonly variant: "bw";
215
+ readonly columns: 2;
216
+ readonly lineRepeat: 1;
217
+ readonly frameSize: 1568;
218
+ };
219
+ readonly "doc-8on16-sent": {
220
+ readonly font: "8x13";
221
+ readonly cellWidth: 8;
222
+ readonly cellHeight: 16;
223
+ readonly stretch: false;
224
+ readonly variant: "sent";
225
+ readonly columns: 2;
226
+ readonly lineRepeat: 1;
227
+ readonly frameSize: 1568;
228
+ };
229
+ readonly "doc-8on16-sent-dim": {
230
+ readonly font: "8x13";
231
+ readonly cellWidth: 8;
232
+ readonly cellHeight: 16;
233
+ readonly stretch: false;
234
+ readonly variant: "sent";
235
+ readonly stopwordDim: true;
236
+ readonly columns: 2;
237
+ readonly lineRepeat: 1;
238
+ readonly frameSize: 1568;
239
+ };
240
+ };
241
+ /** Research name of one renderable frame variant. */
242
+ export type ShapeVariantName = keyof typeof SHAPE_VARIANTS;
243
+ /** All variant names, in declaration order (for settings enums). */
244
+ export declare const SHAPE_VARIANT_NAMES: readonly ShapeVariantName[];
245
+ /** Runtime guard for variant names loaded from config. */
246
+ export declare function isShapeVariantName(value: unknown): value is ShapeVariantName;
247
+ /** Eval-validated shapes, keyed by the provider family they won on. */
248
+ export declare const SHAPES: {
249
+ /** `11on16-bw`: 8x13 glyphs on an 11px advance (extra tracking), black ink.
250
+ * Tool-result legibility bench (real search/read/find output, structure QA)
251
+ * on opus-4.8: f1 .806 vs .755 for plain `8on16-bw` and .351 for the prior
252
+ * `6x12-dim` default — letter-spacing the readable cell wins; the dense
253
+ * 6x12 was below the OCR ~16px/char floor and abstained. */
254
+ anthropic: Shape;
255
+ /** `8on22-bw`: 8x13 glyphs on a 22px pitch (extra leading), black ink.
256
+ * Tool-result legibility bench on gemini-3.5-flash: f1 .934 vs .807 for
257
+ * plain `8on16-bw` and .287 for the prior `doc-8on16-sent-dim`; the
258
+ * line-spacing reduces row crowding so line numbers stay legible. */
259
+ google: Shape;
260
+ /** `8on22-bw`: 8x13 glyphs on a 22px pitch (extra leading), black ink.
261
+ * Same line-spacing win for OpenAI; bench on gpt-5.5/gpt-5.4-mini showed
262
+ * leading lifts recall on the readable cell over plain `8on16-bw`. */
263
+ openai: Shape;
264
+ /** Original 5x8 X.org shape (pre-shape-table sessions rendered this). */
265
+ legacy: Shape;
266
+ };
267
+ /** Runtime guard for shape overrides loaded from config or preserve data. */
268
+ export declare function isShape(value: unknown): value is Shape;
269
+ /** One model line's ideal format: variant plus an optional frame-size
270
+ * override when the line reads larger frames at no extra cost. */
271
+ export interface IdealShape {
272
+ variant: ShapeVariantName;
273
+ frameSize?: number;
274
+ }
275
+ /** Eval-ideal format for a model id, or undefined when unmeasured. */
276
+ export declare function idealShapeVariant(modelId: string): IdealShape | undefined;
277
+ /** What will read the frames: the wire API (billing) and model id (shape). */
278
+ export interface ShapeTarget {
279
+ api?: Api;
280
+ id?: string;
281
+ }
282
+ /**
283
+ * Pick the frame shape for a reader. An explicit `variant` (anything but
284
+ * `"auto"`) forces that geometry; otherwise the model id selects the
285
+ * eval-winning shape — and frame size — for its model line, falling back to
286
+ * the API family's winner when the model is unmeasured. Billing (token
287
+ * estimate, detail hint) always follows the API family actually carrying
288
+ * the request, computed for the resolved frame size. Accepts a full pi-ai
289
+ * `Model` or any `{ api, id }` subset.
290
+ */
291
+ export declare function resolveShape(model?: ShapeTarget, variant?: ShapeVariantName | "auto"): Shape;
292
+ /**
293
+ * Pick the frame shape for `text` without changing the selected shape.
294
+ *
295
+ * Glyph-level Silver fallback happens during normalization/rendering, so this
296
+ * helper exists for callers that need a text-aware API name while preserving
297
+ * explicit and provider-selected shapes.
298
+ */
299
+ export declare function resolveShapeForText(_text: string, model?: ShapeTarget, variant?: ShapeVariantName | "auto"): Shape;
300
+ /** Legacy frame edge in pixels (the 5x8 shape's eval-validated size). New
301
+ * shapes carry their own `frameSize`. */
302
+ export declare const FRAME_SIZE = 2576;
303
+ /** Default upper bound on archive frames carried per compaction. Sized to hold
304
+ * ~400k tokens of the high-res Anthropic frame Opus reads (1932px ≈ 5,000
305
+ * billed tokens each → 80 frames) while staying under the ~100-image
306
+ * per-request wire cap. Oldest frames are dropped first once the budget is
307
+ * exceeded (mirrors how iterative text summaries fade the oldest detail); a
308
+ * caller may pass a lower `maxFrames` upper limit, and per-model context
309
+ * fitting is handled by the caller's overflow guard. */
310
+ export declare const MAX_FRAMES_DEFAULT = 80;
311
+ /** High-quality (legible) frames rendered at each chronological edge of a
312
+ * foveated archive — the session head (oldest) and the slice just before the
313
+ * text region (newest) — with the denser low-quality tier filling the middle. */
314
+ export declare const HQ_EDGE_FRAMES = 3;
315
+ /** Conservative per-frame token estimate used for context budgeting — the
316
+ * upper bound across shapes: high-res Claude frames hit the 4,784 visual-token
317
+ * cap, billed at +5% margin (ceil(4784 * 1.05)). Keeps the overflow guard from
318
+ * undercounting a high-res archive at the raised {@link MAX_FRAMES_DEFAULT}. */
319
+ export declare const FRAME_TOKEN_ESTIMATE = 5024;
320
+ /** Conservative upper bound for one persisted frame's base64 payload. The
321
+ * measured high-res Anthropic `8x13`/`11on16` PNG frames sit around 159 KB;
322
+ * 170 KB leaves margin for denser glyph pages without permitting multi-MB
323
+ * standing request bodies at large context windows. */
324
+ export declare const FRAME_DATA_BYTES_ESTIMATE = 170000;
325
+ /** Maximum snapcompact image base64 carried in every rebuilt provider request.
326
+ * Above this, provider backends can accept the HTTP body but fail mid-stream
327
+ * with opaque 5xx errors. Keep this independent from visual-token budgeting:
328
+ * a 1M-token model can afford 70 images on paper, but not the resulting
329
+ * ~11 MB JSON payload on every turn. */
330
+ export declare const FRAME_DATA_BYTES_BUDGET = 3000000;
331
+ /** Frame-count cap implied by {@link FRAME_DATA_BYTES_BUDGET}. */
332
+ export declare function maxFramesForDataBudget(maxFrameDataBytes?: number): number;
333
+ /** Base64 byte length for persisted snapcompact frames. */
334
+ export declare function frameDataBytes(frames: readonly Pick<Frame, "data">[]): number;
335
+ /**
336
+ * Per-request image-count budgets by provider id. These cap how many images an
337
+ * entire request may carry (archive/system-prompt/tool-result imaging combined).
338
+ * The values are conservative policy caps under the vendor hard limits
339
+ * (Anthropic 100, OpenAI 500, Gemini ~2500); unknown providers fall to a safe
340
+ * floor rather than sending unbounded attachments.
341
+ */
342
+ export declare const PROVIDER_IMAGE_BUDGETS: Record<string, number>;
343
+ /** Safe floor for unknown providers (strictest mainstream measured: Groq ~5). */
344
+ export declare const DEFAULT_PROVIDER_IMAGE_BUDGET = 5;
345
+ /** Per-request image budget for `provider`; unknown providers get the floor. */
346
+ export declare function providerImageBudget(provider: string | undefined): number;
347
+ /** Key under `CompactionEntry.preserveData` holding the frame archive. */
348
+ export declare const PRESERVE_KEY = "snapcompact";
349
+ /** One developed snapcompact frame: a base64 PNG plus its reading geometry. */
350
+ export interface Frame {
351
+ /** Base64-encoded PNG. */
352
+ data: string;
353
+ mimeType: string;
354
+ /** Characters per row in the frame grid (per-column width on doc frames). */
355
+ cols: number;
356
+ /** Text rows in the frame grid (unique lines, not repeated copies). */
357
+ rows: number;
358
+ /** Characters actually printed onto this frame. */
359
+ chars: number;
360
+ /** Shape metadata (absent on legacy frames, which are 5x8 `sent`). */
361
+ font?: Shape["font"];
362
+ variant?: Shape["variant"];
363
+ lineRepeat?: number;
364
+ /** 2 on two-column doc frames; absent on row-major grid frames. */
365
+ columns?: number;
366
+ /** True when stopwords were printed in dim ink. */
367
+ stopwordDim?: boolean;
368
+ /** Resolution hint forwarded to the provider when re-attaching. */
369
+ detail?: ImageContent["detail"];
370
+ }
371
+ /** Frame archive persisted under `preserveData[PRESERVE_KEY]`. */
372
+ export interface Archive {
373
+ /** Rendered frames ordered oldest to newest, re-derived from {@link text}
374
+ * each compaction with foveated quality tiers (HQ/LQ/HQ inside the imaged
375
+ * middle). May be empty when the whole archive fits in text. */
376
+ frames: Frame[];
377
+ /** Characters currently readable across all frames plus the text regions. */
378
+ totalChars: number;
379
+ /** Characters dropped so far to respect the archive budget. */
380
+ truncatedChars: number;
381
+ /** Full kept archive source (oldest to newest, normalized, bounded to the
382
+ * rendered budget) — the single source re-rendered each compaction. */
383
+ text?: string;
384
+ /** Oldest text region kept verbatim around the imaged middle. */
385
+ textHead?: string;
386
+ /** Newest text region kept verbatim around the imaged middle. */
387
+ textTail?: string;
388
+ }
389
+ export interface Geometry {
390
+ /** Characters per row (per-column line width when `columns === 2`). */
391
+ cols: number;
392
+ rows: number;
393
+ /** Characters that fit one frame (nominal upper bound on doc shapes,
394
+ * where real consumption is wrap-dependent). */
395
+ capacity: number;
396
+ }
397
+ export interface Options<TMessage = Message> extends SerializeOptions {
398
+ /** App-level message transformer (same contract as agent-core's `SummaryOptions.convertToLlm`). */
399
+ convertToLlm?: ConvertToLlm<TMessage>;
400
+ /** Model whose provider API and id select the frame shape. */
401
+ model?: ShapeTarget;
402
+ /** Explicit shape override; wins over `model`. */
403
+ shape?: Shape;
404
+ /** Frame edge in pixels. Defaults to the shape's `frameSize`. */
405
+ frameSize?: number;
406
+ /** Upper limit on archive frames; clamped to (and defaulting to) {@link MAX_FRAMES_DEFAULT}. */
407
+ maxFrames?: number;
408
+ }
409
+ /** Result of rendering one frame. */
410
+ export interface RenderedFrame {
411
+ /** Base64-encoded PNG, as returned by the native renderer. */
412
+ data: string;
413
+ cols: number;
414
+ rows: number;
415
+ /** Characters printed (ink toggles excluded; input may be shorter than capacity). */
416
+ chars: number;
417
+ }
418
+ export interface FileOperations {
419
+ read: Set<string>;
420
+ written: Set<string>;
421
+ edited: Set<string>;
422
+ }
423
+ export interface CompactionDetails {
424
+ readFiles: string[];
425
+ modifiedFiles: string[];
426
+ }
427
+ export interface CompactionPreparation<TMessage = Message> {
428
+ /** UUID of first entry to keep. */
429
+ firstKeptEntryId: string;
430
+ /** Messages that will be archived and discarded. */
431
+ messagesToSummarize: TMessage[];
432
+ /** Messages that will be archived as the split-turn prefix, if any. */
433
+ turnPrefixMessages: TMessage[];
434
+ tokensBefore: number;
435
+ /** Summary from previous compaction, for continuity when no prior snapcompact archive exists. */
436
+ previousSummary?: string;
437
+ /** Preserved opaque compaction payload from the previous compaction, if any. */
438
+ previousPreserveData?: Record<string, unknown>;
439
+ /** File operations extracted by the host agent. */
440
+ fileOps: FileOperations;
441
+ }
442
+ export interface CompactionResult<T = CompactionDetails> {
443
+ summary: string;
444
+ shortSummary?: string;
445
+ firstKeptEntryId: string;
446
+ tokensBefore: number;
447
+ details?: T;
448
+ preserveData?: Record<string, unknown>;
449
+ }
450
+ export type ConvertToLlm<TMessage = Message> = (messages: TMessage[]) => Message[];
451
+ export declare function createFileOps(): FileOperations;
452
+ export declare function isUrlSchemePath(path: string): boolean;
453
+ export declare function computeFileLists(fileOps: FileOperations): CompactionDetails;
454
+ export declare function upsertFileOperations(summary: string, readFiles: string[], modifiedFiles: string[], readSet?: ReadonlySet<string>): string;
455
+ /** Default per-tool-result character cap in serialized history. */
456
+ export declare const TOOL_RESULT_MAX_CHARS = 2000;
457
+ /** Default per-argument-value character cap inside serialized tool calls
458
+ * (write/edit bodies otherwise dump whole files into the archive). */
459
+ export declare const TOOL_ARG_MAX_CHARS = 500;
460
+ /** Default character cap across one tool call's full serialized argument list. */
461
+ export declare const TOOL_CALL_MAX_CHARS = 2000;
462
+ /** Default fraction of a truncation budget spent on the head; the remainder
463
+ * keeps the tail, where command errors and test failures usually land. */
464
+ export declare const TRUNCATE_HEAD_RATIO = 0.6;
465
+ /** Zero-width ink toggles understood by the native renderer (shift-out/in):
466
+ * text between them prints in dim gray ink without occupying a cell. */
467
+ export declare const DIM_ON = "\u000E";
468
+ export declare const DIM_OFF = "\u000F";
469
+ /** Character budgets applied while serializing discarded history for frame
470
+ * rendering. Pass `Infinity` to disable an individual cap. */
471
+ export interface SerializeOptions {
472
+ /** Per-tool-result cap. Defaults to {@link TOOL_RESULT_MAX_CHARS}. */
473
+ toolResultMaxChars?: number;
474
+ /** Per-argument-value cap. Defaults to {@link TOOL_ARG_MAX_CHARS}. */
475
+ toolArgMaxChars?: number;
476
+ /** Whole-argument-list cap per call. Defaults to {@link TOOL_CALL_MAX_CHARS}. */
477
+ toolCallMaxChars?: number;
478
+ /** Head share of each budget, clamped to [0, 1]. Defaults to {@link TRUNCATE_HEAD_RATIO}. */
479
+ truncateHeadRatio?: number;
480
+ /** Print tool-result text in dim gray ink so archived conversation reads
481
+ * louder than archived tool noise. Defaults to `true`. */
482
+ dimToolResults?: boolean;
483
+ }
484
+ export declare function serializeConversation(messages: Message[], options?: SerializeOptions): string;
485
+ /** Printed in place of newline runs: the native renderer fills this cell
486
+ * entirely with pitch-black ink, so line structure survives whitespace
487
+ * collapsing at a one-cell cost. */
488
+ export declare const NEWLINE_GLYPH = "\u2588";
489
+ export interface NormalizeOptions {
490
+ /** Shape whose font is tried before the embedded Silver fallback. */
491
+ shape?: Pick<Shape, "font">;
492
+ /** Native font name when a full shape is not available. */
493
+ font?: Shape["font"];
494
+ }
495
+ /**
496
+ * Prepare text for printing: strip ANSI escape sequences, collapse horizontal
497
+ * whitespace runs, fold unsupported symbols (including box drawing to ASCII),
498
+ * preserve Unicode glyphs that either the selected font or embedded Silver
499
+ * fallback can render, and drop decorative emoji instead of printing `?`.
500
+ */
501
+ export declare function normalize(text: string, options?: NormalizeOptions): string;
502
+ /**
503
+ * Scan text with the same font-aware path as {@link normalize}; unsafe means
504
+ * more than 5% of graphic characters would hit the `?` fallback.
505
+ */
506
+ export declare function scanRenderability(text: string, options?: NormalizeOptions): {
507
+ isSafe: boolean;
508
+ unrenderableRatio: number;
509
+ };
510
+ /**
511
+ * Wrap each maximal alphabetic run that is a stopword in {@link DIM_ON} /
512
+ * {@link DIM_OFF} so it prints in dim gray ink. Spans that are already dim
513
+ * (e.g. archived tool output) pass through untouched — wrapping there would
514
+ * terminate the enclosing dim span early. Markers are zero-width, so the
515
+ * visible glyph count is unchanged.
516
+ */
517
+ export declare function dimStopwords(text: string): string;
518
+ /**
519
+ * Greedy word-wrap, no mid-word breaks (hard split only for width+ words) —
520
+ * ported verbatim from `research/exp14_bestgpt.py` `wrap()`. Zero-width dim
521
+ * markers count toward word length here; serialized history places them at
522
+ * word boundaries, so the drift is at most one cell per affected line.
523
+ */
524
+ export declare function wrap(text: string, width: number, wideCells?: boolean): string[];
525
+ export declare function geometry(shape: Shape, size?: number): Geometry;
526
+ /** Render one snapcompact frame from already-normalized text. Doc shapes
527
+ * (`columns === 2`) expect one page of `\n`-joined pre-wrapped lines. */
528
+ export declare function render(text: string, shape: Shape, size?: number): Promise<RenderedFrame>;
529
+ /** Options for {@link renderMany} and {@link frames}. */
530
+ export interface RenderManyOptions {
531
+ /** Explicit shape; wins over `model`. */
532
+ shape?: Shape;
533
+ /** Model whose provider API and id select the frame shape. */
534
+ model?: ShapeTarget;
535
+ /** Frame edge in px; defaults to the shape's `frameSize`. */
536
+ frameSize?: number;
537
+ /** Hard cap on frames produced; omit for unbounded (caller decides usage). */
538
+ maxFrames?: number;
539
+ }
540
+ /**
541
+ * Render arbitrary text into snapcompact PNG frames as LLM image blocks
542
+ * (first page first). Empty/whitespace-only input yields no frames.
543
+ */
544
+ export declare function renderMany(text: string, options?: RenderManyOptions): Promise<ImageContent[]>;
545
+ /** Frames needed to hold `text` at the given shape/size, without rendering.
546
+ * For doc shapes this wraps the text once and counts pages of `2 * rows`
547
+ * lines; for grid shapes it divides by the frame capacity. */
548
+ export declare function frames(text: string, options?: Pick<RenderManyOptions, "shape" | "model" | "frameSize">): number;
549
+ /** Validate and extract a persisted frame archive from `preserveData`. */
550
+ export declare function getPreservedArchive(preserveData: Record<string, unknown> | undefined): Archive | undefined;
551
+ /** Drop the persisted frame archive ({@link PRESERVE_KEY}) from `preserveData`,
552
+ * returning the remaining state — or `undefined` when nothing else remains, so
553
+ * an empty `{}` is never persisted. Callers strip the archive once its frames
554
+ * have been migrated into a new compaction's text, preventing the stale frames
555
+ * from leaking back into the rebuilt context. */
556
+ export declare function stripPreservedArchive(preserveData: Record<string, unknown> | undefined): Record<string, unknown> | undefined;
557
+ /** Extract persisted archive source text as plain text for LLM summarization. */
558
+ export declare function archiveSourceText(archive: Archive): string | undefined;
559
+ /** Build the text used to choose and preflight a font-aware snapcompact shape. */
560
+ export declare function renderabilityProbeText(serialized: string, previousPreserveData?: Record<string, unknown>, previousSummary?: string): string;
561
+ /** Options for reconstructing a persisted snapcompact archive into prompt blocks. */
562
+ export interface HistoryBlockOptions {
563
+ /** Hard cap on image base64 bytes attached to one rebuilt provider request. */
564
+ maxFrameDataBytes?: number;
565
+ }
566
+ /** Convert archive frames into LLM image blocks (oldest first). */
567
+ export declare function images(archive: Archive): ImageContent[];
568
+ /** Ordered archive blocks for a compaction summary message, oldest to newest:
569
+ * the oldest text region, the imaged middle, then the newest text region.
570
+ * Runtime-only; reconstructed from {@link Archive} on each context rebuild
571
+ * instead of persisted on the session entry. */
572
+ export declare function historyBlocks(archive: Archive, options?: HistoryBlockOptions): (TextContent | ImageContent)[];
573
+ /**
574
+ * Run a snapcompact compaction over prepared messages. Fully local: serializes
575
+ * the discarded history, appends it to the accumulated archive source text, and
576
+ * re-renders that source into an ordered history layout: plain text at the
577
+ * oldest edge, imaged middle, then plain text at the newest edge. The imaged
578
+ * middle itself foveates (HQ/LQ/HQ) when it grows large.
579
+ *
580
+ * The full kept source persists on the archive (`text`) so each later compaction
581
+ * unfolds and re-renders it coherently alongside the newly archived history.
582
+ *
583
+ * If the previous compaction was text-based, its summary is printed at the head
584
+ * of the archive as `[Summary of earlier history]` so no continuity is lost.
585
+ */
586
+ export declare function compact<TMessage = Message>(preparation: CompactionPreparation<TMessage>, options?: Options<TMessage>): Promise<CompactionResult>;
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "type": "module",
3
+ "name": "@oh-my-pi-zen/snapcompact",
4
+ "version": "16.3.6-zen.1",
5
+ "description": "Bitmap-frame context compression for vision-capable LLMs",
6
+ "homepage": "https://omp.sh",
7
+ "author": "Can Boluk",
8
+ "license": "MIT",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/cagedbird043/oh-my-pi-zen.git",
12
+ "directory": "packages/snapcompact"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/cagedbird043/oh-my-pi-zen/issues"
16
+ },
17
+ "keywords": [
18
+ "context-compression",
19
+ "vision",
20
+ "compaction",
21
+ "llm"
22
+ ],
23
+ "main": "./src/index.ts",
24
+ "types": "./dist/types/index.d.ts",
25
+ "scripts": {
26
+ "check": "biome check . && bun run check:types",
27
+ "check:types": "tsgo -p tsconfig.json --noEmit",
28
+ "lint": "biome lint .",
29
+ "test": "bun test --parallel",
30
+ "fix": "biome check --write --unsafe .",
31
+ "fmt": "biome format --write ."
32
+ },
33
+ "dependencies": {
34
+ "@oh-my-pi-zen/pi-ai": "16.3.6-zen.1",
35
+ "@oh-my-pi-zen/pi-natives": "16.3.6-zen.1",
36
+ "@oh-my-pi-zen/pi-utils": "16.3.6-zen.1",
37
+ "@oh-my-pi-zen/pi-wire": "16.3.6-zen.1"
38
+ },
39
+ "devDependencies": {
40
+ "@types/bun": "^1.3.14"
41
+ },
42
+ "engines": {
43
+ "bun": ">=1.3.14"
44
+ },
45
+ "files": [
46
+ "src",
47
+ "README.md",
48
+ "CHANGELOG.md",
49
+ "dist/types"
50
+ ],
51
+ "exports": {
52
+ ".": {
53
+ "types": "./dist/types/index.d.ts",
54
+ "import": "./src/index.ts"
55
+ },
56
+ "./snapcompact": {
57
+ "types": "./dist/types/snapcompact.d.ts",
58
+ "import": "./src/snapcompact.ts"
59
+ },
60
+ "./*": {
61
+ "types": "./dist/types/*.d.ts",
62
+ "import": "./src/*.ts"
63
+ }
64
+ }
65
+ }
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./snapcompact";
@@ -0,0 +1,5 @@
1
+ {{#if files}}
2
+ {{#xml "files"}}
3
+ {{files}}
4
+ {{/xml}}
5
+ {{/if}}