<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Tesseract</title>
    <description>The latest articles on DEV Community by Tesseract (@tesseract_app).</description>
    <link>https://dev.to/tesseract_app</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4231015%2Ffc5b160a-fb6c-42e8-ac55-86dc02bef2f1.png</url>
      <title>DEV Community: Tesseract</title>
      <link>https://dev.to/tesseract_app</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tesseract_app"/>
    <language>en</language>
    <item>
      <title>Lessons from putting 14 coding-agent CLIs behind one interface</title>
      <dc:creator>Tesseract</dc:creator>
      <pubDate>Fri, 06 Nov 2026 19:18:21 +0000</pubDate>
      <link>https://dev.to/tesseract_app/lessons-from-putting-14-coding-agent-clis-behind-one-interface-gch</link>
      <guid>https://dev.to/tesseract_app/lessons-from-putting-14-coding-agent-clis-behind-one-interface-gch</guid>
      <description>&lt;p&gt;We build a desktop app where one chat can be driven by any of 14 agent CLIs: Claude Code, Codex, OpenCode, Gemini&lt;br&gt;
CLI, Kimi Code, Qwen Code, MiniMax Code, CodeBuddy, Grok Build, Cursor, Hermes Agent, OpenClaw, NanoClaw and&lt;br&gt;
Antigravity CLI. You can switch the framework partway through a conversation and keep going. Getting there taught us&lt;br&gt;
more about the differences between these tools than about what they share. Here is what held up.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Don't standardise away the strongest framework's features
&lt;/h2&gt;

&lt;p&gt;Twelve of the fourteen speak the &lt;a href="https://agentclientprotocol.com" rel="noopener noreferrer"&gt;Agent Client Protocol&lt;/a&gt; (ACP), so they share one&lt;br&gt;
backend. Claude Code runs through its SDK, in-process. Codex runs through &lt;code&gt;codex app-server&lt;/code&gt;, its own JSON-RPC&lt;br&gt;
server, which has a lovely property: &lt;code&gt;codex app-server generate-json-schema&lt;/code&gt; prints its entire interface (at 0.149:&lt;br&gt;
83 client methods, 10 server requests, 65 notifications). We wrote that backend against the generated schema&lt;br&gt;
instead of guessing.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  2. Make capabilities explicit, and test the claim both ways
&lt;/h2&gt;

&lt;p&gt;Every framework is one object behind one interface: eight required operations (&lt;code&gt;sendMessage&lt;/code&gt;, &lt;code&gt;interrupt&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;listSessions&lt;/code&gt;, &lt;code&gt;getMessages&lt;/code&gt;, &lt;code&gt;setConfig&lt;/code&gt;, &lt;code&gt;reclaim&lt;/code&gt;, plus &lt;code&gt;id&lt;/code&gt; and &lt;code&gt;capabilities&lt;/code&gt;) and optional ones like&lt;br&gt;
&lt;code&gt;forkSession&lt;/code&gt; or &lt;code&gt;respondToPermission&lt;/code&gt;.&lt;/p&gt;

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

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

&lt;h2&gt;
  
  
  3. List the skills; don't make the model search for them
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  4. Freeze the prompt prefix for the whole session
&lt;/h2&gt;

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

&lt;h2&gt;
  
  
  5. Host the tools yourself and hand out an address
&lt;/h2&gt;

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

&lt;p&gt;The catch is naming. The same tool reaches each model under a different name, with different limits:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Framework&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;skill_load&lt;/code&gt; as the model sees it&lt;/th&gt;
&lt;th&gt;Limit that bites&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcp__tesseract__skill_load&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;descriptions cut at 2,048 chars&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;mcp__tesseract__&lt;/code&gt; namespace + &lt;code&gt;skill_load&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;names &lt;code&gt;[A-Za-z0-9_]&lt;/code&gt; only; schemas over 5,000 bytes shrunk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;td&gt;&lt;code&gt;mcp_tesseract_skill_load&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;names over 63 chars mangled; &lt;code&gt;$schema&lt;/code&gt; stripped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OpenCode&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tesseract_skill_load&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;forces &lt;code&gt;additionalProperties: false&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;h2&gt;
  
  
  6. Load tools lazily — 82 tools became 19
&lt;/h2&gt;

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

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

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

&lt;h2&gt;
  
  
  7. Keep each model's own shell and file tools
&lt;/h2&gt;

&lt;p&gt;We replace planning, questions, subagents, skills and tool search on every framework — but not the shell and the file&lt;br&gt;
editor. A harness is tuned with its model. Terminal-Bench 2.0 (&lt;a href="https://arxiv.org/abs/2601.11868" rel="noopener noreferrer"&gt;arXiv 2601.11868&lt;/a&gt;)&lt;br&gt;
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&lt;br&gt;
neutral one; another went the other way, 52.1% vs 57.8%. The edit dialect a model was trained on is worth more than&lt;br&gt;
identical tooling.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we'd tell anyone starting this
&lt;/h2&gt;

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

&lt;p&gt;The full write-up, with the event mapping and how history moves between frameworks, is on our blog:&lt;br&gt;
&lt;a href="https://tesseract.dev/blog/one-chat-fourteen-agent-frameworks/?utm_source=27a2vo" rel="noopener noreferrer"&gt;https://tesseract.dev/blog/one-chat-fourteen-agent-frameworks/?utm_source=27a2vo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>architecture</category>
      <category>agents</category>
    </item>
  </channel>
</rss>
