How to Make a Control Plane for Coding Agents in 2026 A 2026 guide by Abhishek Gahlot outlines the three essential functions of a control plane for coding agents—communicating with each agent, translating events into a unified vocabulary, and rendering them—and identifies three integration methods: PTY (terminal), ACP (Agent Client Protocol), and native SDKs/APIs. The article notes that Claude Code, Codex, OpenCode, and Pi each use different protocols, with ACP supported natively by Gemini, OpenCode, Cursor CLI, Copilot CLI, Kiro, Factory Droid, Devin's CLI, and DeepSeek's harness, while Claude Code and Codex require adapters like claude-agent-acp and codex-acp. Back to writing / writing How to Make a Control Plane for Coding Agents in 2026 Claude Code, Codex, OpenCode and Pi all talk differently. Claude: Anthropic's SDK. Codex: a small server you send JSON to. OpenCode: an HTTP server with an event stream. Pi: a terminal. Every product that puts them in one window has to do the same three jobs, and there are only three ways to do the first one. Written by Abhishek Gahlot /about and how others are making it Every tool does the same three things Talk to each agent One adapter per agent. Send the prompt, receive events, answer "can I run this?" Translate into one vocabulary Whatever any agent sends becomes the same few words: said text, thinking, tool started, tool finished, needs permission, plan changed, turn done. Draw it Deltas become one message. Tool started plus tool finished become one row. Rows group into a work block. Job 1 is where the products differ. Three ways to speak to an agent, and each one caps what the screen can ever show. The quickest way to see it: the same edit, one line in src/billing.ts , as it arrives each way. Speaking through a terminal PTY A PTY is a pseudo-terminal. A fake terminal a program runs inside, so the program thinks a person is typing. The product launches the ordinary CLI, the one you'd run yourself, inside a PTY, types the prompt in, and reads the text that comes out. A person at a keyboard, except the person is a program. js ● Update src/billing.ts 12 - const rate = 0.2 12 + const rate = 0.25 no prompt: --dangerously-skip-permissions Text, not data. The path is a regex over scrollback. "Done" comes from a hook installed into the agent's config. Approval would mean typing y, so the bypass flag is on. Who: Orca and Superset, by default. Both keep a real driver behind a flag. Speaking through ACP Agent Client Protocol ACP is Zed's protocol. JSON-RPC over stdio, one wire format for start a session, send a prompt, get updates, answer permissions. Gemini, OpenCode, Cursor CLI, Copilot CLI, Kiro, Factory Droid, Devin's CLI and DeepSeek's harness speak it natively. Claude Code and Codex don't. They get there through adapters, claude-agent-acp and codex-acp , separate processes that run the native driver underneath. session/update tool call call 7 kind: edit status: pending locations: src/billing.ts content: diff … if the adapter fills it session/request permission → allow once session/update tool call update call 7 completed no usage · no subagent kind · nothing in between Data, and a permission you can answer. For Claude and Codex it comes through an adapter process: the session lives there, so your restart has nothing to reattach to; it applies its own approval policy; and the wire never says which driver or version is behind it. Who: Emdash, by default. Paseo and Waku for agents that speak nothing else. Editors, where it started. Speaking the native language SDK · app-server · HTTP · RPC Each agent has a machine interface its own vendor's products use. Claude Code: the Agent SDK runs the loop in your process, hands you every event, asks you through a callback before each tool. Codex: the app-server, a JSON-RPC process with thread and turn methods, item lifecycle notifications, approval requests per kind. OpenCode: a server with sessions, an SSE stream, permissions, questions. Pi: an RPC mode over its terminal with prompt, steer, follow-up, abort, fork. canUseTool "Edit", input → allow tool use Edit toolu 01 file path: src/billing.ts old string: const rate = 0.2 new string: const rate = 0.25 tool result toolu 01 updated result session id: ses 9f2 cost usd: 0.0123 Everything, in the agent's own words: path and both strings, the question before the tool runs, a session id to resume from, cost per turn, subagent calls tagged with their parent. Codex, OpenCode and Pi give the same in their own shapes. The price is one adapter per agent, pinned. Who: Paseo, Waku, T3 Code and UseAgent, for all four agents. Orca and Superset behind a flag. What the screen can draw from each | Capability | TerminalOrca, Superset | ACPEmdash; adapters for Claude and Codex | NativePaseo, Waku, T3, UseAgent | |---|---|---|---| | The edited file's path | scrape the scrollback | yes | yes | | The diff | scrape | if the adapter fills it | yes, old and new text | | Ask before the tool runs | no, bypass flag on | yes, adapter's policy | yes, your code answers | | Progress while a command runs | text scrolls | no, pending then completed | yes, deltas | | Resume after your process restarts | no | only if the agent implements session/load | yes, session id | | Token cost per turn | no | no in version 1 | yes | | A subagent shown as a child | no | no, a tool of kind "other" | yes, parent tool id | | Know which driver ran | yes, it is the CLI | no | yes | | Adapters to maintain | 0 | 1 | one per agent | Scroll to compare → Row labels stay in view. The middle column is the one that surprises people. ACP looks like data and mostly is. What it costs is the three rows a control plane exists for: resume, approvals that mean what the agent meant, and knowing what is running. For an agent that speaks nothing else, ACP is the right door. For Claude and Codex it is a second door to an agent you can already reach. The Rust way: Waku Every other product here is TypeScript. So are the agents' own client libraries: the Claude Agent SDK is a Node package, OpenCode's SDK is TypeScript, T3's Codex driver is a TypeScript port of the app-server schema. Waku is Rust end to end. A daemon, a GPUI desktop app, a protocol crate. That one choice changes how all three jobs get done. Every other product here is TypeScript, like the agents' own client libraries. Waku is Rust end to end: a daemon, a GPUI desktop app, a protocol crate. So its vocabulary is a Rust enum, not a schema. Every driver must produce it and every consumer must match it exhaustively; add a variant and the daemon and the app fail to compile until both handle it. The protocol types carry a TS derive. ts-rs generates the TypeScript from them into the client package, and the web and mobile apps import it. Daemon, native app, browser and phone share one definition of a session, an activity, a permission. Codex's app-server protocol is Rust with ts-rs bindings of its own, so Waku speaks it type to type; Claude has no Rust library, so Waku speaks its stream-json wire directly, no Node sidecar. The screen is the point: "staying smooth under a long transcript on a high-refresh display is the point of being native." A virtualized GPUI list, drivers feeding a channel the frame loop drains. What it leaves out is everything multi-user: one token, SQLite and git refs as the record, no organization. The vocabulary they share Whichever way an agent is spoken to, what comes out is folded into the same nouns. Read across a row and you are reading translations. | Event family | ACPthe wire spec, session/update | T3 Codeactivity kinds at ingestion | WakuDriverEvent, Rust | Paseotimeline items | Emdashparser output from ACP | UseAgentcanonical events | |---|---|---|---|---|---|---| | Assistant text | agent message chunk | a provider event, not an activity | TextDelta | assistant message | message text | message.started .delta .completed | | Reasoning | agent thought chunk | same, via textDelta | ReasoningDelta | reasoning | thinking | reasoning.delta .completed | | Tool lifecycle | tool call tool call update | tool.started .updated .progress .completed .denied | Activity RichActivity | tool call with a kind | tool call tool update , then typed calls | tool.started .progress .completed | | Permission | session/request permission | approval.requested .resolved | Permission | permission requested resolved | answered by the client, not folded | approval.requested .resolved | | Question | elicitation/create | user-input.requested .resolved | UserInputRequested | attention required | select | question.requested .resolved | | Plan | plan | turn.plan.updated | ActivityKind::Plan | plan todo | plan | plan.updated | | Subagent | a tool call of kind other | task.started .updated .progress .completed | BackgroundWork | sub agent | subagent update | child.started .updated .completed | | Turn | prompt request, stop reason | a thread event | TurnStarted TurnParked | turn started completed failed | turn end | turn.started turn.completed | | Usage | none in v1 | context-window.updated | UsageUpdated | usage updated | usage | usage.updated | | Trouble | JSON-RPC error | runtime.error .warning | event journal | error warning | ignored | harness.error .warning | Scroll to compare → Row labels stay in view. Tool calls fall into the same eight families everywhere: read, edit or write, execute, search, fetch, plan, subagent, other. ACP names them in the spec; the others classify by the tool's name. Keeping the record Most tools translate on the way in and keep only the translation. On one laptop that's fine, the agent's own transcript on disk is the raw copy. A hosted product doesn't have that transcript. The sandbox that wrote it is gone. So it has to keep its own. Most tools translate on the way in and keep only the translation. On one laptop that's fine, the agent's own transcript on disk is the raw copy. A hosted product doesn't have that transcript. The sandbox that wrote it is gone. So it keeps its own: the event exactly as the agent sent it, then the vocabulary derived from that. The translator is the piece most likely to be wrong, because the agents change weekly, and with the raw copy kept a fix is a re-run over history instead of a permanent hole. Emdash keeps nothing and re-asks the agent. Paseo keeps rows in memory. Waku keeps SQLite and a resume cursor. T3 keeps an event store. A hosted control plane keeps the raw frame and the translation, sealed per run, scoped to an organization. Seven products, side by side Read each column top to bottom. The driver row decides everything under it. | Compare by | OrcaStably AI, YC · MIT | SupersetSuperset Inc. · Elastic 2.0 | EmdashGeneral Action, YC · Apache-2.0 | Paseoone maintainer · Apache-2.0 | Wakuegoist · GPL-3.0 · Rust | T3 Coderuntime, Effect | UseAgentAGPL core + pro | |---|---|---|---|---|---|---|---| | Spoken tojob 1 | PTY 36 agents, hooks for status | PTY 19 agents, hooks for status | ACP 25 agents, official adapters for Claude and Codex | native 4 agents; ACP for about 30 more | native 7 agents; ACP for 4 more | native 3 agents; ACP for 3 more | native 4 agents, via T3 and an RPC bridge for Pi | | Translatedjob 2 | none by default | chat mode only, off | parsed from ACP into typed tool calls | own timeline items, in memory | a Rust enum | activity kinds | raw frame, then canonical events | | Kept where | scrollback checkpoints | 64 KB ring buffer | re-asked from the agent on load | in memory; provider history | SQLite + git refs | event store | Postgres, org-scoped, sealed | | Drawnjob 3 | xterm, diff view | terminal, diff, PRs | transcript, tool groups | desktop, mobile, voice | timeline with rewind | work groups | one timeline for all four | | Whose record | one person | one person; org in the cloud API | one person | one person | one person | one person | an organization | Scroll to compare → Row labels stay in view. About the author I build UseAgent , the hosted one in the last column: the four agents in cloud workstations, one timeline, scoped to a company. It runs on T3 Code's runtime and grammar, because T3 already spoke the native protocols and its event core is the right seam. UseAgent keeps the raw frame T3 hands over and translates it again for the record. Sources - Agent Client Protocol · agentclientprotocol.com https://agentclientprotocol.com · github https://github.com/agentclientprotocol/agent-client-protocol - Claude Code · code.claude.com/docs https://code.claude.com/docs · github https://github.com/anthropics/claude-agent-sdk-typescript - Codex · learn.chatgpt.com/docs https://learn.chatgpt.com/docs · github https://github.com/openai/codex - OpenCode · opencode.ai/docs https://opencode.ai/docs · github https://github.com/sst/opencode - Pi · github https://github.com/badlogic/pi-mono - Cursor · cursor.com/docs/cli https://cursor.com/docs/cli - Orca · onorca.dev https://onorca.dev · github https://github.com/stablyai/orca - Superset · superset.sh https://superset.sh · github https://github.com/superset-sh/superset - Emdash · emdash.com https://emdash.com · github https://github.com/generalaction/emdash - Paseo · paseo.sh https://paseo.sh · github https://github.com/getpaseo/paseo - Waku · waku.sh https://waku.sh · github https://github.com/egoist/waku - T3 Code · github https://github.com/pingdotgg/t3code - UseAgent · useagent.org https://useagent.org · github https://github.com/useagenthq/useagent