Field notes
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.
coding-agentscontrol-planesacpagent-protocolsclaude-codecodexopencodepiagent-harnessesnative-driversevent-streamingagent-uipaseowakut3-code
and how others are making it
Claude CodeCodexOpenCodePiCursor
01 · The three jobs
Every tool does the same three things
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.
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 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.
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
| 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.
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 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.
ONE GRAMMAR, MANY AGENTSILLUSTRATIVE MAPPING
| 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.
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.
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.
ONE GRAMMAR, MANY AGENTSCONCEPTUAL VIEW
| 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.
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
- Agent Client Protocol · agentclientprotocol.com · github
- Claude Code · code.claude.com/docs · github
- Codex · learn.chatgpt.com/docs · github
- OpenCode · opencode.ai/docs · github
- Pi · github
- Cursor · cursor.com/docs/cli
- Orca · onorca.dev · github
- Superset · superset.sh · github
- Emdash · emdash.com · github
- Paseo · paseo.sh · github
- Waku · waku.sh · github
- T3 Code · github
- UseAgent · useagent.org · github
Wire shapes abbreviated from each vendor's documentation and the ACP specification. Product facts read from source.