toolgz 0.2.8 → 0.2.9

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/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  <p><strong>Your agent spends 30–70k tokens of context on tool definitions before the user types a word. toolgz gets up to ~85% of it back.</strong></p>
4
4
 
5
- <p><em>On a large tool set, at level 3. The safe default (level 1) reclaims 13–39% and gives up nothing <code>recommendLevel()</code> picks for you.</em></p>
5
+ <p><em>On a large tool set, at level 3. The default (level 1) reclaims 13–39% and gives up nothing. <code>recommendLevel()</code> tells you which one your tools want; you pass its answer in.</em></p>
6
6
 
7
7
  <p>
8
8
  <a href="#measured-results">420-run cross-provider sweep</a> ·
@@ -33,7 +33,7 @@ Reclaiming the room is what this does.
33
33
  ```ts
34
34
  import { compress, recommendLevel, forAnthropic } from "toolgz";
35
35
 
36
- const { level } = recommendLevel(myTools); // 1 for small sets, 3 for large ones
36
+ const { level } = recommendLevel(myTools); // advice: 1 for a small block, 3 for a big one
37
37
  const c = compress(myTools, { level }); // your existing MCP/SDK tool array
38
38
  const { tools, system } = forAnthropic(c); // send these instead
39
39
  ```
@@ -41,7 +41,11 @@ const { tools, system } = forAnthropic(c); // send these instead
41
41
  `compress(myTools)` with no `level` gives you **level 1** — safe, native tool calling,
42
42
  provider schema enforcement intact, and 13–39% smaller (13–32% on the synthetic benchmark, 39% on the real 149-tool corpus). The 71–85% figures below are
43
43
  **level 3**, which is what `recommendLevel` returns once a tool set is big enough to
44
- amortise the dispatcher. Ask for it explicitly with `{ level: 3 }` if you prefer.
44
+ amortise the dispatcher.
45
+
46
+ **Note the shape of those three lines: `recommendLevel` only advises, and you pass its
47
+ answer back in.** Nothing upgrades itself — `compress(myTools)` is level 1 whether you
48
+ have 2 tools or 500. See [Which level to use](#which-level-to-use) for why.
45
49
 
46
50
  Then translate the model's call back before you dispatch:
47
51
 
@@ -226,6 +230,62 @@ tried and removed in 0.2.0 because they were smaller and still worse.
226
230
 
227
231
  ## Which level to use
228
232
 
233
+ ### The whole idea, in plain English
234
+
235
+ Your tool definitions are a **menu** handed to the model at the start of every single
236
+ conversation. It is long, and most of it is flowery prose about each dish.
237
+
238
+ - **Level 1** — the same menu with the prose cut. It is still a real menu: the model
239
+ points at a dish **by name**, and *the kitchen checks the order makes sense* before
240
+ cooking. This is the default, and it gives up nothing.
241
+ - **Level 3** — throw the menu away. Hand the model a **numbered list** and one waiter.
242
+ It says "number 12, no onions," and the waiter knows what that means. The list is
243
+ tiny. But the kitchen no longer checks the order — **toolgz checks it instead**,
244
+ against your original schema, and hands back a readable error if it's wrong.
245
+
246
+ If the model needs to know what number 12 comes with, it asks. That's the `q()` lookup,
247
+ and it costs about half a turn.
248
+
249
+ So: **level 1 is smaller. Level 3 is much smaller and you take over order-checking.**
250
+
251
+ ### Two things that surprise people
252
+
253
+ **1. Nothing changes level on its own. You are always the one who picks.**
254
+
255
+ ```ts
256
+ compress(myTools) // level 1. ALWAYS — 2 tools or 500.
257
+ compress(myTools, { level: 3 }) // level 3, because you asked for it
258
+
259
+ const { level } = recommendLevel(myTools); // just advice: returns 1 or 3
260
+ compress(myTools, { level }); // now it's 3, because you passed it in
261
+ ```
262
+
263
+ `recommendLevel()` **advises**; it does not act. On our 149-tool corpus,
264
+ `compress(myTools)` saves 45.2% and `compress(myTools, { level: 3 })` saves 96.5% — so
265
+ forgetting to pass the level back in quietly leaves half the win on the table.
266
+
267
+ This is deliberate. Level 3 gives up provider-side schema enforcement, and silently
268
+ changing a caller's correctness guarantees because their tool array grew would be a
269
+ worse bug than the tokens are worth.
270
+
271
+ **2. It switches on how *big* the block is, not how *many* tools you have.**
272
+
273
+ A tool can be 20 tokens or 460, so counting them tells you very little:
274
+
275
+ | Tool set | Level 1 block | Recommends |
276
+ |---|---:|:-:|
277
+ | 72 tools, one parameter each | ~5,000 tokens | **1** |
278
+ | 200 tools, one parameter each | ~14,100 tokens | **3** |
279
+ | **40** real MCP tools | ~10,600 tokens | **3** |
280
+ | 149 real MCP tools | ~42,700 tokens | **3** |
281
+
282
+ Forty chatty tools cross the line while 72 terse ones don't. The threshold is **10,000
283
+ tokens** — about 5% of a 200k window. Below that, reclaiming the block doesn't change
284
+ what fits, so keeping the provider's own argument validation is worth more than the
285
+ saving.
286
+
287
+ ### The levels in full
288
+
229
289
  Ask the library. It returns 1 or 3, never 2, and explains itself:
230
290
 
231
291
  ```ts
@@ -302,6 +362,11 @@ Level 3 has several map styles. Which one is cheapest turns out to depend on the
302
362
  model, so you can hand `compress()` a model id and let it use what was actually
303
363
  measured:
304
364
 
365
+ > **This is the one thing the library does choose for you, and only if you pass
366
+ > `model`.** Note the difference from levels: a map style is a pure encoding choice, so
367
+ > picking a better one cannot change your results. The level *can* — level 3 hands
368
+ > argument validation from the provider to toolgz — so that stays your explicit call.
369
+
305
370
  ```ts
306
371
  compress(myTools, { level: 3, model: "gpt-5.6-sol", objective: "cost" });
307
372
  ```
@@ -783,8 +848,13 @@ Use `forGemini`, which strips the keywords Gemini won't accept. If a new one
783
848
  appears, it's a one-line addition to the adapter.
784
849
 
785
850
  **Savings look small.**
786
- Check tool count and shape with `recommendLevel(myTools)`. Under ~15 tools
787
- there's little to reclaim. Also check `c.stats`:
851
+ First check you actually passed a level: `compress(myTools)` is **level 1**, and it
852
+ stays level 1 no matter how many tools you hand it. `recommendLevel` advises, it does
853
+ not act — you have to pass its answer in as `{ level }`.
854
+
855
+ If you did, ask `recommendLevel(myTools)` and read the `reason`. It reports the size of
856
+ your level-1 block, and under ~10,000 tokens there is little worth reclaiming. Also
857
+ check `c.stats`:
788
858
 
789
859
  ```ts
790
860
  console.log(c.stats);
@@ -885,7 +955,15 @@ Returns:
885
955
 
886
956
  ### `recommendLevel(tools, namespaceOf?) → Recommendation`
887
957
 
888
- `{ level, reason, toolCount, namespaceCount, opsPerNamespace }`. Returns 1 or 3.
958
+ `{ level, reason, toolCount, namespaceCount, opsPerNamespace }`. Returns 1 or 3, never 2.
959
+
960
+ **It advises; it does not act.** Pass the answer back in yourself —
961
+ `compress(tools, { level })`. Calling `compress(tools)` alone is level 1 regardless of
962
+ how large `tools` is.
963
+
964
+ The decision is on the **size** of the level-1 block (threshold 10,000 tokens ≈ 5% of a
965
+ 200k window), not on `toolCount`. The three shape fields are reported for your own
966
+ logging; only block size drives the level.
889
967
 
890
968
  ### Provider adapters
891
969
 
@@ -974,8 +1052,13 @@ xAI is OpenAI-compatible — use `forOpenAI` with `baseURL: "https://api.x.ai/v1
974
1052
  - **It has not been measured on a non-frontier model at level 3.** On Haiku 4.5, argument
975
1053
  errors rose sharply (17 of 30 runs) — all caught and retried, no task lost, but that is the
976
1054
  known edge.
977
- - **It is not magic on ten tools.** Under ~15 tools there is little to reclaim;
978
- `recommendLevel()` will tell you so.
1055
+ - **It is not magic on a small tool block.** Under ~10,000 tokens at level 1 there is
1056
+ little worth reclaiming, and `recommendLevel()` will say so and keep you on level 1.
1057
+ That is usually a small number of tools, but not always — it depends on how verbose
1058
+ your schemas are, not on the count.
1059
+ - **It does not pick a level for you.** `compress()` defaults to level 1 and stays there;
1060
+ reaching level 3 is always an explicit `{ level }`. Deliberate — level 3 trades away
1061
+ provider-side schema enforcement, and that is not a trade to make behind your back.
979
1062
 
980
1063
  ## Determinism
981
1064
 
@@ -31,9 +31,19 @@ export type Recommendation = {
31
31
  * slower and dearer than level 3. It is never recommended. It stays in the
32
32
  * API for callers who need real operation names on the wire.
33
33
  *
34
- * The level 1 → 3 threshold is driven by tools-per-namespace, not tool count:
35
- * the compound-dispatcher overhead is paid per namespace, so a wide, sparse
36
- * tool set stays cheaper at level 1 even when the total count is large.
34
+ * The level 1 → 3 threshold is an absolute size test on the level-1 block
35
+ * neither tool count nor namespace shape. A tool is 20 to 460 tokens depending
36
+ * on how verbose its schema is, so 40 real MCP tools cross the line while 72
37
+ * one-parameter tools do not.
38
+ *
39
+ * An earlier version gated on tools-per-namespace. That is a *level 2* question
40
+ * (level 2 pays dispatcher overhead per namespace); level 3 uses one flat
41
+ * dispatcher and is indifferent to shape. See the comment on THRESHOLD_TOKENS.
42
+ *
43
+ * This function only ever *advises*. `compress()` defaults to level 1 and stays
44
+ * there unless the caller passes a level explicitly — level 3 trades away the
45
+ * provider's own schema enforcement, and that is not a trade to make on a
46
+ * caller's behalf because their tool array grew.
37
47
  */
38
48
  export declare function recommendLevel(tools: Tool[], namespaceOf?: (n: string) => {
39
49
  ns: string;
package/dist/recommend.js CHANGED
@@ -25,9 +25,19 @@ import { compress } from "./index.js";
25
25
  * slower and dearer than level 3. It is never recommended. It stays in the
26
26
  * API for callers who need real operation names on the wire.
27
27
  *
28
- * The level 1 → 3 threshold is driven by tools-per-namespace, not tool count:
29
- * the compound-dispatcher overhead is paid per namespace, so a wide, sparse
30
- * tool set stays cheaper at level 1 even when the total count is large.
28
+ * The level 1 → 3 threshold is an absolute size test on the level-1 block
29
+ * neither tool count nor namespace shape. A tool is 20 to 460 tokens depending
30
+ * on how verbose its schema is, so 40 real MCP tools cross the line while 72
31
+ * one-parameter tools do not.
32
+ *
33
+ * An earlier version gated on tools-per-namespace. That is a *level 2* question
34
+ * (level 2 pays dispatcher overhead per namespace); level 3 uses one flat
35
+ * dispatcher and is indifferent to shape. See the comment on THRESHOLD_TOKENS.
36
+ *
37
+ * This function only ever *advises*. `compress()` defaults to level 1 and stays
38
+ * there unless the caller passes a level explicitly — level 3 trades away the
39
+ * provider's own schema enforcement, and that is not a trade to make on a
40
+ * caller's behalf because their tool array grew.
31
41
  */
32
42
  export function recommendLevel(tools, namespaceOf = defaultNamespaceOf) {
33
43
  const namespaces = new Set(tools.map((t) => namespaceOf(t.name).ns));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolgz",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "Compress LLM tool definitions to reclaim context window. Measured across Anthropic, OpenAI, Google and xAI.",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {