Skip to content

fix: encode TOON from the JSON form, so agents see what the API returned (COR-14205) - #34

Open
Bradenream wants to merge 2 commits into
masterfrom
braden/toon-tag-keys/COR-14205
Open

Bradenream wants to merge 2 commits into
masterfrom
braden/toon-tag-keys/COR-14205

Conversation

@Bradenream

Copy link
Copy Markdown
Contributor

Summary

TOON is the default output in agent mode (CLAUDECODE, CURSOR_AGENT, …), so it is what every coding agent reads from vf. output.Result handed the SDK's Go values straight to gotoon.Encode, which walks them by reflection and gets the SDK's types wrong in four ways:

  • The key is the whole json tag. agent get showed 42 distinct keys like "instructions,omitzero".
  • Optional-nullable fields render as null even when set. They are a map[bool]*T underneath, and gotoon turns a map with non-string keys into null. agent get --include-instructions returned 11,934 characters of instructions as JSON and null as TOON, so agents never saw the instructions.
  • Unions render as their Go wrapper (StableToolV2API: null, StableToolV2Function: {…}) instead of the member the API returned.
  • omitempty and omitzero are ignored. Every unset union member is printed, so document list came to 4.9 MB of TOON against 1.27 MB of JSON.

The --include-headers path already avoided all of this by encoding the JSON form. A shared jsonValue helper now does the same for plain TOON: marshal with the SDK's JSON rules, decode into plain maps and slices, then encode. TOON carries exactly what --output-format json does. JSON, YAML, table and pretty output are unchanged.

Before and after

Measured live and read-only on a real project.

master this PR
agent get: keys with tag options 42 0
agent get --include-instructions: instructions null present (11,934 chars)
tool list --global: shape Go wrappers the API's shape
document list: TOON size (JSON is 1.27 MB) 4.9 MB 1.4 MB
document list: run time ~1.1 s ~1.1 s

Test plan

  • gofmt, go vet ./... and go test ./... pass; go.mod is unchanged.
  • internal/output/toon_test.go: 4 Go tests on real SDK types. They cover plain keys, a set optional-nullable field, a flat union, and TOON equal to the JSON output re-encoded. All 4 fail on master and pass here.
  • test/toon-output.test.ts: 3 hermetic cases on the built binary. They run with a mock server, isolated HOME and agent mode. All 3 fail with master's binary and pass here.
  • Live before and after on a real project, as in the table.
  • CI

The 4 vitest cases that already fail on master (docs-command ×2, flag-errors, flag-raw-text) still fail here and are unrelated.

Fixes COR-14205

TOON is the default output in agent mode, so it is what every coding agent
reads. It was encoded from the SDK's Go values by reflection, and gotoon gets
those wrong in four ways:

- It uses a json tag verbatim as the key: "instructions,omitzero". On
  `agent get`, 42 distinct keys looked like this.
- It turns an optional-nullable field (a map[bool]*T) into null even when
  the field is set. `agent get --include-instructions` returned 11,934
  characters of instructions as JSON and `null` as TOON, so agents never saw
  the instructions.
- It prints a union as its Go wrapper (StableToolV2API: null,
  StableToolV2Function: {...}) instead of the member the API returned.
- It ignores omitempty and omitzero, so every unset member of every union
  was printed. `document list` came to 4.9 MB of TOON against 1.27 MB of
  JSON; it is now 1.4 MB.

The --include-headers path already avoided all of this by encoding the JSON
form. jsonValue now does that for both paths: marshal with the SDK's own JSON
rules, decode into plain maps and slices, then encode. TOON carries exactly
what the json format does, and a test holds the two together. On the 4.9 MB
response, run time is unchanged (about 1.1 s, dominated by the network).
Copilot AI balanced review requested due to automatic review settings September 29, 2026 17:56
@linear-code

linear-code Bot commented Sep 29, 2026

Copy link
Copy Markdown

COR-14205

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

JSON normalization currently rounds integer values above the float64 safe range.

Review effort: Balanced
Findings: 1 Medium severity

Open (1)
What changed in this PR

Updates TOON output to encode SDK responses using their JSON representation.

Changes:

  • Adds shared JSON normalization before TOON encoding.
  • Adds unit and CLI regression coverage for SDK tags, nullable fields, and unions.
File Description
internal/​output/​output.go Normalizes responses through JSON before TOON encoding.
internal/​output/​toon_test.go Adds Go regression tests for TOON rendering.
test/​toon-output.test.ts Adds end-to-end agent-mode TOON tests.
Files not reviewed (1)
  • internal/output/output.go: Generated file

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread internal/output/output.go
…(COR-14205)

Review found that jsonValue decodes every number to float64, so an integer
past ±(2^53−1) reached the TOON encoder already rounded: 9007199254740993
became 9007199254740992. That predates this PR, since gotoon itself holds
every number as a float64, but the helper's doc claimed an exact rendering it
did not deliver.

TOON now decodes numbers exactly and follows the TOON spec's rule for integers
outside the encoder's numeric domain (Appendix E): a safe integer stays a
number, and one beyond the range is written as a quoted decimal string, with
every digit kept. Other numbers come back as the float64 they were marshaled
from. JSON, YAML and jq output are unchanged; jsonValue's doc now says numbers
decode to float64.

Tests: 2^53+1 and 2^53−1 on both TOON paths (plain and --include-headers),
plus a table test of the number conversion.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants