squeezr-ai 1.81.2 → 1.99.2

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.
Files changed (69) hide show
  1. package/bin/squeezr.js +82 -1
  2. package/dist/__tests__/bench.test.d.ts +1 -0
  3. package/dist/__tests__/bench.test.js +55 -0
  4. package/dist/__tests__/cacheAligner.test.d.ts +1 -0
  5. package/dist/__tests__/cacheAligner.test.js +75 -0
  6. package/dist/__tests__/codeStructure.test.d.ts +1 -0
  7. package/dist/__tests__/codeStructure.test.js +74 -0
  8. package/dist/__tests__/compressor.test.js +5 -1
  9. package/dist/__tests__/contentRouter.test.d.ts +1 -0
  10. package/dist/__tests__/contentRouter.test.js +76 -0
  11. package/dist/__tests__/controlEndpointGuard.test.d.ts +1 -0
  12. package/dist/__tests__/controlEndpointGuard.test.js +61 -0
  13. package/dist/__tests__/deterministic.test.js +13 -0
  14. package/dist/__tests__/doctor.test.d.ts +1 -0
  15. package/dist/__tests__/doctor.test.js +86 -0
  16. package/dist/__tests__/jsonCrush.test.d.ts +1 -0
  17. package/dist/__tests__/jsonCrush.test.js +168 -0
  18. package/dist/__tests__/learn.test.d.ts +1 -0
  19. package/dist/__tests__/learn.test.js +159 -0
  20. package/dist/__tests__/outputSavings.test.d.ts +1 -0
  21. package/dist/__tests__/outputSavings.test.js +49 -0
  22. package/dist/__tests__/outputShaper.test.d.ts +1 -0
  23. package/dist/__tests__/outputShaper.test.js +216 -0
  24. package/dist/__tests__/relevance.test.d.ts +1 -0
  25. package/dist/__tests__/relevance.test.js +66 -0
  26. package/dist/__tests__/secureKeyFile.test.d.ts +1 -0
  27. package/dist/__tests__/secureKeyFile.test.js +27 -0
  28. package/dist/__tests__/serverBinding.test.d.ts +1 -0
  29. package/dist/__tests__/serverBinding.test.js +18 -0
  30. package/dist/__tests__/statsOutput.test.d.ts +1 -0
  31. package/dist/__tests__/statsOutput.test.js +46 -0
  32. package/dist/__tests__/textCrusher.test.d.ts +1 -0
  33. package/dist/__tests__/textCrusher.test.js +133 -0
  34. package/dist/bench.d.ts +45 -0
  35. package/dist/bench.js +114 -0
  36. package/dist/cacheAligner.d.ts +34 -0
  37. package/dist/cacheAligner.js +73 -0
  38. package/dist/codexMitm.d.ts +1 -0
  39. package/dist/codexMitm.js +26 -2
  40. package/dist/compressor.js +42 -4
  41. package/dist/config.d.ts +6 -0
  42. package/dist/config.js +25 -0
  43. package/dist/contentRouter.d.ts +30 -0
  44. package/dist/contentRouter.js +112 -0
  45. package/dist/dashboard.d.ts +1 -1
  46. package/dist/dashboard.js +26 -7
  47. package/dist/deterministic.d.ts +4 -1
  48. package/dist/deterministic.js +43 -10
  49. package/dist/doctor.d.ts +38 -0
  50. package/dist/doctor.js +113 -0
  51. package/dist/index.js +4 -1
  52. package/dist/jsonCrush.d.ts +34 -0
  53. package/dist/jsonCrush.js +177 -0
  54. package/dist/learn.d.ts +65 -0
  55. package/dist/learn.js +244 -0
  56. package/dist/outputSavings.d.ts +20 -0
  57. package/dist/outputSavings.js +52 -0
  58. package/dist/outputShaper.d.ts +71 -0
  59. package/dist/outputShaper.js +155 -0
  60. package/dist/relevance.d.ts +27 -0
  61. package/dist/relevance.js +78 -0
  62. package/dist/server.js +106 -11
  63. package/dist/stats.d.ts +17 -0
  64. package/dist/stats.js +55 -0
  65. package/dist/systemPrompt.js +3 -2
  66. package/dist/textCrusher.d.ts +35 -0
  67. package/dist/textCrusher.js +160 -0
  68. package/package.json +69 -69
  69. package/squeezr.toml +15 -1
package/dist/learn.js ADDED
@@ -0,0 +1,244 @@
1
+ /**
2
+ * squeezr learn — offline mining of Claude Code sessions for wasteful LOOPS, writing
3
+ * corrections to CLAUDE.local.md. Squeezr's take on headroom's `headroom learn`.
4
+ *
5
+ * The highest-value, cheapest signal is loop detection (no LLM needed):
6
+ * - error loops — the same command fails and is retried ≥3× (the agent is stuck).
7
+ * - refetch loops — the agent re-runs pagination VARIANTS of a command (`| head -50`
8
+ * → `head -100` → `head -200`) because the output kept getting truncated. These
9
+ * SUCCEED, so failure-only analysis misses them, yet each re-fetch re-bills the
10
+ * whole (bigger) output. Canonicalising away the pagination fragment collapses the
11
+ * variants to one signature so the loop becomes visible.
12
+ *
13
+ * Wasted bytes are a MEASURED lower bound from real tool-output sizes, not a guess.
14
+ *
15
+ * The pure functions here (canonicalSignature / extractToolCalls / detectLoops /
16
+ * renderCorrections / writeMarkerBlock) are unit-tested; runLearn() is the thin
17
+ * filesystem orchestrator the CLI calls.
18
+ */
19
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from 'node:fs';
20
+ import { join } from 'node:path';
21
+ import { homedir } from 'node:os';
22
+ export const MIN_OCCURRENCES = 3;
23
+ // ── Canonical signature ──────────────────────────────────────────────────────
24
+ // Fragments that vary between re-fetches of "the same" query but don't change intent.
25
+ const PAGINATION_PATTERNS = [
26
+ /\|\s*head\s+-?n?\s*\d+/gi, // | head -50 / | head -n 50
27
+ /\|\s*tail\s+-?n?\s*\d+/gi, // | tail -50
28
+ /\bhead\s+-n\s*\d+/gi,
29
+ /\btail\s+-n\s*\d+/gi,
30
+ /\b-n\s+\d+/gi, // -n 50
31
+ /\blimit\s+\d+/gi, // LIMIT 50
32
+ /\boffset\s+\d+/gi, // OFFSET 40
33
+ /--limit[=\s]+\d+/gi,
34
+ /--offset[=\s]+\d+/gi,
35
+ /[?&](?:per_page|page|limit|offset)=\d+/gi,
36
+ ];
37
+ function inputToString(tool, input) {
38
+ if (input && typeof input === 'object') {
39
+ const obj = input;
40
+ if (typeof obj.command === 'string')
41
+ return obj.command;
42
+ // Deterministic: sort keys so the signature is stable.
43
+ const keys = Object.keys(obj).sort();
44
+ return keys.map(k => `${k}=${typeof obj[k] === 'object' ? JSON.stringify(obj[k]) : String(obj[k])}`).join(' ');
45
+ }
46
+ return String(input ?? '');
47
+ }
48
+ /** Stable, pagination-stripped signature. Same intent → same signature. */
49
+ export function canonicalSignature(tool, input) {
50
+ let s = `${tool.toLowerCase()} ${inputToString(tool, input)}`;
51
+ for (const re of PAGINATION_PATTERNS)
52
+ s = s.replace(re, ' ');
53
+ s = s.replace(/\b\d+\b/g, 'N'); // collapse remaining bare integers
54
+ s = s.replace(/\s+/g, ' ').trim().toLowerCase();
55
+ return s;
56
+ }
57
+ // ── Parse Claude Code JSONL ──────────────────────────────────────────────────
58
+ function contentBytes(content) {
59
+ if (typeof content === 'string')
60
+ return content.length;
61
+ if (Array.isArray(content))
62
+ return JSON.stringify(content).length;
63
+ if (content && typeof content === 'object')
64
+ return JSON.stringify(content).length;
65
+ return 0;
66
+ }
67
+ /**
68
+ * Extract normalized tool calls from Claude Code session JSONL lines. Tolerant of
69
+ * malformed lines and both string/array tool_result content shapes.
70
+ */
71
+ export function extractToolCalls(lines) {
72
+ const uses = new Map();
73
+ const results = new Map();
74
+ const order = [];
75
+ for (const line of lines) {
76
+ const trimmed = line.trim();
77
+ if (!trimmed)
78
+ continue;
79
+ let obj;
80
+ try {
81
+ obj = JSON.parse(trimmed);
82
+ }
83
+ catch {
84
+ continue;
85
+ }
86
+ const msg = obj?.message;
87
+ const content = msg?.content;
88
+ if (!Array.isArray(content))
89
+ continue;
90
+ for (const block of content) {
91
+ if (block?.type === 'tool_use' && typeof block.id === 'string' && typeof block.name === 'string') {
92
+ const raw = inputToString(block.name, block.input);
93
+ uses.set(block.id, { tool: block.name, raw, signature: canonicalSignature(block.name, block.input) });
94
+ order.push(block.id);
95
+ }
96
+ else if (block?.type === 'tool_result' && typeof block.tool_use_id === 'string') {
97
+ results.set(block.tool_use_id, {
98
+ isError: !!block.is_error,
99
+ outputBytes: contentBytes(block.content),
100
+ });
101
+ }
102
+ }
103
+ }
104
+ const calls = [];
105
+ for (const id of order) {
106
+ const u = uses.get(id);
107
+ if (!u)
108
+ continue;
109
+ const r = results.get(id);
110
+ calls.push({ tool: u.tool, signature: u.signature, raw: u.raw, isError: r?.isError ?? false, outputBytes: r?.outputBytes ?? 0 });
111
+ }
112
+ return calls;
113
+ }
114
+ // ── Loop detection ───────────────────────────────────────────────────────────
115
+ export function detectLoops(calls, minOccurrences = MIN_OCCURRENCES) {
116
+ const groups = new Map();
117
+ for (const c of calls) {
118
+ const arr = groups.get(c.signature) ?? [];
119
+ arr.push(c);
120
+ groups.set(c.signature, arr);
121
+ }
122
+ const loops = [];
123
+ for (const [signature, arr] of groups) {
124
+ if (arr.length < minOccurrences)
125
+ continue;
126
+ const errors = arr.filter(c => c.isError);
127
+ if (errors.length >= minOccurrences) {
128
+ // Error loop: measured waste = every failed repetition's output.
129
+ loops.push({
130
+ kind: 'error',
131
+ signature,
132
+ count: errors.length,
133
+ wastedBytes: errors.reduce((s, c) => s + c.outputBytes, 0),
134
+ sampleRaw: errors[0].raw,
135
+ tool: errors[0].tool,
136
+ });
137
+ continue;
138
+ }
139
+ // Refetch loop: same signature, ≥2 distinct raw variants (pagination churn).
140
+ const distinctRaw = new Set(arr.map(c => c.raw));
141
+ if (distinctRaw.size >= 2) {
142
+ const sorted = [...arr].sort((a, b) => a.outputBytes - b.outputBytes);
143
+ // Redundant = all but the single legitimate fetch (the smallest).
144
+ const wastedBytes = sorted.slice(1).reduce((s, c) => s + c.outputBytes, 0);
145
+ loops.push({
146
+ kind: 'refetch',
147
+ signature,
148
+ count: arr.length,
149
+ wastedBytes,
150
+ sampleRaw: arr[0].raw,
151
+ tool: arr[0].tool,
152
+ });
153
+ }
154
+ }
155
+ return loops.sort((a, b) => b.wastedBytes - a.wastedBytes);
156
+ }
157
+ // ── Corrections + marker writer ──────────────────────────────────────────────
158
+ export const LEARN_START = '<!-- squeezr:learn:start -->';
159
+ export const LEARN_END = '<!-- squeezr:learn:end -->';
160
+ function approxTokens(bytes) {
161
+ return Math.round(bytes / 4);
162
+ }
163
+ export function renderCorrections(loops) {
164
+ if (loops.length === 0)
165
+ return '';
166
+ const lines = [];
167
+ for (const l of loops) {
168
+ const tok = approxTokens(l.wastedBytes);
169
+ if (l.kind === 'error') {
170
+ lines.push(`- \`${l.sampleRaw}\` failed and was retried ${l.count}× (~${tok} tokens wasted). Fix the root cause before retrying, or don't re-run the identical failing command.`);
171
+ }
172
+ else {
173
+ lines.push(`- \`${l.sampleRaw}\` was re-fetched with growing pagination ${l.count}× (~${tok} tokens wasted). Ask for the full/needed range in ONE call instead of re-running with a bigger limit.`);
174
+ }
175
+ }
176
+ return lines.join('\n');
177
+ }
178
+ /**
179
+ * Insert/replace the squeezr-managed block in a CLAUDE.local.md-style file. Idempotent:
180
+ * re-applying the same rules yields byte-identical content; a new set replaces the old.
181
+ */
182
+ export function writeMarkerBlock(existing, rulesBody) {
183
+ const block = `${LEARN_START}\n## Squeezr learned rules (auto-generated — avoid these token-wasting loops)\n${rulesBody}\n${LEARN_END}`;
184
+ const re = new RegExp(`${LEARN_START}[\\s\\S]*?${LEARN_END}`);
185
+ if (re.test(existing)) {
186
+ return existing.replace(re, block);
187
+ }
188
+ const sep = existing.length === 0 || existing.endsWith('\n') ? '\n' : '\n\n';
189
+ return `${existing}${sep}${block}\n`;
190
+ }
191
+ function newestJsonl(dir, limit) {
192
+ const found = [];
193
+ const walk = (d) => {
194
+ let entries = [];
195
+ try {
196
+ entries = readdirSync(d);
197
+ }
198
+ catch {
199
+ return;
200
+ }
201
+ for (const name of entries) {
202
+ const p = join(d, name);
203
+ let st;
204
+ try {
205
+ st = statSync(p);
206
+ }
207
+ catch {
208
+ continue;
209
+ }
210
+ if (st.isDirectory())
211
+ walk(p);
212
+ else if (name.endsWith('.jsonl'))
213
+ found.push({ path: p, mtime: st.mtimeMs });
214
+ }
215
+ };
216
+ walk(dir);
217
+ return found.sort((a, b) => b.mtime - a.mtime).slice(0, limit).map(f => f.path);
218
+ }
219
+ export function runLearn(opts = {}) {
220
+ const projectsDir = opts.projectsDir ?? join(homedir(), '.claude', 'projects');
221
+ const targetFile = opts.targetFile ?? join(process.cwd(), 'CLAUDE.local.md');
222
+ const limit = opts.maxSessions ?? 20;
223
+ const files = existsSync(projectsDir) ? newestJsonl(projectsDir, limit) : [];
224
+ const allCalls = [];
225
+ for (const f of files) {
226
+ try {
227
+ allCalls.push(...extractToolCalls(readFileSync(f, 'utf-8').split('\n')));
228
+ }
229
+ catch { /* skip */ }
230
+ }
231
+ const loops = detectLoops(allCalls);
232
+ const totalTokens = approxTokens(loops.reduce((s, l) => s + l.wastedBytes, 0));
233
+ const header = files.length === 0
234
+ ? `No Claude Code sessions found under ${projectsDir}.`
235
+ : `Scanned ${files.length} session(s), ${allCalls.length} tool call(s). Found ${loops.length} loop(s), ~${totalTokens} tokens wasted.`;
236
+ const report = loops.length ? `${header}\n\n${renderCorrections(loops)}` : header;
237
+ let applied = false;
238
+ if (opts.apply && loops.length > 0) {
239
+ const existing = existsSync(targetFile) ? readFileSync(targetFile, 'utf-8') : '';
240
+ writeFileSync(targetFile, writeMarkerBlock(existing, renderCorrections(loops)), 'utf-8');
241
+ applied = true;
242
+ }
243
+ return { sessionsScanned: files.length, loops, report, applied, targetFile };
244
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Output-savings signals — Squeezr's honest, no-counterfactual take on headroom's output
3
+ * measurement. The fully honest number (headroom's holdout A/B) needs a control arm; that
4
+ * is deferred. This ships the tier that needs NO counterfactual: the ECHO RATIO — how much
5
+ * of what the model just wrote merely restated context it was already given.
6
+ *
7
+ * High echo = wasted output tokens = exactly what verbosity steering (1.83) targets. Reading
8
+ * the response to measure this never changes the bytes we forward, so it's cache-safe.
9
+ */
10
+ /** Word n-grams (set) of a text. */
11
+ export declare function wordNgrams(text: string, n?: number): Set<string>;
12
+ /**
13
+ * Fraction of the OUTPUT's n-grams that also appear in the CONTEXT (0..1). 1 = the model
14
+ * only restated what it was already shown; 0 = fully novel output.
15
+ */
16
+ export declare function echoRatio(output: string, context: string, n?: number): number;
17
+ /** Pull the assistant's text out of an Anthropic SSE stream (concatenated text_delta). */
18
+ export declare function extractAssistantTextFromSse(sse: string): string;
19
+ /** Assistant text from a non-streaming Anthropic response body's content blocks. */
20
+ export declare function extractAssistantTextFromContent(content: unknown): string;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Output-savings signals — Squeezr's honest, no-counterfactual take on headroom's output
3
+ * measurement. The fully honest number (headroom's holdout A/B) needs a control arm; that
4
+ * is deferred. This ships the tier that needs NO counterfactual: the ECHO RATIO — how much
5
+ * of what the model just wrote merely restated context it was already given.
6
+ *
7
+ * High echo = wasted output tokens = exactly what verbosity steering (1.83) targets. Reading
8
+ * the response to measure this never changes the bytes we forward, so it's cache-safe.
9
+ */
10
+ /** Word n-grams (set) of a text. */
11
+ export function wordNgrams(text, n = 3) {
12
+ const words = text.toLowerCase().match(/[a-z0-9_]+/g) ?? [];
13
+ const set = new Set();
14
+ for (let i = 0; i + n <= words.length; i++)
15
+ set.add(words.slice(i, i + n).join(' '));
16
+ return set;
17
+ }
18
+ /**
19
+ * Fraction of the OUTPUT's n-grams that also appear in the CONTEXT (0..1). 1 = the model
20
+ * only restated what it was already shown; 0 = fully novel output.
21
+ */
22
+ export function echoRatio(output, context, n = 3) {
23
+ const out = wordNgrams(output, n);
24
+ if (out.size === 0)
25
+ return 0;
26
+ const ctx = wordNgrams(context, n);
27
+ let echoed = 0;
28
+ for (const g of out)
29
+ if (ctx.has(g))
30
+ echoed++;
31
+ return echoed / out.size;
32
+ }
33
+ /** Pull the assistant's text out of an Anthropic SSE stream (concatenated text_delta). */
34
+ export function extractAssistantTextFromSse(sse) {
35
+ let out = '';
36
+ for (const m of sse.matchAll(/"type":"text_delta","text":"((?:[^"\\]|\\.)*)"/g)) {
37
+ try {
38
+ out += JSON.parse(`"${m[1]}"`);
39
+ }
40
+ catch { /* skip malformed delta */ }
41
+ }
42
+ return out;
43
+ }
44
+ /** Assistant text from a non-streaming Anthropic response body's content blocks. */
45
+ export function extractAssistantTextFromContent(content) {
46
+ if (!Array.isArray(content))
47
+ return '';
48
+ return content
49
+ .filter(b => b && b.type === 'text' && typeof b.text === 'string')
50
+ .map(b => b.text)
51
+ .join('');
52
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Output-side token reduction.
3
+ *
4
+ * Everything else in Squeezr compresses what goes INTO the model. This module is
5
+ * the first lever on what comes OUT of it. The proxy never generates output
6
+ * tokens, so both levers work by RESHAPING THE REQUEST:
7
+ *
8
+ * 1. Verbosity steering — a byte-stable instruction block appended to the TAIL
9
+ * of the system prompt (after any cache_control breakpoint, so the cached
10
+ * prefix is never touched → Anthropic prompt cache still hits). Same append
11
+ * pattern already proven safe by injectExpandDirectiveAnthropic().
12
+ *
13
+ * 2. Effort routing — agentic loops are mostly mechanical continuations (the
14
+ * last message is a clean tool_result: a file read, a passing test). Thinking
15
+ * bills as OUTPUT tokens (≈5× input on Opus). On mechanical turns we LOWER an
16
+ * explicitly-present thinking budget / effort; on errors or new user asks we
17
+ * leave it alone.
18
+ *
19
+ * Safety rules (each prevents a concrete failure mode):
20
+ * - Never INJECT an effort lever the client didn't send (models without effort
21
+ * support 400 on it). Only lowering an existing value is always valid.
22
+ * - Never toggle thinking.type (disabling thinking while history carries thinking
23
+ * blocks 400s on some models AND busts the messages cache tier).
24
+ * - Steering text is byte-stable per level and applied idempotently, so repeated
25
+ * requests keep an identical trailing block.
26
+ *
27
+ * Turn classification is purely STRUCTURAL (block types + is_error flags) — no
28
+ * content regexes, no keyword matching.
29
+ */
30
+ export type TurnKind = 'mechanical' | 'new-ask' | 'error' | 'unknown';
31
+ export type VerbosityLevel = 1 | 2 | 3 | 4;
32
+ export interface OutputShaperSettings {
33
+ enabled: boolean;
34
+ verbositySteering: boolean;
35
+ level: VerbosityLevel;
36
+ effortRouting: boolean;
37
+ /** Floor to clamp an existing thinking.budget_tokens to on mechanical turns. */
38
+ mechanicalThinkingFloor: number;
39
+ }
40
+ export interface ShapeResult {
41
+ steered: boolean;
42
+ effortLowered: boolean;
43
+ turn: TurnKind;
44
+ }
45
+ export declare const STEERING_START = "<squeezr_output_shaping>";
46
+ export declare const STEERING_END = "</squeezr_output_shaping>";
47
+ /**
48
+ * Classify the latest turn structurally.
49
+ * error — the last user message carries a tool_result with is_error
50
+ * new-ask — the last user message carries real typed text (string, or a text block)
51
+ * mechanical — the last user message is only tool_result(s), none errored
52
+ * unknown — no messages, or the last message isn't a user turn
53
+ */
54
+ export declare function classifyTurn(messages: unknown[]): TurnKind;
55
+ /** Byte-stable steering block for a level, wrapped in the sentinel markers. */
56
+ export declare function steeringText(level: VerbosityLevel): string;
57
+ /**
58
+ * Append the steering block to the TAIL of the system prompt. Idempotent: any
59
+ * previous steering block is removed first, so re-applying the same level yields
60
+ * a byte-identical result and re-applying a new level replaces (never stacks).
61
+ * Returns true if a steering block is present after the call.
62
+ */
63
+ export declare function applyVerbositySteering(body: Record<string, unknown>, level: VerbosityLevel): boolean;
64
+ /**
65
+ * Lower an explicitly-present output lever. Never injects one. Two lever shapes:
66
+ * - Anthropic legacy: thinking.budget_tokens → clamp down to `floor`
67
+ * - Newer: output_config.effort → lower to MECHANICAL_EFFORT
68
+ * Returns true if it actually lowered something.
69
+ */
70
+ export declare function routeEffort(body: Record<string, unknown>, floor: number): boolean;
71
+ export declare function shapeRequest(body: Record<string, unknown>, settings: OutputShaperSettings): ShapeResult;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Output-side token reduction.
3
+ *
4
+ * Everything else in Squeezr compresses what goes INTO the model. This module is
5
+ * the first lever on what comes OUT of it. The proxy never generates output
6
+ * tokens, so both levers work by RESHAPING THE REQUEST:
7
+ *
8
+ * 1. Verbosity steering — a byte-stable instruction block appended to the TAIL
9
+ * of the system prompt (after any cache_control breakpoint, so the cached
10
+ * prefix is never touched → Anthropic prompt cache still hits). Same append
11
+ * pattern already proven safe by injectExpandDirectiveAnthropic().
12
+ *
13
+ * 2. Effort routing — agentic loops are mostly mechanical continuations (the
14
+ * last message is a clean tool_result: a file read, a passing test). Thinking
15
+ * bills as OUTPUT tokens (≈5× input on Opus). On mechanical turns we LOWER an
16
+ * explicitly-present thinking budget / effort; on errors or new user asks we
17
+ * leave it alone.
18
+ *
19
+ * Safety rules (each prevents a concrete failure mode):
20
+ * - Never INJECT an effort lever the client didn't send (models without effort
21
+ * support 400 on it). Only lowering an existing value is always valid.
22
+ * - Never toggle thinking.type (disabling thinking while history carries thinking
23
+ * blocks 400s on some models AND busts the messages cache tier).
24
+ * - Steering text is byte-stable per level and applied idempotently, so repeated
25
+ * requests keep an identical trailing block.
26
+ *
27
+ * Turn classification is purely STRUCTURAL (block types + is_error flags) — no
28
+ * content regexes, no keyword matching.
29
+ */
30
+ export const STEERING_START = '<squeezr_output_shaping>';
31
+ export const STEERING_END = '</squeezr_output_shaping>';
32
+ function blocksOf(content) {
33
+ return Array.isArray(content) ? content : [];
34
+ }
35
+ /**
36
+ * Classify the latest turn structurally.
37
+ * error — the last user message carries a tool_result with is_error
38
+ * new-ask — the last user message carries real typed text (string, or a text block)
39
+ * mechanical — the last user message is only tool_result(s), none errored
40
+ * unknown — no messages, or the last message isn't a user turn
41
+ */
42
+ export function classifyTurn(messages) {
43
+ if (!Array.isArray(messages) || messages.length === 0)
44
+ return 'unknown';
45
+ const last = messages[messages.length - 1];
46
+ if (!last || last.role !== 'user')
47
+ return 'unknown';
48
+ if (typeof last.content === 'string') {
49
+ return last.content.trim().length > 0 ? 'new-ask' : 'unknown';
50
+ }
51
+ const blocks = blocksOf(last.content);
52
+ if (blocks.length === 0)
53
+ return 'unknown';
54
+ const hasErrorResult = blocks.some(b => b.type === 'tool_result' && !!b.is_error);
55
+ if (hasErrorResult)
56
+ return 'error';
57
+ const hasRealText = blocks.some(b => b.type === 'text' && typeof b.text === 'string' && b.text.trim().length > 0);
58
+ if (hasRealText)
59
+ return 'new-ask';
60
+ const hasToolResult = blocks.some(b => b.type === 'tool_result');
61
+ if (hasToolResult)
62
+ return 'mechanical';
63
+ return 'unknown';
64
+ }
65
+ // ── Verbosity steering ───────────────────────────────────────────────────────
66
+ // Cumulative levels. Each string is a fixed literal → byte-stable per level,
67
+ // which is what keeps the appended trailing block identical across requests.
68
+ const LEVEL_RULES = {
69
+ 1: [
70
+ 'Skip preamble and postamble. Open with the substance, not with "Great" / "Sure" / "Let me".',
71
+ ],
72
+ 2: [
73
+ 'Skip preamble and postamble. Open with the substance, not with "Great" / "Sure" / "Let me".',
74
+ 'Do not restate code, file contents, or tool output already visible in this conversation — reference it by path:line instead of reprinting it.',
75
+ 'After a tool call succeeds, do not narrate what it did unless it changes the plan.',
76
+ ],
77
+ 3: [
78
+ 'Skip preamble and postamble. Open with the substance, not with "Great" / "Sure" / "Let me".',
79
+ 'Do not restate code, file contents, or tool output already visible in this conversation — reference it by path:line instead of reprinting it.',
80
+ 'After a tool call succeeds, do not narrate what it did unless it changes the plan.',
81
+ 'Give conclusions, not step-by-step narration of routine work. Prefer the smallest edit over a full rewrite.',
82
+ ],
83
+ 4: [
84
+ 'Answer in the minimum number of tokens. Fragments over full sentences. No rationale unless explicitly asked. No restated context.',
85
+ ],
86
+ };
87
+ /** Byte-stable steering block for a level, wrapped in the sentinel markers. */
88
+ export function steeringText(level) {
89
+ const rules = LEVEL_RULES[level] ?? LEVEL_RULES[2];
90
+ const body = rules.map(r => `- ${r}`).join('\n');
91
+ return `${STEERING_START}\nBe terse. Output tokens are expensive.\n${body}\n${STEERING_END}`;
92
+ }
93
+ function stripSteeringString(s) {
94
+ const re = new RegExp(`\\n*${STEERING_START}[\\s\\S]*?${STEERING_END}`, 'g');
95
+ return s.replace(re, '');
96
+ }
97
+ /**
98
+ * Append the steering block to the TAIL of the system prompt. Idempotent: any
99
+ * previous steering block is removed first, so re-applying the same level yields
100
+ * a byte-identical result and re-applying a new level replaces (never stacks).
101
+ * Returns true if a steering block is present after the call.
102
+ */
103
+ export function applyVerbositySteering(body, level) {
104
+ const block = steeringText(level);
105
+ const sys = body.system;
106
+ if (typeof sys === 'string') {
107
+ body.system = `${stripSteeringString(sys)}\n\n${block}`;
108
+ return true;
109
+ }
110
+ if (Array.isArray(sys)) {
111
+ const arr = sys.filter(b => !(typeof b.text === 'string' && b.text.includes(STEERING_START)));
112
+ arr.push({ type: 'text', text: block });
113
+ body.system = arr;
114
+ return true;
115
+ }
116
+ // No system prompt at all (rare): create one carrying only the steering block.
117
+ body.system = block;
118
+ return true;
119
+ }
120
+ // ── Effort routing ───────────────────────────────────────────────────────────
121
+ const EFFORT_RANK = { minimal: 0, low: 1, medium: 2, high: 3, xhigh: 4 };
122
+ const MECHANICAL_EFFORT = 'low';
123
+ /**
124
+ * Lower an explicitly-present output lever. Never injects one. Two lever shapes:
125
+ * - Anthropic legacy: thinking.budget_tokens → clamp down to `floor`
126
+ * - Newer: output_config.effort → lower to MECHANICAL_EFFORT
127
+ * Returns true if it actually lowered something.
128
+ */
129
+ export function routeEffort(body, floor) {
130
+ let lowered = false;
131
+ const thinking = body.thinking;
132
+ if (thinking && typeof thinking.budget_tokens === 'number' && thinking.budget_tokens > floor) {
133
+ thinking.budget_tokens = floor;
134
+ lowered = true;
135
+ }
136
+ const oc = body.output_config;
137
+ if (oc && typeof oc.effort === 'string') {
138
+ const cur = EFFORT_RANK[oc.effort.toLowerCase()];
139
+ const target = EFFORT_RANK[MECHANICAL_EFFORT];
140
+ if (cur !== undefined && cur > target) {
141
+ oc.effort = MECHANICAL_EFFORT;
142
+ lowered = true;
143
+ }
144
+ }
145
+ return lowered;
146
+ }
147
+ // ── Orchestration ────────────────────────────────────────────────────────────
148
+ export function shapeRequest(body, settings) {
149
+ if (!settings.enabled)
150
+ return { steered: false, effortLowered: false, turn: 'unknown' };
151
+ const turn = classifyTurn(body.messages ?? []);
152
+ const steered = settings.verbositySteering ? applyVerbositySteering(body, settings.level) : false;
153
+ const effortLowered = settings.effortRouting && turn === 'mechanical' ? routeEffort(body, settings.mechanicalThinkingFloor) : false;
154
+ return { steered, effortLowered, turn };
155
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * BM25 relevance scorer — a shared primitive so Squeezr's compressors can keep content
3
+ * by RELEVANCE to the current task, not just by position (head/tail) or recency.
4
+ *
5
+ * This is the single highest-leverage idea copied from headroom: their SmartCrusher,
6
+ * TextCrusher and CodeCompressor all route "what to keep" through one relevance scorer.
7
+ * Squeezr, until now, kept blindly. This module is that scorer — pure TS, zero deps,
8
+ * deterministic (→ cache-safe when used to drive byte-stable compression).
9
+ *
10
+ * BM25 (Okapi) is the standard, well-understood ranking function: term frequency with
11
+ * saturation (k1) and document-length normalization (b), weighted by inverse document
12
+ * frequency so rare query terms (an error code, a filename) count far more than common
13
+ * words. The idf uses +0.5 smoothing so it is always positive.
14
+ */
15
+ export interface BM25Options {
16
+ k1?: number;
17
+ b?: number;
18
+ }
19
+ /** Lowercase, split on non-alphanumerics, keep tokens of length >= 2. */
20
+ export declare function tokenize(text: string): string[];
21
+ /**
22
+ * BM25 score of each doc in `docs` against `query`. One score per doc, in order.
23
+ * A doc with no query-term overlap scores exactly 0.
24
+ */
25
+ export declare function bm25Scores(query: string, docs: string[], opts?: BM25Options): number[];
26
+ /** Document indices sorted by descending relevance (ties keep original order). */
27
+ export declare function rankByRelevance(query: string, docs: string[], opts?: BM25Options): number[];
@@ -0,0 +1,78 @@
1
+ /**
2
+ * BM25 relevance scorer — a shared primitive so Squeezr's compressors can keep content
3
+ * by RELEVANCE to the current task, not just by position (head/tail) or recency.
4
+ *
5
+ * This is the single highest-leverage idea copied from headroom: their SmartCrusher,
6
+ * TextCrusher and CodeCompressor all route "what to keep" through one relevance scorer.
7
+ * Squeezr, until now, kept blindly. This module is that scorer — pure TS, zero deps,
8
+ * deterministic (→ cache-safe when used to drive byte-stable compression).
9
+ *
10
+ * BM25 (Okapi) is the standard, well-understood ranking function: term frequency with
11
+ * saturation (k1) and document-length normalization (b), weighted by inverse document
12
+ * frequency so rare query terms (an error code, a filename) count far more than common
13
+ * words. The idf uses +0.5 smoothing so it is always positive.
14
+ */
15
+ const DEFAULT_K1 = 1.5;
16
+ const DEFAULT_B = 0.75;
17
+ /** Lowercase, split on non-alphanumerics, keep tokens of length >= 2. */
18
+ export function tokenize(text) {
19
+ const out = [];
20
+ for (const m of text.toLowerCase().matchAll(/[a-z0-9_]+/g)) {
21
+ if (m[0].length >= 2)
22
+ out.push(m[0]);
23
+ }
24
+ return out;
25
+ }
26
+ /**
27
+ * BM25 score of each doc in `docs` against `query`. One score per doc, in order.
28
+ * A doc with no query-term overlap scores exactly 0.
29
+ */
30
+ export function bm25Scores(query, docs, opts = {}) {
31
+ const k1 = opts.k1 ?? DEFAULT_K1;
32
+ const b = opts.b ?? DEFAULT_B;
33
+ const queryTerms = [...new Set(tokenize(query))];
34
+ const N = docs.length;
35
+ if (queryTerms.length === 0 || N === 0)
36
+ return new Array(N).fill(0);
37
+ // Tokenize docs once; compute lengths and average length.
38
+ const docTokens = docs.map(tokenize);
39
+ const docLen = docTokens.map(t => t.length);
40
+ const avgdl = docLen.reduce((s, l) => s + l, 0) / N || 1;
41
+ // Per-doc term-frequency maps.
42
+ const tf = docTokens.map(tokens => {
43
+ const m = new Map();
44
+ for (const t of tokens)
45
+ m.set(t, (m.get(t) ?? 0) + 1);
46
+ return m;
47
+ });
48
+ // Document frequency + idf per query term.
49
+ const idf = new Map();
50
+ for (const term of queryTerms) {
51
+ let df = 0;
52
+ for (const m of tf)
53
+ if (m.has(term))
54
+ df++;
55
+ // Always-positive smoothed idf.
56
+ idf.set(term, Math.log(1 + (N - df + 0.5) / (df + 0.5)));
57
+ }
58
+ return docTokens.map((_, i) => {
59
+ let score = 0;
60
+ const len = docLen[i];
61
+ for (const term of queryTerms) {
62
+ const f = tf[i].get(term) ?? 0;
63
+ if (f === 0)
64
+ continue;
65
+ const denom = f + k1 * (1 - b + (b * len) / avgdl);
66
+ score += (idf.get(term) ?? 0) * ((f * (k1 + 1)) / denom);
67
+ }
68
+ return score;
69
+ });
70
+ }
71
+ /** Document indices sorted by descending relevance (ties keep original order). */
72
+ export function rankByRelevance(query, docs, opts) {
73
+ const scores = bm25Scores(query, docs, opts);
74
+ return scores
75
+ .map((score, i) => ({ score, i }))
76
+ .sort((a, b) => (b.score - a.score) || (a.i - b.i))
77
+ .map(x => x.i);
78
+ }