@oh-my-pi/snapcompact 15.11.2 → 15.11.4

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,16 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.11.4] - 2026-06-12
6
+
7
+ ### Breaking Changes
8
+
9
+ - Renamed every export to drop the `snapcompact`/`Snapcompact`/`SNAPCOMPACT_` qualifier — the package is meant to be consumed via `import * as snapcompact from "@oh-my-pi/snapcompact"`. Functions: `snapcompactCompact` → `compact`, `renderSnapcompactFrame` → `render`, `snapcompactGeometry` → `geometry`, `normalizeForSnapcompact` → `normalize`, `serializeSnapcompactConversation` → `serializeConversation`, `snapcompactImages` → `images`, `getPreservedSnapcompactArchive` → `getPreservedArchive`, `isSnapcompactShape` → `isShape`, `resolveSnapcompactShape` → `resolveShape`, `createSnapcompactFileOps` → `createFileOps`, `computeSnapcompactFileLists` → `computeFileLists`, `upsertSnapcompactFileOperations` → `upsertFileOperations`. Types: `SnapcompactShape` → `Shape`, `SnapcompactFrame` → `Frame`, `SnapcompactArchive` → `Archive`, `SnapcompactGeometry` → `Geometry`, `SnapcompactOptions` → `Options`, `SnapcompactSerializeOptions` → `SerializeOptions`, `SnapcompactFileOperations` → `FileOperations`, `SnapcompactCompactionDetails`/`Preparation`/`Result` → `CompactionDetails`/`CompactionPreparation`/`CompactionResult`, `SnapcompactConvertToLlm` → `ConvertToLlm`. Constants: `SNAPCOMPACT_X` → `X` (`SHAPES`, `FRAME_SIZE`, `MAX_FRAMES`, `FRAME_TOKEN_ESTIMATE`, `PRESERVE_KEY`, `TOOL_RESULT_MAX_CHARS`, `TOOL_ARG_MAX_CHARS`, `TOOL_CALL_MAX_CHARS`, `TRUNCATE_HEAD_RATIO`, `DIM_ON`, `DIM_OFF`).
10
+
11
+ ### Added
12
+
13
+ - Added `renderMany()` for paging arbitrary text into snapcompact PNG frames as LLM image blocks, and `frames()` for predicting the frame count without rendering
14
+
5
15
  ## [15.11.0] - 2026-06-10
6
16
 
7
17
  ### Breaking Changes
@@ -32,7 +32,7 @@
32
32
  */
33
33
  import type { Api, ImageContent, Message, Model } from "@oh-my-pi/pi-ai";
34
34
  /** One eval-validated frame shape: font, cell, ink, repetition, and size. */
35
- export interface SnapcompactShape {
35
+ export interface Shape {
36
36
  /** Bundled font in the native renderer. */
37
37
  font: "5x8" | "8x8";
38
38
  /** Target cell advance in pixels; differing from the font's natural cell
@@ -53,7 +53,7 @@ export interface SnapcompactShape {
53
53
  imageDetail?: ImageContent["detail"];
54
54
  }
55
55
  /** Eval-validated shapes, keyed by the provider family they won on. */
56
- export declare const SNAPCOMPACT_SHAPES: {
56
+ export declare const SHAPES: {
57
57
  /** `8x8r-bw`: unscii square, black ink, lines doubled on highlight bands. */
58
58
  readonly anthropic: {
59
59
  readonly font: "8x8";
@@ -98,23 +98,23 @@ export declare const SNAPCOMPACT_SHAPES: {
98
98
  };
99
99
  };
100
100
  /** Runtime guard for shape overrides loaded from config or preserve data. */
101
- export declare function isSnapcompactShape(value: unknown): value is SnapcompactShape;
101
+ export declare function isShape(value: unknown): value is Shape;
102
102
  /** Pick the eval-optimal frame shape for a provider API. */
103
- export declare function resolveSnapcompactShape(api?: Api): SnapcompactShape;
103
+ export declare function resolveShape(api?: Api): Shape;
104
104
  /** Legacy frame edge in pixels (the 5x8 shape's eval-validated size). New
105
105
  * shapes carry their own `frameSize`. */
106
- export declare const SNAPCOMPACT_FRAME_SIZE = 2576;
106
+ export declare const FRAME_SIZE = 2576;
107
107
  /** Maximum frames carried on a compaction entry. Oldest frames are dropped
108
108
  * first once the budget is exceeded (mirrors how iterative text summaries
109
109
  * fade the oldest detail). */
110
- export declare const SNAPCOMPACT_MAX_FRAMES = 8;
110
+ export declare const MAX_FRAMES = 8;
111
111
  /** Conservative per-frame token estimate used for context budgeting
112
112
  * (upper bound across shapes: Anthropic bills 1568*1568/750 ≈ 3,278). */
113
- export declare const SNAPCOMPACT_FRAME_TOKEN_ESTIMATE = 3300;
113
+ export declare const FRAME_TOKEN_ESTIMATE = 3300;
114
114
  /** Key under `CompactionEntry.preserveData` holding the frame archive. */
115
- export declare const SNAPCOMPACT_PRESERVE_KEY = "snapcompact";
115
+ export declare const PRESERVE_KEY = "snapcompact";
116
116
  /** One developed snapcompact frame: a base64 PNG plus its reading geometry. */
117
- export interface SnapcompactFrame {
117
+ export interface Frame {
118
118
  /** Base64-encoded PNG. */
119
119
  data: string;
120
120
  mimeType: string;
@@ -125,37 +125,37 @@ export interface SnapcompactFrame {
125
125
  /** Characters actually printed onto this frame. */
126
126
  chars: number;
127
127
  /** Shape metadata (absent on legacy frames, which are 5x8 `sent`). */
128
- font?: SnapcompactShape["font"];
129
- variant?: SnapcompactShape["variant"];
128
+ font?: Shape["font"];
129
+ variant?: Shape["variant"];
130
130
  lineRepeat?: number;
131
131
  /** Resolution hint forwarded to the provider when re-attaching. */
132
132
  detail?: ImageContent["detail"];
133
133
  }
134
- /** Frame archive persisted under `preserveData[SNAPCOMPACT_PRESERVE_KEY]`. */
135
- export interface SnapcompactArchive {
134
+ /** Frame archive persisted under `preserveData[PRESERVE_KEY]`. */
135
+ export interface Archive {
136
136
  /** Frames ordered oldest to newest. */
137
- frames: SnapcompactFrame[];
137
+ frames: Frame[];
138
138
  /** Characters currently readable across all frames. */
139
139
  totalChars: number;
140
140
  /** Characters dropped so far to respect the frame budget. */
141
141
  truncatedChars: number;
142
142
  }
143
- export interface SnapcompactGeometry {
143
+ export interface Geometry {
144
144
  cols: number;
145
145
  rows: number;
146
146
  /** Characters that fit one frame (cols * rows). */
147
147
  capacity: number;
148
148
  }
149
- export interface SnapcompactOptions<TMessage = Message> extends SnapcompactSerializeOptions {
149
+ export interface Options<TMessage = Message> extends SerializeOptions {
150
150
  /** App-level message transformer (same contract as agent-core's `SummaryOptions.convertToLlm`). */
151
- convertToLlm?: SnapcompactConvertToLlm<TMessage>;
151
+ convertToLlm?: ConvertToLlm<TMessage>;
152
152
  /** Model whose provider API selects the frame shape. */
153
153
  model?: Pick<Model, "api">;
154
154
  /** Explicit shape override; wins over `model`. */
155
- shape?: SnapcompactShape;
155
+ shape?: Shape;
156
156
  /** Frame edge in pixels. Defaults to the shape's `frameSize`. */
157
157
  frameSize?: number;
158
- /** Frame budget. Defaults to {@link SNAPCOMPACT_MAX_FRAMES}. */
158
+ /** Frame budget. Defaults to {@link MAX_FRAMES}. */
159
159
  maxFrames?: number;
160
160
  }
161
161
  /** Result of rendering one frame. */
@@ -167,16 +167,16 @@ export interface RenderedFrame {
167
167
  /** Characters printed (ink toggles excluded; input may be shorter than capacity). */
168
168
  chars: number;
169
169
  }
170
- export interface SnapcompactFileOperations {
170
+ export interface FileOperations {
171
171
  read: Set<string>;
172
172
  written: Set<string>;
173
173
  edited: Set<string>;
174
174
  }
175
- export interface SnapcompactCompactionDetails {
175
+ export interface CompactionDetails {
176
176
  readFiles: string[];
177
177
  modifiedFiles: string[];
178
178
  }
179
- export interface SnapcompactCompactionPreparation<TMessage = Message> {
179
+ export interface CompactionPreparation<TMessage = Message> {
180
180
  /** UUID of first entry to keep. */
181
181
  firstKeptEntryId: string;
182
182
  /** Messages that will be archived and discarded. */
@@ -189,9 +189,9 @@ export interface SnapcompactCompactionPreparation<TMessage = Message> {
189
189
  /** Preserved opaque compaction payload from the previous compaction, if any. */
190
190
  previousPreserveData?: Record<string, unknown>;
191
191
  /** File operations extracted by the host agent. */
192
- fileOps: SnapcompactFileOperations;
192
+ fileOps: FileOperations;
193
193
  }
194
- export interface SnapcompactCompactionResult<T = SnapcompactCompactionDetails> {
194
+ export interface CompactionResult<T = CompactionDetails> {
195
195
  summary: string;
196
196
  shortSummary?: string;
197
197
  firstKeptEntryId: string;
@@ -199,54 +199,73 @@ export interface SnapcompactCompactionResult<T = SnapcompactCompactionDetails> {
199
199
  details?: T;
200
200
  preserveData?: Record<string, unknown>;
201
201
  }
202
- export type SnapcompactConvertToLlm<TMessage = Message> = (messages: TMessage[]) => Message[];
203
- export declare function createSnapcompactFileOps(): SnapcompactFileOperations;
204
- export declare function computeSnapcompactFileLists(fileOps: SnapcompactFileOperations): SnapcompactCompactionDetails;
205
- export declare function upsertSnapcompactFileOperations(summary: string, readFiles: string[], modifiedFiles: string[], readSet?: ReadonlySet<string>): string;
202
+ export type ConvertToLlm<TMessage = Message> = (messages: TMessage[]) => Message[];
203
+ export declare function createFileOps(): FileOperations;
204
+ export declare function computeFileLists(fileOps: FileOperations): CompactionDetails;
205
+ export declare function upsertFileOperations(summary: string, readFiles: string[], modifiedFiles: string[], readSet?: ReadonlySet<string>): string;
206
206
  /** Default per-tool-result character cap in serialized history. */
207
- export declare const SNAPCOMPACT_TOOL_RESULT_MAX_CHARS = 2000;
207
+ export declare const TOOL_RESULT_MAX_CHARS = 2000;
208
208
  /** Default per-argument-value character cap inside serialized tool calls
209
209
  * (write/edit bodies otherwise dump whole files into the archive). */
210
- export declare const SNAPCOMPACT_TOOL_ARG_MAX_CHARS = 500;
210
+ export declare const TOOL_ARG_MAX_CHARS = 500;
211
211
  /** Default character cap across one tool call's full serialized argument list. */
212
- export declare const SNAPCOMPACT_TOOL_CALL_MAX_CHARS = 2000;
212
+ export declare const TOOL_CALL_MAX_CHARS = 2000;
213
213
  /** Default fraction of a truncation budget spent on the head; the remainder
214
214
  * keeps the tail, where command errors and test failures usually land. */
215
- export declare const SNAPCOMPACT_TRUNCATE_HEAD_RATIO = 0.6;
215
+ export declare const TRUNCATE_HEAD_RATIO = 0.6;
216
216
  /** Zero-width ink toggles understood by the native renderer (shift-out/in):
217
217
  * text between them prints in dim gray ink without occupying a cell. */
218
- export declare const SNAPCOMPACT_DIM_ON = "\u000E";
219
- export declare const SNAPCOMPACT_DIM_OFF = "\u000F";
218
+ export declare const DIM_ON = "\u000E";
219
+ export declare const DIM_OFF = "\u000F";
220
220
  /** Character budgets applied while serializing discarded history for frame
221
221
  * rendering. Pass `Infinity` to disable an individual cap. */
222
- export interface SnapcompactSerializeOptions {
223
- /** Per-tool-result cap. Defaults to {@link SNAPCOMPACT_TOOL_RESULT_MAX_CHARS}. */
222
+ export interface SerializeOptions {
223
+ /** Per-tool-result cap. Defaults to {@link TOOL_RESULT_MAX_CHARS}. */
224
224
  toolResultMaxChars?: number;
225
- /** Per-argument-value cap. Defaults to {@link SNAPCOMPACT_TOOL_ARG_MAX_CHARS}. */
225
+ /** Per-argument-value cap. Defaults to {@link TOOL_ARG_MAX_CHARS}. */
226
226
  toolArgMaxChars?: number;
227
- /** Whole-argument-list cap per call. Defaults to {@link SNAPCOMPACT_TOOL_CALL_MAX_CHARS}. */
227
+ /** Whole-argument-list cap per call. Defaults to {@link TOOL_CALL_MAX_CHARS}. */
228
228
  toolCallMaxChars?: number;
229
- /** Head share of each budget, clamped to [0, 1]. Defaults to {@link SNAPCOMPACT_TRUNCATE_HEAD_RATIO}. */
229
+ /** Head share of each budget, clamped to [0, 1]. Defaults to {@link TRUNCATE_HEAD_RATIO}. */
230
230
  truncateHeadRatio?: number;
231
231
  /** Print tool-result text in dim gray ink so archived conversation reads
232
232
  * louder than archived tool noise. Defaults to `true`. */
233
233
  dimToolResults?: boolean;
234
234
  }
235
- export declare function serializeSnapcompactConversation(messages: Message[], options?: SnapcompactSerializeOptions): string;
235
+ export declare function serializeConversation(messages: Message[], options?: SerializeOptions): string;
236
236
  /**
237
237
  * Prepare text for printing: collapse whitespace runs (incl. newlines) to
238
238
  * single spaces — the eval's "paragraph breaks collapsed to spaces" format —
239
239
  * then fold everything outside the fonts' ASCII + Latin-1 coverage to ASCII
240
240
  * approximations (`?` as the last resort).
241
241
  */
242
- export declare function normalizeForSnapcompact(text: string): string;
243
- export declare function snapcompactGeometry(shape: SnapcompactShape, size?: number): SnapcompactGeometry;
242
+ export declare function normalize(text: string): string;
243
+ export declare function geometry(shape: Shape, size?: number): Geometry;
244
244
  /** Render one snapcompact frame from already-normalized text. */
245
- export declare function renderSnapcompactFrame(text: string, shape: SnapcompactShape, size?: number): RenderedFrame;
245
+ export declare function render(text: string, shape: Shape, size?: number): RenderedFrame;
246
+ /** Options for {@link renderMany} and {@link frames}. */
247
+ export interface RenderManyOptions {
248
+ /** Explicit shape; wins over `model`. */
249
+ shape?: Shape;
250
+ /** Model whose `api` selects the eval-optimal shape. */
251
+ model?: Pick<Model, "api">;
252
+ /** Frame edge in px; defaults to the shape's `frameSize`. */
253
+ frameSize?: number;
254
+ /** Hard cap on frames produced; omit for unbounded (caller decides usage). */
255
+ maxFrames?: number;
256
+ }
257
+ /**
258
+ * Render arbitrary text into snapcompact PNG frames as LLM image blocks
259
+ * (first page first). Synchronous: safe to call from per-request transforms.
260
+ * Empty/whitespace-only input yields no frames.
261
+ */
262
+ export declare function renderMany(text: string, options?: RenderManyOptions): ImageContent[];
263
+ /** Frames needed to hold `text` at the given shape/size, without rendering. */
264
+ export declare function frames(text: string, options?: Pick<RenderManyOptions, "shape" | "model" | "frameSize">): number;
246
265
  /** Validate and extract a persisted frame archive from `preserveData`. */
247
- export declare function getPreservedSnapcompactArchive(preserveData: Record<string, unknown> | undefined): SnapcompactArchive | undefined;
266
+ export declare function getPreservedArchive(preserveData: Record<string, unknown> | undefined): Archive | undefined;
248
267
  /** Convert archive frames into LLM image blocks (oldest first). */
249
- export declare function snapcompactImages(archive: SnapcompactArchive): ImageContent[];
268
+ export declare function images(archive: Archive): ImageContent[];
250
269
  /**
251
270
  * Run a snapcompact compaction over prepared messages. Fully local: serializes
252
271
  * the discarded history, prints it onto PNG frames in the provider-optimal
@@ -262,4 +281,4 @@ export declare function snapcompactImages(archive: SnapcompactArchive): ImageCon
262
281
  * If the previous compaction was text-based, its summary is printed at the
263
282
  * head of the frame archive as `[Summary of earlier history]` so no continuity is lost.
264
283
  */
265
- export declare function snapcompactCompact<TMessage = Message>(preparation: SnapcompactCompactionPreparation<TMessage>, options?: SnapcompactOptions<TMessage>): Promise<SnapcompactCompactionResult>;
284
+ export declare function compact<TMessage = Message>(preparation: CompactionPreparation<TMessage>, options?: Options<TMessage>): Promise<CompactionResult>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/snapcompact",
4
- "version": "15.11.2",
4
+ "version": "15.11.4",
5
5
  "description": "Bitmap-frame context compression for vision-capable LLMs",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -31,9 +31,9 @@
31
31
  "fmt": "biome format --write ."
32
32
  },
33
33
  "dependencies": {
34
- "@oh-my-pi/pi-ai": "15.11.2",
35
- "@oh-my-pi/pi-natives": "15.11.2",
36
- "@oh-my-pi/pi-utils": "15.11.2"
34
+ "@oh-my-pi/pi-ai": "15.11.4",
35
+ "@oh-my-pi/pi-natives": "15.11.4",
36
+ "@oh-my-pi/pi-utils": "15.11.4"
37
37
  },
38
38
  "devDependencies": {
39
39
  "@types/bun": "^1.3.14"
@@ -42,7 +42,7 @@ import snapcompactSummaryPrompt from "./prompts/snapcompact-summary.md" with { t
42
42
  // ============================================================================
43
43
 
44
44
  /** One eval-validated frame shape: font, cell, ink, repetition, and size. */
45
- export interface SnapcompactShape {
45
+ export interface Shape {
46
46
  /** Bundled font in the native renderer. */
47
47
  font: "5x8" | "8x8";
48
48
  /** Target cell advance in pixels; differing from the font's natural cell
@@ -64,7 +64,7 @@ export interface SnapcompactShape {
64
64
  }
65
65
 
66
66
  /** Eval-validated shapes, keyed by the provider family they won on. */
67
- export const SNAPCOMPACT_SHAPES = {
67
+ export const SHAPES = {
68
68
  /** `8x8r-bw`: unscii square, black ink, lines doubled on highlight bands. */
69
69
  anthropic: {
70
70
  font: "8x8",
@@ -107,10 +107,10 @@ export const SNAPCOMPACT_SHAPES = {
107
107
  frameSize: 2576,
108
108
  frameTokenEstimate: 3300,
109
109
  },
110
- } as const satisfies Record<string, SnapcompactShape>;
110
+ } as const satisfies Record<string, Shape>;
111
111
 
112
112
  /** Runtime guard for shape overrides loaded from config or preserve data. */
113
- export function isSnapcompactShape(value: unknown): value is SnapcompactShape {
113
+ export function isShape(value: unknown): value is Shape {
114
114
  if (!value || typeof value !== "object") return false;
115
115
  const shape = value as Record<string, unknown>;
116
116
  const font = shape.font;
@@ -134,21 +134,21 @@ export function isSnapcompactShape(value: unknown): value is SnapcompactShape {
134
134
  }
135
135
 
136
136
  /** Pick the eval-optimal frame shape for a provider API. */
137
- export function resolveSnapcompactShape(api?: Api): SnapcompactShape {
137
+ export function resolveShape(api?: Api): Shape {
138
138
  switch (api) {
139
139
  case "openai-completions":
140
140
  case "openai-responses":
141
141
  case "openai-codex-responses":
142
142
  case "azure-openai-responses":
143
- return SNAPCOMPACT_SHAPES.openaiDense;
143
+ return SHAPES.openaiDense;
144
144
  case "google-generative-ai":
145
145
  case "google-gemini-cli":
146
146
  case "google-vertex":
147
- return SNAPCOMPACT_SHAPES.google;
147
+ return SHAPES.google;
148
148
  default:
149
149
  // anthropic-messages, bedrock-converse-stream, and anything unknown:
150
150
  // the plain repeated grid is the most refusal-robust reader shape.
151
- return SNAPCOMPACT_SHAPES.anthropic;
151
+ return SHAPES.anthropic;
152
152
  }
153
153
  }
154
154
 
@@ -158,26 +158,26 @@ export function resolveSnapcompactShape(api?: Api): SnapcompactShape {
158
158
 
159
159
  /** Legacy frame edge in pixels (the 5x8 shape's eval-validated size). New
160
160
  * shapes carry their own `frameSize`. */
161
- export const SNAPCOMPACT_FRAME_SIZE = 2576;
161
+ export const FRAME_SIZE = 2576;
162
162
 
163
163
  /** Maximum frames carried on a compaction entry. Oldest frames are dropped
164
164
  * first once the budget is exceeded (mirrors how iterative text summaries
165
165
  * fade the oldest detail). */
166
- export const SNAPCOMPACT_MAX_FRAMES = 8;
166
+ export const MAX_FRAMES = 8;
167
167
 
168
168
  /** Conservative per-frame token estimate used for context budgeting
169
169
  * (upper bound across shapes: Anthropic bills 1568*1568/750 ≈ 3,278). */
170
- export const SNAPCOMPACT_FRAME_TOKEN_ESTIMATE = 3300;
170
+ export const FRAME_TOKEN_ESTIMATE = 3300;
171
171
 
172
172
  /** Key under `CompactionEntry.preserveData` holding the frame archive. */
173
- export const SNAPCOMPACT_PRESERVE_KEY = "snapcompact";
173
+ export const PRESERVE_KEY = "snapcompact";
174
174
 
175
175
  // ============================================================================
176
176
  // Types
177
177
  // ============================================================================
178
178
 
179
179
  /** One developed snapcompact frame: a base64 PNG plus its reading geometry. */
180
- export interface SnapcompactFrame {
180
+ export interface Frame {
181
181
  /** Base64-encoded PNG. */
182
182
  data: string;
183
183
  mimeType: string;
@@ -188,40 +188,40 @@ export interface SnapcompactFrame {
188
188
  /** Characters actually printed onto this frame. */
189
189
  chars: number;
190
190
  /** Shape metadata (absent on legacy frames, which are 5x8 `sent`). */
191
- font?: SnapcompactShape["font"];
192
- variant?: SnapcompactShape["variant"];
191
+ font?: Shape["font"];
192
+ variant?: Shape["variant"];
193
193
  lineRepeat?: number;
194
194
  /** Resolution hint forwarded to the provider when re-attaching. */
195
195
  detail?: ImageContent["detail"];
196
196
  }
197
197
 
198
- /** Frame archive persisted under `preserveData[SNAPCOMPACT_PRESERVE_KEY]`. */
199
- export interface SnapcompactArchive {
198
+ /** Frame archive persisted under `preserveData[PRESERVE_KEY]`. */
199
+ export interface Archive {
200
200
  /** Frames ordered oldest to newest. */
201
- frames: SnapcompactFrame[];
201
+ frames: Frame[];
202
202
  /** Characters currently readable across all frames. */
203
203
  totalChars: number;
204
204
  /** Characters dropped so far to respect the frame budget. */
205
205
  truncatedChars: number;
206
206
  }
207
207
 
208
- export interface SnapcompactGeometry {
208
+ export interface Geometry {
209
209
  cols: number;
210
210
  rows: number;
211
211
  /** Characters that fit one frame (cols * rows). */
212
212
  capacity: number;
213
213
  }
214
214
 
215
- export interface SnapcompactOptions<TMessage = Message> extends SnapcompactSerializeOptions {
215
+ export interface Options<TMessage = Message> extends SerializeOptions {
216
216
  /** App-level message transformer (same contract as agent-core's `SummaryOptions.convertToLlm`). */
217
- convertToLlm?: SnapcompactConvertToLlm<TMessage>;
217
+ convertToLlm?: ConvertToLlm<TMessage>;
218
218
  /** Model whose provider API selects the frame shape. */
219
219
  model?: Pick<Model, "api">;
220
220
  /** Explicit shape override; wins over `model`. */
221
- shape?: SnapcompactShape;
221
+ shape?: Shape;
222
222
  /** Frame edge in pixels. Defaults to the shape's `frameSize`. */
223
223
  frameSize?: number;
224
- /** Frame budget. Defaults to {@link SNAPCOMPACT_MAX_FRAMES}. */
224
+ /** Frame budget. Defaults to {@link MAX_FRAMES}. */
225
225
  maxFrames?: number;
226
226
  }
227
227
 
@@ -239,18 +239,18 @@ export interface RenderedFrame {
239
239
  // Compaction data contracts
240
240
  // ============================================================================
241
241
 
242
- export interface SnapcompactFileOperations {
242
+ export interface FileOperations {
243
243
  read: Set<string>;
244
244
  written: Set<string>;
245
245
  edited: Set<string>;
246
246
  }
247
247
 
248
- export interface SnapcompactCompactionDetails {
248
+ export interface CompactionDetails {
249
249
  readFiles: string[];
250
250
  modifiedFiles: string[];
251
251
  }
252
252
 
253
- export interface SnapcompactCompactionPreparation<TMessage = Message> {
253
+ export interface CompactionPreparation<TMessage = Message> {
254
254
  /** UUID of first entry to keep. */
255
255
  firstKeptEntryId: string;
256
256
  /** Messages that will be archived and discarded. */
@@ -263,10 +263,10 @@ export interface SnapcompactCompactionPreparation<TMessage = Message> {
263
263
  /** Preserved opaque compaction payload from the previous compaction, if any. */
264
264
  previousPreserveData?: Record<string, unknown>;
265
265
  /** File operations extracted by the host agent. */
266
- fileOps: SnapcompactFileOperations;
266
+ fileOps: FileOperations;
267
267
  }
268
268
 
269
- export interface SnapcompactCompactionResult<T = SnapcompactCompactionDetails> {
269
+ export interface CompactionResult<T = CompactionDetails> {
270
270
  summary: string;
271
271
  shortSummary?: string;
272
272
  firstKeptEntryId: string;
@@ -275,7 +275,7 @@ export interface SnapcompactCompactionResult<T = SnapcompactCompactionDetails> {
275
275
  preserveData?: Record<string, unknown>;
276
276
  }
277
277
 
278
- export type SnapcompactConvertToLlm<TMessage = Message> = (messages: TMessage[]) => Message[];
278
+ export type ConvertToLlm<TMessage = Message> = (messages: TMessage[]) => Message[];
279
279
 
280
280
  function defaultConvertToLlm<TMessage>(messages: TMessage[]): Message[] {
281
281
  return messages as unknown as Message[];
@@ -285,7 +285,7 @@ function defaultConvertToLlm<TMessage>(messages: TMessage[]): Message[] {
285
285
  // File operation helpers
286
286
  // ============================================================================
287
287
 
288
- export function createSnapcompactFileOps(): SnapcompactFileOperations {
288
+ export function createFileOps(): FileOperations {
289
289
  return {
290
290
  read: new Set(),
291
291
  written: new Set(),
@@ -293,7 +293,7 @@ export function createSnapcompactFileOps(): SnapcompactFileOperations {
293
293
  };
294
294
  }
295
295
 
296
- export function computeSnapcompactFileLists(fileOps: SnapcompactFileOperations): SnapcompactCompactionDetails {
296
+ export function computeFileLists(fileOps: FileOperations): CompactionDetails {
297
297
  const modified = new Set([...fileOps.edited, ...fileOps.written]);
298
298
  const readFiles = [...fileOps.read].filter(file => !modified.has(file)).sort();
299
299
  const modifiedFiles = [...modified].sort();
@@ -331,7 +331,7 @@ function formatFileOperations(readFiles: string[], modifiedFiles: string[], read
331
331
  return prompt.render(fileOperationsTemplate, { files });
332
332
  }
333
333
 
334
- export function upsertSnapcompactFileOperations(
334
+ export function upsertFileOperations(
335
335
  summary: string,
336
336
  readFiles: string[],
337
337
  modifiedFiles: string[],
@@ -349,34 +349,34 @@ export function upsertSnapcompactFileOperations(
349
349
  // ============================================================================
350
350
 
351
351
  /** Default per-tool-result character cap in serialized history. */
352
- export const SNAPCOMPACT_TOOL_RESULT_MAX_CHARS = 2000;
352
+ export const TOOL_RESULT_MAX_CHARS = 2000;
353
353
 
354
354
  /** Default per-argument-value character cap inside serialized tool calls
355
355
  * (write/edit bodies otherwise dump whole files into the archive). */
356
- export const SNAPCOMPACT_TOOL_ARG_MAX_CHARS = 500;
356
+ export const TOOL_ARG_MAX_CHARS = 500;
357
357
 
358
358
  /** Default character cap across one tool call's full serialized argument list. */
359
- export const SNAPCOMPACT_TOOL_CALL_MAX_CHARS = 2000;
359
+ export const TOOL_CALL_MAX_CHARS = 2000;
360
360
 
361
361
  /** Default fraction of a truncation budget spent on the head; the remainder
362
362
  * keeps the tail, where command errors and test failures usually land. */
363
- export const SNAPCOMPACT_TRUNCATE_HEAD_RATIO = 0.6;
363
+ export const TRUNCATE_HEAD_RATIO = 0.6;
364
364
 
365
365
  /** Zero-width ink toggles understood by the native renderer (shift-out/in):
366
366
  * text between them prints in dim gray ink without occupying a cell. */
367
- export const SNAPCOMPACT_DIM_ON = "\u000e";
368
- export const SNAPCOMPACT_DIM_OFF = "\u000f";
367
+ export const DIM_ON = "\u000e";
368
+ export const DIM_OFF = "\u000f";
369
369
 
370
370
  /** Character budgets applied while serializing discarded history for frame
371
371
  * rendering. Pass `Infinity` to disable an individual cap. */
372
- export interface SnapcompactSerializeOptions {
373
- /** Per-tool-result cap. Defaults to {@link SNAPCOMPACT_TOOL_RESULT_MAX_CHARS}. */
372
+ export interface SerializeOptions {
373
+ /** Per-tool-result cap. Defaults to {@link TOOL_RESULT_MAX_CHARS}. */
374
374
  toolResultMaxChars?: number;
375
- /** Per-argument-value cap. Defaults to {@link SNAPCOMPACT_TOOL_ARG_MAX_CHARS}. */
375
+ /** Per-argument-value cap. Defaults to {@link TOOL_ARG_MAX_CHARS}. */
376
376
  toolArgMaxChars?: number;
377
- /** Whole-argument-list cap per call. Defaults to {@link SNAPCOMPACT_TOOL_CALL_MAX_CHARS}. */
377
+ /** Whole-argument-list cap per call. Defaults to {@link TOOL_CALL_MAX_CHARS}. */
378
378
  toolCallMaxChars?: number;
379
- /** Head share of each budget, clamped to [0, 1]. Defaults to {@link SNAPCOMPACT_TRUNCATE_HEAD_RATIO}. */
379
+ /** Head share of each budget, clamped to [0, 1]. Defaults to {@link TRUNCATE_HEAD_RATIO}. */
380
380
  truncateHeadRatio?: number;
381
381
  /** Print tool-result text in dim gray ink so archived conversation reads
382
382
  * louder than archived tool noise. Defaults to `true`. */
@@ -401,11 +401,11 @@ function stripDimMarkers(text: string): string {
401
401
  return text.replace(DIM_MARKERS, "");
402
402
  }
403
403
 
404
- export function serializeSnapcompactConversation(messages: Message[], options?: SnapcompactSerializeOptions): string {
405
- const toolResultMaxChars = options?.toolResultMaxChars ?? SNAPCOMPACT_TOOL_RESULT_MAX_CHARS;
406
- const toolArgMaxChars = options?.toolArgMaxChars ?? SNAPCOMPACT_TOOL_ARG_MAX_CHARS;
407
- const toolCallMaxChars = options?.toolCallMaxChars ?? SNAPCOMPACT_TOOL_CALL_MAX_CHARS;
408
- const headRatio = options?.truncateHeadRatio ?? SNAPCOMPACT_TRUNCATE_HEAD_RATIO;
404
+ export function serializeConversation(messages: Message[], options?: SerializeOptions): string {
405
+ const toolResultMaxChars = options?.toolResultMaxChars ?? TOOL_RESULT_MAX_CHARS;
406
+ const toolArgMaxChars = options?.toolArgMaxChars ?? TOOL_ARG_MAX_CHARS;
407
+ const toolCallMaxChars = options?.toolCallMaxChars ?? TOOL_CALL_MAX_CHARS;
408
+ const headRatio = options?.truncateHeadRatio ?? TRUNCATE_HEAD_RATIO;
409
409
  const dimToolResults = options?.dimToolResults !== false;
410
410
  const parts: string[] = [];
411
411
 
@@ -462,11 +462,7 @@ export function serializeSnapcompactConversation(messages: Message[], options?:
462
462
  if (content) {
463
463
  // Args above are JSON-escaped, so only raw result text can carry toggles.
464
464
  const body = truncateForSummary(stripDimMarkers(content), toolResultMaxChars, headRatio);
465
- parts.push(
466
- dimToolResults
467
- ? `[Tool result]: ${SNAPCOMPACT_DIM_ON}${body}${SNAPCOMPACT_DIM_OFF}`
468
- : `[Tool result]: ${body}`,
469
- );
465
+ parts.push(dimToolResults ? `[Tool result]: ${DIM_ON}${body}${DIM_OFF}` : `[Tool result]: ${body}`);
470
466
  }
471
467
  }
472
468
  }
@@ -527,7 +523,7 @@ const CHAR_FOLD: Record<string, string> = {
527
523
  * then fold everything outside the fonts' ASCII + Latin-1 coverage to ASCII
528
524
  * approximations (`?` as the last resort).
529
525
  */
530
- export function normalizeForSnapcompact(text: string): string {
526
+ export function normalize(text: string): string {
531
527
  const collapsed = text.replace(/\s+/g, " ").trim();
532
528
  let out = "";
533
529
  for (const ch of collapsed) {
@@ -553,19 +549,15 @@ export function normalizeForSnapcompact(text: string): string {
553
549
  // Rendering
554
550
  // ============================================================================
555
551
 
556
- export function snapcompactGeometry(shape: SnapcompactShape, size: number = shape.frameSize): SnapcompactGeometry {
552
+ export function geometry(shape: Shape, size: number = shape.frameSize): Geometry {
557
553
  const cols = Math.floor(size / shape.cellWidth);
558
554
  const rows = Math.floor(size / shape.cellHeight / shape.lineRepeat);
559
555
  return { cols, rows, capacity: cols * rows };
560
556
  }
561
557
 
562
558
  /** Render one snapcompact frame from already-normalized text. */
563
- export function renderSnapcompactFrame(
564
- text: string,
565
- shape: SnapcompactShape,
566
- size: number = shape.frameSize,
567
- ): RenderedFrame {
568
- const { cols, rows, capacity } = snapcompactGeometry(shape, size);
559
+ export function render(text: string, shape: Shape, size: number = shape.frameSize): RenderedFrame {
560
+ const { cols, rows, capacity } = geometry(shape, size);
569
561
  const visible = text.length - (text.match(DIM_MARKERS)?.length ?? 0);
570
562
  const chars = Math.min(visible, capacity);
571
563
  const data = renderSnapcompactPng(text, {
@@ -579,17 +571,58 @@ export function renderSnapcompactFrame(
579
571
  return { data, cols, rows, chars };
580
572
  }
581
573
 
574
+ /** Options for {@link renderMany} and {@link frames}. */
575
+ export interface RenderManyOptions {
576
+ /** Explicit shape; wins over `model`. */
577
+ shape?: Shape;
578
+ /** Model whose `api` selects the eval-optimal shape. */
579
+ model?: Pick<Model, "api">;
580
+ /** Frame edge in px; defaults to the shape's `frameSize`. */
581
+ frameSize?: number;
582
+ /** Hard cap on frames produced; omit for unbounded (caller decides usage). */
583
+ maxFrames?: number;
584
+ }
585
+
586
+ /**
587
+ * Render arbitrary text into snapcompact PNG frames as LLM image blocks
588
+ * (first page first). Synchronous: safe to call from per-request transforms.
589
+ * Empty/whitespace-only input yields no frames.
590
+ */
591
+ export function renderMany(text: string, options?: RenderManyOptions): ImageContent[] {
592
+ const shape = options?.shape ?? resolveShape(options?.model?.api);
593
+ const frameSize = options?.frameSize ?? shape.frameSize;
594
+ const geo = geometry(shape, frameSize);
595
+ const normalized = normalize(text);
596
+ const frames: ImageContent[] = [];
597
+ for (let offset = 0; offset < normalized.length; offset += geo.capacity) {
598
+ if (options?.maxFrames !== undefined && frames.length >= options.maxFrames) break;
599
+ const rendered = render(normalized.slice(offset, offset + geo.capacity), shape, frameSize);
600
+ frames.push({
601
+ type: "image",
602
+ data: rendered.data,
603
+ mimeType: "image/png",
604
+ ...(shape.imageDetail ? { detail: shape.imageDetail } : {}),
605
+ });
606
+ }
607
+ return frames;
608
+ }
609
+
610
+ /** Frames needed to hold `text` at the given shape/size, without rendering. */
611
+ export function frames(text: string, options?: Pick<RenderManyOptions, "shape" | "model" | "frameSize">): number {
612
+ const shape = options?.shape ?? resolveShape(options?.model?.api);
613
+ const geo = geometry(shape, options?.frameSize ?? shape.frameSize);
614
+ return Math.ceil(normalize(text).length / geo.capacity);
615
+ }
616
+
582
617
  // ============================================================================
583
618
  // Archive helpers
584
619
  // ============================================================================
585
620
 
586
621
  /** Validate and extract a persisted frame archive from `preserveData`. */
587
- export function getPreservedSnapcompactArchive(
588
- preserveData: Record<string, unknown> | undefined,
589
- ): SnapcompactArchive | undefined {
590
- const candidate = preserveData?.[SNAPCOMPACT_PRESERVE_KEY];
622
+ export function getPreservedArchive(preserveData: Record<string, unknown> | undefined): Archive | undefined {
623
+ const candidate = preserveData?.[PRESERVE_KEY];
591
624
  if (!candidate || typeof candidate !== "object") return undefined;
592
- const archive = candidate as SnapcompactArchive;
625
+ const archive = candidate as Archive;
593
626
  if (!Array.isArray(archive.frames)) return undefined;
594
627
  const frames = archive.frames.filter(
595
628
  frame =>
@@ -610,7 +643,7 @@ export function getPreservedSnapcompactArchive(
610
643
  }
611
644
 
612
645
  /** Convert archive frames into LLM image blocks (oldest first). */
613
- export function snapcompactImages(archive: SnapcompactArchive): ImageContent[] {
646
+ export function images(archive: Archive): ImageContent[] {
614
647
  return archive.frames.map(frame => ({
615
648
  type: "image",
616
649
  data: frame.data,
@@ -638,40 +671,40 @@ export function snapcompactImages(archive: SnapcompactArchive): ImageContent[] {
638
671
  * If the previous compaction was text-based, its summary is printed at the
639
672
  * head of the frame archive as `[Summary of earlier history]` so no continuity is lost.
640
673
  */
641
- export async function snapcompactCompact<TMessage = Message>(
642
- preparation: SnapcompactCompactionPreparation<TMessage>,
643
- options?: SnapcompactOptions<TMessage>,
644
- ): Promise<SnapcompactCompactionResult> {
674
+ export async function compact<TMessage = Message>(
675
+ preparation: CompactionPreparation<TMessage>,
676
+ options?: Options<TMessage>,
677
+ ): Promise<CompactionResult> {
645
678
  const { firstKeptEntryId, tokensBefore, previousSummary, previousPreserveData, fileOps } = preparation;
646
679
  if (!firstKeptEntryId) {
647
680
  throw new Error("First kept entry has no ID - session may need migration");
648
681
  }
649
- const shape = options?.shape ?? resolveSnapcompactShape(options?.model?.api);
682
+ const shape = options?.shape ?? resolveShape(options?.model?.api);
650
683
  const frameSize = options?.frameSize ?? shape.frameSize;
651
- const maxFrames = Math.max(1, options?.maxFrames ?? SNAPCOMPACT_MAX_FRAMES);
652
- const geometry = snapcompactGeometry(shape, frameSize);
684
+ const maxFrames = Math.max(1, options?.maxFrames ?? MAX_FRAMES);
685
+ const geo = geometry(shape, frameSize);
653
686
 
654
687
  const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
655
688
  const llmMessages = (options?.convertToLlm ?? defaultConvertToLlm)(messages);
656
- let archiveText = normalizeForSnapcompact(serializeSnapcompactConversation(llmMessages, options));
689
+ let archiveText = normalize(serializeConversation(llmMessages, options));
657
690
 
658
- const previousArchive = getPreservedSnapcompactArchive(previousPreserveData);
691
+ const previousArchive = getPreservedArchive(previousPreserveData);
659
692
  const includedPreviousSummary = !previousArchive && !!previousSummary;
660
693
  if (includedPreviousSummary && previousSummary) {
661
- const head = `[Summary of earlier history] ${normalizeForSnapcompact(previousSummary)}`;
694
+ const head = `[Summary of earlier history] ${normalize(previousSummary)}`;
662
695
  archiveText = archiveText.length > 0 ? `${head} [Recent conversation] ${archiveText}` : head;
663
696
  }
664
697
 
665
698
  let truncatedChars = previousArchive?.truncatedChars ?? 0;
666
699
 
667
- const newFrames: SnapcompactFrame[] = [];
700
+ const newFrames: Frame[] = [];
668
701
  let dimOpen = false;
669
- for (let offset = 0; offset < archiveText.length; offset += geometry.capacity) {
670
- let chunk = archiveText.slice(offset, offset + geometry.capacity);
702
+ for (let offset = 0; offset < archiveText.length; offset += geo.capacity) {
703
+ let chunk = archiveText.slice(offset, offset + geo.capacity);
671
704
  // Re-open a dim span that the previous frame boundary cut through.
672
- if (dimOpen) chunk = SNAPCOMPACT_DIM_ON + chunk;
673
- dimOpen = chunk.lastIndexOf(SNAPCOMPACT_DIM_ON) > chunk.lastIndexOf(SNAPCOMPACT_DIM_OFF);
674
- const rendered = renderSnapcompactFrame(chunk, shape, frameSize);
705
+ if (dimOpen) chunk = DIM_ON + chunk;
706
+ dimOpen = chunk.lastIndexOf(DIM_ON) > chunk.lastIndexOf(DIM_OFF);
707
+ const rendered = render(chunk, shape, frameSize);
675
708
  newFrames.push({
676
709
  data: rendered.data,
677
710
  mimeType: "image/png",
@@ -702,8 +735,8 @@ export async function snapcompactCompact<TMessage = Message>(
702
735
  const totalChars = frames.reduce((sum, frame) => sum + frame.chars, 0);
703
736
  const mixedShapes = frames.some(
704
737
  frame =>
705
- frame.cols !== geometry.cols ||
706
- frame.rows !== geometry.rows ||
738
+ frame.cols !== geo.cols ||
739
+ frame.rows !== geo.rows ||
707
740
  (frame.variant ?? "sent") !== shape.variant ||
708
741
  (frame.lineRepeat ?? 1) !== shape.lineRepeat,
709
742
  );
@@ -716,8 +749,8 @@ export async function snapcompactCompact<TMessage = Message>(
716
749
  frameCount: frames.length,
717
750
  multipleFrames: frames.length > 1,
718
751
  fontCell: `${shape.cellWidth}x${shape.cellHeight}`,
719
- cols: geometry.cols,
720
- rows: geometry.rows,
752
+ cols: geo.cols,
753
+ rows: geo.rows,
721
754
  sentenceInk: shape.variant === "sent",
722
755
  lineRepeated: shape.lineRepeat > 1,
723
756
  dimmedToolResults: options?.dimToolResults !== false,
@@ -727,13 +760,13 @@ export async function snapcompactCompact<TMessage = Message>(
727
760
  includedPreviousSummary,
728
761
  });
729
762
  }
730
- const { readFiles, modifiedFiles } = computeSnapcompactFileLists(fileOps);
731
- summary = upsertSnapcompactFileOperations(summary, readFiles, modifiedFiles, fileOps.read);
763
+ const { readFiles, modifiedFiles } = computeFileLists(fileOps);
764
+ summary = upsertFileOperations(summary, readFiles, modifiedFiles, fileOps.read);
732
765
 
733
766
  // A snapcompact pass replaces any provider-side replacement history; strip the
734
767
  // OpenAI remote-compaction payload like the default summarizer path does.
735
768
  const basePreserve = stripOpenAiRemoteCompactionPreserveData(previousPreserveData) ?? {};
736
- const archive: SnapcompactArchive = { frames, totalChars, truncatedChars };
769
+ const archive: Archive = { frames, totalChars, truncatedChars };
737
770
 
738
771
  return {
739
772
  summary,
@@ -741,6 +774,6 @@ export async function snapcompactCompact<TMessage = Message>(
741
774
  firstKeptEntryId,
742
775
  tokensBefore,
743
776
  details: { readFiles, modifiedFiles },
744
- preserveData: { ...basePreserve, [SNAPCOMPACT_PRESERVE_KEY]: archive },
777
+ preserveData: { ...basePreserve, [PRESERVE_KEY]: archive },
745
778
  };
746
779
  }