| kova ecosystem | I. Podtsebnev |
| Request for Comments: UFN-2 | Kovanex |
| Obsoletes: UFN-1 (game profile) | June 2026 |
| Category: Standards Track | |
| Status: Stable (locked) |
This document specifies a Standards Track notation for the kova ecosystem and requests discussion and suggestions for improvements. UFN v2 is the current, stable (locked) revision. It generalizes the original game-frame profile (UFN v1, see Section 10) into a domain-agnostic, nesting-capable wire format for the agent edge.
Distribution of this memo is unlimited. The notation is published openly: UFN is a frame, not a moat — it carries no proprietary semantics of its own.
UFN (USE Frame Notation) is a one-line, dictionary-driven notation for structured
output produced by Large Language Models. Where JSON spends a per-token tax on
{ } [ ] " , on every structured reply, UFN moves structure into named,
self-delimiting tags and a tight separator, so a model spends its tokens on content,
recovers cleanly from truncation, and streams its header before its prose.
UFN is intended for the edge between a model and a consumer — not for storage. Storage stays JSON; UFN is the token-economical wire format an LLM emits and a parser ingests. On a representative task list it is approximately 59% smaller than compact JSON on the structural portion, raises parse-failure-on-truncation from JSON's total loss to a usable partial frame, and cuts time-to-first-content from tens of seconds to roughly 1.5 seconds by streaming the header ahead of the prose.
Copyright © 2026 Kovanex / I. Podtsebnev. This specification is licensed under the Apache License, Version 2.0. Implementations are unrestricted.
UFN is the smallest legal notation that gives a producer tagged records plus a free
prose tail on a single line. It is what git log --pretty and mail headers do,
conceptually: a tight set of keyed records, no nesting ceremony, robust to a line that
gets cut short. The conceptual model is 1990s SMS / ICQ / IRC compression — sender and
receiver share a tiny convention dictionary, every byte counts, and a partial message
still makes sense.
JSON was the obvious first choice for LLM structured output: models are well trained on it and every language ships a parser. In production over local models, three costs become visible:
}
makes the whole reply unparseable. Measured: ~20% of benchmark runs hit the fallback path
purely because a brace was missing — not because the model broke character.The key words MUST, MUST NOT, REQUIRED, SHALL, SHOULD, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119) when, and only when, they appear in all capitals.
Everything is one line, ;-separated. A token is classified by
its shape alone — there are exactly three kinds:
| Shape | Kind | Meaning |
|---|---|---|
key:value (contains :) | field | A scalar record. Split on the first :; the value may itself contain :. |
acr (no :, no leading /) | scope open | Opens a container of class acr. |
/ | scope close | Closes the nearest open scope (a stack). /acr (named) is an optional resync mode. |
A field is key:value. The key MUST come from the shared
dictionary; the value is free text up to the next field separator. The field separator is
; (tight, no surrounding spaces) at the general edge. (The v1 game profile uses
; with surrounding spaces; see Section 10.)
A bare token that is not a field and does not start with / opens
a scope; a bare / closes the top of the stack. This is how UFN
expresses nesting without JSON's {}/[] brittleness: an unclosed
scope at end-of-line is simply closed implicitly, so a truncated object is still usable.
A class is array-typed or object-typed by the dictionary, not by punctuation. An array scope holds repeated item scopes. There is no separate array syntax to get wrong.
Everything after the first top-level ::: (triple colon with
surrounding spaces — the one readability exception) is verbatim prose to
end-of-line: no escaping, no structure. This is the streaming seam — a producer emits the
header, then streams prose token by token. The prose tail is top-level only; nested free text
is carried as a field.
line = element *( ";" element ) [ " ::: " prose ]
element = field / open / close
field = key ":" value
open = acr ; bare token, no ":", not starting "/"
close = "/" [ acr ] ; bare "/" closes top-of-stack;
; named "/acr" = robustness/resync mode
key = token ; from the shared dictionary
acr = token ; class name from the shared dictionary
value = *( vchar / escape ) ; "\;" "\/" "\\" escaped (Section 4)
prose = *VCHAR ; verbatim to end-of-line
This section is normative and mandatory. Correct round-tripping depends on exactly these rules and no others.
Only the following are reserved. Everything else is data.
: in a field only.A backslash escapes a control character that must appear literally in data. There are exactly three sequences; an implementation MUST NOT define others:
| Sequence | Produces | Use when |
|---|---|---|
\; | literal ; | a value contains a semicolon |
\/ | literal leading / | a value would otherwise be read as a scope close |
\\ | literal \ | a value contains a backslash |
Deliberately, almost nothing needs escaping — this keeps producers reliable:
: after the first in a field is data — values may contain colons (timestamps, URLs)./ that is not leading is data — paths and dates need no escaping.\" on every embedded quote and \\n on every newline —
the exact boundary where models trip on long prose. UFN's prose tail removes that failure
mode entirely.A short list of scalar values is carried inside one value, comma-separated,
e.g. lb:bug,infra. No scope is opened for the common small-list case.
A reference value MAY carry a human-readable name after a literal
|: @:player|Grond. If the name is omitted, the consumer resolves it
from its catalogue. For improvised or unknown entities the producer
MUST include the name.
UFN keeps JSON's brittleness out. A conforming parser MUST:
| Condition | Recovery |
|---|---|
| Unknown key or class | Skip the token, continue. |
| Scope still open at end-of-line | Close it implicitly (truncation-safe; partial object is usable). |
/acr with no matching open | Ignore. |
| No tokens recognized at all | Treat the whole line as prose. |
| Empty value after a key | Skip the field. |
Principle: a partial frame is always better than a parse error. A syntactic failure MUST NOT trigger a consumer's hard fallback; only genuine semantic failure should.
UFN is dictionary-driven: senders and receivers share a token dictionary verbatim — no inference, no fuzzy matching. Object/array classes and field keys are per-domain and declared by the producer.
Acronyms and markers MAY be letters, symbols, or sequences; only the control tokens of Section 4.1 are reserved.
UFN saves structure, not content. The levers below are ranked by measured impact on a 50-task list (against compact JSON at 8201 characters).
| # | Lever | Impact | Notes |
|---|---|---|---|
| 1 | Short ids, not UUIDs | −37% | Emit an 8-hex prefix (ce3760ed), not the 36-char UUID. Bigger than all dictionary tweaks combined. |
| 2 | Enum value codes + 1-char keys | −11…16% | s:in_progress→s:i, ty:feature→y:f. Pays most on lists. |
| 3 | Anonymous close / | −16%* | On nested output. Stack-based; /acr only for resync. |
| 4 | Omit defaults | — | A field equal to its declared default is dropped (p:3, y:f). |
* on the nested portion. Combined, these reach approximately −59% vs compact JSON and about −44% vs naive UFN.
| Metric | JSON | UFN | Improvement |
|---|---|---|---|
| Header tokens (4-field frame) | ~110 | ~38 | 65% fewer |
| Short response (60-token prose) | 170 | 98 | 42% smaller |
| Long response (600-token prose) | 710 | 638 | 10% smaller |
| 50-task list, structure | — | — | ~59% smaller vs compact JSON |
| Parse failure on truncation | total loss | partial frame | always recoverable |
| Time to first visible content | 30–60 s | ~1.5 s | 20–40× faster |
The shorter the response, the bigger the win — structural overhead amortizes across less content. Quest updates, warnings, and tool acknowledgements (the most frequent outputs) see the largest savings.
UFN splits naturally into two phases: a header (~38 tokens, arriving in ~1.5 s on a 25 tok/s decoder) and a streamed prose tail. The consumer can set scene/type/roles from the header while prose appears live, instead of waiting tens of seconds for a complete, parseable JSON document.
Atomic tokens (keys, enum codes) are tier 1. Ligatures are tier 2:
one dictionary token that expands to a combination — the way ROFL or
:-) carried whole phrases or states, not glyphs.
A producer SHOULD encode only the Pareto-frequent combinations and SHOULD declare the ligatures it used so a generic receiver can expand them. Because both producer and consumer are LLMs, the shorthand vocabulary can be far richer than IRC ever was, and savings compound exactly where structured output repeats.
| Use UFN when… | Use JSON when… |
|---|---|
| Output is LLM-generated, especially on local models | Data is machine-generated and machine-consumed |
| You mix metadata with long prose | Data is purely structured, deeply nested |
| You stream and want time-to-first-content low | You need a one-shot document with a fixed schema |
| Truncation is a real risk | You are storing the system of record |
Operational guidance:
t:241;s:done;ty:feature;ti:kovasite skill
t;id:9c92;n:241;s:done;ti:kovasite skill;rp;r;id:ce37;nm:pr;/r;r;id:94be;nm:kovanex-server;/r;/rp;/t
Parse: t…/t is an object task; rp…/rp an array of repos; each
r…/r an item object.
f:a ; @:player|Grond ; ^:shadow_figure|dark figure ; ~:tavern|Rusty Mug ; s:steel clash, muffled gasp ; l:torchlit ; c:clear ; >:atk shadow_figure ::: The air freezes for one long second...
One line. The header is the structure; everything after ::: is verbatim prose
the consumer streams as it arrives.
{"frame":"action","roles":[{"type":"agent","entity_id":"player","name":"Grond"},
{"type":"patient","entity_id":"shadow_figure","name":"dark figure"},
{"type":"location","entity_id":"tavern","name":"Rusty Mug"}],
"atmosphere":{"sounds":["steel clash","muffled gasp"],"light":"torchlit","weather":"clear"},
"state_changes":[{"type":"attack","target":"shadow_figure"}],"prose":"The air freezes..."}
~110 structural tokens against UFN's ~38, and a single missing brace renders all of it unparseable.
| UFN v1 (game profile) | UFN v2 (general) | |
|---|---|---|
| Origin | Tormund GM output (replaces a JSON HybridResponse) | Generalized for the agent edge |
| Shape | Flat: header records + prose tail, no nesting | Nesting-capable via scope open/close |
| Field separator | ; (spaces) | ; (tight) |
| Dictionary | Fixed game dictionary (frame/role/atmosphere) | Per-domain, producer-declared |
| Status | Locked (v1.x adds only) | Stable (locked) |
v2 is backward-compatible in spirit: a flat v1 line is a valid v2 line with no open scopes. v2 adds nesting, the tight separator, enum/ligature economy, and the domain-agnostic dictionary model. The original "is UFN worth it" question resolved yes — flat v1 roughly matched JSON, but v2 nesting plus ligatures win decisively.
ctl CLI and the MCP server.ufn.go, ufn_bridge.go, bundle_ufn.go).Shipped surfaces: kovanex-ctl --ufn (list economy + nested get) and
MCP tool output in UFN v2 (with a compact-JSON fallback).
UFN's producers and consumers are LLM agents. The standard is published in agent-consumable form: a curated /llms.txt, the full spec as plain text at /llms-full.txt (single-fetch ingestion), and a machine-readable /ufn.dict.json starter dictionary.
; (general edge) or ; (v1 game profile) as the field separator.\; \/ \\ as escapes; puts long text in the prose tail after ::: .:. ::: as verbatim prose.Normative.
docs/UFN_v2.md, 2026.Informative.
docs/UFN_SPEC.md, 2026.docs/LANGUAGE_STACK.md.Author's Address
I. Podtsebnev — Kovanex — [email protected] —
https://kovanex.dev