feat(tools): use --format llm for the body, keep each tool's envelope - #15
Open
KageBinary wants to merge 1 commit into
Open
feat(tools): use --format llm for the body, keep each tool's envelope#15KageBinary wants to merge 1 commit into
KageBinary wants to merge 1 commit into
Conversation
Nine tools fetched `--format json`, parsed it, and hand-rendered markdown. The
rendering carries real value — a header the model orients on, an empty-graph
message, an error envelope — but the *body* is usually a table or list that
`--format llm` already emits, 2-4x smaller than the JSON it was rebuilt from.
So this keeps the envelope and swaps the body. A tool that gets llm text back
emits its own header and then the records verbatim; a tool that gets null runs
its existing JSON path untouched.
**The version floor is per command.** `--format llm` arrived tier by tier:
Tier 1-4 in v0.7.0, Tier 5 (`explain`, `read`) in v0.9.2. `ix-explain` therefore
carries a different floor from the other eight.
Getting that wrong would not have raised. `ix` does not validate `--format` —
every renderer is `if json … else if llm … else text` — so an unrecognised
value falls through to human text and exits 0. On 0.9.1, `explain --format llm`
returns prose, successfully, and an ungated call would have handed the model
prose dressed as records.
The same property is what makes this safe: there is no version of `ix` on which
asking for `llm` breaks. The floors buy output quality, not crash-avoidance.
**Pro commands are excluded outright.** `briefing` and `decisions` come from
`@ix/pro`, which declares only `text|json`; no llm renderer exists at any
version, so no gate can help. They are absent from the table, and a test walks
every `tryLlm` call site to prove none of them names one.
Two places where the envelope needed more than a header:
- `ix-stats` defers to JSON when the records report an empty graph, so the
"run `ix map` to index the codebase" line survives. That is the most useful
thing this tool says, and it is worth one extra call in the rare case. A
substring check, not a parse — the fast-path never interprets its output.
- `ix-neighbors` stacks several sections under one header, so the fast-path
keeps the per-section label; unlabelled records would leave the model unable
to tell callers from callees.
Also carried across from ix-cursor-plugin's implementation: the `error code=`
check (ix reports some failures as a record on stdout *with exit 0*, so checking
only the exit status would forward an error line as a result), the
`IX_DISABLE_LLM_FORMAT=1` kill switch, a version probe memoised per process, and
the rule that llm output is never parsed. Output is run through `redactSecrets`
before it reaches the model.
24 tests. `bun test`: 112 pass; the 3 PluginHookContract failures are
pre-existing (`@opencode-ai/plugin` is not installed locally) and unchanged —
verified against a clean tree.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The middle path
Nine tools fetched
--format json, parsed it, and hand-rendered markdown. The rendering carries real value — a header the model orients on, an empty-graph message, an error envelope — but the body is usually a table or list that--format llmalready emits, 2-4x smaller than the JSON it was rebuilt from.So: keep the envelope, swap the body. A tool that gets llm text back emits its own header and then the records verbatim; a tool that gets
nullruns its existing JSON path untouched.Tools wired:
statssubsystemssmellstracelocaterankinventoryexplainneighbors.Backwards compatibility
The floor is per command.
--format llmarrived tier by tier — Tier 1-4 in v0.7.0, Tier 5 (explain,read) in v0.9.2 — soix-explaincarries a different floor from the other eight. Confirmed against the tags containing each tier's commit.Getting it wrong would not have raised.
ixdoes not validate--format. Every renderer isif json … else if llm … else text, so an unrecognised value falls through to human text and exits 0. On 0.9.1,explain --format llmreturns prose, successfully. An ungated call would have handed the model prose dressed as records, with nothing to catch.The same property is what makes this safe to ship: there is no version of
ixon which asking forllmbreaks. The floors buy output quality, not crash-avoidance — which means the downside of a mistake here is degraded output, never a broken tool.Pro commands are excluded outright.
briefinganddecisionscome from@ix/pro, which declares onlytext|json; there is no llm renderer at any version, so no gate can help. They are absent from the table, and a test walks everytryLlmcall site to prove none of them names one.Where the envelope needed more than a header
ix-statsdefers to JSON when the records report an empty graph, so"runix mapto index the codebase"survives. That is the most useful thing this tool says, and it is worth one extra call in the rare case. A substring check, not a parse — the fast-path never interprets its output.ix-neighborsstacks several sections under one header, so the fast-path keeps the per-section label. Unlabelled records would leave the model unable to tell callers from callees.Carried across from ix-cursor-plugin's implementation
error code=deferral.ixreports some failures as a record on stdout with exit 0; checking only the exit status would forward an error line as a result.IX_DISABLE_LLM_FORMAT=1kill switch.redactSecretsfirst. (The JSON path does not scrub today; bringing it up to match is a separate change.)Tests
24 new in
tests/llm.test.ts: the table's contents for all three categories,gteacross each position,diff --contentstaying on text, theerror code=detector including cases that must not fire, the kill switch, and a wiring check that every fast path keeps its header.bun test→ 112 pass. The 3PluginHookContractfailures are pre-existing (@opencode-ai/pluginis not installed in this checkout) and unchanged — verified against a clean tree before starting.