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 */
24import 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";
36import type * as Effect from "effect/Effect";
37import type * as Schema from "effect/Schema";
38import type * as Scope from "effect/Scope";
39
40import type * as ProviderAdapter from "./ProviderAdapter.ts";
41import type { ProviderAuthController } from "./auth.ts";
42import type { ProviderDriverError } from "./errors.ts";
43import type { ManagedServerProvider } from "./snapshot.ts";
44import type { ProviderTextGeneration } from "./textGeneration.ts";
45import 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 */
52export 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
70export 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 */
82export 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
133export interface ProviderContinuationIdentity {
134 readonly driverKind: ProviderDriverKind;
135 readonly continuationKey: string;
136}
137
138export 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 */
154export 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 */
176export 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.
228export type AnyProviderDriver<R = never, UsageR = unknown> = ProviderDriver<any, R, UsageR>;
229
230/** The services a driver's usage reader needs. */
231export type ProviderUsageReaderEnv<Driver> =
232 Driver extends ProviderDriver<any, any, infer UsageR> ? UsageR : never;