toolgz 0.2.11 → 0.2.12

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
@@ -438,6 +438,35 @@ style in 0.2.0.
438
438
 
439
439
  ---
440
440
 
441
+ ## See it run
442
+
443
+ A demo you can put on a screen. It prints every step — the tool definitions going in,
444
+ what `compress()` turns them into, the request, the model's actual response, the
445
+ `resolve()` translation, and your dispatcher running unchanged.
446
+
447
+ ```bash
448
+ npm run demo -- --level=3 # one level, every step
449
+ npm run demo -- --compare # levels 0, 1 and 3 back to back
450
+ npm run demo -- --level=3 --offline # no API key: scripted model, real library
451
+ ```
452
+
453
+ `--compare` ends with the side-by-side:
454
+
455
+ ```
456
+ level wire tools definition chars reclaimed turns argument checking
457
+ 0 6 → 6 3,283 → 3,289 -0.2% 2 provider
458
+ 1 6 → 6 3,283 → 1,999 39.1% 2 provider
459
+ 3 6 → 2 3,283 → 807 75.4% 2 toolgz
460
+ ```
461
+
462
+ It really runs the loop against a real model unless you pass `--offline`, and it says
463
+ which it did. Level 3 also demonstrates the two outcomes a happy path never shows —
464
+ a bad code coming back `kind: "error"` and recoverable, and a `q()` lookup coming back
465
+ `kind: "meta"` with nothing dispatched.
466
+
467
+ Not shipped in the npm package: it imports a provider SDK, which is a devDependency,
468
+ and the library's zero-runtime-dependency guarantee is not negotiable.
469
+
441
470
  ## Runnable examples
442
471
 
443
472
  Five files in [`examples/`](examples), all offline — no API key, no cost. Every one is
@@ -951,11 +980,37 @@ number can be recomputed rather than trusted.
951
980
  | `mapStyle` | `"name+required" \| "explicit" \| "signature"` | `"name+required"` | level 3 only |
952
981
  | `namespaceOf` | `(name) => {ns, op}` | split on first `_`/`.` | levels 2–3 grouping |
953
982
  | `aliasOf` | `(ns) => string` | identity | level 2 tool naming |
983
+ | `signaturePrefix` | `boolean` | `true` | level 1 only; see below |
954
984
  | `searchLimit` | `number` | `8` | max results from a `q` search |
955
985
  | `validate` | `boolean` | `true` | **leave this on** |
956
986
  | `model` | `string` | — | exact model id; picks the measured style. Omit and nothing changes |
957
987
  | `objective` | `"occupancy" \| "cost"` | `"occupancy"` | what the pick optimises. Only `cost` has entries |
958
988
 
989
+ #### `signaturePrefix` — measured, and not the default for a reason
990
+
991
+ Level 1 prepends `name(a,b?) — ` to each description while keeping the full
992
+ `input_schema`. That prefix restates the tool name, property names, required list, enums
993
+ and item types that the schema already carries, and it is **18.5% of the level-1
994
+ payload** on the real corpus. Setting it `false` makes level 1 strictly smaller — 45.2%
995
+ → **55.3%** on 149 real tools — and removes the case where level 1 *inflates* a terse
996
+ catalogue at all (−14.4% → −0.6%, which is level 0's own floor).
997
+
998
+ It stays `true` by default because size was not the question. Over 64 live runs, removing
999
+ it was a clear win on three providers and **not** on the fourth:
1000
+
1001
+ | Provider | Block | Median cost | Turns |
1002
+ |---|---:|---:|---|
1003
+ | `gemini-3.1-pro-preview` | −18.5% | −15.3% | 2.00 → 2.00 |
1004
+ | `gpt-5.6-sol` | −26.0% | −13.8% | 2.38 → 2.13 |
1005
+ | `grok-4.5` | −18.2% | −17.2% | 3.00 → 3.00 |
1006
+ | `claude-opus-5` | −18.3% | **+3.8%** | **3.88 → 4.63** |
1007
+
1008
+ On `claude-opus-5` the smaller block is spent on extra turns, and one turn is worth
1009
+ ~3,300 prompt tokens here. **Zero malformed arguments and zero hallucinated names on both
1010
+ arms, 64/64 tasks.** So it is safe either way; it is just not universally cheaper. Try it
1011
+ if you are not on Anthropic, and measure. Full write-up in
1012
+ [docs/RESULTS.md](docs/RESULTS.md) Round 7.
1013
+
959
1014
  Returns:
960
1015
 
961
1016
  | Field | Type | Notes |
package/dist/index.js CHANGED
@@ -228,11 +228,14 @@ export function compress(input, options = {}) {
228
228
  // Level 1 — signature flattening, native tools, real names
229
229
  // -------------------------------------------------------------------------
230
230
  if (level === 1) {
231
+ const withSig = options.signaturePrefix ?? true;
231
232
  const wire = tools.map((t) => {
232
233
  const d = firstSentence(t.description);
234
+ // With no description the signature is the only content there is, so it stays
235
+ // regardless — an empty description would leave the model nothing to read.
233
236
  return {
234
237
  name: t.name,
235
- description: d ? `${signatureLine(t)} — ${d}` : signatureLine(t),
238
+ description: withSig || !d ? (d ? `${signatureLine(t)} — ${d}` : signatureLine(t)) : d,
236
239
  input_schema: flattenSchema(t.schema),
237
240
  };
238
241
  });
package/dist/types.d.ts CHANGED
@@ -80,6 +80,24 @@ export type CompressOptions = {
80
80
  };
81
81
  /** Override the short alias used for a namespace at level 2. */
82
82
  aliasOf?: (ns: string) => string;
83
+ /**
84
+ * Level 1 only. Prepend `name(a,b?)` to each description. Default true, which is
85
+ * the historical behaviour and what every published level-1 figure was measured with.
86
+ *
87
+ * **Experimental, and measured before it may become a default.** The prefix is fully
88
+ * redundant with the `input_schema` level 1 retains — the model already has the tool
89
+ * name, the property names, the required list, the enums and the item types — and it
90
+ * costs 18.5% of the level-1 payload on the real 149-tool corpus. Setting this false
91
+ * makes level 1 strictly smaller and removes the case where level 1 *inflates* a
92
+ * terse catalogue.
93
+ *
94
+ * It is not the default because "smaller" is not the question. Every arm that
95
+ * measured clean — zero malformed arguments, no extra turns — had the prefix, and a
96
+ * one-line signature may be easier for a model to read than the equivalent JSON.
97
+ * Whether that matters at level 1, where the schema is present anyway, is a
98
+ * benchmark result and not yet in hand.
99
+ */
100
+ signaturePrefix?: boolean;
83
101
  /** Cap how many results a search/query meta-call returns. Default 8. */
84
102
  searchLimit?: number;
85
103
  /** Validate arguments against the original schema before dispatch. Default true. */
package/llms.txt CHANGED
@@ -170,8 +170,15 @@ recommendLevel(tools, namespaceOf?) → { level, reason, toolCount, namespaceCou
170
170
  selectMapStyle(options) → { mapStyle, requestedMapStyle?, fallbackReason? } // pure
171
171
  ```
172
172
 
173
- `CompressOptions`: `level`, `mapStyle`, `namespaceOf`, `aliasOf`, `searchLimit`,
174
- `validate`, `model`, `objective`.
173
+ `CompressOptions`: `level`, `mapStyle`, `namespaceOf`, `aliasOf`, `signaturePrefix`,
174
+ `searchLimit`, `validate`, `model`, `objective`.
175
+
176
+ `signaturePrefix` (level 1, default true) controls the `name(a,b?) — ` prefix on each
177
+ description. Setting it false makes level 1 smaller — 45.2% → 55.3% on real MCP tools —
178
+ and measured cheaper on Gemini, OpenAI and xAI over 64 runs. On `claude-opus-5` it was
179
+ **+3.8% dearer** because turns rose 3.88 → 4.63. Safe either way (zero malformed
180
+ arguments on both arms); not universally cheaper. Do not set it without measuring your
181
+ own workload.
175
182
 
176
183
  `CompressResult`: `tools`, `systemPreamble`, `cachePreamble`, `resolve()`, `codeFor()`,
177
184
  `encodeCallForTest()`, `stats`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolgz",
3
- "version": "0.2.11",
3
+ "version": "0.2.12",
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": {
@@ -13,7 +13,9 @@
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
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
+ "img:metaphor": "tsx bench/metaphor.ts",
17
+ "demo": "tsx demo/cli.ts",
18
+ "bench:noprefix": "tsx -r dotenv/config bench/harness/run-multi.ts --provider=all --suite=real --variants --arms=signatures,signatures-noprefix --reps=2 --scenario=real-daily-vs-weekly,real-overwrite-vs-append,real-status-vs-result,real-rollup-vs-detail"
17
19
  },
18
20
  "keywords": [
19
21
  "llm",