toolgz 0.2.9 โ†’ 0.2.10

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 (3) hide show
  1. package/README.md +15 -0
  2. package/llms.txt +205 -0
  3. package/package.json +2 -1
package/README.md CHANGED
@@ -15,6 +15,17 @@ zero runtime dependencies ยท
15
15
  npm install toolgz
16
16
  ```
17
17
 
18
+ > ### ๐Ÿค– If a coding agent is reading this
19
+ >
20
+ > Point it at **[`llms.txt`](llms.txt)** โ€” prescriptive integration instructions written
21
+ > for an agent, shipped inside the package at `node_modules/toolgz/llms.txt`. It covers
22
+ > the three-step contract, the rules that are not optional, per-model behaviour, and a
23
+ > `Do not` list of the mistakes we have actually fielded.
24
+ >
25
+ > ```
26
+ > Read node_modules/toolgz/llms.txt before integrating toolgz.
27
+ > ```
28
+
18
29
  ---
19
30
 
20
31
  ## The problem
@@ -421,6 +432,10 @@ than a bug report from you.
421
432
  npx tsx examples/01-minimal.ts
422
433
  ```
423
434
 
435
+ **[`examples/README.md`](examples/README.md) walks through all five** with their real
436
+ output and what each one is trying to teach โ€” start there if you would rather read than
437
+ run.
438
+
424
439
  | File | Shows |
425
440
  |---|---|
426
441
  | [`01-minimal.ts`](examples/01-minimal.ts) | the smallest useful thing: `recommendLevel` โ†’ `compress` โ†’ `resolve` |
package/llms.txt ADDED
@@ -0,0 +1,205 @@
1
+ # toolgz
2
+
3
+ > Compresses LLM tool definitions to reclaim context window. Zero runtime dependencies,
4
+ > TypeScript, Apache-2.0. Works with any model and any framework: it transforms a tool
5
+ > array before you send it, and translates the model's call back afterwards.
6
+
7
+ **This file is written for a coding agent integrating toolgz into a project.** It is
8
+ prescriptive on purpose. If you are contributing to toolgz itself, read `AGENTS.md` in the
9
+ repository instead โ€” different audience, different rules.
10
+
11
+ Installed copy lives at `node_modules/toolgz/llms.txt`. Full prose docs: `README.md`.
12
+
13
+ ---
14
+
15
+ ## The whole integration, correctly
16
+
17
+ ```ts
18
+ import { compress, recommendLevel, forAnthropic } from "toolgz";
19
+
20
+ // 1. Decide the level ONCE, at startup โ€” not per request.
21
+ const { level } = recommendLevel(myTools);
22
+ const c = compress(myTools, { level });
23
+
24
+ // 2. Send c.tools instead of your tool array, and append the preamble to your system prompt.
25
+ const { tools, system } = forAnthropic(c);
26
+
27
+ // 3. Translate every tool call back before you dispatch.
28
+ for (const block of res.content.filter((b) => b.type === "tool_use")) {
29
+ const r = c.resolve(block.name, block.input);
30
+ if (r.kind === "call") {
31
+ const out = await myDispatch(r.name, r.args); // real name, real args
32
+ results.push({ type: "tool_result", tool_use_id: block.id, content: JSON.stringify(out) });
33
+ } else if (r.kind === "meta") {
34
+ // toolgz answered a lookup itself. DO NOT dispatch. Return its text to the model.
35
+ results.push({ type: "tool_result", tool_use_id: block.id, content: r.result });
36
+ } else {
37
+ // Recoverable by design: hand the message back and the model retries.
38
+ results.push({ type: "tool_result", tool_use_id: block.id, content: `Error: ${r.message}`, is_error: true });
39
+ }
40
+ }
41
+ ```
42
+
43
+ Those three steps are the entire contract. `myDispatch` receives exactly what it received
44
+ before toolgz existed.
45
+
46
+ ## Rules that are not optional
47
+
48
+ 1. **`resolve()` every tool call.** At level 3 the model emits opaque codes (`t(f="a0")`),
49
+ so dispatching `block.name` directly will fail or dispatch the wrong tool. Route
50
+ everything through `resolve()` even at level 0, so the level becomes a one-line change.
51
+
52
+ 2. **Handle all three `kind` values.** `call` โ†’ dispatch. `meta` โ†’ toolgz already answered
53
+ a lookup; return `r.result` to the model and dispatch nothing. `error` โ†’ return
54
+ `r.message` to the model as an error tool_result; do **not** throw. The `error` path is
55
+ the recovery mechanism, not a failure.
56
+
57
+ 3. **Send the system preamble.** At level 3 the tool map lives in the system prompt. Drop
58
+ it and the model has no idea what any code means. It is `""` at levels 0โ€“1, so
59
+ appending it unconditionally is always safe.
60
+
61
+ 4. **`compress()` never changes level on its own.** `compress(tools)` is level 1 forever,
62
+ 2 tools or 500. `recommendLevel()` *advises*; you pass its answer in as `{ level }`.
63
+ Deliberate: level 3 moves argument validation from the provider to toolgz, which is not
64
+ a change to make behind a caller's back.
65
+
66
+ 5. **Leave `validate` on** (the default). Levels 2โ€“3 give up the provider's constrained
67
+ decoding, so toolgz validating against the original schema is what catches malformed
68
+ arguments. On a weak model, disabling it converts roughly half of all runs from a
69
+ recovered retry into a bad dispatch.
70
+
71
+ 6. **Compress once, reuse the result.** `compress()` is deterministic โ€” same tools in,
72
+ byte-identical payload out โ€” so the output is prompt-cache friendly. Recomputing per
73
+ request is wasted work; changing the tool list mid-conversation invalidates the cache
74
+ and strands codes the model has already seen.
75
+
76
+ ## Choosing a level
77
+
78
+ Ask, don't guess: `recommendLevel(myTools)` returns `{ level, reason }` โ€” 1 or 3, never 2.
79
+
80
+ | Level | Wire | Names | Provider schema enforcement | When |
81
+ |:-:|---|:-:|:-:|---|
82
+ | 0 | unchanged | real | yes | A/B control only |
83
+ | **1** | one native tool each, flattened schemas | real | **yes** | **default.** Reclaims 13โ€“39%, gives up nothing |
84
+ | 2 | one tool per namespace | real | no | only if you need real op names on the wire |
85
+ | **3** | one dispatcher + one lookup | codes | no | **large tool sets.** Up to ~85โ€“96% |
86
+
87
+ The switch is the **size** of the level-1 block, not the tool count โ€” threshold 10,000
88
+ tokens (~5% of a 200k window). A tool is 20 to 460 tokens depending on schema verbosity,
89
+ so 40 real MCP tools cross it while 72 one-parameter tools do not.
90
+
91
+ Level 2 is dominated by level 3 on accuracy (six times the malformed arguments) and is
92
+ never recommended. It stays in the API for callers who need real operation names.
93
+
94
+ ## Provider adapters โ€” pick the right one
95
+
96
+ ```ts
97
+ forAnthropic(c, { cache?, ttl? }) // โ†’ { tools, system } Messages API
98
+ forOpenAI(c) // โ†’ { tools, systemPreamble } /v1/chat/completions
99
+ forOpenAIResponses(c) // โ†’ { tools, systemPreamble } /v1/responses
100
+ forGemini(c) // โ†’ { tools, systemPreamble } generateContent
101
+ ```
102
+
103
+ Two mistakes that produce confusing provider errors:
104
+
105
+ - **OpenAI has two incompatible tool shapes.** `/v1/chat/completions` wants nested
106
+ `{type, function:{โ€ฆ}}`; `/v1/responses` wants flat `{type, name, โ€ฆ}`. If you set
107
+ reasoning effort on a GPT-5.x model you must use `/v1/responses`, therefore
108
+ `forOpenAIResponses`. Using the wrong adapter is rejected at the API.
109
+ - **Gemini returns ONE wrapper object,** not one entry per tool. Read
110
+ `tools[0].functionDeclarations.length`; `tools.length` is always 1.
111
+
112
+ `forGemini` also repairs three schema forms Gemini rejects with a 400: an array-valued
113
+ `type`, `type: "array"` with no `items`, and a non-string `enum`. Constraints stripped to
114
+ satisfy Gemini are still enforced by `resolve()` against your original schema โ€” nothing is
115
+ silently dropped.
116
+
117
+ xAI is OpenAI-compatible: use `forOpenAI` with `baseURL: "https://api.x.ai/v1"`. **xAI
118
+ caps tool arrays at 350**; other providers accept 1,200+. Level 3 sends 2 either way.
119
+
120
+ ## Model-specific behaviour
121
+
122
+ Pass an exact model id and toolgz uses the best *measured* map style for it:
123
+
124
+ ```ts
125
+ compress(myTools, { level: 3, model: "gpt-5.6-sol", objective: "cost" });
126
+ ```
127
+
128
+ | Model | `objective: "cost"` | Measured vs default |
129
+ |---|---|---:|
130
+ | `gpt-5.6-sol` | `explicit` | โˆ’20.7% |
131
+ | `gemini-3.1-pro-preview` | `explicit` | โˆ’15.4% |
132
+ | `claude-opus-5` | `explicit` | โˆ’9.0% |
133
+ | `grok-4.5` | *(no entry)* | `explicit` is **+13.2%** there |
134
+
135
+ - Model ids are **exact, never families**. A result for `gpt-5.6-sol` says nothing about
136
+ `gpt-5.7`. An unknown model gets the conservative default โ€” absence of evidence, not a
137
+ prediction.
138
+ - The default objective is `occupancy`, which has **no** entries: nothing beat the default
139
+ by more than 3.1% on that axis, under the 5% effect-size floor. Only `cost` has rows.
140
+ - This is the one thing the library picks for you, and only when you pass `model`. A map
141
+ style is a pure encoding choice, so a better one cannot change results. The *level*
142
+ can โ€” which is why that stays explicit.
143
+ - Do not set `mapStyle` by hand unless you have measured your own workload. Prefer
144
+ `model` + `objective`.
145
+
146
+ ## MCP servers
147
+
148
+ `tools/list` returns `{ name, description, inputSchema }` per tool โ€” exactly what
149
+ `compress()` accepts. **There is no adapter layer.** Merge the arrays from every server
150
+ and pass them in.
151
+
152
+ `input_schema` and `inputSchema` are both accepted, so Anthropic-shaped and MCP-shaped
153
+ tools can be mixed.
154
+
155
+ Watch for name collisions when merging catalogues from independent servers. If two tool
156
+ names normalise identically, toolgz refuses to alias either one rather than guess โ€” the
157
+ map code still works.
158
+
159
+ ## API surface
160
+
161
+ ```ts
162
+ compress(tools, options?) โ†’ CompressResult
163
+ recommendLevel(tools, namespaceOf?) โ†’ { level, reason, toolCount, namespaceCount, opsPerNamespace }
164
+ selectMapStyle(options) โ†’ { mapStyle, requestedMapStyle?, fallbackReason? } // pure
165
+ ```
166
+
167
+ `CompressOptions`: `level`, `mapStyle`, `namespaceOf`, `aliasOf`, `searchLimit`,
168
+ `validate`, `model`, `objective`.
169
+
170
+ `CompressResult`: `tools`, `systemPreamble`, `cachePreamble`, `resolve()`, `codeFor()`,
171
+ `encodeCallForTest()`, `stats`.
172
+
173
+ `stats`: `level`, `mapStyle`, `requestedMapStyle`, `fallbackReason`, `toolCount`,
174
+ `wireToolCount`, `originalChars`, `compressedChars`, `savedPct`.
175
+
176
+ `namespaceOf` must return `{ ns, op }`, **not** a bare string. Returning a string throws
177
+ with a corrected example โ€” it used to fail silently and produce a plausible-looking but
178
+ wrong map.
179
+
180
+ ## Things to tell the user, not assume
181
+
182
+ - **`savedPct` is a CHARACTER saving**, a few points optimistic against tokens. Providers
183
+ charge a fixed framing cost per tool definition that character counting cannot see. For
184
+ any published figure, measure with the provider's own token counter.
185
+ - **Never use `tiktoken` to count Claude tokens** โ€” it is OpenAI's tokenizer and is wrong
186
+ for Claude by 15โ€“20%+. Use Anthropic's `count_tokens`.
187
+ - **Token saving โ‰  cost saving.** Prompt caching already makes tool tokens cheap; the claim
188
+ here is context-window *occupancy*. Cost follows by a variable amount, and on OpenAI โ€”
189
+ where reasoning output dominates the bill โ€” it was as little as 7%.
190
+ - **Level 3 costs turns.** Roughly 0.3โ€“1.7 lookup calls per task, about half an extra turn.
191
+ If latency matters more than context, stay at level 1.
192
+ - **Below the frontier tier, argument formatting degrades at level 3.** On Haiku 4.5, 17 of
193
+ 30 runs produced a malformed argument โ€” all caught by `validate` and recovered, no task
194
+ lost, but that is the known edge. Tool *choice* did not degrade.
195
+
196
+ ## Do not
197
+
198
+ - Do not dispatch `block.name` without `resolve()`.
199
+ - Do not throw on `kind: "error"` โ€” return it to the model.
200
+ - Do not dispatch on `kind: "meta"`.
201
+ - Do not omit `systemPreamble` at level 3.
202
+ - Do not set `validate: false` to silence a malformed-argument error; fix the schema.
203
+ - Do not call `compress()` inside the request loop.
204
+ - Do not report `savedPct` as a token or cost saving.
205
+ - Do not assume a level was chosen for you.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "toolgz",
3
- "version": "0.2.9",
3
+ "version": "0.2.10",
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": {
@@ -45,6 +45,7 @@
45
45
  "files": [
46
46
  "dist",
47
47
  "README.md",
48
+ "llms.txt",
48
49
  "LICENSE",
49
50
  "NOTICE"
50
51
  ],