cd /news/developer-tools/how-to-make-a-control-plane-for-codi… · home topics developer-tools article
[ARTICLE · art-123285] src=abhishek.it ↗ pub= topic=developer-tools verified=true sentiment=· neutral

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.

read10 min views2 publishedSep 8, 2026

Back to writing

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

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.

● 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 #

── more in #developer-tools 4 stories · sorted by recency
── more on @abhishek gahlot 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/how-to-make-a-contro…] indexed:0 read:10min 2026-09-08 ·