DEV Community

Cover image for Lessons from putting 14 coding-agent CLIs behind one interface
Tesseract
Tesseract

Posted on

Lessons from putting 14 coding-agent CLIs behind one interface

We build a desktop app where one chat can be driven by any of 14 agent CLIs: Claude Code, Codex, OpenCode, Gemini
CLI, Kimi Code, Qwen Code, MiniMax Code, CodeBuddy, Grok Build, Cursor, Hermes Agent, OpenClaw, NanoClaw and
Antigravity CLI. You can switch the framework partway through a conversation and keep going. Getting there taught us
more about the differences between these tools than about what they share. Here is what held up.

1. Don't standardise away the strongest framework's features

Twelve of the fourteen speak the Agent Client Protocol (ACP), so they share one
backend. Claude Code runs through its SDK, in-process. Codex runs through codex app-server, its own JSON-RPC
server, which has a lovely property: codex app-server generate-json-schema prints its entire interface (at 0.149:
83 client methods, 10 server requests, 65 notifications). We wrote that backend against the generated schema
instead of guessing.

Why not put everything on ACP? Because a shared protocol only covers what everyone has. Route Claude Code through it
and constrained structured output becomes "ask for JSON and retry until it validates". Route Codex through it and a
real compaction call becomes a slash command. Our rule: if standardising costs the strongest framework a
capability it has today, the layer is wrong.
The layer should describe the differences, not remove them.

2. Make capabilities explicit, and test the claim both ways

Every framework is one object behind one interface: eight required operations (sendMessage, interrupt,
listSessions, getMessages, setConfig, reclaim, plus id and capabilities) and optional ones like
forkSession or respondToPermission.

Each optional method is tied to one of 32 capability flags, and a test checks the pairing in both directions:
declared means implemented, and undeclared means absent. The second direction is the one people skip, and it's what
makes "what the app believes a framework can do" equal "what it does". The catalog entry is typed so every flag
needs an explicit answer — add a 33rd flag and the build fails until all 14 frameworks answer it.

A tick is a claim about our implementation, so we also record how each capability is reached. For Claude Code all
32 are native. For Gemini CLI 14 are native, 8 are the app's by design (things no framework will ship, like a
scheduler), and 10 are stand-ins until the framework ships its own — each stand-in records where to look for the
real one.

3. List the skills; don't make the model search for them

We tested this live: 22 skills, 14 requests (10 that need a skill, 4 that must not load one), in English, Romanian
and Russian, on five framework/model pairs — 630 runs. With search only, the right skill loaded 47–63% of the time.
With the skill index listed in the prompt: 97–100%, and no wrong skill in any of the 180 runs that should have loaded
none.

4. Freeze the prompt prefix for the whole session

The instructions, skill index and tool index are built once when the session starts and never rewritten. A prefix
that never changes gets cached: on Claude Code, turn one wrote 45,021 tokens to the prompt cache; turn two read all
45,021 back and wrote 75 new ones. If something changes mid-session (a skill installed), the next turn carries a
one-line note instead of a rewritten prompt.

5. Host the tools yourself and hand out an address

ACP has no method for subagents, background tasks or forms; other protocols have different gaps. Building each
capability once per protocol means 14 implementations of everything. Instead the app runs a local
MCP server and every framework connects to it. A tool is written once — name,
description, schema, handler — and because the handler runs in the app's process, it can open a form and wait for
the answer, or start a second session as a subagent, with no protocol support at all.

The catch is naming. The same tool reaches each model under a different name, with different limits:

Framework skill_load as the model sees it Limit that bites
Claude Code mcp__tesseract__skill_load descriptions cut at 2,048 chars
Codex mcp__tesseract__ namespace + skill_load names [A-Za-z0-9_] only; schemas over 5,000 bytes shrunk
Gemini CLI mcp_tesseract_skill_load names over 63 chars mangled; $schema stripped
OpenCode tesseract_skill_load forces additionalProperties: false

So every tool meets the strictest common standard at build time: a lower-case area_verb name of at most 40
characters, a description of at most 2,048, a schema of at most 5 KB with no root-level combinators.

6. Load tools lazily — 82 tools became 19

Asking OpenCode for an image with every tool handed over directly: 82 tools, about 43,600 tokens on every model call.
With a small always-loaded core (skill_find, skill_load, tool_find, tool_call, tool_call_readonly,
ask_user, plan_update, plan_propose) and everything else behind tool_find: 19 tools, about 8,600 tokens, and
the picture still arrived.

Two dispatchers, not one, because frameworks run MCP calls without a read-only hint one at a time: three 4-second
tools took 12 s without the hint and 4 s with it. But one dispatcher marked read-only would let a framework
auto-approve tools that change things — so read-only tools go through tool_call_readonly, which refuses anything
off its allowlist.

Make the dispatcher forgiving: accept tool/args because that's what models send, parse args if it arrives as a
JSON string, and answer a wrong shape with the schema. Across five framework/model pairs and 166 real tool
definitions, the right tool with valid arguments came back 76 times out of 80.

7. Keep each model's own shell and file tools

We replace planning, questions, subagents, skills and tool search on every framework — but not the shell and the file
editor. A harness is tuned with its model. Terminal-Bench 2.0 (arXiv 2601.11868)
ran the same models in their vendors' harnesses and in a neutral one: one scored 62.9% in its own and 54.0% in the
neutral one; another went the other way, 52.1% vs 57.8%. The edit dialect a model was trained on is worth more than
identical tooling.

What we'd tell anyone starting this

  • Write against generated schemas where a CLI offers them.
  • Make capability differences data, and test declared-vs-implemented in both directions.
  • Spend your prompt budget on a stable, cacheable prefix.
  • Serve tools once, over MCP, and adapt names per client.
  • Leave the model's own editing tools alone.

The full write-up, with the event mapping and how history moves between frameworks, is on our blog:
https://tesseract.dev/blog/one-chat-fourteen-agent-frameworks/?utm_source=27a2vo

Top comments (0)