8.6 KiB
Adding a provider
A provider integration is judged by whether every T3 feature behaves honestly on it, not by whether a turn runs. This page lists the decisions and evidence a new driver needs. The provider constraints and the capability system explain why these rules exist.
Choose the integration shape
- ACP agents start as ACP Registry entries, which follow the ACP spec with no per-agent handling. An agent gets a dedicated driver only when it needs behavior the spec does not cover, and then it is a small flavor over the shared ACP adapter, like Grok and Antigravity. Never add agent-id checks to the generic registry adapter.
- Other protocols get a native adapter that implements
ProviderAdapterV2, like Codex, Claude, Cursor, OpenCode, Pi, and Muse.
Provider-specific behavior stays in the adapter and driver. Orchestration and clients read capabilities, never the driver kind.
Report only what the provider does
- Capabilities. Set every flag in
OrchestrationV2ProviderCapabilitiesto what the provider really does. Turn off fork, rollback, steering, or subagent support it lacks; orchestration then falls back, for example to portable context handoff for forks. A capability left on that fails at runtime is a bug. - Permission modes. Offer only modes the provider enforces natively, through
supportedRuntimeModesin the provider presentation (Grok and Pi are examples). Do not imitate a missing mode by answering approvals in T3: T3's check is weaker than the agent's own enforcement. The server runs an unoffered stored mode as Supervised (RuntimePolicy.ts). - Approval and question options. Pass the provider's own option IDs through unchanged. Every request needs a way to decline that the provider honors.
- Interaction modes. Hide the plan toggle (
showInteractionModeToggle: false) unless the provider's plan output becomes T3's proposed-plan card.
Process, account, and setup boundaries
- Instances. Session state, catalogs, and credentials belong to a provider instance, not the driver. Remove ambient credentials the instance did not configure, so two instances cannot silently share an account or billing.
- Status checks. Background status and model refreshes must not open a session that can start MCP servers, run hooks, or launch a login. Keep heavier probes behind an explicit refresh.
- T3 MCP tools. Inject the thread's MCP server so agents can use T3's tools, and make a turn survive when that server is unreachable.
- Updates. Run an update only through the installer that provably owns the binary; otherwise
leave it manual. See
providerMaintenance.ts.
Tests
Prove adapter behavior with replay fixtures: a provider transcript replayed through the real orchestrator, adapter, and projections. See the testing strategy.
- Record transcripts from the real provider with a recorder script in
apps/server/scripts(record-*-replay-fixture.ts). A new protocol usually needs its own recorder. - Cover the shared scenarios under
testkit/fixturesthat the provider supports:simple,multi_turn,queued_turn,turn_interrupt,message_steering,provider_thread_resume, and thetool_call_*approval cases. Add provider-specific fixtures for behavior the shared ones do not reach. - Unit tests are for pure logic and for failures a real provider cannot produce on demand. A test that drives the adapter with hand-written frames is a guess about the protocol; live testing regularly finds what those guesses missed.
- A gated live test against the real binary is welcome, but it does not replace replays, because CI skips it.
Traps several providers hit
Each of these broke more than one adapter. A replay fixture for each case your provider can produce catches them.
- Settings applied before the provider advertises them. Agents often list config options after the session opens, in a later update, or only once a model runs (an effort option that exists only for models with effort). An agent can accept a write before then and change nothing. Wait for the option to be advertised before writing it, and treat a stored option the live session does not offer as skipped, not as a failure.
- "Default" is a sentinel, not a model. A default-model selection must switch the session back after a named model ran, including in a resumed session. Never send the sentinel to the provider.
- Stop sent before the turn started. A cancel that reaches the provider before it registered the prompt can be ignored while the turn runs to completion, or can leave a session the provider never saved. Find the provider's own turn-started signal, and test a Stop pressed right after sending.
- Work the provider starts on its own. Background commands, subagents, workflows, and goals can
finish or start a turn after T3's turn settled. Offer a continuation through
ProviderContinuationRequestsso the parent wakes, and reporthasPendingBackgroundWorkso the session is not released as idle. Dropping it, or killing the session, loses the result. - Subagents outlive the run that launched them. A child can report after its parent's turn settled, or resume during a later run. Route its updates by the subagent, not by whichever run is currently active.
- A failed or stopped turn leaves nothing open. Settle the turn's tool calls, subagents, and pending approvals and questions. Each terminal path needs its own replay: completion, Stop, provider failure, and process exit.
- Usage limits are not generic failures. Classify quota and rate-limit errors as
usage_limitwith a reset time when the provider gives one, so the thread shows Limited and can resume. - Paths and environment. Test a symlinked workspace, such as macOS
/tmp; an agent that compares canonical paths fails every turn there. Drop inherited credentials the instance did not configure. Report unknown sign-in as unknown rather than as signed in.
Where a driver plugs in
A provider lives in its own packages/provider-<name> package, with
Pi as the reference. Drivers not moved yet still live in
provider/Drivers.
- Package exports.
./settingsholds the instance settings schema, built withmakeProviderSettingsSchemafrom contracts../clientexports aProviderClientDefinition(label, settings schema, plain-data icon, badge) and must stay browser- and React Native-safe../serverexports theProviderDriverand its adapter driver../testingexposes internals the server's replay testkits drive; testkits that need the orchestrator stay in the server. Settings live inproviderInstances; new providers are off by default. - Package boundaries. Server code imports only
@t3tools/provider-core/server/*, contracts, and shared. It reaches the server throughProviderHost(paths, settings, background demand, attachments, stored credentials), never throughapps/server. Tests use@t3tools/provider-testing. - Server registration: the driver's entry in
builtInDrivers.tsand the adapter driver's inbuiltInProviderAdapterDrivers.ts. Also its position in the status order, a compatibility policy inmodel-manifest.json(bumpupdatedAt; see model manifest), and its default model in contracts. - Client registration: the client definition in the web and mobile provider registries, and settings search terms. Clients draw the package icon; do not add a per-driver icon branch.
- Docs: a
docs/user/providers-<name>.mdguide in the product's voice, a row in the install table, and any provider difference that changes the permission modes page.