Skip to content

feat(tools): use --format llm for the body, keep each tool's envelope - #15

Open
KageBinary wants to merge 1 commit into
mainfrom
feat/llm-fastpath
Open

feat(tools): use --format llm for the body, keep each tool's envelope#15
KageBinary wants to merge 1 commit into
mainfrom
feat/llm-fastpath

Conversation

@KageBinary

Copy link
Copy Markdown
Collaborator

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 llm already 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 null runs its existing JSON path untouched.

Tools wired: stats subsystems smells trace locate rank inventory explain neighbors.

Backwards compatibility

The 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 — so ix-explain carries a different floor from the other eight. Confirmed against the tags containing each tier's commit.

Getting it 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. 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 ix on which asking for llm breaks. 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. briefing and decisions come from @ix/pro, which declares only text|json; there is no llm renderer 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.

Where the envelope needed more than a header

  • ix-stats defers to JSON when the records report an empty graph, so "run ix map to 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-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.

Carried across from ix-cursor-plugin's implementation

  • error code= deferral. ix reports 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=1 kill switch.
  • Version probe memoised per process; fails closed.
  • llm output is never parsed, only forwarded — and run through redactSecrets first. (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, gte across each position, diff --content staying on text, the error code= detector including cases that must not fire, the kill switch, and a wiring check that every fast path keeps its header.

bun test112 pass. The 3 PluginHookContract failures are pre-existing (@opencode-ai/plugin is not installed in this checkout) and unchanged — verified against a clean tree before starting.

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.
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.

1 participant