Part IV · Five harnesses, one product modelProviderAdapter contract
Chapter 14source checked

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;
  • ProviderService resolves a configured instance and owns durable session bindings, recovery policy, MCP preparation, logging, and event fan-in;
  • one ProviderAdapter owns native process/session behavior;
  • ProviderRuntimeIngestion translates 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

apps/server/src/provider/Services/ProviderAdapter.ts:47–71 ↗verbatim · typescript · a75847d1
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>;
Read this as: The adapter exposes its driver kind and one capability, then session start, turn send, and interruption. Instance lookup and orchestration semantics live outside this interface.
apps/server/src/provider/Services/ProviderAdapter.ts:73–94 ↗verbatim · typescript · 67b46105
  /**
   * 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>;
Read this as: Approval and structured-input responses are separate operations. Session stop is part of the same native boundary.

The complete shape has fourteen required members and one optional operation. Reading them by responsibility is more useful than reading them in file order.

ProviderAdapter operations, immediate results, and boundaries
ResponsibilityMembersImmediate meaningNot implied
Session lifecyclestartSession, stopSession, listSessions, hasSession, stopAllmanage adapter-owned live sessions keyed by T3 thread idthat native state is stored in the orchestration transaction
Turn controlsendTurn, interruptTurnrequest a native turn start or interruptionassistant output or completed interruption
Human interactionrespondToRequest, respondToUserInputresolve a pending native approval or structured-input requestthat every provider can originate both request kinds
Conversation statereadThread, rollbackThreadread or mutate the provider’s own thread representationthat rollback has identical semantics or support everywhere
Optional extensionuploadFeedback?provider-specific feedback upload when presenta negotiated generic extension mechanism
OutputstreamEventsone canonical hot runtime-event streamdurable 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

apps/server/src/provider/Services/ProviderAdapter.ts:126–134 ↗verbatim · typescript · 4a95ebce
  /**
   * 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>;
Read this as: The adapter exposes one stream of ProviderRuntimeEvent values. The interface does not attach replay or acknowledgement semantics to it.

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.

Figure 14.1 · One normalized boundary, two asynchronous crossingssolid arrows are calls; dashed arrows are hot events
Provider adapter outbound and inbound round tripDiagram loading

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.

Provider adapter outbound and inbound round trip
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.

Figure 14.1. A committed orchestration intent wakes the provider reactor, which asks ProviderService to route the thread to one configured instance. The adapter translates the generic call into a native protocol. Immediate return values acknowledge session or turn start, not assistant output. Native messages flow back through the adapter's canonical hot stream; ProviderService correlates the source instance and offers the event to its own hot stream. Runtime ingestion then issues internal commands whose events, synchronous projections, and accepted receipt become durable together inside each orchestration command transaction.

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

Provider adapter, driver, registry, and service error responsibilities
BoundaryTyped familiesWhat the distinction preserves
Adaptervalidation, session-not-found, session-closed, request, processbad T3 input is different from a missing native session, protocol failure, or dead process
Driver constructionProviderDriverErrora configured instance can become an unavailable UI shadow without crashing server boot
Registry/serviceunsupported, instance-not-found, session-not-found, directory persistence, validationrouting/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.

Interactive contract tracer

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.

Orchestration

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
CallImmediate/native meaningEvent/durable meaningCaveat
Start an absent session
startSession(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 session
sendTurn(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 running
sendTurn(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 approval
respondToRequest(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 input
respondToUserInput(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 turn
interruptTurn(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 history
rollbackThread(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.

T3
Source-locked editionRead against fa219001d · 23 Aug 2026
Book search

Find a concept, module, or source path

Type two or more characters.