toolgz 0.2.10 → 0.2.11
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 +34 -10
- package/dist/recommend.d.ts +5 -0
- package/dist/recommend.js +28 -2
- package/llms.txt +8 -2
- package/package.json +3 -2
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 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>
|
|
5
|
+
<p><em>On a large tool set, at level 3. The default (level 1) reclaims 13–39% on real-world tool definitions 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> ·
|
|
@@ -44,7 +44,7 @@ Reclaiming the room is what this does.
|
|
|
44
44
|
```ts
|
|
45
45
|
import { compress, recommendLevel, forAnthropic } from "toolgz";
|
|
46
46
|
|
|
47
|
-
const { level } = recommendLevel(myTools); // advice: 1
|
|
47
|
+
const { level } = recommendLevel(myTools); // advice: 0, 1 or 3 — see below
|
|
48
48
|
const c = compress(myTools, { level }); // your existing MCP/SDK tool array
|
|
49
49
|
const { tools, system } = forAnthropic(c); // send these instead
|
|
50
50
|
```
|
|
@@ -54,6 +54,11 @@ provider schema enforcement intact, and 13–39% smaller (13–32% on the synthe
|
|
|
54
54
|
**level 3**, which is what `recommendLevel` returns once a tool set is big enough to
|
|
55
55
|
amortise the dispatcher.
|
|
56
56
|
|
|
57
|
+
Level 1 earns that by stripping prose, so it needs prose to strip: if your descriptions
|
|
58
|
+
are already one short sentence, it **adds** ~15% instead and `recommendLevel` returns
|
|
59
|
+
**0** — leave the tools alone. Real MCP catalogues are verbose, which is why they land at
|
|
60
|
+
the top of that range.
|
|
61
|
+
|
|
57
62
|
**Note the shape of those three lines: `recommendLevel` only advises, and you pass its
|
|
58
63
|
answer back in.** Nothing upgrades itself — `compress(myTools)` is level 1 whether you
|
|
59
64
|
have 2 tools or 500. See [Which level to use](#which-level-to-use) for why.
|
|
@@ -78,8 +83,10 @@ committed in [`bench/results/`](bench/results/); recompute any figure with
|
|
|
78
83
|
|
|
79
84
|
**The table below uses a synthetic catalogue** — 100 realistic-but-invented MCP-style
|
|
80
85
|
tools across 9 namespaces — because it lets us build deliberately confusable clusters
|
|
81
|
-
that a real catalogue may not contain.
|
|
82
|
-
|
|
86
|
+
that a real catalogue may not contain. Four of the five scenarios draw a 30-tool
|
|
87
|
+
confusable subset from it; the fifth (`acc-haystack`) uses all 100.
|
|
88
|
+
|
|
89
|
+
Two things follow from that, and the first is uncomfortable:
|
|
83
90
|
|
|
84
91
|
- Synthetic naming can flatter a compression style. One map style measured −21% on
|
|
85
92
|
this fixture because every tool name carried a `namespace_op` prefix to factor out.
|
|
@@ -206,7 +213,7 @@ the tool name that compression takes away.
|
|
|
206
213
|
|
|
207
214
|
<picture>
|
|
208
215
|
<source media="(prefers-color-scheme: dark)" srcset="docs/img/reliability-dark.svg">
|
|
209
|
-
<img src="docs/img/reliability-light.svg" alt="Task completion by level-3 map style and provider
|
|
216
|
+
<img src="docs/img/reliability-light.svg" alt="Task completion by level-3 map style and provider. Bare names fail on grok-4.5; that style and the terse-description style were removed in 0.2.0 and are marked as such">
|
|
210
217
|
</picture>
|
|
211
218
|
|
|
212
219
|
The model doesn't lose the ability to choose — it converts a recall problem into a retrieval
|
|
@@ -214,6 +221,10 @@ problem and looks up what it needs. The default map style exists because of the
|
|
|
214
221
|
bare tool names failed on `grok-4.5` **deterministically**, 3 of 3 attempts on one scenario,
|
|
215
222
|
answering with zero tool calls and no error raised. Naming the required arguments fixed it.
|
|
216
223
|
|
|
224
|
+
Two of the four rows are marked **(removed)**: those styles were deleted in 0.2.0 and you
|
|
225
|
+
cannot select them. They are charted anyway because they *are* the evidence for the
|
|
226
|
+
default — deleting the losing arms would delete the reason.
|
|
227
|
+
|
|
217
228
|
### How we found the cost story, and got it wrong twice
|
|
218
229
|
|
|
219
230
|
<picture>
|
|
@@ -243,6 +254,11 @@ tried and removed in 0.2.0 because they were smaller and still worse.
|
|
|
243
254
|
|
|
244
255
|
### The whole idea, in plain English
|
|
245
256
|
|
|
257
|
+
<picture>
|
|
258
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/img/metaphor-dark.svg">
|
|
259
|
+
<img src="docs/img/metaphor-light.svg" alt="Three panels. Level 0: the full menu, 149 tools on the wire, nothing saved, kitchen checks the order. Level 1: the same menu with prose cut, 149 tools on the wire, 45.2% smaller, kitchen still checks the order. Level 3: a small numbered card and one waiter, 2 tools on the wire, 96.5% smaller, toolgz checks the order instead of the provider.">
|
|
260
|
+
</picture>
|
|
261
|
+
|
|
246
262
|
Your tool definitions are a **menu** handed to the model at the start of every single
|
|
247
263
|
conversation. It is long, and most of it is flowery prose about each dish.
|
|
248
264
|
|
|
@@ -267,7 +283,7 @@ So: **level 1 is smaller. Level 3 is much smaller and you take over order-checki
|
|
|
267
283
|
compress(myTools) // level 1. ALWAYS — 2 tools or 500.
|
|
268
284
|
compress(myTools, { level: 3 }) // level 3, because you asked for it
|
|
269
285
|
|
|
270
|
-
const { level } = recommendLevel(myTools); // just advice: returns 1 or 3
|
|
286
|
+
const { level } = recommendLevel(myTools); // just advice: returns 0, 1 or 3
|
|
271
287
|
compress(myTools, { level }); // now it's 3, because you passed it in
|
|
272
288
|
```
|
|
273
289
|
|
|
@@ -297,7 +313,7 @@ saving.
|
|
|
297
313
|
|
|
298
314
|
### The levels in full
|
|
299
315
|
|
|
300
|
-
Ask the library. It returns 1 or 3
|
|
316
|
+
Ask the library. It returns 0, 1 or 3 — never 2 — and explains itself:
|
|
301
317
|
|
|
302
318
|
```ts
|
|
303
319
|
import { recommendLevel } from "toolgz";
|
|
@@ -964,13 +980,21 @@ Returns:
|
|
|
964
980
|
> Use it as a local signal. For anything you publish, measure with your provider's token
|
|
965
981
|
> counter. `originalChars` and `compressedChars` are on `stats` for the raw counts.
|
|
966
982
|
>
|
|
967
|
-
> A negative value is possible and correct
|
|
968
|
-
>
|
|
983
|
+
> A negative value is possible and correct. Level 1 pays for a signature line and gets
|
|
984
|
+
> paid by the prose it strips, so on tools whose descriptions are already terse it comes
|
|
985
|
+
> out **~15% larger** — `recommendLevel` returns 0 in that case rather than recommending
|
|
986
|
+
> it. Even level 0 reports **−0.6%**: `c.tools` is Anthropic-shaped, and `input_schema` is
|
|
987
|
+
> one character longer than the `inputSchema` most callers pass in. Structural, not waste.
|
|
969
988
|
| `encodeCallForTest(name, args)` | `→ {name, args}` | build the raw call a model would emit; test aid |
|
|
970
989
|
|
|
971
990
|
### `recommendLevel(tools, namespaceOf?) → Recommendation`
|
|
972
991
|
|
|
973
|
-
`{ level, reason, toolCount, namespaceCount, opsPerNamespace }`. Returns 1 or 3, never 2.
|
|
992
|
+
`{ level, reason, toolCount, namespaceCount, opsPerNamespace }`. Returns 0, 1 or 3, never 2.
|
|
993
|
+
|
|
994
|
+
**`level: 0` means this library has nothing to offer your tool set.** Level 1 would
|
|
995
|
+
inflate it — terse descriptions leave no prose to strip, so the signature line is pure
|
|
996
|
+
addition — and the block is too small for level 3's trade to be worth it. Telling you that
|
|
997
|
+
is more useful than recommending a transform that makes the payload bigger.
|
|
974
998
|
|
|
975
999
|
**It advises; it does not act.** Pass the answer back in yourself —
|
|
976
1000
|
`compress(tools, { level })`. Calling `compress(tools)` alone is level 1 regardless of
|
package/dist/recommend.d.ts
CHANGED
|
@@ -44,6 +44,11 @@ export type Recommendation = {
|
|
|
44
44
|
* there unless the caller passes a level explicitly — level 3 trades away the
|
|
45
45
|
* provider's own schema enforcement, and that is not a trade to make on a
|
|
46
46
|
* caller's behalf because their tool array grew.
|
|
47
|
+
*
|
|
48
|
+
* Returns 0, 1 or 3 — never 2. **0 means "this library has nothing to offer
|
|
49
|
+
* here"**: level 1 would inflate the block, and it is too small for level 3's
|
|
50
|
+
* trade to be worth it. Saying so is more useful than recommending a transform
|
|
51
|
+
* that makes the payload bigger.
|
|
47
52
|
*/
|
|
48
53
|
export declare function recommendLevel(tools: Tool[], namespaceOf?: (n: string) => {
|
|
49
54
|
ns: string;
|
package/dist/recommend.js
CHANGED
|
@@ -38,6 +38,11 @@ import { compress } from "./index.js";
|
|
|
38
38
|
* there unless the caller passes a level explicitly — level 3 trades away the
|
|
39
39
|
* provider's own schema enforcement, and that is not a trade to make on a
|
|
40
40
|
* caller's behalf because their tool array grew.
|
|
41
|
+
*
|
|
42
|
+
* Returns 0, 1 or 3 — never 2. **0 means "this library has nothing to offer
|
|
43
|
+
* here"**: level 1 would inflate the block, and it is too small for level 3's
|
|
44
|
+
* trade to be worth it. Saying so is more useful than recommending a transform
|
|
45
|
+
* that makes the payload bigger.
|
|
41
46
|
*/
|
|
42
47
|
export function recommendLevel(tools, namespaceOf = defaultNamespaceOf) {
|
|
43
48
|
const namespaces = new Set(tools.map((t) => namespaceOf(t.name).ns));
|
|
@@ -53,7 +58,8 @@ export function recommendLevel(tools, namespaceOf = defaultNamespaceOf) {
|
|
|
53
58
|
// real MCP tools the ratio is a stable ~2.1 chars/token (1.98 at 10 tools, 2.15 at
|
|
54
59
|
// 149), so dividing gives a fair local estimate with no API call.
|
|
55
60
|
const CHARS_PER_TOKEN = 2.1;
|
|
56
|
-
const
|
|
61
|
+
const l1 = compress(tools, { level: 1 });
|
|
62
|
+
const l1Tokens = Math.round(l1.stats.compressedChars / CHARS_PER_TOKEN);
|
|
57
63
|
// The old heuristic gated level 3 on `opsPerNamespace >= 4`. That is a LEVEL-2
|
|
58
64
|
// question: level 2 pays dispatcher overhead per namespace, so its shape matters
|
|
59
65
|
// there. Level 3 uses one flat dispatcher and does not care about namespaces at all.
|
|
@@ -73,10 +79,30 @@ export function recommendLevel(tools, namespaceOf = defaultNamespaceOf) {
|
|
|
73
79
|
// 4,000 threshold for pushing 14-tool sets into dispatcher mode; they were right.
|
|
74
80
|
const THRESHOLD_TOKENS = 10000;
|
|
75
81
|
if (l1Tokens < THRESHOLD_TOKENS) {
|
|
82
|
+
// Level 1 is not unconditionally free, and this function used to assume it was.
|
|
83
|
+
//
|
|
84
|
+
// Level 1 saves by truncating each description to its first sentence and stripping
|
|
85
|
+
// per-property prose from the schema, and it spends by prepending a signature line.
|
|
86
|
+
// On a real MCP catalogue the prose dominates and it nets ~45%. On tools whose
|
|
87
|
+
// descriptions are already one short sentence there is nothing to strip, the
|
|
88
|
+
// signature is pure addition, and level 1 comes out ~15% LARGER than doing nothing.
|
|
89
|
+
//
|
|
90
|
+
// Reported by an external reviewer at 25-1,000 generated tools; they read it as
|
|
91
|
+
// correct behaviour for level 1, which it is. What is not correct is *recommending*
|
|
92
|
+
// it: a recommendation that inflates the payload is worse than no recommendation.
|
|
93
|
+
// So say so, and name level 3 as the real alternative rather than implying the
|
|
94
|
+
// choice is between level 1 and nothing.
|
|
95
|
+
if (l1.stats.savedPct <= 0) {
|
|
96
|
+
return {
|
|
97
|
+
...base,
|
|
98
|
+
level: 0,
|
|
99
|
+
reason: `Level 1 would make this block ${Math.abs(l1.stats.savedPct).toFixed(1)}% LARGER, not smaller — your descriptions are already terse, so there is no prose to strip and level 1's signature line is pure overhead. Leave the tools alone. If you want the room anyway, level 3 does compress this (${compress(tools, { level: 3 }).stats.savedPct.toFixed(1)}%), at the cost of the provider's own argument validation; at ~${l1Tokens.toLocaleString()} tokens that trade is usually not worth it.`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
76
102
|
return {
|
|
77
103
|
...base,
|
|
78
104
|
level: 1,
|
|
79
|
-
reason: `The tool block is only ~${l1Tokens.toLocaleString()} tokens at level 1. Level 3 would be smaller, but not by enough to be worth giving up the provider's own argument validation, which level 1 keeps. Reach for level 3 when the block is large enough that reclaiming it changes what fits in your context.`,
|
|
105
|
+
reason: `The tool block is only ~${l1Tokens.toLocaleString()} tokens at level 1, which saves ${l1.stats.savedPct.toFixed(1)}%. Level 3 would be smaller, but not by enough to be worth giving up the provider's own argument validation, which level 1 keeps. Reach for level 3 when the block is large enough that reclaiming it changes what fits in your context.`,
|
|
80
106
|
};
|
|
81
107
|
}
|
|
82
108
|
return {
|
package/llms.txt
CHANGED
|
@@ -75,12 +75,18 @@ before toolgz existed.
|
|
|
75
75
|
|
|
76
76
|
## Choosing a level
|
|
77
77
|
|
|
78
|
-
Ask, don't guess: `recommendLevel(myTools)` returns `{ level, reason }` — 1 or 3
|
|
78
|
+
Ask, don't guess: `recommendLevel(myTools)` returns `{ level, reason }` — **0, 1 or 3**,
|
|
79
|
+
never 2.
|
|
80
|
+
|
|
81
|
+
`level: 0` means *leave the tools alone*: level 1 would make this block larger, and it is
|
|
82
|
+
too small for level 3 to be worth giving up provider-side validation. Level 1 is not
|
|
83
|
+
unconditionally free — it saves by stripping prose, so on tools whose descriptions are
|
|
84
|
+
already one short sentence it adds ~15% instead. Do not override a 0 without measuring.
|
|
79
85
|
|
|
80
86
|
| Level | Wire | Names | Provider schema enforcement | When |
|
|
81
87
|
|:-:|---|:-:|:-:|---|
|
|
82
88
|
| 0 | unchanged | real | yes | A/B control only |
|
|
83
|
-
| **1** | one native tool each, flattened schemas | real | **yes** | **default.** Reclaims 13–39
|
|
89
|
+
| **1** | one native tool each, flattened schemas | real | **yes** | **default.** Reclaims 13–39% on verbose real-world schemas; **inflates ~15% on already-terse ones** |
|
|
84
90
|
| 2 | one tool per namespace | real | no | only if you need real op names on the wire |
|
|
85
91
|
| **3** | one dispatcher + one lookup | codes | no | **large tool sets.** Up to ~85–96% |
|
|
86
92
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "toolgz",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.11",
|
|
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": {
|
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
"test:live": "vitest run tests/integration",
|
|
13
13
|
"bench:tier1": "tsx -r dotenv/config bench/harness/run-multi.ts --provider=all --suite=real --variants --reps=1 --scenario=real-daily-vs-weekly,real-overwrite-vs-append,real-status-vs-result,real-rollup-vs-detail",
|
|
14
14
|
"bench:tier2": "tsx -r dotenv/config bench/harness/run-multi.ts --provider=all --suite=real --variants --reps=2 --scenario=real-daily-vs-weekly,real-overwrite-vs-append,real-status-vs-result,real-rollup-vs-detail,real-two-step-scheduling,real-summary-vs-details",
|
|
15
|
-
"bench:tier3": "tsx -r dotenv/config bench/harness/run-multi.ts --provider=all --suite=real --variants --reps=3"
|
|
15
|
+
"bench:tier3": "tsx -r dotenv/config bench/harness/run-multi.ts --provider=all --suite=real --variants --reps=3",
|
|
16
|
+
"img:metaphor": "tsx bench/metaphor.ts"
|
|
16
17
|
},
|
|
17
18
|
"keywords": [
|
|
18
19
|
"llm",
|