Back to 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.

and how others are making it

01 · The three jobs

Every tool does the same three things

Different inputs. One readable timeline.FIG. 01
Different inputs. One readable timeline.Illustrative flow: four agent interfaces feed a shared product event, then one tool row. Native event names and normalized payload fields are schematic. 01 / CONNECT02 / NORMALIZE03 / RENDERClaudeSDK eventsCodexapp-server eventsOpenCodeHTTP / SSE eventsoh-my-piRPC eventsProduct eventcall: edit_42kind: tool.completedpath: billing.tsstatus: successWork completedEdited billing.ts+12 −3One UI, stable meaning
Different inputs. One readable timeline.Illustrative flow: four agent interfaces feed a shared product event, then one tool row. Native event names and normalized payload fields are schematic. ClaudeSDKCodexapp-serverOpenCodeHTTP / SSEoh-my-piRPCOne product vocabularyidentity · event kind · stateONE TIMELINE ROWEdited billing.ts+12 −3 · call: edit_42
ONE GRAMMAR, MANY AGENTSILLUSTRATIVE MAPPING
1

Talk to each agent

One adapter per agent. Send the prompt, receive events, answer "can I run this?"

2

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.

3

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.

02 · The first 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.

A terminal carries bytes. Structure is extra.FIG. 02
A terminal carries bytes. Structure is extra.Terminal input and output can be rendered directly. Hooks, logs, parsers or RPC can provide a companion structured channel. $ agent "run tests"Running test suite…Allow command? [y/n]bytes + terminal controlsstdin ⇄ stdoutTerminal viewRender the byte streamStructured companion channelHooks, logs, parser or RPCA PTY provides terminal I/O. Additional channels can supply structured state.
A terminal carries bytes. Structure is extra.Terminal input and output can be rendered directly. Hooks, logs, parsers or RPC can provide a companion structured channel. $ agent "run tests"Running test suite…Allow command? [y/n]bytes + terminal controlsstdin ⇄ stdoutTerminal viewDisplay the agent’s screenStructured companion channelHooks, logs, parser or RPCRich controls need structured meaning.
ONE GRAMMAR, MANY AGENTSCONCEPTUAL VIEW
What arrives
● Update(src/billing.ts)
     12 -  const rate = 0.2
     12 +  const rate = 0.25
no prompt: --dangerously-skip-permissions
What that is

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.

03 · The second way

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.

One tool call, updated as it runs.FIG. 03
One tool call, updated as it runs.ACP tool calls can progress from pending to in progress to completed. Permission requests carry user decisions back to the agent. Agent / adapterSpeaks the ACP contractClient UITracks tool-call stateACP · session/updatepending01in_progress02completed03session/request_permission ⇄ allow / reject
One tool call, updated as it runs.ACP tool calls can progress from pending to in progress to completed. Permission requests carry user decisions back to the agent. Agent / optional adapterNative agent behind the protocolACP · session/updatependingin_progresscompletedClient UISame tool ID through its lifecyclePermission request ⇄ user decision
ONE GRAMMAR, MANY AGENTSCONCEPTUAL VIEW
What arrives
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
What that is

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.

04 · The third way

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.

Four protocols. One product contract.FIG. 04
Four protocols. One product contract.Claude, Codex, OpenCode and oh-my-pi retain distinct native interfaces. Drivers normalize meaning for a shared product contract. Claude CodeAgent SDKNative drivercallbacksCodexapp-serverNative driverJSON-RPCOpenCodeHTTP + SSENative drivereventsoh-my-pinative RPCNative driverframesShared product contractSession identityDeclared capabilitiesNormalized eventsConsistent UINative protocols remain distinct; the product-facing meaning is shared.
Four protocols. One product contract.Claude, Codex, OpenCode and oh-my-pi retain distinct native interfaces. Drivers normalize meaning for a shared product contract. Claude CodeAgent SDKcallbacksCodexapp-serverJSON-RPCOpenCodeHTTP + SSEeventsoh-my-pinative RPCframesShared product contractKeep native identity and capability
ONE GRAMMAR, MANY AGENTSCONCEPTUAL VIEW
What arrives · Claude Agent SDK
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
What that is

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

CapabilityTerminalOrca, SupersetACPEmdash; adapters for Claude and CodexNativePaseo, Waku, T3, UseAgent
The edited file's pathscrape the scrollbackyesyes
The diffscrapeif the adapter fills ityes, old and new text
Ask before the tool runsno, bypass flag onyes, adapter's policyyes, your code answers
Progress while a command runstext scrollsno, pending then completedyes, deltas
Resume after your process restartsnoonly if the agent implements session/loadyes, session id
Token cost per turnnono in version 1yes
A subagent shown as a childnono, a tool of kind "other"yes, parent tool id
Know which driver ranyes, it is the CLInoyes
Adapters to maintain01one 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.

05 · A different philosophy

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.

One definition, more than one interface.FIG. 05
One definition, more than one interface.A shared Rust protocol is consumed by native components, while generated TypeScript bindings serve other clients. The paths are conceptual, not a call graph. Shared Rust protocolDriverEventTextDelta · PermissionUsageUpdatedsession · epoch · sequenceNative Rust consumerDaemon → GPUI rendererGenerated TypeScript bindingsWeb / mobile → their own renderersts-rsOne protocol definition. Multiple interfaces.
One definition, more than one interface.A shared Rust protocol is consumed by native components, while generated TypeScript bindings serve other clients. The paths are conceptual, not a call graph. Shared Rust protocolDriverEventsession · epoch · sequenceOne versioned definitionRust consumerDaemon + GPUI applicationGenerated TypeScript typesBrowser + mobile clientsShared types; a renderer for each client.
ONE GRAMMAR, MANY AGENTSCONCEPTUAL VIEW

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.

06 · Job 2

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.

Translate the meaning, not just the name.FIG. 06
Translate the meaning, not just the name.Illustrative mapping of a completed edit across ACP, Codex and UseAgent. Event identity, path and status must survive normalization. ACPtool_call_updateCodexitem/completedUseAgenttool.completedOne completed edit, expressed differentlyIDENTITYedit_42PATHbilling.tsSTATEcompletedSimilar nouns are the start. Identity, order and lifecycle preserve the meaning.
Translate the meaning, not just the name.Illustrative mapping of a completed edit across ACP, Codex and UseAgent. Event identity, path and status must survive normalization. ACPtool_call_updateCodexitem/completedUseAgenttool.completedThe same completed editcall_id: edit_42path: billing.tsstate: completedPreserve identity, order and details.
ONE GRAMMAR, MANY AGENTSILLUSTRATIVE MAPPING
Event familyACPthe wire spec, session/updateT3 Codeactivity kinds at ingestionWakuDriverEvent, RustPaseotimeline itemsEmdashparser output from ACPUseAgentcanonical events
Assistant textagent_message_chunka provider event, not an activityTextDeltaassistant_messagemessage textmessage.started .delta .completed
Reasoningagent_thought_chunksame, via textDeltaReasoningDeltareasoningthinkingreasoning.delta .completed
Tool lifecycletool_call tool_call_updatetool.started .updated .progress .completed .deniedActivity RichActivitytool_call with a kindtool_call tool_update, then typed callstool.started .progress .completed
Permissionsession/request_permissionapproval.requested .resolvedPermissionpermission_requested _resolvedanswered by the client, not foldedapproval.requested .resolved
Questionelicitation/createuser-input.requested .resolvedUserInputRequestedattention_requiredselectquestion.requested .resolved
Planplanturn.plan.updatedActivityKind::Planplan todoplanplan.updated
Subagenta tool call of kind othertask.started .updated .progress .completedBackgroundWorksub_agentsubagent_updatechild.started .updated .completed
Turnprompt request, stop reasona thread eventTurnStarted TurnParkedturn_started _completed _failedturn_endturn.started turn.completed
Usagenone in v1context-window.updatedUsageUpdatedusage_updatedusageusage.updated
TroubleJSON-RPC errorruntime.error .warningevent journalerror warningignoredharness.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.

07 · What survives

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.

A translator fix can repair a saved history.FIG. 07
A translator fix can repair a saved history.Capture a bounded source record. Replay retained detail through an updated translator to correct a product projection. Uncaptured detail cannot be restored. Provider eventCapture / redact / boundRetained sourceevent_id: edit_42Original captured detailTranslator v1Current product projectionreplay saved evidenceTranslator v2Corrected timelineA fix can repair history only where source detail was retained.
A translator fix can repair a saved history.Capture a bounded source record. Replay retained detail through an updated translator to correct a product projection. Uncaptured detail cannot be restored. Provider eventApply the capture / redaction policyRetained sourceBounded evidence for later replayTranslator v1 → v2Re-run against the same saved eventCorrected timelineSame source, improved projectionReplay cannot restore uncaptured detail.
ONE GRAMMAR, MANY AGENTSCONCEPTUAL VIEW

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.

08 · Reference

Seven products, side by side

Read each column top to bottom. The driver row decides everything under it.

Seven products. Different starting points.FIG. 08
Seven products. Different starting points.Conceptual grouping of the defaults described by the article: terminal-first Orca and Superset, ACP-first Emdash, and native or mixed Paseo, Waku, T3 Code and UseAgent. This is not an independent feature certification. Terminal-firstOrcaSupersetACP-firstEmdashNative / mixedPaseoWakuT3 CodeUseAgentDefault approaches, not a ranking. Products can support more than one path.
Seven products. Different starting points.Conceptual grouping of the defaults described by the article: terminal-first Orca and Superset, ACP-first Emdash, and native or mixed Paseo, Waku, T3 Code and UseAgent. This is not an independent feature certification. Terminal-firstOrcaSupersetACP-firstEmdashNative / mixedPaseoWakuT3 CodeUseAgentGrouping from the article’s pinned snapshots.
ONE GRAMMAR, MANY AGENTSCONCEPTUAL VIEW
Compare byOrcaStably AI, YC · MITSupersetSuperset Inc. · Elastic 2.0EmdashGeneral Action, YC · Apache-2.0Paseoone maintainer · Apache-2.0Wakuegoist · GPL-3.0 · RustT3 Coderuntime, EffectUseAgentAGPL core + pro
Spoken tojob 1PTY 36 agents, hooks for statusPTY 19 agents, hooks for statusACP 25 agents, official adapters for Claude and Codexnative 4 agents; ACP for about 30 morenative 7 agents; ACP for 4 morenative 3 agents; ACP for 3 morenative 4 agents, via T3 and an RPC bridge for Pi
Translatedjob 2none by defaultchat mode only, offparsed from ACP into typed tool callsown timeline items, in memorya Rust enumactivity kindsraw frame, then canonical events
Kept wherescrollback checkpoints64 KB ring bufferre-asked from the agent on loadin memory; provider historySQLite + git refsevent storePostgres, org-scoped, sealed
Drawnjob 3xterm, diff viewterminal, diff, PRstranscript, tool groupsdesktop, mobile, voicetimeline with rewindwork groupsone timeline for all four
Whose recordone personone person; org in the cloud APIone personone personone personone personan organization

Scroll to compare →   Row labels stay in view.

Where this comes from

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.

References

Sources

Wire shapes abbreviated from each vendor's documentation and the ACP specification. Product facts read from source.