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 +91 -8
- package/dist/recommend.d.ts +13 -3
- package/dist/recommend.js +13 -3
- package/package.json +1 -1
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
787
|
-
|
|
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
|
|
978
|
-
`recommendLevel()` will
|
|
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
|
|
package/dist/recommend.d.ts
CHANGED
|
@@ -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
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
|
29
|
-
*
|
|
30
|
-
*
|
|
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));
|