Skip to main content
Providers are the bridge between T3 Code’s orchestration layer and external agent systems. The provider architecture uses a clean adapter pattern to enable multi-provider support while keeping provider-specific logic isolated.

Provider Architecture

The provider layer follows a hierarchical service architecture:
┌─────────────────────────────────────────┐
│        ProviderService                  │
│  (Cross-provider facade)                │
└──────────────┬──────────────────────────┘

┌──────────────▼──────────────────────────┐
│    ProviderAdapterRegistry              │
│  (Resolves provider → adapter)          │
└──────────────┬──────────────────────────┘

         ┌─────┴─────┐
         │           │
┌────────▼─────┐ ┌───▼──────────┐
│ CodexAdapter │ │ Future       │
│              │ │ Adapters     │
└──────────────┘ └──────────────┘
T3 Code is currently Codex-first, with support for additional providers (like Claude Code) reserved in the contracts and architecture.

Provider Service

The ProviderService acts as the unified facade for all provider operations:
apps/server/src/provider/Services/ProviderService.ts
export interface ProviderServiceShape {
  // Session lifecycle
  readonly startSession: (
    threadId: ThreadId,
    input: ProviderSessionStartInput,
  ) => Effect.Effect<ProviderSession, ProviderServiceError>;
  
  readonly stopSession: (
    input: ProviderStopSessionInput,
  ) => Effect.Effect<void, ProviderServiceError>;
  
  // Turn execution
  readonly sendTurn: (
    input: ProviderSendTurnInput,
  ) => Effect.Effect<ProviderTurnStartResult, ProviderServiceError>;
  
  readonly interruptTurn: (
    input: ProviderInterruptTurnInput,
  ) => Effect.Effect<void, ProviderServiceError>;
  
  // Interactive requests
  readonly respondToRequest: (
    input: ProviderRespondToRequestInput,
  ) => Effect.Effect<void, ProviderServiceError>;
  
  readonly respondToUserInput: (
    input: ProviderRespondToUserInputInput,
  ) => Effect.Effect<void, ProviderServiceError>;
  
  // Conversation management
  readonly rollbackConversation: (input: {
    readonly threadId: ThreadId;
    readonly numTurns: number;
  }) => Effect.Effect<void, ProviderServiceError>;
  
  // Metadata
  readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>;
  readonly getCapabilities: (
    provider: ProviderKind,
  ) => Effect.Effect<ProviderAdapterCapabilities, ProviderServiceError>;
  
  // Event stream
  readonly streamEvents: Stream.Stream<ProviderRuntimeEvent>;
}
ProviderService resolves provider adapters through ProviderAdapterRegistry, routes session-scoped calls via ProviderSessionDirectory, and exposes one unified provider event stream to callers.

Provider Adapter Contract

Each provider implements the ProviderAdapterShape interface:
apps/server/src/provider/Services/ProviderAdapter.ts
export interface ProviderAdapterShape<TError> {
  readonly provider: ProviderKind;
  readonly capabilities: ProviderAdapterCapabilities;
  
  // Session operations
  readonly startSession: (
    input: ProviderSessionStartInput,
  ) => Effect.Effect<ProviderSession, TError>;
  
  readonly stopSession: (
    threadId: ThreadId
  ) => Effect.Effect<void, TError>;
  
  readonly stopAll: () => Effect.Effect<void, TError>;
  
  // Turn operations
  readonly sendTurn: (
    input: ProviderSendTurnInput,
  ) => Effect.Effect<ProviderTurnStartResult, TError>;
  
  readonly interruptTurn: (
    threadId: ThreadId,
    turnId?: TurnId,
  ) => Effect.Effect<void, TError>;
  
  // Approval handling
  readonly respondToRequest: (
    threadId: ThreadId,
    requestId: ApprovalRequestId,
    decision: ProviderApprovalDecision,
  ) => Effect.Effect<void, TError>;
  
  readonly respondToUserInput: (
    threadId: ThreadId,
    requestId: ApprovalRequestId,
    answers: ProviderUserInputAnswers,
  ) => Effect.Effect<void, TError>;
  
  // Thread state
  readonly readThread: (
    threadId: ThreadId,
  ) => Effect.Effect<ProviderThreadSnapshot, TError>;
  
  readonly rollbackThread: (
    threadId: ThreadId,
    numTurns: number,
  ) => Effect.Effect<ProviderThreadSnapshot, TError>;
  
  // Session directory
  readonly listSessions: () => Effect.Effect<ReadonlyArray<ProviderSession>>;
  readonly hasSession: (threadId: ThreadId) => Effect.Effect<boolean>;
  
  // Event stream
  readonly streamEvents: Stream.Stream<ProviderRuntimeEvent>;
}

Provider Capabilities

Adapters declare their capabilities through static metadata:
export type ProviderSessionModelSwitchMode =
  | "in-session"      // Can change model without restarting
  | "restart-session" // Must restart session to change model
  | "unsupported";    // Model switching not supported

export interface ProviderAdapterCapabilities {
  readonly sessionModelSwitch: ProviderSessionModelSwitchMode;
}
Capabilities enable T3 Code to adapt its behavior based on provider limitations. For example, the UI can prompt for session restart when changing models if the provider requires it.

Codex Adapter

The CodexAdapter implements provider support for Codex:
1

Process Management

Spawns and manages codex app-server child processes via CodexAppServerManager
2

JSON-RPC Communication

Handles JSON-RPC protocol over stdio for session and turn operations
3

Event Transformation

Transforms Codex notifications into standardized ProviderRuntimeEvent format
4

Session Directory Integration

Registers sessions with ProviderSessionDirectory for tracking and routing

Codex-Specific Features

Detects Codex account type and plan to determine model availability:
interface CodexAccountSnapshot {
  readonly type: "apiKey" | "chatgpt" | "unknown";
  readonly planType: CodexPlanType | null;
  readonly sparkEnabled: boolean;
}

// Spark model restrictions based on plan
const CODEX_SPARK_DISABLED_PLAN_TYPES = new Set<CodexPlanType>([
  "free", "go", "plus"
]);
Normalizes model slugs and applies account-based restrictions:
export function resolveCodexModelForAccount(
  model: string | undefined,
  account: CodexAccountSnapshot,
): string | undefined {
  if (model !== CODEX_SPARK_MODEL || account.sparkEnabled) {
    return model;
  }
  return CODEX_DEFAULT_MODEL; // Fallback for restricted accounts
}
Supports Codex-specific collaboration modes (default and plan):
function buildCodexCollaborationMode(input: {
  readonly interactionMode?: "default" | "plan";
  readonly model?: string;
  readonly effort?: string;
}): {
  mode: "default" | "plan";
  settings: {
    model: string;
    reasoning_effort: string;
    developer_instructions: string;
  };
};
Implements intelligent thread resume with automatic fallback:
const RECOVERABLE_THREAD_RESUME_ERROR_SNIPPETS = [
  "not found",
  "missing thread",
  "no such thread",
  "unknown thread",
  "does not exist",
];

Provider Events

Providers emit events through a unified event stream:
packages/contracts/src/provider.ts
export const ProviderEvent = Schema.Struct({
  id: EventId,
  kind: ProviderEventKind,           // "session" | "notification" | "request" | "error"
  provider: ProviderKind,             // "codex"
  threadId: ThreadId,
  createdAt: IsoDateTime,
  method: TrimmedNonEmptyStringSchema,
  message: Schema.optional(TrimmedNonEmptyStringSchema),
  turnId: Schema.optional(TurnId),
  itemId: Schema.optional(ProviderItemId),
  requestId: Schema.optional(ApprovalRequestId),
  requestKind: Schema.optional(ProviderRequestKind),
  textDelta: Schema.optional(Schema.String),
  payload: Schema.optional(Schema.Unknown),
});

Event Kinds

session

Lifecycle events: connecting, ready, closed

notification

Provider activity: tool calls, message deltas, turn completion

request

Approval requests: command, file-read, file-change

error

Error events: session errors, protocol errors

Provider Session Directory

The ProviderSessionDirectory tracks active sessions across all providers:
interface ProviderSessionDirectory {
  // Register new session
  register(
    session: ProviderSession
  ): Effect.Effect<void, ProviderSessionDirectoryError>;
  
  // Update session state
  update(
    threadId: ThreadId,
    updates: Partial<ProviderSession>,
  ): Effect.Effect<void, ProviderSessionDirectoryError>;
  
  // Get session by thread ID
  get(
    threadId: ThreadId
  ): Effect.Effect<ProviderSession | undefined>;
  
  // Remove session
  remove(threadId: ThreadId): Effect.Effect<void>;
  
  // List all sessions
  list(): Effect.Effect<ReadonlyArray<ProviderSession>>;
  
  // Event stream for session changes
  streamEvents: Stream.Stream<ProviderSessionDirectoryEvent>;
}
The session directory maintains an in-memory registry of active sessions, enabling quick lookups and routing without database queries.

Provider Adapter Registry

The ProviderAdapterRegistry resolves provider kinds to concrete adapters:
interface ProviderAdapterRegistry {
  // Register an adapter
  register(
    adapter: ProviderAdapterShape<unknown>
  ): Effect.Effect<void>;
  
  // Get adapter by provider kind
  get(
    provider: ProviderKind
  ): Effect.Effect<ProviderAdapterShape<unknown>, ProviderServiceError>;
  
  // List all registered providers
  listProviders(): Effect.Effect<ReadonlyArray<ProviderKind>>;
}

Adding New Providers

To add a new provider adapter:
1

Update Contracts

Add the provider to ProviderKind literal in packages/contracts/src/orchestration.ts:
export const ProviderKind = Schema.Literal("codex", "claude-code");
2

Implement Adapter

Create a new adapter implementing ProviderAdapterShape<TError>:
export class ClaudeCodeAdapter implements ProviderAdapterShape<ClaudeCodeError> {
  readonly provider = "claude-code";
  readonly capabilities = { sessionModelSwitch: "in-session" };
  
  // Implement all required methods...
}
3

Register Adapter

Register the adapter with ProviderAdapterRegistry during server startup
4

Update UI

Add provider selection and configuration UI in the web app

Provider Errors

Providers use typed errors for robust error handling:
apps/server/src/provider/Errors.ts
export type ProviderServiceError =
  | ProviderValidationError
  | ProviderSessionError
  | CodexError
  | CheckpointError;

export class ProviderSessionError extends Data.TaggedError("ProviderSessionError")<{
  readonly reason: string;
}> {}

export class CodexError extends Data.TaggedError("CodexError")<{
  readonly reason: string;
  readonly cause?: unknown;
}> {}
Using Effect’s typed errors enables exhaustive error handling and better debugging at the call site.

Event Projection

Provider events are projected into orchestration domain events:
ProviderRuntimeEvent

[Server-side projection]

OrchestrationEvent

[WebSocket push]

Client receives via "orchestration.domainEvent" channel
This projection:
  1. Decouples provider protocols from client expectations
  2. Enables consistent event replay and debugging
  3. Supports multiple concurrent provider sessions
  4. Facilitates event sourcing and audit trails

Best Practices

Isolate Provider Logic

Keep provider-specific code in adapters, not in ProviderService

Use Typed Errors

Define specific error types for each provider to enable precise error handling

Emit Rich Events

Include sufficient context in events for debugging and replay

Handle Graceful Degradation

Implement fallback behavior for unsupported capabilities

Provider Health Monitoring

The ProviderHealth service monitors adapter availability:
interface ProviderHealth {
  // Check if provider is healthy
  check(
    provider: ProviderKind
  ): Effect.Effect<ProviderHealthStatus>;
  
  // Get health status for all providers
  checkAll(): Effect.Effect<ReadonlyArray<ProviderHealthStatus>>;
}

interface ProviderHealthStatus {
  provider: ProviderKind;
  healthy: boolean;
  message?: string;
  lastChecked: IsoDateTime;
}

Next Steps

Sessions

Learn about session lifecycle and management

Runtime Modes

Understand approval policies and security controls

Architecture

Explore the overall system design

API Reference

Browse the complete API documentation

Build docs developers (and LLMs) love