@blockrun/runcode 2.2.7 → 2.4.0

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,258 @@
1
+ /**
2
+ * LLM-Safe Context Compression
3
+ *
4
+ * Reduces token usage by 15-40% while preserving semantic meaning.
5
+ * Implements 7 compression layers inspired by claw-compactor.
6
+ *
7
+ * Usage:
8
+ * const result = await compressContext(messages);
9
+ * // result.messages -> compressed version to send to provider
10
+ * // result.originalMessages -> original for logging
11
+ */
12
+ import { DEFAULT_COMPRESSION_CONFIG, } from "./types.js";
13
+ import { deduplicateMessages } from "./layers/deduplication.js";
14
+ import { normalizeMessagesWhitespace } from "./layers/whitespace.js";
15
+ import { encodeMessages } from "./layers/dictionary.js";
16
+ import { shortenPaths } from "./layers/paths.js";
17
+ import { compactMessagesJson } from "./layers/json-compact.js";
18
+ import { compressObservations } from "./layers/observation.js";
19
+ import { applyDynamicCodebook, generateDynamicCodebookHeader } from "./layers/dynamic-codebook.js";
20
+ import { generateCodebookHeader, STATIC_CODEBOOK } from "./codebook.js";
21
+ export * from "./types.js";
22
+ export { STATIC_CODEBOOK } from "./codebook.js";
23
+ /**
24
+ * Calculate total character count for messages.
25
+ */
26
+ function calculateTotalChars(messages) {
27
+ return messages.reduce((total, msg) => {
28
+ let chars = 0;
29
+ if (Array.isArray(msg.content)) {
30
+ for (const part of msg.content) {
31
+ if (part.type === "text" && part.text)
32
+ chars += part.text.length;
33
+ else if (part.type === "image_url")
34
+ chars += 2500; // ~1000 tokens worth
35
+ }
36
+ }
37
+ else {
38
+ chars = msg.content?.length || 0;
39
+ }
40
+ if (msg.tool_calls) {
41
+ chars += JSON.stringify(msg.tool_calls).length;
42
+ }
43
+ return total + chars;
44
+ }, 0);
45
+ }
46
+ /**
47
+ * Check if any message contains image_url content parts.
48
+ */
49
+ function hasVisionContent(messages) {
50
+ return messages.some((m) => Array.isArray(m.content) && m.content.some((p) => p.type === "image_url"));
51
+ }
52
+ /**
53
+ * Deep clone messages to preserve originals.
54
+ */
55
+ function cloneMessages(messages) {
56
+ return JSON.parse(JSON.stringify(messages));
57
+ }
58
+ /**
59
+ * Prepend codebook header to the first USER message (not system).
60
+ *
61
+ * Why not system message?
62
+ * - Google Gemini uses systemInstruction which doesn't support codebook format
63
+ * - The codebook header in user message is still visible to all LLMs
64
+ * - This ensures compatibility across all providers
65
+ */
66
+ function prependCodebookHeader(messages, usedCodes, pathMap) {
67
+ const header = generateCodebookHeader(usedCodes, pathMap);
68
+ if (!header)
69
+ return messages;
70
+ // Find first user message (not system - Google's systemInstruction doesn't support codebook)
71
+ const userIndex = messages.findIndex((m) => m.role === "user");
72
+ if (userIndex === -1) {
73
+ // No user message, add codebook as system (fallback)
74
+ return [
75
+ { role: "system", content: header },
76
+ ...messages,
77
+ ];
78
+ }
79
+ // Prepend to first user message
80
+ return messages.map((msg, i) => {
81
+ if (i === userIndex) {
82
+ return {
83
+ ...msg,
84
+ content: `${header}\n\n${msg.content || ""}`,
85
+ };
86
+ }
87
+ return msg;
88
+ });
89
+ }
90
+ /**
91
+ * Main compression function.
92
+ *
93
+ * Applies 5 layers in sequence:
94
+ * 1. Deduplication - Remove exact duplicate messages
95
+ * 2. Whitespace - Normalize excessive whitespace
96
+ * 3. Dictionary - Replace common phrases with codes
97
+ * 4. Paths - Shorten repeated file paths
98
+ * 5. JSON - Compact JSON in tool calls
99
+ *
100
+ * Then prepends a codebook header for the LLM to decode in-context.
101
+ */
102
+ export async function compressContext(messages, config = {}) {
103
+ const fullConfig = {
104
+ ...DEFAULT_COMPRESSION_CONFIG,
105
+ ...config,
106
+ layers: {
107
+ ...DEFAULT_COMPRESSION_CONFIG.layers,
108
+ ...config.layers,
109
+ },
110
+ dictionary: {
111
+ ...DEFAULT_COMPRESSION_CONFIG.dictionary,
112
+ ...config.dictionary,
113
+ },
114
+ };
115
+ // If compression disabled, return as-is
116
+ if (!fullConfig.enabled) {
117
+ const originalChars = calculateTotalChars(messages);
118
+ return {
119
+ messages,
120
+ originalMessages: messages,
121
+ originalChars,
122
+ compressedChars: originalChars,
123
+ compressionRatio: 1,
124
+ stats: {
125
+ duplicatesRemoved: 0,
126
+ whitespaceSavedChars: 0,
127
+ dictionarySubstitutions: 0,
128
+ pathsShortened: 0,
129
+ jsonCompactedChars: 0,
130
+ observationsCompressed: 0,
131
+ observationCharsSaved: 0,
132
+ dynamicSubstitutions: 0,
133
+ dynamicCharsSaved: 0,
134
+ },
135
+ codebook: {},
136
+ pathMap: {},
137
+ dynamicCodes: {},
138
+ };
139
+ }
140
+ // Preserve originals for logging
141
+ const originalMessages = fullConfig.preserveRaw
142
+ ? cloneMessages(messages)
143
+ : messages;
144
+ const originalChars = calculateTotalChars(messages);
145
+ // Initialize stats
146
+ const stats = {
147
+ duplicatesRemoved: 0,
148
+ whitespaceSavedChars: 0,
149
+ dictionarySubstitutions: 0,
150
+ pathsShortened: 0,
151
+ jsonCompactedChars: 0,
152
+ observationsCompressed: 0,
153
+ observationCharsSaved: 0,
154
+ dynamicSubstitutions: 0,
155
+ dynamicCharsSaved: 0,
156
+ };
157
+ let result = cloneMessages(messages);
158
+ let usedCodes = new Set();
159
+ let pathMap = {};
160
+ let dynamicCodes = {};
161
+ // Layer 1: Deduplication
162
+ if (fullConfig.layers.deduplication) {
163
+ const dedupResult = deduplicateMessages(result);
164
+ result = dedupResult.messages;
165
+ stats.duplicatesRemoved = dedupResult.duplicatesRemoved;
166
+ }
167
+ // Layer 2: Whitespace normalization
168
+ if (fullConfig.layers.whitespace) {
169
+ const wsResult = normalizeMessagesWhitespace(result);
170
+ result = wsResult.messages;
171
+ stats.whitespaceSavedChars = wsResult.charsSaved;
172
+ }
173
+ // Layer 3: Dictionary encoding
174
+ if (fullConfig.layers.dictionary) {
175
+ const dictResult = encodeMessages(result);
176
+ result = dictResult.messages;
177
+ stats.dictionarySubstitutions = dictResult.substitutionCount;
178
+ usedCodes = dictResult.usedCodes;
179
+ }
180
+ // Layer 4: Path shortening
181
+ if (fullConfig.layers.paths) {
182
+ const pathResult = shortenPaths(result);
183
+ result = pathResult.messages;
184
+ pathMap = pathResult.pathMap;
185
+ stats.pathsShortened = Object.keys(pathMap).length;
186
+ }
187
+ // Layer 5: JSON compaction
188
+ if (fullConfig.layers.jsonCompact) {
189
+ const jsonResult = compactMessagesJson(result);
190
+ result = jsonResult.messages;
191
+ stats.jsonCompactedChars = jsonResult.charsSaved;
192
+ }
193
+ // Layer 6: Observation compression (BIG WIN - 97% on tool results)
194
+ if (fullConfig.layers.observation) {
195
+ const obsResult = compressObservations(result);
196
+ result = obsResult.messages;
197
+ stats.observationsCompressed = obsResult.observationsCompressed;
198
+ stats.observationCharsSaved = obsResult.charsSaved;
199
+ }
200
+ // Layer 7: Dynamic codebook (learns from actual content)
201
+ if (fullConfig.layers.dynamicCodebook) {
202
+ const dynResult = applyDynamicCodebook(result);
203
+ result = dynResult.messages;
204
+ stats.dynamicSubstitutions = dynResult.substitutions;
205
+ stats.dynamicCharsSaved = dynResult.charsSaved;
206
+ dynamicCodes = dynResult.dynamicCodes;
207
+ }
208
+ // Add codebook header if enabled and we have codes to include
209
+ if (fullConfig.dictionary.includeCodebookHeader &&
210
+ (usedCodes.size > 0 || Object.keys(pathMap).length > 0 || Object.keys(dynamicCodes).length > 0)) {
211
+ result = prependCodebookHeader(result, usedCodes, pathMap);
212
+ // Also add dynamic codebook header if we have dynamic codes
213
+ if (Object.keys(dynamicCodes).length > 0) {
214
+ const dynHeader = generateDynamicCodebookHeader(dynamicCodes);
215
+ if (dynHeader) {
216
+ const systemIndex = result.findIndex((m) => m.role === "system");
217
+ if (systemIndex >= 0) {
218
+ result[systemIndex] = {
219
+ ...result[systemIndex],
220
+ content: `${dynHeader}\n${result[systemIndex].content || ""}`,
221
+ };
222
+ }
223
+ }
224
+ }
225
+ }
226
+ // Calculate final stats
227
+ const compressedChars = calculateTotalChars(result);
228
+ const compressionRatio = compressedChars / originalChars;
229
+ // Build used codebook for logging
230
+ const usedCodebook = {};
231
+ usedCodes.forEach((code) => {
232
+ usedCodebook[code] = STATIC_CODEBOOK[code];
233
+ });
234
+ return {
235
+ messages: result,
236
+ originalMessages,
237
+ originalChars,
238
+ compressedChars,
239
+ compressionRatio,
240
+ stats,
241
+ codebook: usedCodebook,
242
+ pathMap,
243
+ dynamicCodes,
244
+ };
245
+ }
246
+ /**
247
+ * Quick check if compression would benefit these messages.
248
+ * Returns true if messages are large enough to warrant compression.
249
+ */
250
+ export function shouldCompress(messages) {
251
+ // Skip compression entirely when messages contain images —
252
+ // compression layers operate on string content and would corrupt image_url parts
253
+ if (hasVisionContent(messages))
254
+ return false;
255
+ const chars = calculateTotalChars(messages);
256
+ // Only compress if > 5000 chars (roughly 1000 tokens)
257
+ return chars > 5000;
258
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Layer 1: Message Deduplication
3
+ *
4
+ * Removes exact duplicate messages from conversation history.
5
+ * Common in heartbeat patterns and repeated tool calls.
6
+ *
7
+ * Safe for LLM: Identical messages add no new information.
8
+ * Expected savings: 2-5%
9
+ */
10
+ import { NormalizedMessage } from "../types.js";
11
+ export interface DeduplicationResult {
12
+ messages: NormalizedMessage[];
13
+ duplicatesRemoved: number;
14
+ originalCount: number;
15
+ }
16
+ /**
17
+ * Remove exact duplicate messages from the conversation.
18
+ *
19
+ * Strategy:
20
+ * - Keep first occurrence of each unique message
21
+ * - Preserve order for semantic coherence
22
+ * - Never dedupe system messages (they set context)
23
+ * - Allow duplicate user messages (user might repeat intentionally)
24
+ * - CRITICAL: Never dedupe assistant messages with tool_calls that are
25
+ * referenced by subsequent tool messages (breaks Anthropic tool_use/tool_result pairing)
26
+ */
27
+ export declare function deduplicateMessages(messages: NormalizedMessage[]): DeduplicationResult;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Layer 1: Message Deduplication
3
+ *
4
+ * Removes exact duplicate messages from conversation history.
5
+ * Common in heartbeat patterns and repeated tool calls.
6
+ *
7
+ * Safe for LLM: Identical messages add no new information.
8
+ * Expected savings: 2-5%
9
+ */
10
+ import { createHash } from 'node:crypto';
11
+ /**
12
+ * Generate a hash for a message based on its semantic content.
13
+ * Uses role + content + tool_call_id to identify duplicates.
14
+ */
15
+ function hashMessage(message) {
16
+ const parts = [
17
+ message.role,
18
+ message.content || "",
19
+ message.tool_call_id || "",
20
+ message.name || "",
21
+ ];
22
+ // Include tool_calls if present
23
+ if (message.tool_calls) {
24
+ parts.push(JSON.stringify(message.tool_calls.map((tc) => ({
25
+ name: tc.function.name,
26
+ args: tc.function.arguments,
27
+ }))));
28
+ }
29
+ const content = parts.join("|");
30
+ return createHash("md5").update(content).digest("hex");
31
+ }
32
+ /**
33
+ * Remove exact duplicate messages from the conversation.
34
+ *
35
+ * Strategy:
36
+ * - Keep first occurrence of each unique message
37
+ * - Preserve order for semantic coherence
38
+ * - Never dedupe system messages (they set context)
39
+ * - Allow duplicate user messages (user might repeat intentionally)
40
+ * - CRITICAL: Never dedupe assistant messages with tool_calls that are
41
+ * referenced by subsequent tool messages (breaks Anthropic tool_use/tool_result pairing)
42
+ */
43
+ export function deduplicateMessages(messages) {
44
+ const seen = new Set();
45
+ const result = [];
46
+ let duplicatesRemoved = 0;
47
+ // First pass: collect all tool_call_ids that are referenced by tool messages
48
+ // These tool_calls MUST be preserved to maintain tool_use/tool_result pairing
49
+ const referencedToolCallIds = new Set();
50
+ for (const message of messages) {
51
+ if (message.role === "tool" && message.tool_call_id) {
52
+ referencedToolCallIds.add(message.tool_call_id);
53
+ }
54
+ }
55
+ for (const message of messages) {
56
+ // Always keep system messages (they set important context)
57
+ if (message.role === "system") {
58
+ result.push(message);
59
+ continue;
60
+ }
61
+ // Always keep user messages (user might repeat intentionally)
62
+ if (message.role === "user") {
63
+ result.push(message);
64
+ continue;
65
+ }
66
+ // Always keep tool messages (they are results of tool calls)
67
+ // Removing them would break the tool_use/tool_result pairing
68
+ if (message.role === "tool") {
69
+ result.push(message);
70
+ continue;
71
+ }
72
+ // For assistant messages with tool_calls, check if any are referenced
73
+ // by subsequent tool messages - if so, we MUST keep this message
74
+ if (message.role === "assistant" && message.tool_calls) {
75
+ const hasReferencedToolCall = message.tool_calls.some((tc) => referencedToolCallIds.has(tc.id));
76
+ if (hasReferencedToolCall) {
77
+ // This assistant message has tool_calls that are referenced - keep it
78
+ result.push(message);
79
+ continue;
80
+ }
81
+ }
82
+ // For other assistant messages, check for duplicates
83
+ const hash = hashMessage(message);
84
+ if (!seen.has(hash)) {
85
+ seen.add(hash);
86
+ result.push(message);
87
+ }
88
+ else {
89
+ duplicatesRemoved++;
90
+ }
91
+ }
92
+ return {
93
+ messages: result,
94
+ duplicatesRemoved,
95
+ originalCount: messages.length,
96
+ };
97
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Layer 3: Dictionary Encoding
3
+ *
4
+ * Replaces frequently repeated long phrases with short codes.
5
+ * Uses a static codebook of common patterns from production logs.
6
+ *
7
+ * Safe for LLM: Reversible substitution with codebook header.
8
+ * Expected savings: 4-8%
9
+ */
10
+ import { NormalizedMessage } from "../types.js";
11
+ export interface DictionaryResult {
12
+ messages: NormalizedMessage[];
13
+ substitutionCount: number;
14
+ usedCodes: Set<string>;
15
+ charsSaved: number;
16
+ }
17
+ /**
18
+ * Apply dictionary encoding to all messages.
19
+ */
20
+ export declare function encodeMessages(messages: NormalizedMessage[]): DictionaryResult;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Layer 3: Dictionary Encoding
3
+ *
4
+ * Replaces frequently repeated long phrases with short codes.
5
+ * Uses a static codebook of common patterns from production logs.
6
+ *
7
+ * Safe for LLM: Reversible substitution with codebook header.
8
+ * Expected savings: 4-8%
9
+ */
10
+ import { getInverseCodebook } from "../codebook.js";
11
+ /**
12
+ * Apply dictionary encoding to a string.
13
+ * Returns the encoded string and stats.
14
+ */
15
+ function encodeContent(content, inverseCodebook) {
16
+ let encoded = content;
17
+ let substitutions = 0;
18
+ let charsSaved = 0;
19
+ const codes = new Set();
20
+ // Sort phrases by length (longest first) to avoid partial matches
21
+ const phrases = Object.keys(inverseCodebook).sort((a, b) => b.length - a.length);
22
+ for (const phrase of phrases) {
23
+ const code = inverseCodebook[phrase];
24
+ const regex = new RegExp(escapeRegex(phrase), "g");
25
+ const matches = encoded.match(regex);
26
+ if (matches && matches.length > 0) {
27
+ encoded = encoded.replace(regex, code);
28
+ substitutions += matches.length;
29
+ charsSaved += matches.length * (phrase.length - code.length);
30
+ codes.add(code);
31
+ }
32
+ }
33
+ return { encoded, substitutions, codes, charsSaved };
34
+ }
35
+ /**
36
+ * Escape special regex characters in a string.
37
+ */
38
+ function escapeRegex(str) {
39
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
40
+ }
41
+ /**
42
+ * Apply dictionary encoding to all messages.
43
+ */
44
+ export function encodeMessages(messages) {
45
+ const inverseCodebook = getInverseCodebook();
46
+ let totalSubstitutions = 0;
47
+ let totalCharsSaved = 0;
48
+ const allUsedCodes = new Set();
49
+ const result = messages.map((message) => {
50
+ if (!message.content || typeof message.content !== "string")
51
+ return message;
52
+ const { encoded, substitutions, codes, charsSaved } = encodeContent(message.content, inverseCodebook);
53
+ totalSubstitutions += substitutions;
54
+ totalCharsSaved += charsSaved;
55
+ codes.forEach((code) => allUsedCodes.add(code));
56
+ return {
57
+ ...message,
58
+ content: encoded,
59
+ };
60
+ });
61
+ return {
62
+ messages: result,
63
+ substitutionCount: totalSubstitutions,
64
+ usedCodes: allUsedCodes,
65
+ charsSaved: totalCharsSaved,
66
+ };
67
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * L7: Dynamic Codebook Builder
3
+ *
4
+ * Inspired by claw-compactor's frequency-based codebook.
5
+ * Builds codebook from actual content being compressed,
6
+ * rather than relying on static patterns.
7
+ *
8
+ * Finds phrases that appear 3+ times and replaces with short codes.
9
+ */
10
+ import { NormalizedMessage } from "../types.js";
11
+ interface DynamicCodebookResult {
12
+ messages: NormalizedMessage[];
13
+ charsSaved: number;
14
+ dynamicCodes: Record<string, string>;
15
+ substitutions: number;
16
+ }
17
+ /**
18
+ * Apply dynamic codebook to messages.
19
+ */
20
+ export declare function applyDynamicCodebook(messages: NormalizedMessage[]): DynamicCodebookResult;
21
+ /**
22
+ * Generate header for dynamic codes (to include in system message).
23
+ */
24
+ export declare function generateDynamicCodebookHeader(codebook: Record<string, string>): string;
25
+ export {};
@@ -0,0 +1,145 @@
1
+ /**
2
+ * L7: Dynamic Codebook Builder
3
+ *
4
+ * Inspired by claw-compactor's frequency-based codebook.
5
+ * Builds codebook from actual content being compressed,
6
+ * rather than relying on static patterns.
7
+ *
8
+ * Finds phrases that appear 3+ times and replaces with short codes.
9
+ */
10
+ // Config
11
+ const MIN_PHRASE_LENGTH = 20;
12
+ const MAX_PHRASE_LENGTH = 200;
13
+ const MIN_FREQUENCY = 3;
14
+ const MAX_ENTRIES = 100;
15
+ const CODE_PREFIX = "$D"; // Dynamic codes: $D01, $D02, etc.
16
+ /**
17
+ * Find repeated phrases in content.
18
+ */
19
+ function findRepeatedPhrases(allContent) {
20
+ const phrases = new Map();
21
+ // Split by sentence-like boundaries
22
+ const segments = allContent.split(/(?<=[.!?\n])\s+/);
23
+ for (const segment of segments) {
24
+ const trimmed = segment.trim();
25
+ if (trimmed.length >= MIN_PHRASE_LENGTH &&
26
+ trimmed.length <= MAX_PHRASE_LENGTH) {
27
+ phrases.set(trimmed, (phrases.get(trimmed) || 0) + 1);
28
+ }
29
+ }
30
+ // Also find repeated lines
31
+ const lines = allContent.split("\n");
32
+ for (const line of lines) {
33
+ const trimmed = line.trim();
34
+ if (trimmed.length >= MIN_PHRASE_LENGTH &&
35
+ trimmed.length <= MAX_PHRASE_LENGTH) {
36
+ phrases.set(trimmed, (phrases.get(trimmed) || 0) + 1);
37
+ }
38
+ }
39
+ return phrases;
40
+ }
41
+ /**
42
+ * Build dynamic codebook from message content.
43
+ */
44
+ function buildDynamicCodebook(messages) {
45
+ // Combine all content
46
+ let allContent = "";
47
+ for (const msg of messages) {
48
+ if (msg.content) {
49
+ allContent += msg.content + "\n";
50
+ }
51
+ }
52
+ // Find repeated phrases
53
+ const phrases = findRepeatedPhrases(allContent);
54
+ // Filter by frequency and sort by savings potential
55
+ const candidates = [];
56
+ for (const [phrase, count] of phrases.entries()) {
57
+ if (count >= MIN_FREQUENCY) {
58
+ // Savings = (phrase length - code length) * occurrences
59
+ const codeLength = 4; // e.g., "$D01"
60
+ const savings = (phrase.length - codeLength) * count;
61
+ if (savings > 50) {
62
+ candidates.push({ phrase, count, savings });
63
+ }
64
+ }
65
+ }
66
+ // Sort by savings (descending) and take top entries
67
+ candidates.sort((a, b) => b.savings - a.savings);
68
+ const topCandidates = candidates.slice(0, MAX_ENTRIES);
69
+ // Build codebook
70
+ const codebook = {};
71
+ topCandidates.forEach((c, i) => {
72
+ const code = `${CODE_PREFIX}${String(i + 1).padStart(2, "0")}`;
73
+ codebook[code] = c.phrase;
74
+ });
75
+ return codebook;
76
+ }
77
+ /**
78
+ * Escape special regex characters.
79
+ */
80
+ function escapeRegex(str) {
81
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
82
+ }
83
+ /**
84
+ * Apply dynamic codebook to messages.
85
+ */
86
+ export function applyDynamicCodebook(messages) {
87
+ // Build codebook from content
88
+ const codebook = buildDynamicCodebook(messages);
89
+ if (Object.keys(codebook).length === 0) {
90
+ return {
91
+ messages,
92
+ charsSaved: 0,
93
+ dynamicCodes: {},
94
+ substitutions: 0,
95
+ };
96
+ }
97
+ // Create inverse map for replacement
98
+ const phraseToCode = {};
99
+ for (const [code, phrase] of Object.entries(codebook)) {
100
+ phraseToCode[phrase] = code;
101
+ }
102
+ // Sort phrases by length (longest first) to avoid partial replacements
103
+ const sortedPhrases = Object.keys(phraseToCode).sort((a, b) => b.length - a.length);
104
+ let charsSaved = 0;
105
+ let substitutions = 0;
106
+ // Apply replacements
107
+ const result = messages.map((msg) => {
108
+ if (!msg.content || typeof msg.content !== "string")
109
+ return msg;
110
+ let content = msg.content;
111
+ for (const phrase of sortedPhrases) {
112
+ const code = phraseToCode[phrase];
113
+ const regex = new RegExp(escapeRegex(phrase), "g");
114
+ const matches = content.match(regex);
115
+ if (matches) {
116
+ content = content.replace(regex, code);
117
+ charsSaved += (phrase.length - code.length) * matches.length;
118
+ substitutions += matches.length;
119
+ }
120
+ }
121
+ return { ...msg, content };
122
+ });
123
+ return {
124
+ messages: result,
125
+ charsSaved,
126
+ dynamicCodes: codebook,
127
+ substitutions,
128
+ };
129
+ }
130
+ /**
131
+ * Generate header for dynamic codes (to include in system message).
132
+ */
133
+ export function generateDynamicCodebookHeader(codebook) {
134
+ if (Object.keys(codebook).length === 0)
135
+ return "";
136
+ const entries = Object.entries(codebook)
137
+ .slice(0, 20) // Limit header size
138
+ .map(([code, phrase]) => {
139
+ // Truncate long phrases in header
140
+ const displayPhrase = phrase.length > 40 ? phrase.slice(0, 37) + "..." : phrase;
141
+ return `${code}=${displayPhrase}`;
142
+ })
143
+ .join(", ");
144
+ return `[DynDict: ${entries}]`;
145
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Layer 5: JSON Compaction
3
+ *
4
+ * Minifies JSON in tool_call arguments and tool results.
5
+ * Removes pretty-print whitespace from JSON strings.
6
+ *
7
+ * Safe for LLM: JSON semantics unchanged.
8
+ * Expected savings: 2-4%
9
+ */
10
+ import { NormalizedMessage } from "../types.js";
11
+ export interface JsonCompactResult {
12
+ messages: NormalizedMessage[];
13
+ charsSaved: number;
14
+ }
15
+ /**
16
+ * Apply JSON compaction to all messages.
17
+ *
18
+ * Targets:
19
+ * - tool_call arguments (in assistant messages)
20
+ * - tool message content (often JSON)
21
+ */
22
+ export declare function compactMessagesJson(messages: NormalizedMessage[]): JsonCompactResult;