| 1 | /** |
| 2 | * ProviderDriver / ProviderInstance — driver SPI as plain values. |
| 3 | * |
| 4 | * `ProviderDriver` is a record, not a Context.Service. The thing it produces |
| 5 | * (`ProviderInstance`) is also a record of captured closures |
| 6 | * (`snapshot`, `orchestrationAdapter`, `textGeneration`), an id, and a driver kind. There |
| 7 | * are intentionally no per-driver Context tags because tags are |
| 8 | * singleton-per-runtime and we need many instances of the same driver. |
| 9 | * |
| 10 | * The only Effect service involved is `ProviderInstanceRegistry`, which |
| 11 | * owns the live `Map<InstanceId, ProviderInstance>` and is itself a |
| 12 | * singleton. |
| 13 | * |
| 14 | * Driver factories are functions of `(typed config, env)` where: |
| 15 | * - `typed config` is decoded once by the registry via `configSchema`, |
| 16 | * so drivers never deal with raw `unknown`. |
| 17 | * - `env` flows through Effect's R channel. Each driver declares the |
| 18 | * subset of infrastructure services it needs (FileSystem, |
| 19 | * ChildProcessSpawner, …) on its `create` return type; the registry |
| 20 | * layer's R is the union of those, and the runtime layer satisfies it. |
| 21 | * |
| 22 | * @module provider-core/server/driver |
| 23 | */ |
| 24 | import type { |
| 25 | ProviderConsumeResetCreditOutcome, |
| 26 | AcpRegistryListSessionsResult, |
| 27 | AcpRegistryListProvidersResult, |
| 28 | AcpRegistryOperationError, |
| 29 | AcpRegistrySetProviderInput, |
| 30 | ProviderDriverKind, |
| 31 | ProviderInstanceEnvironment, |
| 32 | ProviderInstanceId, |
| 33 | ServerProvider, |
| 34 | ServerProviderWorkspaceSnapshot, |
| 35 | } from "@t3tools/contracts"; |
| 36 | import type * as Effect from "effect/Effect"; |
| 37 | import type * as Schema from "effect/Schema"; |
| 38 | import type * as Scope from "effect/Scope"; |
| 39 | |
| 40 | import type * as ProviderAdapter from "./ProviderAdapter.ts"; |
| 41 | import type { ProviderAuthController } from "./auth.ts"; |
| 42 | import type { ProviderDriverError } from "./errors.ts"; |
| 43 | import type { ManagedServerProvider } from "./snapshot.ts"; |
| 44 | import type { ProviderTextGeneration } from "./textGeneration.ts"; |
| 45 | import type { ProviderUsageReader } from "./usage.ts"; |
| 46 | |
| 47 | /** |
| 48 | * Static metadata advertised by a driver. Used for default presentation |
| 49 | * and (later) settings UI. Doesn't need to be Effect-typed because nothing |
| 50 | * about it is dynamic — drivers are registered at startup. |
| 51 | */ |
| 52 | export interface ProviderDriverMetadata { |
| 53 | /** Human-readable name for the driver itself (e.g. "Codex"). */ |
| 54 | readonly displayName: string; |
| 55 | /** |
| 56 | * Whether the driver may be instantiated more than once concurrently. |
| 57 | * Defaults to `true`. Set to `false` for drivers that wrap a global |
| 58 | * resource (e.g. a single desktop app socket) — the registry then |
| 59 | * rejects multi-instance configurations with a clear error. |
| 60 | */ |
| 61 | readonly supportsMultipleInstances?: boolean; |
| 62 | /** |
| 63 | * Whether the server runs an instance at `defaultInstanceIdForDriver(kind)` |
| 64 | * even when settings have no entry for it. Defaults to `true`; drivers that |
| 65 | * need user input before they can run (such as the ACP Registry) opt out. |
| 66 | */ |
| 67 | readonly hasDefaultInstance?: boolean; |
| 68 | } |
| 69 | |
| 70 | export type ProviderWorkspaceSnapshot = ServerProvider & |
| 71 | Pick<ServerProviderWorkspaceSnapshot, "slashCommandsPending">; |
| 72 | |
| 73 | /** |
| 74 | * One materialized provider instance. Held by the registry, looked up by |
| 75 | * `instanceId`, torn down by closing the scope it was created in. |
| 76 | * |
| 77 | * The three "shape" fields are captured closures owned by this instance — |
| 78 | * stopping one instance cannot affect another, and starting a second |
| 79 | * instance of the same driver does not reach into the first instance's |
| 80 | * state. |
| 81 | */ |
| 82 | export interface ProviderInstance { |
| 83 | readonly instanceId: ProviderInstanceId; |
| 84 | readonly driverKind: ProviderDriverKind; |
| 85 | readonly continuationIdentity: ProviderContinuationIdentity; |
| 86 | readonly displayName: string | undefined; |
| 87 | readonly accentColor?: string | undefined; |
| 88 | readonly enabled: boolean; |
| 89 | readonly snapshot: ManagedServerProvider; |
| 90 | readonly snapshotForCwd?: ( |
| 91 | cwd: string, |
| 92 | ) => Effect.Effect<ProviderWorkspaceSnapshot, ProviderDriverError>; |
| 93 | readonly refreshModels?: () => Effect.Effect<void, ProviderDriverError>; |
| 94 | /** Invalidate T3-owned discovery caches before an explicit provider refresh. */ |
| 95 | readonly invalidateCaches?: Effect.Effect<void>; |
| 96 | /** |
| 97 | * Redeem one banked rate-limit reset credit on the signed-in account, then |
| 98 | * re-probe so the snapshot reflects the cleared windows. Account-level, |
| 99 | * not thread-level, which is why it lives here rather than on the adapter. |
| 100 | */ |
| 101 | readonly consumeResetCredit?: () => Effect.Effect< |
| 102 | ProviderConsumeResetCreditOutcome, |
| 103 | ProviderDriverError |
| 104 | >; |
| 105 | readonly orchestrationAdapter: ProviderAdapter.ProviderAdapterV2["Service"]; |
| 106 | readonly textGeneration: ProviderTextGeneration; |
| 107 | readonly auth?: ProviderAuthController; |
| 108 | readonly acpSessionManagement?: { |
| 109 | readonly listSessions: (input: { |
| 110 | readonly cwd: string; |
| 111 | readonly cursor?: string; |
| 112 | }) => Effect.Effect<AcpRegistryListSessionsResult, AcpRegistryOperationError>; |
| 113 | readonly logout: (cwd: string) => Effect.Effect<void, AcpRegistryOperationError>; |
| 114 | readonly deleteSession: (input: { |
| 115 | readonly cwd: string; |
| 116 | readonly sessionId: string; |
| 117 | }) => Effect.Effect<void, AcpRegistryOperationError>; |
| 118 | readonly listProviders: ( |
| 119 | cwd: string, |
| 120 | ) => Effect.Effect<AcpRegistryListProvidersResult, AcpRegistryOperationError>; |
| 121 | readonly setProvider: ( |
| 122 | input: Omit<AcpRegistrySetProviderInput, "instanceId" | "projectId"> & { |
| 123 | readonly cwd: string; |
| 124 | }, |
| 125 | ) => Effect.Effect<void, AcpRegistryOperationError>; |
| 126 | readonly disableProvider: (input: { |
| 127 | readonly cwd: string; |
| 128 | readonly providerId: string; |
| 129 | }) => Effect.Effect<void, AcpRegistryOperationError>; |
| 130 | }; |
| 131 | } |
| 132 | |
| 133 | export interface ProviderContinuationIdentity { |
| 134 | readonly driverKind: ProviderDriverKind; |
| 135 | readonly continuationKey: string; |
| 136 | } |
| 137 | |
| 138 | export function defaultProviderContinuationIdentity(input: { |
| 139 | readonly driverKind: ProviderDriverKind; |
| 140 | readonly instanceId: ProviderInstanceId; |
| 141 | }): ProviderContinuationIdentity { |
| 142 | return { |
| 143 | driverKind: input.driverKind, |
| 144 | continuationKey: `${input.driverKind}:instance:${input.instanceId}`, |
| 145 | }; |
| 146 | } |
| 147 | |
| 148 | /** |
| 149 | * Inputs the registry passes to a driver's `create` function. |
| 150 | * |
| 151 | * `config` is the typed payload — already decoded by the registry through |
| 152 | * `driver.configSchema`. Drivers never decode their own raw envelope. |
| 153 | */ |
| 154 | export interface ProviderDriverCreateInput<Config> { |
| 155 | readonly instanceId: ProviderInstanceId; |
| 156 | readonly displayName: string | undefined; |
| 157 | readonly accentColor?: string | undefined; |
| 158 | readonly environment: ProviderInstanceEnvironment; |
| 159 | readonly enabled: boolean; |
| 160 | readonly config: Config; |
| 161 | } |
| 162 | |
| 163 | /** |
| 164 | * Driver SPI — registered as a plain value, not a Layer. |
| 165 | * |
| 166 | * `Config` is whatever the driver decoded from |
| 167 | * `ProviderInstanceConfig.config`. `R` is the union of infrastructure |
| 168 | * services the driver depends on; the registry layer aggregates `R` across |
| 169 | * all registered drivers and the runtime supplies them. |
| 170 | * |
| 171 | * `create` is responsible for *all* per-instance state — process handles, |
| 172 | * pubsub topics, refs, file watchers — and must release them when its |
| 173 | * scope closes. Two calls to `create` with different `instanceId` / |
| 174 | * `config` MUST yield instances with no shared mutable state. |
| 175 | */ |
| 176 | export interface ProviderDriver<Config, R = never, UsageR = never> { |
| 177 | readonly driverKind: ProviderDriverKind; |
| 178 | readonly metadata: ProviderDriverMetadata; |
| 179 | /** |
| 180 | * Decoder for the opaque `ProviderInstanceConfig.config` envelope. The |
| 181 | * registry runs this exactly once per (re)load of an instance; a decode |
| 182 | * failure is surfaced as `ProviderDriverError` and downgraded to an |
| 183 | * unavailable shadow snapshot. |
| 184 | * |
| 185 | * The `Encoded` parameter is intentionally left as `unknown` (not |
| 186 | * `Config`) so schemas with `withDecodingDefault` / transformations — where |
| 187 | * the encoded shape differs from the decoded shape — satisfy the SPI |
| 188 | * without casts. The registry only ever decodes `unknown` envelopes here, |
| 189 | * so the precise encoded type is irrelevant at this boundary. |
| 190 | * |
| 191 | * Using `Codec` rather than `Schema` pins `DecodingServices = never` — if |
| 192 | * we used `Schema<Config>`, the erased `any` in `AnyProviderDriver` would |
| 193 | * widen `DecodingServices` to `unknown` and poison the R channel of every |
| 194 | * caller of `decodeUnknownEffect`. |
| 195 | */ |
| 196 | readonly configSchema: Schema.Codec<Config, unknown>; |
| 197 | /** |
| 198 | * Config used when an instance has no config blob, such as the default |
| 199 | * instance of a driver the user never configured. |
| 200 | */ |
| 201 | readonly defaultConfig: () => Config; |
| 202 | /** |
| 203 | * Materialize one instance. The returned effect runs in a scope owned |
| 204 | * by the registry; closing that scope releases every resource the |
| 205 | * driver opened. Failures become unavailable shadow snapshots — the |
| 206 | * driver MUST NOT throw defects. |
| 207 | */ |
| 208 | readonly create: ( |
| 209 | input: ProviderDriverCreateInput<Config>, |
| 210 | ) => Effect.Effect<ProviderInstance, ProviderDriverError, R | Scope.Scope>; |
| 211 | /** |
| 212 | * Where the usage page reads this provider's token history, when it keeps |
| 213 | * one. `UsageR` is what the reader needs, usually far less than `R`. |
| 214 | */ |
| 215 | readonly usage?: ProviderUsageReader<Config, UsageR>; |
| 216 | } |
| 217 | |
| 218 | /** |
| 219 | * Heterogeneous-array convenience: the registry stores drivers as |
| 220 | * `ReadonlyArray<AnyProviderDriver<R>>` where `R` is the union of all |
| 221 | * registered drivers' env requirements. |
| 222 | */ |
| 223 | // `any` here intentionally erases the per-driver Config; the registry |
| 224 | // already decoded it before invoking `create`, so downstream code never |
| 225 | // needs the original `Config` type. Using `unknown` instead would force |
| 226 | // `create` callers into casts since `unknown` is not assignable to a |
| 227 | // concrete `Config` from inside the driver body. |
| 228 | export type AnyProviderDriver<R = never, UsageR = unknown> = ProviderDriver<any, R, UsageR>; |
| 229 | |
| 230 | /** The services a driver's usage reader needs. */ |
| 231 | export type ProviderUsageReaderEnv<Driver> = |
| 232 | Driver extends ProviderDriver<any, any, infer UsageR> ? UsageR : never; |