How one chat drives 14 AI agent frameworks the same way

The agent contract behind Tesseract: three integration routes, 32 capability flags, tools the app serves itself, one event stream, and how each tool reaches the model.

Tesseract's chat window with the model menu open beside a conversation, OpenCode selected as the framework at the bottom of the composer

Every conversation in Tesseract starts with one contract, fixed when the session opens: the same instructions, the same skill list and the same core tools, whichever framework runs it. All 14 frameworks sit behind one connection point that requires eight operations and makes them declare 32 capabilities. Claude Code is reached through its SDK, Codex through its app server and the other twelve through ACP; the app serves the tools itself and turns every framework’s events into one message stream.

Below: how each piece works and how each framework keeps what it does best. The numbers come from recorded requests, live runs against the real CLIs, and tables generated from the code. The 14 are Claude Code, Codex, OpenCode, Gemini CLI, Kimi Code, Qwen Code, MiniMax Code, CodeBuddy, Grok Build, Cursor, Hermes Agent, OpenClaw, NanoClaw and Antigravity CLI.

What are the three ways in, and why not just one?

RouteFrameworksWhat it is
Claude Agent SDKClaude CodeThe SDK runs inside the app’s own process
Codex app serverCodexCodex’s own JSON-RPC server, one long-lived child process
Agent Client Protocol (ACP)the other 12One shared backend that speaks ACP over JSON-RPC to each CLI

Each framework runs as a long-lived process that keeps its sessions and speaks a real protocol, so past conversations resume, approvals pause the turn, the app’s tools stay available and replies stream as they are written.

ACP has become the common protocol for agent CLIs, which is why twelve of the fourteen share one backend. Codex has no ACP mode, but codex app-server has a feature worth more: it describes itself. codex app-server generate-json-schema prints its whole interface: at version 0.149, 83 client methods, 10 server requests and 65 notifications. Our Codex backend is written against that generated schema, not against guesses.

Why not put everything on ACP, Claude included? Because a shared protocol only covers what everyone has. OpenCode reaches all 32 of our capabilities over ACP, but 14 of those 32 are substitutes the app supplies, not features of the protocol. Route Claude Code through ACP and you swap its native mechanisms for those substitutes. Constrained structured output, for example, becomes “ask for JSON and retry if it fails to validate”. Route Codex through it and a real compaction call becomes a slash command. The rule we hold to: if standardising costs the strongest framework a capability it has today, the layer is wrong. The layer exists to describe the differences, not to remove them.

What does the one connection point require?

Every framework, whatever its route, is one object behind one interface. Eight operations are mandatory:

interface AgentBackend {
  id: EngineId;
  capabilities: EngineCapabilities;
  sendMessage(params): Promise<void>;
  interrupt(sessionId): Promise<void>;
  listSessions(projectDir): Promise<SessionSummary[]>;
  getMessages(sessionId): Promise<Message[]>;
  setConfig(config): void;
  reclaim(): Promise<void>;
  // optional, each bound to one capability flag:
  forkSession?(…); respondToPermission?(…); getContextUsage?(…);
  stopTask?(…); mcpServerStatus?(…); /* … */
}

Each optional method belongs to one capability flag, and a test checks the pairing both ways: declared means implemented, undeclared means absent. The second direction matters as much as the first: what the app believes a framework does is exactly what it does.

The browser bridge and the plugin runtime are one required object handed to every backend, and a test asserts every framework receives the same one, so browser and plugin tools are there on all 14.

How does each framework declare what it can do?

A catalog holds one entry per framework, with an explicit answer for each of 32 capability flags. Four are required of every framework (permission prompts, browser control, switching run modes live, resume). The other 28 are optional: forking, compaction, sandboxing, subagents, structured output, file rewind and so on. The entries are typed so every flag must be present: add a 33rd and the build fails until all 14 frameworks answer it.

Declaring things in one place keeps the catalog, the backends and the screens in agreement: the run-mode picker appears for every framework that raises permission prompts, resume works wherever it is declared, and every framework receives the app’s tools.

The flags do work at run time. Before a turn reaches a framework, options it never declared are stripped, and if the person chose one of them, the app says so.

A tick in the matrix is a claim about our implementation, not about the framework, so the catalog also records how each capability is reached. This is the count, generated from the catalog and that provenance file, for a few of the 14:

FrameworkCapabilities declaredNative to the frameworkThe app’s own, by designThe app’s own until the framework has it
Claude Code323200
Codex322174
OpenCode321886
Gemini CLI3214810
Qwen Code3112811
Cursor279810
MiniMax Code24789

“By design” covers parts of the product no framework will ship, such as the scheduler and the trace. The last column covers capabilities the app supplies so they work on that framework today. Every substitute records where to look for the framework’s own version, so the app can switch to it as soon as the framework ships it, and a test checks that every substitute carries that record.

What does every conversation start with?

The contract. It is one object per session, built when the session starts and never changed while it runs:

interface AgentContract {
  version: 1;
  instructions: string;   // the house instructions, framework-neutral
  skills: { listed: IndexEntry[]; searchable: CategoryCount[] };
  tools: { core: string[]; areas: AreaSummary[] };
  natives: { slot: Slot; provider: 'native' | 'tesseract' | 'off' }[];
  hash: string;           // of all of the above
}

Each part has a budget: about 1,200 tokens for the instructions, a few thousand for the skill index, 400 for the tool index, at most 12 core tool definitions. The hash is the prompt-cache key and the key the parity tests compare.

Two decisions shaped it. The first was to list the skills instead of making the model search for them. We tested listing against search live: 22 real skills, 14 requests (10 that need a skill, 4 that must not load one), each in English, Romanian and Russian, on five framework and model pairs, 630 runs in total. With search alone, the right skill was loaded 47–63% of the time. With the skill index listed in the prompt, 97–100%, and no wrong skill loaded in any of the 180 runs that should have loaded none. On Gemini CLI, the right skill was loaded for 38 of 39 listed skills but only 19 of 28 that had to be found by search. So every built-in skill is listed, grouped by the kind of work, in the open Agent Skills standard. A one-line note on each turn naming the likely skill, chosen by a small hosted classifier and sent when it is confident, lifts Codex to 101 to 105 of 109 English requests. Across every framework, on the wording that ships, the right skill was chosen for 392 of 429 requests (91%) in an end-to-end live run.

The second decision was to freeze the contract for the whole session. A prefix that never changes is cached. On Claude Code, the first turn of a session wrote 45,021 tokens of instructions and index to the prompt cache; the second turn read all 45,021 from the cache and wrote 75 new ones. When something changes mid-conversation, such as a skill installed or switched off, the next turn carries a short note saying what changed instead of a rewritten prompt.

Each framework receives the instructions through its own channel: an appended system prompt on Claude Code, developer instructions per thread on Codex, its configuration on OpenCode, a GEMINI.md in a configuration home the app owns on Gemini CLI, per-session rules on Grok Build. Anything specific to one conversation rides on the first turn instead, because several ACP frameworks run one process for many sessions. The frameworks’ own skill discovery is off, and the app reads those skill folders itself, so a skill appears the same way everywhere.

Why does the app serve the tools instead of each framework?

ACP has no method for subagents, background tasks, server health or forms. Codex’s protocol has some of these, and other frameworks have others. Building each capability once per protocol would mean 14 implementations of everything. The rule we settled on is short: don’t ask the framework to carry the capability; host the tools and hand it an address.

The app runs a Model Context Protocol server on the local machine, named tesseract. Claude Code gets the same toolsets inside its own process. Every other framework connects to the local server. Each tool is written once, as a name, a description, a schema and a handler, and served both ways. Because the handler runs in the app’s process, a tool can open a form in the chat and wait for the answer, or start a second session as a subagent, with no protocol support needed.

A capability counts when a live model uses it, and the tool’s own handler confirms each call. The first live proof: a real OpenCode model called the hosted browser tool, the request crossed into the app’s browser bridge, and the tool returned the active tab’s title, a value set by the test that the model could not have guessed.

Serving one tool set to 14 frameworks means meeting each one’s naming rules. The same tool reaches the model under different names, and each client has its own limits:

Frameworkskill_load as the model sees itLimits that bind us
Claude Codemcp__tesseract__skill_loaddescriptions cut at 2,048 characters
Codexmcp__tesseract__ namespace + skill_loadnames only [A-Za-z0-9_]; schemas over 5,000 bytes shrunk
Gemini CLImcp_tesseract_skill_loadnames over 63 characters mangled; $schema stripped
OpenCodetesseract_skill_loadforces additionalProperties: false
Grok Buildtesseract__skill_load, reached through its own tool searchdescriptions cut at 2,048 characters

So every Tesseract tool meets one standard, checked at build time: a lower-case area_verb name of at most 40 characters, a description of at most 2,048 characters, and a schema of at most 5 KB without root-level combinators. The instructions name tools in a neutral form, and the contract writes in the exact name each framework will show its model. OpenCode’s model only called a tool when given the exact prefixed name. Antigravity CLI is the opposite: it calls every MCP tool through a single tool of its own, so for it the names stay neutral.

How did 82 tools become 19?

We recorded what an OpenCode conversation is given when asked for an image. Handing it every tool directly came to 82 tools and about 43,600 tokens on every model call. Under the contract, the same recorded request carries 19 tools and about 8,600 tokens, with the instructions, the skills and the image tool all in reach. Every turn runs with the full contract. Measured live the same day: we asked for “an image of a goldfish on a skateboard” without naming a tool, and a picture reached the chat on 12 of the frameworks tested.

The 19 are OpenCode’s own kept tools plus the few the app serves directly, such as background jobs and helpers. At the centre is a core that is the same on every framework and always loaded:

  • skill_find, skill_load: list and load skills.
  • tool_find, tool_call, tool_call_readonly: search the full catalog and run what was found.
  • ask_user, plan_update, plan_propose: questions, the checklist and the plan card.

Everything else, more than a hundred tools from canvas and video to scheduling and browser control, stays behind tool_find and costs nothing until it is needed.

There are two dispatchers because frameworks run MCP calls without the read-only hint one at a time. Three tools that each took 4 seconds finished in 12 seconds without the hint and in 4 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 not on its allowlist, and everything else through tool_call.

tool_call is forgiving on purpose. Its keys are tool and args because that is what models send, even against a schema that said name. It parses args sent as a JSON string, and a wrong shape gets a reply naming the schema, which guides the model to a correct call. args is required in the schema and optional in what is accepted, so a model that sends only the required fields still passes its arguments. Across eight requests in two languages, against 166 real tool definitions, five framework and model pairs called the right tool with valid arguments 76 times out of 80.

Why does each framework keep its own shell and file tools?

The contract replaces a framework’s native tools slot by slot: planning, asking the user, subagents, skills, tool search, scheduling and web search are Tesseract’s on every framework. Two slots stay native by default, the shell and the file tools.

The reason is that a harness is tuned together with its model. Terminal-Bench 2.0 (arXiv 2601.11868) ran the same models in their vendors’ harnesses and in a neutral one. One model scored 62.9% in its own harness and 54.0% in the neutral one. The effect runs both ways: another model scored 52.1% in its own harness and 57.8% in the neutral one. The file-edit dialect a model was trained on is worth more than identical tooling. So the contract is identical, and each model keeps the shell and editing tools it knows.

Turning off the other natives takes a different mechanism almost everywhere: an exact tool list on Claude Code, feature switches per thread on Codex, a permission ruleset on OpenCode, settings in a managed home on Gemini CLI and Qwen Code. Where a framework has no switch for a tool, the permission gate refuses the call with a message naming Tesseract’s replacement.

How do 14 event streams become one message stream?

Diagram: one chat on top; under it the agent contract, frozen when the session starts; under that one connection point with 8 required operations and 32 capability flags; three routes below it (the Claude Agent SDK for Claude Code, the Codex app server, and ACP for twelve frameworks), with a local tool server feeding Codex and ACP; everything flows back into one message stream and one transcript format

The chat already had a small set of messages: streamed text, tool use, complete, error, status and session events. Every framework maps onto that set, and none of it had to be redesigned. When a framework lacks a capability, the app doesn’t send the matching message, and the optional method is absent. It never sends a different message in its place.

The mapping follows each protocol closely. On Codex:

  • turn/start returns as soon as the turn is accepted. The app ends the turn at the turn/completed notification, so the whole reply arrives.
  • A turn’s outcome arrives on both error and turn/completed, and the app reports it once.
  • A provider’s message, encoded as JSON, is unwrapped one level, so the person reads the provider’s plain sentence.

On ACP, a session’s updates become the same stream. Some frameworks send a tool call’s arguments after the call has opened, so the card fills in when they arrive. When a call goes through tool_call, the card shows the tool that actually ran, with its own arguments, not the dispatcher.

Approvals arrive through different doors and leave through one card. ACP has a permission request. Codex has approval requests for commands, file changes and permissions, answered with accept, acceptForSession, decline or cancel. Permission modes are mapped from whatever modes each framework advertises, never hard-coded. A tool reached through tool_call gets the same approval card it would have had if it were called directly. The plan card, which plan_propose raises, and the question card look the same on every framework.

The app gives every ACP request a deadline of its own: opening a session has 2 minutes and reloading one has 4. A turn with nothing from the framework for 6 minutes ends the process, as long as no tool call is open, no prompt is waiting for the person and nothing is paused. The turn then continues on a fresh process, up to 3 times, and the person is kept informed. A long, quiet tool call is not silence, because the framework has already reported it open.

What does a conversation keep when it moves to another framework?

You can move a conversation to another framework partway through. That works because the app keeps its own copy of every conversation, in one row format for all 14 frameworks: messages, tool calls and what each one returned.

The hard part is giving that history to the next framework in a form it treats as its own. Claude Code resumes a transcript file the app writes. Codex accepts raw history items into a thread over its protocol. For OpenCode, NanoClaw, Qwen Code, CodeBuddy, Grok Build, Kimi Code, Hermes Agent and Gemini CLI (0.64 and later), the app writes a session in each framework’s own storage format and opens it, so the tool calls arrive as tool calls. In a live check, Claude Code and Codex each picked up OpenCode’s file-read and shell calls and answered from what one had returned. The other four, Cursor, OpenClaw, MiniMax Code and Antigravity CLI, get the history as text in their first prompt and carry on from it.

Deleting a message shows the same thinking. “Delete” removes that message and everything after it, on every framework. Claude Code and OpenCode could remove a single message. But Codex’s rollback takes a number of turns counted from the end, and Gemini CLI appends a rewind marker to its log. So the app’s delete does the same thing everywhere.

How is each framework version checked?

Framework CLIs update themselves, so a nightly run tests all 14 CLIs against the backend before a version is approved. The combination you run is one that has already passed.

Questions

Does each framework keep its strengths in Tesseract? Yes. Each framework keeps its own shell and file tools and its own model, and native capabilities are declared rather than flattened. What changes is what sits around it: the same skills, the same Tesseract tools and the same approval cards.

Do I need every framework installed and signed in? Only the ones you use. Claude Code, Codex, OpenCode and Gemini CLI can use a subscription sign-in, an API key or the CLI already on your machine. Six, including Kimi Code, Qwen Code and Cursor, run only through their own installed and signed-in CLI. The other four take an API key or the CLI on your machine.

How much work is a fifteenth framework? If it speaks ACP, mostly one descriptor and one catalog entry with its 32 answers, then live checks for its delivery channel, tool naming and native switches. A framework with its own protocol, as Codex has, needs its own backend.

Why use both ACP and MCP? They do different jobs. ACP runs the session: prompts, streamed updates, permission requests, loading and forking. MCP carries tools. Keeping the tools on MCP served by the app is what lets one tool set reach every framework, whatever its session protocol can express. More on the frameworks is on the agentic IDE page; the short version of this design is in what’s underneath Tesseract. Related posts: how memory works across sessions on every framework and how a task carries on after a restart.

To see the same conversation run on two frameworks, download Tesseract, start a conversation and switch the framework from the composer.

Artikel lainnya di blog