/** * ProviderDriver / ProviderInstance — driver SPI as plain values. * * `ProviderDriver` is a record, not a Context.Service. The thing it produces * (`ProviderInstance`) is also a record — three captured closures * (`snapshot`, `adapter`, `textGeneration`), an id, and a driver kind. There * are intentionally no per-driver Context tags because tags are * singleton-per-runtime and we need many instances of the same driver. * * The only Effect service involved is `ProviderInstanceRegistry`, which * owns the live `Map` and is itself a * singleton. * * Driver factories are functions of `(typed config, env)` where: * - `typed config` is decoded once by the registry via `configSchema`, * so drivers never deal with raw `unknown`. * - `env` flows through Effect's R channel. Each driver declares the * subset of infrastructure services it needs (FileSystem, * ChildProcessSpawner, …) on its `create` return type; the registry * layer's R is the union of those, and the runtime layer satisfies it. * * @module provider/ProviderDriver */ import type { ProviderDriverKind, ProviderInstanceEnvironment, ProviderInstanceId, } from "@t3tools/contracts"; import type * as Effect from "effect/Effect"; import type * as Schema from "effect/Schema"; import type * as Scope from "effect/Scope"; import type * as TextGeneration from "../textGeneration/TextGeneration.ts"; import type { ProviderAdapterError, ProviderDriverError } from "./Errors.ts"; import type { ProviderAdapterShape } from "./Services/ProviderAdapter.ts"; import type { ServerProviderShape } from "./Services/ServerProvider.ts"; /** * Static metadata advertised by a driver. Used for default presentation * and (later) settings UI. Doesn't need to be Effect-typed because nothing * about it is dynamic — drivers are registered at startup. */ export interface ProviderDriverMetadata { /** Human-readable name for the driver itself (e.g. "Codex"). */ readonly displayName: string; /** * Whether the driver may be instantiated more than once concurrently. * Defaults to `true`. Set to `false` for drivers that wrap a global * resource (e.g. a single desktop app socket) — the registry then * rejects multi-instance configurations with a clear error. */ readonly supportsMultipleInstances?: boolean; } /** * One materialized provider instance. Held by the registry, looked up by * `instanceId`, torn down by closing the scope it was created in. * * The three "shape" fields are captured closures owned by this instance — * stopping one instance cannot affect another, and starting a second * instance of the same driver does not reach into the first instance's * state. */ export interface ProviderInstance { readonly instanceId: ProviderInstanceId; readonly driverKind: ProviderDriverKind; readonly continuationIdentity: ProviderContinuationIdentity; readonly displayName: string | undefined; readonly accentColor?: string | undefined; readonly enabled: boolean; readonly snapshot: ServerProviderShape; readonly adapter: ProviderAdapterShape; readonly textGeneration: TextGeneration.TextGeneration["Service"]; } export interface ProviderContinuationIdentity { readonly driverKind: ProviderDriverKind; readonly continuationKey: string; } export function defaultProviderContinuationIdentity(input: { readonly driverKind: ProviderDriverKind; readonly instanceId: ProviderInstanceId; }): ProviderContinuationIdentity { return { driverKind: input.driverKind, continuationKey: `${input.driverKind}:instance:${input.instanceId}`, }; } /** * Inputs the registry passes to a driver's `create` function. * * `config` is the typed payload — already decoded by the registry through * `driver.configSchema`. Drivers never decode their own raw envelope. */ export interface ProviderDriverCreateInput { readonly instanceId: ProviderInstanceId; readonly displayName: string | undefined; readonly accentColor?: string | undefined; readonly environment: ProviderInstanceEnvironment; readonly enabled: boolean; readonly config: Config; } /** * Driver SPI — registered as a plain value, not a Layer. * * `Config` is whatever the driver decoded from * `ProviderInstanceConfig.config`. `R` is the union of infrastructure * services the driver depends on; the registry layer aggregates `R` across * all registered drivers and the runtime supplies them. * * `create` is responsible for *all* per-instance state — process handles, * pubsub topics, refs, file watchers — and must release them when its * scope closes. Two calls to `create` with different `instanceId` / * `config` MUST yield instances with no shared mutable state. */ export interface ProviderDriver { readonly driverKind: ProviderDriverKind; readonly metadata: ProviderDriverMetadata; /** * Decoder for the opaque `ProviderInstanceConfig.config` envelope. The * registry runs this exactly once per (re)load of an instance; a decode * failure is surfaced as `ProviderDriverError` and downgraded to an * unavailable shadow snapshot. * * The `Encoded` parameter is intentionally left as `unknown` (not * `Config`) so schemas with `withDecodingDefault` / transformations — where * the encoded shape differs from the decoded shape — satisfy the SPI * without casts. The registry only ever decodes `unknown` envelopes here, * so the precise encoded type is irrelevant at this boundary. * * Using `Codec` rather than `Schema` pins `DecodingServices = never` — if * we used `Schema`, the erased `any` in `AnyProviderDriver` would * widen `DecodingServices` to `unknown` and poison the R channel of every * caller of `decodeUnknownEffect`. */ readonly configSchema: Schema.Codec; /** * Default config payload used when the legacy * `ServerSettings.providers.` entry is empty or when the driver * is auto-bootstrapped without user configuration. Returning a typed * default keeps the migration path simple — no special-casing needed * to construct a "blank" instance. */ readonly defaultConfig: () => Config; /** * Materialize one instance. The returned effect runs in a scope owned * by the registry; closing that scope releases every resource the * driver opened. Failures become unavailable shadow snapshots — the * driver MUST NOT throw defects. */ readonly create: ( input: ProviderDriverCreateInput, ) => Effect.Effect; } /** * Heterogeneous-array convenience: the registry stores drivers as * `ReadonlyArray>` where `R` is the union of all * registered drivers' env requirements. */ // `any` here intentionally erases the per-driver Config; the registry // already decoded it before invoking `create`, so downstream code never // needs the original `Config` type. Using `unknown` instead would force // `create` callers into casts since `unknown` is not assignable to a // concrete `Config` from inside the driver body. export type AnyProviderDriver = ProviderDriver;