For maintainers. Using T3 Code? See docs/user.
A provider is the agent runtime that does the actual work. T3 Code supports several, and the orchestration layer does not know which one is behind a thread.
builtInDrivers.ts exports BUILT_IN_DRIVERS with five entries:
| Driver kind | Driver source |
|---|---|
codex |
Drivers/CodexDriver.ts |
claudeAgent |
Drivers/ClaudeDriver.ts |
cursor |
Drivers/CursorDriver.ts |
grok |
Drivers/GrokDriver.ts |
opencode |
Drivers/OpenCodeDriver.ts |
Each driver declares its driverKind, a configSchema, and a create function that builds an
adapter in a child scope. Adapter implementations live beside them in
apps/server/src/provider/Layers/ (CodexAdapter.ts, ClaudeAdapter.ts, and so on) and conform to
ProviderAdapter.ts. Read the driver plus its adapter to see how a specific agent's
transport, config, and event shapes are mapped.
Two registries separate configuration from live processes:
ProviderInstanceRegistrykeys configured instances byProviderInstanceId. Creating one looks up the driver bydriverKind, decodesentry.configwith that driver's schema, opens a child scope, and callsdriver.create.ProviderAdapterRegistryresolves an instance ID to its live adapter viagetByInstance.
ProviderService sits on top. It combines the adapter registry with the provider session
directory to route session and turn operations for a thread, so callers name a thread, not an agent.
Adding a driver means writing the driver plus adapter and adding it to BUILT_IN_DRIVERS. No
orchestration, contract, or client change is required for the common case.
The model picker's legacy section is driven by apps/server/src/provider/model-manifest.json, which
lists the current (non-legacy) model slugs per driver kind. The ModelManifest service
(apps/server/src/provider/ModelManifest.ts) refreshes that data from the same file on main via
raw.githubusercontent.com, so moving a model in or out of the legacy section is a commit, not a
release. Preference order is remote fetch, then the on-disk copy of the last successful fetch (in
the state directory), then the bundled copy. Fetches are TTL-gated, run concurrently with provider
probes, respect the enableProviderUpdateChecks setting, and never fail a provider check. The
Codex and Claude drivers apply the classification to every snapshot with applyModelManifest;
driver kinds absent from the manifest have no legacy concept.
The server stores uploaded attachments in its attachment directory, outside the project workspace.
ProviderService adds the absolute path of each attachment to the turn text, then passes every
attachment to the provider adapter. Each adapter decides what its provider ingests natively:
- Codex, Claude, Cursor, and Grok send images as native image inputs and skip generic files. For these providers, generic files reach the agent only as file paths in the turn text.
- OpenCode sends PNG/JPEG/GIF/WebP images, text files, and PDFs up to 20 MB as native file parts with their real mime type. Everything else (ZIP and other binaries, image formats model APIs reject, oversized files) falls back to the file path in the turn text, like the other providers.
Claude receives the attachment directory as an allowed additional directory. Codex keeps its configured sandbox policy, so access depends on that policy and the selected runtime mode. OpenCode allows all paths in full-access mode and requests approval for directories outside the workspace in restricted modes. Cursor and Grok use their own provider permission rules.
The server does not copy attachments into a project or bypass provider approval rules. If an agent cannot read an attachment, the user must approve the access or select a runtime mode that permits it.
Updated attachment schemas tolerate unknown attachment members, but old image-only clients still cannot decode messages that contain file attachments. Client file-picking rollouts must account for this limit.
Do not run an old image-only server against state that contains file attachments. Replay decodes
each persisted event before projection. A file-bearing event can make ProjectionPipeline bootstrap
and OrchestrationEngine startup fail for the entire environment, not only the affected thread.
Clients never call a provider directly. They dispatch orchestration commands over the RPC method
orchestration.dispatchCommand, defined with the rest of the orchestration surface in
orchestration.ts. The client-dispatchable provider-facing commands are
thread.turn.start, thread.turn.interrupt, thread.approval.respond,
thread.user-input.respond, thread.checkpoint.revert, and thread.session.stop, plus the mode
setters thread.runtime-mode.set and thread.interaction-mode.set.
The engine persists an event for the command, and a server-side reactor performs the provider call.
Provider output comes back as internal commands such as thread.message.assistant.delta and
thread.session.set, which clients observe through orchestration.subscribeThread. See
overview.md for the command/event loop.
Provider work flows through three queue-backed workers. All three are built with
makeDrainableWorker from DrainableWorker.ts and expose drain for deterministic test
synchronization.
ProviderRuntimeIngestionconsumes provider runtime streams and emits orchestration commands.ProviderCommandReactorreacts to orchestration intent events and dispatches provider calls.CheckpointReactorcaptures workspace checkpoints on turn start and completion, and performs reverts.
A thread in buffered assistant delivery mode accumulates assistant text instead of streaming each
delta. The buffer is not held until turn completion. In ProviderRuntimeIngestion,
MAX_BUFFERED_ASSISTANT_CHARS is 24,000: the append that would exceed it invalidates the buffer and
spills the whole accumulated text as one delta. The buffer also flushes at interaction boundaries,
when a request opens (approval) or user input is requested, via
flushBufferedAssistantMessagesForTurn.