The ProviderAdapter contract
ProviderAdapter is the narrow provider-native command and event boundary; ProviderService supplies instance routing and recovery, while orchestration remains the durable product model.
What this chapter resolves
- Separate the adapter SPI from ProviderService, orchestration, and each native transport.
- Read every operation, return value, error family, and declared capability without inventing a universal feature matrix.
- Trace a durable turn intent into one adapter call and canonical runtime events back into durable commands.
- Identify acknowledgement, hot-stream, logging, and recovery boundaries precisely.
ProviderAdapter is not T3 Code’s domain model and it is not a transport
protocol. It is the server-side boundary that gives five very different native
runtimes one callable shape and one event vocabulary.
That placement explains both its power and its limits:
- orchestration decides and durably records product intent;
ProviderServiceresolves a configured instance and owns durable session bindings, recovery policy, MCP preparation, logging, and event fan-in;- one
ProviderAdapterowns native process/session behavior; ProviderRuntimeIngestiontranslates the adapter’s hot canonical events back into internal orchestration commands.
The adapter therefore normalizes how to talk to a harness. It does not make the harness itself durable, and it does not prove every provider supports every event in the canonical union.
The contract is deliberately smaller than the product
export interface ProviderAdapterShape<TError> {
/**
* Provider kind implemented by this adapter.
*/
readonly provider: ProviderDriverKind;
readonly capabilities: ProviderAdapterCapabilities;
/**
* Start a provider-backed session.
*/
readonly startSession: (
input: ProviderSessionStartInput,
) => Effect.Effect<ProviderSession, TError>;
/**
* Send a turn to an active provider session.
*/
readonly sendTurn: (
input: ProviderSendTurnInput,
) => Effect.Effect<ProviderTurnStartResult, TError>;
/**
* Interrupt an active turn.
*/
readonly interruptTurn: (threadId: ThreadId, turnId?: TurnId) => Effect.Effect<void, TError>; /**
* Respond to an interactive approval request.
*/
readonly respondToRequest: (
threadId: ThreadId,
requestId: ApprovalRequestId,
decision: ProviderApprovalDecision,
) => Effect.Effect<void, TError>;
/**
* Respond to a structured user-input request.
*/
readonly respondToUserInput: (
threadId: ThreadId,
requestId: ApprovalRequestId,
answers: ProviderUserInputAnswers,
) => Effect.Effect<void, TError>;
/**
* Stop one provider session.
*/
readonly stopSession: (threadId: ThreadId) => Effect.Effect<void, TError>;The complete shape has fourteen required members and one optional operation. Reading them by responsibility is more useful than reading them in file order.
| Responsibility | Members | Immediate meaning | Not implied |
|---|---|---|---|
| Session lifecycle | startSession, stopSession, listSessions, hasSession, stopAll | manage adapter-owned live sessions keyed by T3 thread id | that native state is stored in the orchestration transaction |
| Turn control | sendTurn, interruptTurn | request a native turn start or interruption | assistant output or completed interruption |
| Human interaction | respondToRequest, respondToUserInput | resolve a pending native approval or structured-input request | that every provider can originate both request kinds |
| Conversation state | readThread, rollbackThread | read or mutate the provider’s own thread representation | that rollback has identical semantics or support everywhere |
| Optional extension | uploadFeedback? | provider-specific feedback upload when present | a negotiated generic extension mechanism |
| Output | streamEvents | one canonical hot runtime-event stream | durable replay, delivery acknowledgement, or provider conformance to every event variant |
sendTurn returns ProviderTurnStartResult: a T3 threadId, a turnId, and an
optional opaque resume cursor. It does not return generated text. Text, tool
activity, plan changes, requests, usage, completion, and failures arrive later on
streamEvents.
A session uses T3 identity and opaque native continuation
The public ProviderSession is centered on threadId. It also carries the driver
and, during the ongoing migration, an optional configured instance id, plus status,
runtime mode, working directory, model, active turn, timestamps, last error, and an
opaque resumeCursor. Runtime routing promotes legacy persisted bindings before it
requires that instance identity.
The opaque cursor is an important design choice. Codex can store app-server resume
material, Claude can store SDK/transcript position, ACP adapters can store an ACP
session id, and OpenCode can store a versioned ses_… token without forcing their
native identities into one false universal schema. ProviderService can persist
the envelope while the owning adapter remains responsible for decoding it.
Capability negotiation is only one field today
The declared capability object contains one property:
sessionModelSwitch: "in-session" | "unsupported". It tells the provider reactor
whether a model change may stay within an existing native session or requires a
restart.
That is the entire SPI capability surface at this revision. Approvals, structured input, plans, tasks, skills, usage telemetry, rollback, steering, and native modes must not be inferred from this one object. They are concrete adapter behaviors, sometimes expressed through typed failure, sometimes absent, and sometimes implemented through provider-specific extensions.
Canonical events are a grammar, not a checklist
/**
* Stop all sessions owned by this adapter.
*/
readonly stopAll: () => Effect.Effect<void, TError>;
/**
* Canonical runtime event stream emitted by this adapter.
*/
readonly streamEvents: Stream.Stream<ProviderRuntimeEvent>;Every runtime event shares an id, driver kind, T3 thread, timestamp, optional instance/turn/item/request ids, provider references, and optional raw provenance. The union then supplies typed payloads for session, thread, turn, content, item, approval, structured input, task, hook, tool, authentication, account, MCP, model, configuration, file, warning, and error events.
That breadth provides a stable target vocabulary for adapters and consumers. It is not proof of feature parity. For example, the schema can represent token usage for any provider, while Chapter 20 verifies that only Codex and Claude emit that event at this revision.
Zoom with the controls, +/−, or Ctrl/⌘ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.
Text equivalent
On the outbound lane, a committed thread turn-start intent is published to a hot provider reactor. The reactor calls ProviderService. ProviderService uses the thread's provider-instance binding to resolve a live adapter. The adapter calls a native SDK, JSON-RPC, ACP, or OpenCode runtime. Its immediate ProviderTurnStartResult means a turn request started and carries no generated text. On the inbound lane, native notifications are normalized to ProviderRuntimeEvent. ProviderService validates the driver and instance, writes an optional best-effort canonical event log, then publishes to a hot PubSub. ProviderRuntimeIngestion serially maps selected events to internal commands. For each accepted command, the orchestration engine appends events, runs synchronous projections, and writes the accepted receipt inside one outer SQL transaction. A crash can interrupt either hot bridge.
apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:1060–1174 ↗apps/server/src/provider/Layers/ProviderService.ts:195–409 ↗apps/server/src/provider/Services/ProviderAdapter.ts:28–134 ↗packages/contracts/src/providerRuntime.ts:1139–1193 ↗apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts:1490–1874 ↗apps/server/src/orchestration/Layers/ProviderCommandReactor.ts:1405–1409 ↗apps/server/src/orchestration/Layers/OrchestrationEngine.ts:197–259 ↗ProviderService owns the cross-provider policy
The adapter’s contract intentionally omits several product concerns that
ProviderService supplies:
- decode and validate public inputs;
- resolve
threadId → persisted binding → providerInstanceId → live adapter; - prepare and revoke the T3 MCP credential for the session;
- adopt an already-live adapter session or resume from the stored cursor;
- persist the latest provider binding, runtime mode, model selection, cwd, status, and resume data;
- stop stale sessions for the same thread on other live instances;
- correlate every runtime event with the adapter instance that emitted it;
- fan all instance streams into one provider-runtime PubSub;
- collect metrics and analytics around the operation.
This is why an adapter should not perform cross-provider orchestration. It knows how its native runtime behaves. The service knows how that runtime participates in the product.
The error taxonomy crosses three boundaries
| Boundary | Typed families | What the distinction preserves |
|---|---|---|
| Adapter | validation, session-not-found, session-closed, request, process | bad T3 input is different from a missing native session, protocol failure, or dead process |
| Driver construction | ProviderDriverError | a configured instance can become an unavailable UI shadow without crashing server boot |
| Registry/service | unsupported, instance-not-found, session-not-found, directory persistence, validation | routing/configuration failure remains distinct from native adapter failure |
One naming wrinkle matters: the live adapter facade currently reports a missing or
unavailable instance as ProviderUnsupportedError, even though a separate
ProviderInstanceNotFoundError type exists. The book follows executed paths, not
just exported class names.
Walk the contract boundary
Choose an operation and advance its round trip. The moving token stops at the immediate return boundary before continuing along the independent runtime-event lane. Try sending while a turn is already running: the lab deliberately refuses to label that generic call “steer,” because only each concrete adapter can settle that meaning.
What does one adapter call actually prove?
Choose a call, then move from durable intent to native work and back. The return boundary and the event boundary stay visibly separate.
Start an absent session; boundary 1 of 6: Durable intent.
Durable intent
startSession(input)The product command has committed. No native call is part of that SQL transaction.
- Starting state
- No live adapter session
- Immediate result
- Returns ProviderSession after native start/resume setup; this is not assistant output.
- Native meaning
- Create or adopt the provider-native session and begin its event consumer.
- Event lane
- session.started / thread.started may follow on the hot stream.
- Durable meaning
- ProviderService can upsert the thread → instance binding and opaque cursor separately.
- Critical caveat
- A process crash can still lose native state or an event between hot boundaries.
Static adapter-operation ledger
| Call | Immediate/native meaning | Event/durable meaning | Caveat |
|---|---|---|---|
Start an absent sessionstartSession(input) | Returns ProviderSession after native start/resume setup; this is not assistant output. Create or adopt the provider-native session and begin its event consumer. | session.started / thread.started may follow on the hot stream. ProviderService can upsert the thread → instance binding and opaque cursor separately. | A process crash can still lose native state or an event between hot boundaries. |
Send on a ready sessionsendTurn(input) | Returns ProviderTurnStartResult with threadId, turnId, and optional cursor—not generated text. Submit a prompt/turn through the adapter's native transport. | turn.started, content/item activity, requests, and turn completion can arrive later. Runtime ingestion dispatches separate internal commands for selected events. | The original sequence acknowledgement proves durable intent, not native acceptance or completion. |
Send while a turn is runningsendTurn(input) again | The generic SPI still exposes sendTurn; it has no steer return type or steer capability. Concrete adapters may steer, queue, start another native turn, or reject according to their implementation. | Any accepted behavior must still be expressed through canonical runtime events. The durable product command does not make the provider-specific concurrency rule universal. | OpenCode has tested steering behavior; the ProviderAdapter contract alone does not promise it. |
Resolve an approvalrespondToRequest(threadId, requestId, decision) | Returns void when the adapter accepts the response operation. Resolve the provider-native pending request or deferred handler. | request.resolved can describe the observed resolution. The pending/resolved product activity is durable only through ingestion commands. | Not every provider originates the same approval shapes or policies. |
Answer structured inputrespondToUserInput(threadId, requestId, answers) | Returns void when the adapter accepts the answers. Resolve an SDK deferred, ACP elicitation/extension, or native question as implemented. | user-input.resolved can describe the observed resolution. Request state enters the domain through separate ingestion dispatches. | A method in the shared interface is not proof that every native runtime can originate the request. |
Interrupt an active turninterruptTurn(threadId, turnId?) | Returns void after the adapter's interruption request path succeeds. Send cancellation/interrupt through the provider-specific transport. | turn.aborted, turn.completed, warning, or error explains later observed state. The domain settles only when the corresponding runtime observation is ingested. | A successful interrupt call is not proof that no late native event can race it. |
Roll back provider historyrollbackThread(threadId, numTurns) | Returns a provider-thread snapshot when implemented successfully. Apply the adapter's own rollback/revert semantics. | No universal canonical rollback event is guaranteed by this method. T3's checkpoint/revert workflow remains a separate cross-store saga. | Grok explicitly rejects provider rollback; Cursor and other adapters differ in what is actually reverted. |
What the tests establish—and do not
The canonical runtime schema has decoder tests. ProviderService tests route
sessions, recover persisted bindings, validate instance identity, and merge runtime
events in order. Every concrete adapter has behavior tests for its own transport and
normalization.
There is no repository-wide executable conformance suite that feeds the same full feature script to all five adapters and proves semantic equivalence. That absence is appropriate to surface: one interface gives the product a stable integration point, while Chapters 16–19 retain provider differences instead of hiding them.