AgentFmt

Adaptive JSON-to-terminal output for language-model agents and humans.
AgentFmt is a final display gate, not another storage format. Give it the
JSON value your tool was about to print. It measures several representations
with a real tokenizer and emits the best one for the selected profile:
- readable indentation for nested or text-heavy data;
- schema-once rows for uniform arrays;
- compact JSON when every friendlier representation is materially larger.
$ online search "structured output" --json | agentfmt
query: structured output
status: ok
results[3]{title,url,published,snippet}:
Structured outputs,https://example.org/one,2026-05-28,Evaluation summary
JSON RFC 8259,https://www.rfc-editor.org/rfc/rfc8259,2017-12-01,JSON grammar
TOON specification,https://github.com/toon-format/spec,null,Token-oriented format
Why adaptive?
No single text notation is smallest and clearest for every JSON shape.
Indentation alone does not guarantee token savings. Repeated keys dominate
uniform arrays, while table syntax can become worse for irregular or deeply
nested values.
AgentFmt therefore generates eligible candidates, counts their tokens using
o200k_base or cl100k_base, and applies a profile:
| Profile |
Behavior |
balanced |
Prefer outline; use table at ≥8% savings or JSON at ≥20% additional savings |
tokens |
Select the eligible candidate with the fewest measured tokens |
readable |
Always use the indented outline |
json |
Emit deterministic compact JSON for programmatic consumers |
The output is deterministic, preserves JSON field order and numeric lexemes,
quotes ambiguous strings, rejects duplicate object keys, and isolates multiline
strings with collision-safe literal delimiters.
Install
Universal CLI
go install github.com/khanglvm/agentfmt/cmd/agentfmt@latest
Prebuilt macOS, Linux and Windows archives are also attached to each
GitHub release.
Then place it at the final stdout boundary:
my-agent-tool --json | agentfmt
agentfmt response.json
agentfmt -profile tokens -encoding cl100k_base response.json
agentfmt -stats response.json # decision evidence goes to stderr
Go
import "github.com/khanglvm/agentfmt"
result, err := agentfmt.RenderJSON(payload)
if err != nil {
return err
}
fmt.Println(result.Text)
Configure or reuse a renderer:
renderer, _ := agentfmt.NewRenderer(
agentfmt.WithProfile(agentfmt.ProfileBalanced),
agentfmt.WithEncoding(agentfmt.EncodingO200K),
)
result, err := renderer.Render(toolResult)
Python
The thin adapter delegates to the same executable, so it cannot drift from the
Go engine:
pip install "agentfmt-adapter @ git+https://github.com/khanglvm/agentfmt.git"
go install github.com/khanglvm/agentfmt/cmd/agentfmt@latest
from agentfmt import render
text = render(tool_result)
Use AGENTFMT_BIN=/path/to/agentfmt or binary=... when the executable is not
on PATH.
JavaScript / TypeScript
npm install github:khanglvm/agentfmt
go install github.com/khanglvm/agentfmt/cmd/agentfmt@latest
import { render, renderSync } from "agentfmt-adapter";
const text = await render(toolResult);
const immediate = renderSync(toolResult, { profile: "tokens" });
The Python and JS adapters send JSON through subprocess stdin with shell
disabled. Binary resolution is: explicit option, AGENTFMT_BIN, then PATH.
Agent and MCP integration
Use AgentFmt for human/agent-facing text only:
native result → JSON-compatible value → AgentFmt → terminal/tool text
For MCP, put AgentFmt text in a text content block. Keep structuredContent or
other application-consumed fields as JSON. This avoids breaking clients while
reducing the representation placed in model context.
Benchmark
The committed benchmark uses five fixtures covering uniform rows, search
results, nested configuration, heterogeneous tool output, and long untrusted
content.
go run ./cmd/agentfmt-bench -encoding o200k_base
go run ./cmd/agentfmt-bench -encoding cl100k_base
o200k_base results:
| Fixture |
Compact JSON |
Pretty JSON |
Outline |
Table |
Balanced |
Strategy |
| heterogeneous tool result |
84 |
149 |
117 |
— |
84 |
JSON |
| long content |
87 |
108 |
95 |
— |
95 |
outline |
| nested config |
95 |
169 |
116 |
— |
116 |
outline |
| search results |
248 |
332 |
283 |
214 |
214 |
table |
| uniform records |
183 |
327 |
268 |
118 |
118 |
table |
| Total |
697 |
1,085 |
879 |
— |
627 |
adaptive |
On these fixtures, balanced AgentFmt is 10.0% smaller than compact JSON and
42.2% smaller than pretty JSON. These are reproducible fixture results, not a
claim that AgentFmt wins on every dataset or tokenizer. See
benchmarks/results.md for both encodings and exact
methodology.
Important boundary
AgentFmt output is intentionally not guaranteed to round-trip into the original
JSON. It may omit redundant per-row field names and use human-oriented literal
blocks. Use -profile json or your original JSON API when a program must parse,
persist, sign, hash, or validate the result.
Literal blocks protect the display grammar, not the model from prompt
injection. Treat web pages, logs, and user-controlled strings as untrusted even
when fenced.
License
MIT