Fail-closed compact renderer for structured LLM tool outputs.
Tool results are usually serialized as JSON, which repeats every object key on every row of a list. TOON (Token-Oriented Object Notation) collapses those repeated keys into a one-time column header, so arrays of uniform objects get smaller. But TOON is not a universal win — and this library exists because of that.
Measured against the o200k_base tokenizer (GPT-4o), on eight real agent-tool output shapes (field names taken from a production file/grep/batch tool), TOON's savings depend entirely on shape:
| Output shape | N | JSON tok | TOON tok | Δ% |
|---|---|---|---|---|
flat scalar array (glob paths) |
64 | 820 | 826 | −0.7% (regresses) |
single-key table (grep files_with_matches) |
50 | 753 | 711 | +5.6% |
{path,line,text} (grep content) |
40 | 1533 | 1344 | +12.3% |
{path,line,text} (inspect) |
100 | 4053 | 3564 | +12.1% |
{path,count} (grep count) |
50 | 953 | 812 | +14.8% |
{path,bytes,modified} (glob details) |
64 | 1717 | 1409 | +17.9% |
| 5-field table (replace dry-run) | 20 | 1178 | 952 | +19.2% |
| uniform object page (result paging) | 64 | 1773 | 1316 | +25.8% |
| total | 12780 | 10934 | +14.4% |
- Wins are concentrated in multi-field object tables (12–26%).
- Flat arrays and single-key tables do not meaningfully win — flat arrays actually regress, because o200k tokenizes JSON punctuation efficiently and there are no repeated keys to collapse.
- Break-even is ~3 rows: at N=1 TOON's per-table header makes it larger; N≥3 is where it pays.
So a blanket "switch everything to TOON" is wrong. compact-render picks the smaller encoding
per result and is fail-closed: anything outside the winning regime returns the exact JSON
you would have emitted anyway.
npm install compact-renderimport { compact, renderToolOutput } from 'compact-render'
// Decide the smaller encoding for one tool result.
const { format, text } = compact(value)
// format: 'json' | 'toon'
// Or drop it straight into an agent/MCP tool's output renderer:
const content = renderToolOutput(value) // [{ type: 'text', text }]Default — dependency-free structural gate (recommended).
No tokenizer is shipped. TOON is used only when value.items is a uniform object table with
>= minItems (3) rows and >= minKeys (2) identical keys. This gate reproduces every decision in
the table above.
renderToolOutput(value) // structural gate
renderToolOutput(value, { minItems: 5 }) // tune the thresholdsExact — supply your own token counter.
For o200k-exact decisions without this library depending on a multi-megabyte BPE vocabulary, pass a
countTokens function. The structural gate is bypassed and the smaller token count wins:
import { countTokens } from 'gpt-tokenizer' // o200k_base
renderToolOutput(value, { countTokens })| Export | Signature | Returns |
|---|---|---|
compact |
(value, options?) => { format, text } |
the chosen encoding |
renderToolOutput |
(value, options?) => [{ type:'text', text }] |
agent/MCP content block |
DEFAULTS |
{ minItems: 3, minKeys: 2 } |
calibrated thresholds |
Options: minItems (3), minKeys (2), countTokens (exact mode).
Fail-closed: any TOON encode error, a non-object value, or a degenerate JSON.stringify result
all return the exact prior JSON. Behavior outside the winning regime is unchanged.
This library is meant to sit inside tool runtimes that value leanness. The default path is
dependency-free (only the small, pure-JS TOON encoder). If you want exact per-result decisions, the
countTokens hook lets you bring gpt-tokenizer (o200k) yourself — the library never forces a
multi-megabyte vocabulary on you.
- Token counts are o200k_base, cross-verified with two independent implementations
(Node
gpt-tokenizerand Pythontiktoken); both agree token-for-token. - These are estimates of what the model reads, not provider bills.
- Thresholds are calibrated to the shapes above; re-measure if your output shapes differ.
Publishing is automated by .github/workflows/publish.yml:
- Bump
versioninpackage.jsonand commit. - Cut a GitHub Release whose tag matches that version (e.g.
v0.2.0for0.2.0). The workflow verifies the tag matchespackage.jsonbefore doing anything else. - On release, CI runs
npm ci+ tests, thennpm publish --provenance --access publicto registry.npmjs.org (the global registry, regardless of any local mirror config).
Prereleases are skipped. You can also run it manually from the Actions tab (workflow_dispatch),
which publishes whatever version is currently in package.json.
One-time setup: add a repository secret NPM_TOKEN — an npm token with publish rights on
npmjs.org (Settings → Secrets and variables → Actions → New repository secret). Without it the
publish step fails. The workflow always targets npmjs.org even if your machine defaults to a mirror.
MIT. Depends on @toon-format/toon (MIT).