Package Exports
- loon-core
- loon-core/package.json
Readme
LOON — LLM-Optimized Object Notation
Token-efficient serialization for data pipelines and LLM prompts.
LOON converts structured data (JSON / CSV / XML / YAML / trees) into a compact wire format. Two distinct problems, two distinct optimizations: moving large volumes of data between systems is one job; feeding data to an LLM is another; serializing a small or irregular object is a third. LOON exposes three modes, one per job.
The three modes
| Mode | What it's for | Reads like |
|---|---|---|
full |
Bulk data transmission / .loon storage. System → system, decoder → decoder. Maximum compression. Whether an LLM can read it raw is not a design constraint. |
Dense protocol output |
llm |
Direct LLM consumption. A model — cloud or local, reasoning or not — reads the payload without any spec or primer. | Labeled CSV |
compact |
Small / non-uniform datasets. Single deep objects, sparse rows, anything where a column schema cannot amortize itself. | key: value blocks |
Plus:
compat— JSON-hybrid ({S,T,R}arrays). A 100%-valid-JSON escape hatch for environments that must parse withJSON.parse.local— deprecated alias ofllm(kept for back-compat).
What each mode optimizes
full — minimise tokens, full stop. Base36 integers, arithmetic
sequences, semantic dictionaries, suffix stripping, fixed-point, delta, RLE,
abbreviations. The output is opaque to a human or an LLM, but a deterministic
decoder restores the original bit-for-bit. This is the format you write to a
.loon file or push across a wire when both ends own the decoder.
llm — self-evidence. One header line, plain decimal numbers, literal
strings, null / true / false as JSON does them. No Base36, no
sequences, no dictionaries, no sentinels the model would have to learn. The
LLM reads it, it never decodes it.
compact — get out of the way. A schema header would cost more than it
saves on a 3-row dataset or a single deep config object. compact writes
key: value blocks (or indented hierarchy) — cheap for the common small-data
case where columnar formats lose.
Design rule.
fullis allowed every compression trick because its reader is a decoder.llmrejects every trick that would force the model to compute (Base36, sequences, dictionaries, suffix reattachment) and keeps only the tricks that de-duplicate structure — schema declared once, object arrays tabulated, nested objects grouped. The dividing line is "does this require execution to read?"
Quick start
npm install loon-coreimport { Loon } from 'loon-core';
const loon = new Loon();
const data = [
{ id: 1, name: 'Alice', dept: 'Engineering', salary: 120000, active: true },
{ id: 2, name: 'Bob', dept: 'Sales', salary: 98000, active: false },
];
// LLM consumption (default for prompts)
loon.toLOON(data, { mode: 'llm' });
// Bulk transmission / storage (not for raw LLM reading)
loon.toLOON(data, { mode: 'full' });
// Small / irregular data
loon.toLOON(data, { mode: 'compact' });
// Decode (auto-detects the mode)
loon.fromLOON(encoded);If mode is omitted it is auto-selected: compact for empty or non-uniform
input, micro for 1–4 rows, full for ≥ 5 uniform rows. Override with
{ mode: 'llm' } when the target is a model.
Wire format
full mode — maximum compression
The format that lives in a .loon file or rides between two services.
S:@T1[N]=[col:type,...] schema: row count + columns with type codes
A:fullName,... column aliases (only if names were abbreviated)
DC:col,... integer columns stored decimal, not Base36
C:col=value constant column (omitted from every row)
Q:col=start,step integer arithmetic sequence
QF:col=start,step float arithmetic sequence
QS:col=start,step,prefix string sequence (prefix + counter)
FP:d=col,... fixed-point: row token ÷ 10^d = value
X:col=suffix common suffix stripped, re-appended on decode
D:col={tok:val,...} semantic dictionary (token → value)
D:__global__={tok:prefix} shared prefix dictionary (backs `$tok` cells)
D:defaults=col=val,... per-column default; `~` in a row means "use it"
DL:col=firstValue delta encoding (row tokens are signed deltas)
NM:col=mean,std,sigmaT,mT z-score normalization (LOSSY)
LY:NM marks payload contains lossy NORM columns
AS:col=k1,k2,... uniform object-array sub-schema (see below)
@T1: start of the data block
F:csv rows are comma-delimited
<data rows>
fullis not meant to be read by an LLM directly. If you must, prependgetSpec(encoded)to the prompt — and even then expect output-token costs to be higher thanllmmode, because the model has to mentally decode every Base36 / sequence / dictionary cell.
llm mode — self-evident, LLM-readable
One header line. Plain decimal rows. JSON-style literals.
@T1[N]{id,name,email,dept,salary,active}
C:status=active (optional, when a constant column exists)
AS:items=sku,name,qty (optional, for object-array columns)
1,Alice,alice@x.com,Engineering,120000,true
2,Bob,bob@x.com,Sales,98000,false
3,Carol,carol@x.com,Marketing,null,trueRules the model can apply at sight:
- Numbers bare:
120000. Decoded asNumber(token). - Booleans literal:
true/false. - Null literal:
null(same single BPE token JSON uses). - Strings bare when unambiguous:
Alice. - Strings that look like numbers / bools / null are quote-wrapped:
"123","true","null". The decoder strips the quotes and keeps the string as a string. - Absent key in this row (sparse / non-uniform):
+.
No :type codes in the header — the model infers types from cell shape.
That alone saves ~2 tokens per column. No DC: / @T1: / F:csv scaffolding
either; the @…{…} line is its own start marker.
compact mode — small or irregular data
id: 1
name: Alice
tags[3]: a,b,c scalar array, length 3
items[2]{sku,qty}: A,1;B,2 uniform object array
---
id: 2
...Single deep objects (configs) use an indented hierarchy instead of repeated dot-notation keys.
Row tokens (shared)
| Token | Meaning |
|---|---|
^ |
null (full mode) — llm mode writes the literal null |
~ |
use the column default (full only) |
+ |
key absent from this row — omit it (sparse / non-uniform schema) |
!value |
raw literal string (bypass the dictionary; full only) |
. |
a row that is entirely defaults |
*N[...] |
run-length: repeat the bracketed row N times (full only) |
AS: — uniform object-array sub-schema
A column holding an array of same-shape objects (items: [{sku,name,qty}, …])
would otherwise be inline JSON with the keys repeated on every element. AS:
declares the shared shape once; each cell carries values only:
AS:items=sku,name,qty
cell: A|Mouse|1;B|Cable|2 → [{sku:A,name:Mouse,qty:1},{sku:B,name:Cable,qty:2}]Fields within an object are |-separated; objects are ;-separated.
Type codes (full mode)
i integer · f float · s string · b boolean · a array · o object
llm mode omits the codes — types are inferred from cell shape.
getSpec() — when full must talk to an LLM
full is built for parsers. If you need an LLM to read a full payload (for
example: you stored data in .loon, you now want a model to query it),
getSpec(encoded) returns a minimal decode spec (200–600 tokens) covering
only the headers that this specific payload actually uses, plus a worked
walkthrough of row 0.
import { getSpec } from 'loon-core';
const encoded = loon.toLOON(data, { mode: 'full' });
const spec = getSpec(encoded); // { text, sections, estimatedTokens }
// prepend spec.text to the prompt; the model can now decode `full`.llm mode does not need a spec — that is the whole point of it.
Sessions & context caching
For repeated calls against the same data shape, LoonSession separates the
cacheable prompt prefix from the per-call data block. LLM providers cache an
identical prefix and bill it at a fraction of the normal rate; put the spec
and the schema there and the per-call cost shrinks to just the rows.
import { LoonSession } from 'loon-core';
const s = new LoonSession();
s.init(firstBatch, { mode: 'full' });
// System prompt (mark it for prompt caching): s.primer ← getSpec() + schema
// User message 1: s.dataBlock
for (const batch of moreBatches) {
send(s.encodeRows(batch).dataBlock); // only the rows
}splitLoon(encoded) exposes the raw { schema, dataBlock } split for custom
integrations.
Prompt caching reduces input cost. It does not touch output tokens. A model that has to reason hard about a format still pays full price on output. That is why
llmmode beatsfull + cached specfor direct LLM consumption:llm's output token cost is small because the format requires no reasoning to read.
API
| Method | Behavior |
|---|---|
toLOON(data, opts?) / encode |
JSON array → LOON |
fromLOON(loon) / decode |
LOON → JSON array |
fromCSV / fromXML / fromYAML |
other formats → LOON |
toCSV / toXML / toYAML |
LOON → other formats |
fromTree(tree, opts?) |
tree (nested objects) → LOON (TREE: header) |
toTree(loon) |
LOON → tree |
chunk(data, opts) |
split into context-window-sized LOON chunks |
encodeStream / fromLOONStream |
async streaming codec |
getSpec(loon) |
minimal decode spec for a payload |
LoonSession |
multi-call session: primer, dataBlock, encodeRows, decode |
splitLoon(loon) |
{ schema, dataBlock, full } |
validateDecode(loon, rows) |
post-decode structural check |
repairHint(loon, errors) |
minimal retry prompt for an LLM that mis-decoded |
reset() |
clear per-instance schema state |
Options (LoonOptions)
| Option | Effect |
|---|---|
mode |
force full / llm / compact / compat |
fields |
column projection |
maxDecimals |
trim float precision before encoding |
tableId |
override the default schema id (T1) |
outFile |
write encoded output to a file (Node) |
checkpointEvery |
emit a #CKP: schema checkpoint every N rows (full) |
primaryCols |
promote columns to the front + force decimal |
norm |
z-score normalize float columns (lossy; full only) |
Architecture
Input (JSON / CSV / XML / YAML / tree)
│
Mode Selector ──────────────┐
│ │ (auto-pick when mode omitted)
┌────┴─────┬──────────┐ │
▼ ▼ ▼ ▼
full llm compact compat
│ │ │ │
└── Adaptive ─┘ │ │
pipeline │ │
│ │ │
└─────────────────┴─────┘
│
LOON output| Module | Path | Role |
|---|---|---|
| Public API | src/index.ts |
Loon facade, mode routing |
| Adaptive encoder | src/encoder/adaptive/ |
analyzer → header → rows |
| Adaptive decoder | src/decoder/adaptive/ |
header-parser → row-reconstructor |
| Compact codec | src/encoder/compact.ts, src/decoder/compact.ts |
key: value + indent |
| Adaptive engine | src/compression/adaptive.ts |
cell compress/decompress, dictionaries, AS: tabular |
| Mode selector | src/compression/mode-selector.ts |
dataset-shape heuristics |
| State manager | src/state/state-manager.ts |
per-schema context + reverse-dict cache |
| Spec generator | src/utils/get-spec.ts |
getSpec() minimal decode spec |
| Session | src/session.ts |
LoonSession, splitLoon |
| Codecs | src/codecs/ |
CSV / XML / YAML / tree bridges |
full and llm share one analysis pipeline. The analyzer gates off every
compute-requiring primitive (Base36, sequences, dictionaries, defaults,
suffixes, fixed-point, delta, NORM, RLE, anchor rows) when the mode is llm.
What survives is structural-only: the schema, C: constants, and AS:
sub-schemas. The header emitter also drops :type codes and replaces ^
(null sentinel) with the literal null for llm mode.
Benchmarks
Measured against json-compact on six datasets. Token efficiency uses the
GPT-4o tokenizer; retrieval accuracy is measured on gemini-3-flash-preview.
Full reports in ../comprehension-benchmark/benchmarks/results/.
Payload tokens (vs json-compact, GPT-4o)
| Dataset | TOON | LOON llm |
LOON full |
|---|---|---|---|
| Uniform employees | −36.8% | −41.9% | −51.5% |
| E-commerce (nested) | +5.4% | −35.6% | −42.9% |
| Time-series | −35.8% | −41.0% | −57.8% |
| Event logs | +19.9% | −30.4% | −32.7% |
full is the storage / transmission winner everywhere. llm beats TOON on
every dataset and remains LLM-readable without a spec.
Real billed tokens — LLM retrieval (Gemini 3 Flash)
| Format | Accuracy | Input | Output | Total billed |
|---|---|---|---|---|
LOON llm |
100% | 3,267 | 332 | 3,599 |
| TOON | 100% | 3,365 | 269 | 3,633 |
| json-compact | 100% | 4,746 | 185 | 4,931 |
LOON full |
80% (no spec) | 2,662 | 3,398 | 6,060 |
full minimises input tokens by reasoning the model has to do later —
output cost blows up because the model has to mentally decode every cell. The
remedy is getSpec() (corrects accuracy, partly tames output) or, simpler,
use llm mode when the consumer is an LLM.
Round-trip fidelity
All four LOON modes recover all ten benchmark datasets losslessly (10 / 10).
License
MIT © LOON Thesis Team