Provider adapters

Five agent runtimes, one contract

A provider is the configured agent runtime behind a thread. The orchestration layer addresses that runtime through a common contract and does not depend on its vendor. Five built-in drivers translate their native protocols, from a typed app-server to stdio CLIs, behind one ProviderAdapter contract. The trading grounding rides the first turn of each session as a prefix, never a replacement for a native prompt.

client thread.turn.start reactor ProviderCommandReactor registry ProviderInstanceRegistry adapter prefix · t3-trade MCP runtime codex · claude · cursor · grok · opencode ingestion ProviderRuntimeIngestion clients orchestration.subscribeThread
ONE TURN, BOTH WAYS. Forward: an intent event reaches the reactor, the registry resolves the instance, the adapter grounds the turn. Back: provider events are re-emitted as commands clients subscribe to. The amber edge is the adapter boundary.
5
built-in drivers in BUILT_IN_DRIVERS
apps/server/src/provider/builtInDrivers.ts:47-53
13
operations on the ProviderAdapterShape contract
apps/server/src/provider/Services/ProviderAdapter.ts:47-135
70,000
char hard bound on the TRADE.md context block (PLAN_CONTEXT_MAX_CHARS)
docs/internals/providers.md:92-95
24,000
char cap on buffered assistant text before it spills as one delta
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:101

One contract, five translators

A provider is the agent runtime that does the actual work, and T3 Trade supports several without the orchestration layer knowing which one is behind a thread docs/internals/providers.md:5-6. The unit of registration is the driver: a plain value that declares a driverKind, a configSchema, and a create function that builds an adapter in a child scope docs/internals/providers.md:20-24. The create call returns one ProviderInstance bundling a snapshot, an adapter, and a textGeneration surface apps/server/src/provider/Drivers/CodexDriver.ts:4-8.

Two registries separate configuration from live processes. The ProviderInstanceRegistry keys configured instances by ProviderInstanceId: creating one looks up the driver by driverKind, decodes the entry config with that driver's schema, opens a child scope, and calls driver.create docs/internals/providers.md:28-34. The ProviderAdapterRegistry then resolves an instance ID to its live adapter, and ProviderService routes session and turn operations on top so callers name a thread, not an agent docs/internals/providers.md:33-38.

This lets the fork support five runtimes without five orchestration paths. Adding a first-party driver means implementing ProviderDriver in a sibling Drivers/<Name>Driver.ts, adding it to the BUILT_IN_DRIVERS array, and ensuring the runtime layer satisfies its declared requirements apps/server/src/provider/builtInDrivers.ts:11-19. No orchestration, contract, or client change is required for the common case docs/internals/providers.md:39-40. Anything configured but missing from the array surfaces as an unavailable shadow snapshot rather than a crash apps/server/src/provider/builtInDrivers.ts:5-9.

ProviderAdapterShape
startSessionopen a provider session
sendTurnone turn to a live session
interruptTurnstop a running turn
respondToRequestanswer an approval
respondToUserInputanswer structured input
stopSession · stopAlltear down
listSessions · hasSessionownership queries
readThread · rollbackThreadsnapshot and revert turns
uploadFeedback (optional)where supported
streamEventscanonical runtime event stream
The whole surface every adapter must speak. Implementations "should focus on provider behavior only and avoid cross-provider orchestration concerns." apps/server/src/provider/Services/ProviderAdapter.ts:1-9,47-135

One turn, traced

Clients never call a provider directly. A turn crosses six stages. Only the registry and adapter boundary resolve the provider instance that runs it.

STEP 1 · CLIENT

The client dispatches a command

The turn begins as thread.turn.start over the RPC method orchestration.dispatchCommand. The client-dispatchable provider-facing set also includes thread.turn.interrupt, thread.approval.respond, thread.user-input.respond, thread.checkpoint.revert, thread.session.stop, and the mode setters thread.runtime-mode.set and thread.interaction-mode.set docs/internals/providers.md:56-61.

STEP 2 · ENGINE

The engine persists an intent event

The command becomes a persisted event first, and a server-side reactor performs the provider call docs/internals/providers.md:63-64. The ProviderCommandReactor listens for thread.turn-start-requested intent events and reacts by dispatching thread.turn.start into the provider layer apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:59,512,1520.

STEP 3 · REGISTRY

The registry resolves the instance

The thread names a provider instance, not a vendor. The registry looks up the driver by driverKind, decodes the entry config with that driver's schema, opens a child scope, and calls driver.create docs/internals/providers.md:28-34. Closing the scope tears the instance down, releasing child processes and snapshot refresh work apps/server/src/provider/Drivers/CodexDriver.ts:16-20.

STEP 4 · ADAPTER BOUNDARY

The adapter grounds the first turn

On the first turn of each session instance, the adapter adds the workspace trading preamble before the user's text: Hyperliquid testnet is the only venue, market facts come from tools, TRADE.md is persistent plan context, a direct order is direct, exchange actions run through the typed tools, and server refusals are authoritative apps/server/src/provider/TradingSessionProfile.ts:54-61. It is appended to the turn text and never replaces a native prompt docs/internals/providers.md:94-95. The adapter includes the current TRADE.md as a delimited context block. It caps the block at 70,000 characters and omits it when it exceeds that limit docs/internals/providers.md:92-95. The trading toolkit is mounted as an ordinary MCP server named t3-trade apps/server/src/provider/TradingSessionProfile.ts:41-42.

A turn that failed to send must not swallow the contract: delivery is marked only after the turn dispatched successfully, so the next turn carries the full prefix again apps/server/src/provider/TradingSessionProfile.ts:203-212.

STEP 5 · NATIVE RUNTIME

The provider keeps its native session behavior

A trading thread is a native agent session, not a trading persona. Claude runs the stock claude_code preset, Codex keeps its own base instructions, and the CLI-backed adapters change nothing about their native surface: every session keeps its native tools, cwd, sandbox, and approval mode docs/internals/providers.md:83-86.

STEP 6 · INGESTION

Events stream back as orchestration commands

The ProviderRuntimeIngestion worker consumes the provider runtime stream and re-emits internal commands such as thread.message.assistant.delta and thread.session.set, which clients observe through orchestration.subscribeThread docs/internals/providers.md:63-65, apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:1159,1645. In buffered delivery mode assistant text accumulates up to 24,000 chars; the append that would exceed it spills the whole accumulated text as one delta, and the buffer also flushes at interaction boundaries apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:101,1093, docs/internals/providers.md:110-117.

Five protocols behind one adapter contract

Each driver translates one native protocol. The distinguishing fact on every card is the thing that protocol forced the adapter to solve.

Codex driverKind codex

Speaks the typed app-server protocol from the in-repo package packages/effect-codex-app-server, imported as generated client, errors, rpc, and schema modules apps/server/src/provider/Layers/CodexSessionRuntime.ts:34-37. Each instance gets its own CODEX_HOME: two instances with different home paths run fully independent app-server processes with no shared mutable state apps/server/src/provider/Drivers/CodexDriver.ts:10-14.

The shadow-home overlay is the distinguishing trick. In authOverlay mode the shared home keeps sessions, sqlite, skills, logs, and the other runtime directories as symlinks into one place, while auth.json and models_cache.json stay private per instance, and auth.json appearing as a symlink is a hard error apps/server/src/provider/Drivers/CodexHomeLayout.ts:13,19-34,295-318.

CODEX_HOME (shadow) auth.json · models_cache.json private, real files
~/.codex (shared) sessions · sqlite · skills ... symlinked in

Claude driverKind claudeAgent

Runs on the Claude Agent SDK. Its snapshot probe may invoke a secondary capability probe that reads Anthropic account and slash-command metadata, so the probe cache is per instance and keyed by binary plus resolved HOME: two concurrent Claude instances never cross-contaminate account metadata. The cache holds one entry for five minutes apps/server/src/provider/Drivers/ClaudeDriver.ts:2-11,62,157-167.

probe key = binary + resolved HOME · TTL 5 min

Cursor driverKind cursor

An ACP-based CLI. Spawn shape is cursor-agent with an optional -e endpoint and the acp subcommand, authenticating as cursor_login apps/server/src/provider/acp/CursorAcpSupport.ts:33-47,61. The model catalog and capabilities come exclusively from Cursor's list_available_models extension method during provider checks apps/server/src/provider/Drivers/CursorDriver.ts:152-153.

$ cursor-agent [-e endpoint] acp

Grok driverKind grok

Spawns grok agent stdio and injects GROK_OAUTH2_REFERRER=t3code into the environment apps/server/src/provider/acp/GrokAcpSupport.ts:14-18,32-46. Auth resolves to the xai.api_key method when XAI_API_KEY is set, otherwise the cached token method, and an xAI prompt-completion extension rides the session apps/server/src/provider/acp/GrokAcpSupport.ts:48-52,76.

$ grok agent stdio · GROK_OAUTH2_REFERRER=t3code

OpenCode driverKind opencode

Uses the typed client from @opencode-ai/sdk/v2 apps/server/src/provider/opencodeRuntime.ts:14. Two instances with different serverUrl values talk to independent OpenCode servers; without one, scoped child processes are spun up and released when the registry scope closes apps/server/src/provider/Drivers/OpenCodeDriver.ts:6-12. Readiness is the log line below, awaited for up to 30 seconds apps/server/src/provider/opencodeRuntime.ts:51-53.

opencode server listening · 127.0.0.1 · 30s

Errors fold into one family

Each provider reports failures differently. The adapter maps them into the shared ProviderAdapterError family, so orchestration handles provider-process exits and closed sessions consistently.

ACPmapAcpToAdapterError
Both Cursor and Grok run over ACP, so their error translation is centralized: an AcpProcessExitedError becomes ProviderAdapterSessionClosedError, and an AcpRequestError becomes ProviderAdapterRequestError carrying the method and message apps/server/src/provider/acp/AcpAdapterSupport.ts:17-44.
CODEXapp-server failures
The Codex adapter maps CodexAppServerProcessExitedError and CodexAppServerTransportError to ProviderAdapterSessionClosedError, and wields RequestError, SessionNotFoundError, ValidationError, and ProcessError for the rest apps/server/src/provider/Layers/CodexAdapter.ts:39,51-56,72-124.
APPROVALacpPermissionOutcome
Permission prompts translate the product's approval vocabulary into ACP outcomes before they cross the wire apps/server/src/provider/acp/AcpAdapterSupport.ts:46-56.
acceptForSession → allow-alwayspersisted permission for the session
accept → allow-oncesingle approval, next ask asks again
decline → reject-oncerefuse this request, keep the session

Trading code must not become a sixth runtime

Provider complexity is kept at adapters by a test, not by convention. apps/server/src/trading/ProviderBoundary.test.ts statically scans every TypeScript file under the trading tree and the trading MCP toolkit for seven process-spawning patterns: node:child_process imports, child_process imports, spawn(, exec(, fork(, node-pty, and Command.make apps/server/src/trading/ProviderBoundary.test.ts:20-23,45-53. It also scans for direct claude, codex, and opencode CLI invocations apps/server/src/trading/ProviderBoundary.test.ts:67-71, and forbids trading code from importing a provider adapter from provider/Layers or managing ProviderSessionRuntime state itself apps/server/src/trading/ProviderBoundary.test.ts:112-125.

One audited exemption

The boundary exists so that no trading code becomes a second provider runtime. The single module allowed to spawn a process is ArchiveSupervisor.ts, which starts the in-repo market archiver: a script that reads public market data and writes a SQLite file while holding no provider session and no key apps/server/src/trading/ProviderBoundary.test.ts:56-64. The test pins the exemption down: the supervisor must spawn the archiver's archive main.ts with this process's own Node binary, and the provider-CLI scan still applies to it apps/server/src/trading/ProviderBoundary.test.ts:93-102.

Contract and drivers

  • apps/server/src/provider/builtInDrivers.ts:47-53 the five-entry array
  • apps/server/src/provider/Services/ProviderAdapter.ts:47-135 the shared shape
  • docs/internals/providers.md:8-40 driver table and the no-change recipe
  • packages/effect-codex-app-server/src/ generated protocol, client, errors

Turn seam

  • apps/server/src/provider/TradingSessionProfile.ts:41-61 t3-trade MCP name, preamble
  • apps/server/src/provider/TradingSessionProfile.ts:166-212 once per instance, marked only on success
  • apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:59,1520 intent to call
  • apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:101,1159,1645 events to commands

Nuances worth knowing

The three-way trading classification

When authority binds, the driver kind collapses to a TradingProvider: claude and claudeAgent map to claude, opencode maps to opencode, and everything else, including cursor and grok, maps to codex apps/server/src/trading/TradingAuthorityBinding.ts:318-323.

Chat gets the preamble too

Every thread in the workspace carries the grounding on its first turn per session instance, chat included, because chat is the front door for trading and any thread can take authority on its first plan or execution call apps/server/src/provider/TradingSessionProfile.ts:214-224, docs/internals/providers.md:96-99.

Later turns carry one line

The delivered-contract memory is in-process and cleared by every adapter's startSession, so a restart or resume re-delivers one full copy per instance. Subsequent turns name the frame and nothing else: repeating the grounding would not make it more true apps/server/src/provider/TradingSessionProfile.ts:166-201.

Scope fences are stated as fact, enforced in rows

Analyst and observe sessions get one extra paragraph naming their fence, and the module is explicit that the enforcement is persisted server-side, never prompt-side: the observer refusal reads the mission row's purpose, and the analyst fence reads the persisted analyst registry and the missing mission binding apps/server/src/provider/TradingSessionProfile.ts:116-128, docs/internals/providers.md:103-108.

The model manifest is a commit, not a release

The picker's legacy section is driven by model-manifest.json, refreshed from the same file on main via raw.githubusercontent.com. Fetches are TTL-gated, run concurrently with provider probes, and never fail a provider check; only the Codex and Claude drivers apply the classification docs/internals/providers.md:42-52.