Claude Code, Codex, OpenCode, and Pi in One App
The first version saved the task and left the agent idle. Starting with one OpenCode sandbox, then building the execution and history that let four coding agents share one app.
Source code · useagenthq/useagent
Part 1 · The product
Watch useAgent do the work
Four recorded product clips: submit a task, follow the work, open the result, and try the finished app.
The user typed a task. The app saved it, created a cloud computer, and opened the agent screen. Nothing happened.
OpenCode was running. The prompt was in the database. Nobody had sent the prompt to OpenCode.
This was an early version of useAgent. The goal was to give an agent a repository on a real machine, with a terminal and browser we could watch. In one demo, OpenCode wrote a Flappy Bird clone, started it, opened Chromium, and used it.
Claude Code, Codex, OpenCode, and Pi eventually shared the product. The starting point was one engine doing one task all the way through.
Start with one agent on one computer
A coding agent already has the loop that reads a request, calls the model, uses tools, and continues. OpenCode supplied that loop. The missing piece was a computer where those actions could change a repository and run an application that both the agent and user could inspect.
The July version put OpenCode inside a Daytona sandbox. A Bun control plane created it and returned signed preview URLs for the agent, terminal, and desktop views. Xvfb, XFCE, x11vnc, and noVNC made the headless desktop visible in the browser.
For a first local proof, ask the agent to create greeting.txt with an exact string, then read it back independently. In our July 30 cross-engine check, each engine wrote hello from <engine> and printed it. That tests prompt delivery, working directory, tool execution, and result reporting.
A saved prompt is not a delivered prompt
The idle-agent bug sat between provisioning and execution. The app had fulfilled its own request to create a sandbox, but the coding agent had received no work. Waiting longer could not fix that.
The repair waited for OpenCode's API, created the agent session, and posted the prompt. The computer existing and the agent accepting work became separate conditions.
If no tool call appears, first check whether the prompt reached a ready agent. If it did, inspect its events and then the resulting file.
The later request lifecycle writes the run and command atomically before dispatch. That records the task before execution; reading the file back still tells us whether it worked.
The app started, then the server disappeared
Once the agent could write code, the next failure was in the browser: connection refused. The dev server had started, but it was gone by the time the browser needed it.
OpenCode cleaned up the command's process group when the command returned. Starting npm run dev & did not give the server an independent lifetime. We tried nohup and setsid too. Those processes were still reaped.
The repair was to bake tmux into the image and give the agent a skill for running dev servers under it. The skill also required a curl check before opening the browser. Starting a process and having a page ready to serve are separate events, just as provisioning a machine and having an agent ready were separate events.
The recording above is a later captured product run, not a live model call on this page. Follow the task, worklog, result, and output chapters to see what the agent returned and what the user could open.
The second message changes the architecture
A first task can look complete with a prompt, process, and result. A reply exposes what that model left out.
Consider a follow-up to the game: change its gravity. The files, product conversation, and agent context live under different identities. A transcript cannot keep a dev server alive, and a running sandbox cannot reconstruct a conversation the product never saved.
An early follow-up path started unrelated runs and copied previous conversation into each new visible prompt. Stored text grew without separating the user's words from backend-assembled context.
The repair introduced thread_id and parent_run_id. The database keeps the user's raw prompt, while the invocation path builds the engine context. The run schema persists native session IDs and sandbox mappings. In turn-prompt.ts, a fresh session gets reconstructed thread context; a resumed session receives the new turn.
The workspace became retained per thread. Reuse still checks the machine, runtime, and credentials. Part two follows that lookup.
Reload should read history, not ask the agent to repeat it
The iframe gave us an interface quickly but left conversation history inside OpenCode. Another agent would need another embedded UI or a shared product view.
The rebuild first used a scripted 12-second trace in place of the real agent: clone, commands, file edits, a result, done. The point was to get the path from accepted request to stored steps to streamed UI working with predictable input. A model would have added another variable while that path was still broken.
Runs and steps moved into Postgres through Drizzle, with Hono serving the API. On reconnect, the thread stream returns an authoritative snapshot. The backend lifecycle describes that path. History replay does not rerun a command or prove an interrupted native turn can resume.
Once a thread and its computer outlive a request, every history read, reply, and computer attachment has to check who can access it. We used better-auth for users and organizations, with ownership enforced in the backend. One early review found that run detail and streaming routes fetched by ID without checking organization scope.
The native coding loops still run through their engine runtimes. The product backend owns acceptance, authorized access, thread history, and lifecycle coordination. Moving those responsibilities out of an embedded UI did not require writing our own model loop.
Four agents, one conversation screen
Now the second engine has a concrete job: run the same small file-writing task, report its activity into the same history, and leave a result the same UI can render. It does not need to speak the first engine's wire protocol.
OpenCode has a resident server with REST and an event stream. Claude Code and Codex went through compatibility and native-driver paths at different revisions. Pi uses native RPC over a PTY. Its bridge waits for readiness, obtains both a sessionId and a persistent sessionFile, then sends commands such as prompt and abort. That file path is part of resuming Pi, not something a generic chat message can replace.
Each driver translates the events it actually receives into product messages, tools, files, and run state. If an integration cannot expose an approval or child session, the UI cannot manufacture one. Parts three and four walk through those protocol messages and the translation, including an interactive example where a stale tool update must not turn a completed command back into a running one.
T3 Code was useful here because it was already implementing provider-specific adapters and a common session model. We brought its native Codex app-server, Claude Agent SDK, and OpenCode driver work into the sandbox runtime. The fork lock records the upstream and bundled runtime identity. Pi's bridge was a separate integration.
That choice carried costs: a runtime image could be stale, a WebSocket could reach the frontend instead of the backend, and a provider connection could fail on database permissions. The early T3 candidate was slower in our comparison. I kept it for the provider semantics and maintenance benefit, with the startup path still needing work.
Waku, Multica, and Paperclip were different kinds of reference
In the September 1 child-session study, I asked the same questions about each project: where does its durable state live, what does ACP control, how are child tasks represented, and what survives a restart?
| Project | What I found in the study | What it helped me check |
|---|---|---|
| Waku 0.1.16 | A local Rust daemon with SQLite task/session state, native coding-agent drivers, and ACP for selected CLIs. | The harness boundary was close to ours. Its local, single-user assumptions were a different fit from a hosted service. |
| Multica | Postgres-backed work and conversations, with agents coordinated through issues, comments, mentions, and a squad leader. ACP sat below that product model. | Delegation could be durable without being a live chat with a provider's native child session. |
| Paperclip | Companies, agents, issues, and heartbeat runs in Postgres, with adapters underneath. Child work became another durable issue. | It was closest to the control-plane bookkeeping: ownership, budgets, wakeups, and the record of what happened. |
Waku's durability distinction mattered. In the version I studied, SQLite kept task state, while WebSocket replay used a bounded in-memory journal. A buffer that helps reconnect to a live daemon is not a durable event log. Its subagents were background work items in a flat UI; our child-thread model had to serve a different interaction.
Paperclip's experimental Runner design gave the runner its own protocol and an acknowledged outbox while keeping product authority on the server. That decision was separate from whether an adapter spoke ACP. The design was experimental and default-off; I did not treat it as a production guarantee.
I did not replace our thread model with Waku tasks, Multica issues, or Paperclip heartbeat runs. They made the boundary clearer. A native agent session ID is useful for resuming an agent. It is a poor identity for a conversation that has to outlive that agent process.