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.
BUILT_IN_DRIVERSapps/server/src/provider/builtInDrivers.ts:47-53
ProviderAdapterShape contractapps/server/src/provider/Services/ProviderAdapter.ts:47-135
PLAN_CONTEXT_MAX_CHARS)docs/internals/providers.md:92-95
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.
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.
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.
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.
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.
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.
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.
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 (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.
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.
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.
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.
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.
AcpProcessExitedError becomes
ProviderAdapterSessionClosedError, and an
AcpRequestError becomes
ProviderAdapterRequestError carrying the method and
message
apps/server/src/provider/acp/AcpAdapterSupport.ts:17-44.
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.
apps/server/src/provider/acp/AcpAdapterSupport.ts:46-56.
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-53the five-entry array -
apps/server/src/provider/Services/ProviderAdapter.ts:47-135the shared shape -
docs/internals/providers.md:8-40driver 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-61t3-trade MCP name, preamble -
apps/server/src/provider/TradingSessionProfile.ts:166-212once per instance, marked only on success -
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:59,1520intent to call -
apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:101,1159,1645events 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.