Three coding-agent shells, three very different answers to the same question. T3 Code wraps the official Claude Agent SDK and Codex app-server and deliberately owns no loop. opencode owns the loop, the model call, and the tool execution itself. This report traces a single prompt through each, then shows how the subscription model shapes the experience you actually feel.
The defining decision each project makes: where does the model call live, and where does the agent loop live? Everything else — transport, persistence, auth — flows from that.
T3 delegates the model call and the loop to external SDKs. Its server is an event-sourced orchestration shell that translates provider events into UI updates.
OrchestrationEngine appends events, four independent subscribers react@anthropic-ai/claude-agent-sdk query() — SDK spawns the CLIspawn("codex",["app-server"]) + hand-rolled JSON-RPC over stdin/stdoutProviderRuntimeEvent union is provider-agnostic — adapters project into itclaude/codex CLI login. No T3 account.opencode owns the entire agent. It calls the model directly through the Vercel AI SDK, runs an explicit while(true) loop, and executes every tool itself.
streamText() from the ai package, 75+ providers via @ai-sdk/*session/prompt.ts runLoop() — call → tools → results → call againread, edit, shell, task…) executed in Bun/undoT3 asks "what did the agent just do?" and relays it. opencode asks "what should the agent do next?" and does it. Both are legitimate; they sit at opposite ends of the build-vs-buy spectrum for the agent runtime.
Pick a provider and step through the lifecycle. Each lit node is where bytes actually move; arrows that turn clay show the direction of the current step. The dashed box appears only when the architecture has a loop owned by that layer.
This is the most misunderstood part. "Keeping the loop going" means very different things in each system. The loop is the agent's autonomy — reasoning, calling tools, reasoning again until it decides it's done.
T3 sends one SDKUserMessage per turn onto a queue. Claude/Codex then streams autonomously until turn.completed. No code in T3 re-dispatches a turn.
turn.completed handler in ProviderRuntimeIngestion.ts:1143 only finalizes state — verified, it never emits thread.turn.start.opencode's own runLoop() in session/prompt.ts is a literal while(true): call the model, check if it ended on tool calls, execute them, write results to SQLite, loop. The exit condition is opencode's, not the model's.
agent.steps), inject MAX_STEPS_PROMPT, compact context mid-loop — controls T3 cannot exert without forking the SDK.T3's choice — thin shell — means a new provider is "just" a new adapter emitting the same events. But T3 cannot see inside Claude's reasoning loop, cap its steps, or inject mid-turn guidance.
opencode's choice — own the loop — means full control (steps, compaction, subtasks, any model) but opencode must reimplement tool dispatch, context management, and provider quirks that the Claude/Codex runtimes give T3 for free.
// opencode's explicit loop — the thing T3 deliberately does NOT have const runLoop = Effect.fn(function*(sessionID) { let step = 0; while (true) { yield* status.set(sessionID, { type: "busy" }); const msgs = yield* MessageV2.filterCompactedEffect(sessionID); const { lastAssistant, hasToolCalls } = latest(msgs); // EXIT: assistant finished AND not waiting on tool results if (lastAssistant?.finish && lastAssistant.finish !== "tool-calls" && !hasToolCalls) break; step++; const result = yield* handle.process({ messages, tools, model, ... }); // ↑ streamText() → tool-call events → execute tools locally → write results } });
Every cell below is grounded in source. "T3 (Claude)" and "T3 (Codex)" share the orchestration layer but differ at the adapter; they're shown as one column each because the contrast is instructive.
| Dimension | T3 · Claude | T3 · Codex | opencode |
|---|---|---|---|
| Process ownershipWho spawns the model runtime | SDK does. T3 calls query(); the SDK spawns claude.ClaudeAdapter.ts:2729 |
T3 does. spawn("codex",["app-server"]) directly.codexAppServerManager.ts:552 |
opencode does. Calls streamText() in-process; no CLI subprocess. |
| Transport to modelWire format | Streaming SDKMessage async iterable (Anthropic stream-json-ish, via SDK)Stream.fromAsyncIterable |
Newline-delimited JSON-RPC over stdin/stdout via readlinewriteMessage:1325 |
AI SDK fullStream over HTTPS (Anthropic/OpenAI/75+ providers) |
| How a prompt is sentThe write call | Queue.offer(promptQueue, SDKUserMessage) — one per turnClaudeAdapter.ts:2938 |
stdin.write(JSON + "\n") with method turn/startcodex…:814 |
streamText({ model, messages, tools }) — one call per loop iteration |
| Who owns the agent loopThe "reason → tool → reason" cycle | SDK / Claude CLI | Codex app-server | opencode (explicit while(true)) |
| Client ↔ server transport | WebSocket (req/resp + orchestration.domainEvent push)shared by both T3 providers |
HTTP + Server-Sent Events (Go TUI client) | |
| Tool executionWhere tools run | Inside Claude CLI (SDK-managed); T3 only gates via canUseTool callbackClaudeAdapter.ts:2542 |
Inside Codex app-server; T3 answers approval requests via JSON-RPC responsescodex…:902 |
Inside opencode's own Bun process (TS tool modules: read/edit/shell/task…) |
| Streaming to UI | content.delta → buffered (24k-char coalescing) → WS pushProviderRuntimeIngestion.ts:1035 |
AI SDK deltas → event bus → SSE to TUI (token-by-token) | |
| Persistence model | Event-sourced log + SQLite projections (OrchestrationEngine) |
State-based SQLite + Drizzle (sessions/messages/parts tables); git snapshots for undo | |
| Auth / model access | Bring-your-own — your local claude / codex CLI login. No T3 account. no T3 subscription |
BYOK keys · OAuth-reuse (ChatGPT/Copilot/Claude subs) · optional Zen credits | |
| Default model | claude-sonnet-4-6 |
gpt-5.4 |
Configurable per agent; model recorded per session |
| Provider abstraction | ProviderKind = "codex" | "claudeAgent" · shared ProviderRuntimeEvent unionorchestration.ts:30 |
Provider.Service resolving to AI-SDK LanguageModelV3 · 75+ providers via Models.dev |
|
From "process ownership" down to "tool execution," every row that involves running the model or a tool is owned externally in T3 and internally in opencode. The rows where T3 and opencode look similar (transport to client, persistence) are exactly the rows that are not the agent — they're the shell around it. That's the cleanest way to see what each project has chosen to build vs. buy.
The headline finding from the source: T3 has no subscription system at all. So "how does the subscription create the T3 experience?" has a subtle answer — the subscription that matters is the one you hold with Anthropic or OpenAI, not with T3. Here are the three real models and what each unlocks.
T3 ships zero auth, account, billing, or entitlement code (verified — the persistence layer has no user/subscription tables; the only "auth" is an optional shared WS secret).
It inherits whatever login your local claude and codex CLIs already carry. That means your Claude Pro/Max or ChatGPT Plus/Pro subscription is the T3 subscription.
account/read only to decide if the Spark model is allowed — it never charges you.opencode (the software) is MIT-licensed and free. You paste API keys into /connect (stored locally in auth.json) — tokens billed directly by the provider.
Or you log in with GitHub/OpenAI/Anthropic to reuse subscriptions you already pay for (ChatGPT Plus/Pro, Copilot, Claude Pro/Max).
The only place opencode itself intermediates billing. Zen offers a handpicked, benchmarked set of models on a credit balance. Critically: "no tiered plans that unlock features."
Auto-reloads $20 below $5; card fees passed at cost (4.4% + $0.30). Workspaces free in beta.
For T3, the experience you feel — which models are available, your rate limits, your context window, whether Opus is an option — is entirely determined by the tier of the underlying Claude/ChatGPT subscription your CLI is logged into. T3 is a viewer on top of that entitlement. Swap a Pro login for a free login and the T3 experience degrades instantly, with no T3 code involved.
opencode is the opposite: because it owns the model call, it can offer any provider on equal footing, mix BYOK with Zen, and let you route different sessions to different backends. The experience is shaped by opencode's provider matrix, not by a single subscription's tier.
Same prompt, same model — the "experience" is the orchestration around the call. Here's what each layer adds on top of raw model access.
Both Claude and Codex stream into the same ProviderRuntimeEvent shapes. The browser doesn't know or care which provider ran — same components, same diff viewer, same approval UI.
24k-char delta coalescing keeps the WS quiet while feeling live. Per-turn git checkpoints (thread.checkpoint.revert) give undo that the raw CLIs don't expose.
Because opencode owns the loop, you can run Sonnet for planning and Haiku for subtasks in the same session, or swap to GPT mid-thread. Provider choice is a per-call decision.
provider/provider.ts · Models.devopencode can inject MAX_STEPS_PROMPT on the final allowed step and compact context when the window overflows — controls that exist because the loop is its own.
thread.turn.start command → one message onto the SDK's prompt queue.turn.completed; T3 projects those events to the browser.runLoop(), which calls streamText() itself.while(true) in opencode — call model, run tools, check exit, repeat.T3's elegance is in not owning the hard part — by delegating the loop and model call to official SDKs, it gets a polished multi-provider UX for the cost of two adapters, at the price of forfeiting in-loop control. opencode's elegance is in owning the hard part — it pays the cost of reimplementing tool dispatch and provider coverage to gain total control over agent behavior and model choice. Same destination, opposite bets on where the value sits.