1/**
2 * Provider-instance contracts.
3 *
4 * Splits the historical "provider kind" concept into two:
5 *
6 * - `ProviderDriverKind` is the implementation kind selector (e.g. codex,
7 * claudeAgent, a fork's `ollama`, …). It picks which driver package
8 * handles the protocol, the probe, the adapter, and text generation.
9 *
10 * - `ProviderInstanceId` is the routing key (a user-defined slug).
11 * Threads, sessions, runtime events, and persisted bindings reference
12 * instance ids — never driver kinds — so a user can configure multiple
13 * instances of the same driver (e.g. `codex_personal` + `codex_work`),
14 * each with independent driver-specific configuration.
15 *
16 * Forward/backward compatibility invariant
17 * ----------------------------------------
18 * `ProviderDriverKind` is intentionally an **open** branded slug, not a closed
19 * literal union. The server hosts forks, ships in PRs that add drivers, and
20 * users frequently roll between branches and forks. Any of those paths can
21 * leave `ServerSettings`, persisted thread state, or session bindings
22 * referencing a driver that the currently-running build does not know about.
23 *
24 * The rule: parsing any of those payloads must always succeed, and the
25 * runtime is responsible for marking the unknown driver/instance as
26 * "unavailable" rather than crashing. Built-in drivers shipped by the core
27 * product happens to register in a given build is not part of the contract
28 * layer. Driver availability is discovered through the runtime registry.
29 *
30 * Driver-specific configuration is similarly opaque at the contracts layer:
31 * drivers live in (or will be extracted to) their own packages and own their
32 * config schemas. The contracts package only knows the envelope.
33 *
34 * @module providerInstance
35 */
36import * as Effect from "effect/Effect";
37import * as Schema from "effect/Schema";
38import { TrimmedNonEmptyString } from "./baseSchemas.ts";
39
40const PROVIDER_SLUG_MAX_CHARS = 64;
41/**
42 * Slug pattern shared by driver kinds and instance ids — letters, digits,
43 * dashes, underscores. The first character must be a letter so slugs remain
44 * JS-identifier friendly when used as object keys, log fields, or telemetry
45 * attributes. Mixed case is permitted so historical driver kinds (e.g.
46 * `claudeAgent`) can be used verbatim during the migration and so external
47 * fork authors retain reasonable freedom.
48 */
49const PROVIDER_SLUG_PATTERN = /^[a-zA-Z][a-zA-Z0-9_-]*$/;
50const ENVIRONMENT_VARIABLE_NAME_MAX_CHARS = 128;
51const ENVIRONMENT_VARIABLE_NAME_PATTERN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
52
53const slugSchema = TrimmedNonEmptyString.check(
54 Schema.isMaxLength(PROVIDER_SLUG_MAX_CHARS),
55 Schema.isPattern(PROVIDER_SLUG_PATTERN),
56);
57
58/**
59 * `ProviderDriverKind` — open branded slug naming a driver implementation.
60 *
61 * Constraints (validated at the schema layer):
62 * - starts with a letter
63 * - only letters, digits, `-`, `_` after the first char
64 * - 1..64 characters
65 *
66 * Notably **not** validated: that the driver is one we know how to load.
67 * That check belongs to the runtime registry, which downgrades unknown
68 * drivers gracefully (see module docs).
69 */
70export const ProviderDriverKind = slugSchema.pipe(Schema.brand("ProviderDriverKind"));
71export type ProviderDriverKind = typeof ProviderDriverKind.Type;
72
73const isProviderDriverKindValue = Schema.is(ProviderDriverKind);
74export const isProviderDriverKind = (value: unknown): value is ProviderDriverKind =>
75 isProviderDriverKindValue(value);
76
77/**
78 * `ProviderInstanceId` — user-defined routing key for a configured provider
79 * instance. Same slug rules as `ProviderDriverKind`; branded separately so the
80 * type system cannot confuse the two.
81 */
82export const ProviderInstanceId = slugSchema.pipe(Schema.brand("ProviderInstanceId"));
83export type ProviderInstanceId = typeof ProviderInstanceId.Type;
84
85/**
86 * Lightweight reference identifying which driver implements an instance.
87 * Carried alongside `ProviderInstanceId` on wire shapes so consumers can
88 * branch on driver behavior (icons, capabilities, presentation) without
89 * having to look up the instance in the registry.
90 */
91export const ProviderInstanceRef = Schema.Struct({
92 instanceId: ProviderInstanceId,
93 driver: ProviderDriverKind,
94});
95export type ProviderInstanceRef = typeof ProviderInstanceRef.Type;
96
97export const ProviderInstanceEnvironmentVariableName = TrimmedNonEmptyString.check(
98 Schema.isMaxLength(ENVIRONMENT_VARIABLE_NAME_MAX_CHARS),
99 Schema.isPattern(ENVIRONMENT_VARIABLE_NAME_PATTERN),
100);
101export type ProviderInstanceEnvironmentVariableName =
102 typeof ProviderInstanceEnvironmentVariableName.Type;
103
104export const ProviderInstanceEnvironmentVariable = Schema.Struct({
105 name: ProviderInstanceEnvironmentVariableName,
106 value: Schema.String.pipe(Schema.withDecodingDefault(Effect.succeed(""))),
107 sensitive: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))),
108 valueRedacted: Schema.optionalKey(Schema.Boolean),
109});
110export type ProviderInstanceEnvironmentVariable = typeof ProviderInstanceEnvironmentVariable.Type;
111
112export const ProviderInstanceEnvironment = Schema.Array(ProviderInstanceEnvironmentVariable);
113export type ProviderInstanceEnvironment = typeof ProviderInstanceEnvironment.Type;
114
115/**
116 * Envelope shape for a provider instance configuration in `ServerSettings`.
117 *
118 * `driver` is intentionally accepted as any well-formed slug (see module
119 * docs). The driver-specific config payload is left as `Schema.Unknown`;
120 * each driver registers its own decoder with the runtime registry, and
121 * envelopes for unknown drivers are preserved verbatim so they round-trip
122 * across version changes without data loss.
123 */
124export const ProviderInstanceConfig = Schema.Struct({
125 driver: ProviderDriverKind,
126 displayName: Schema.optional(TrimmedNonEmptyString),
127 accentColor: Schema.optional(TrimmedNonEmptyString),
128 environment: Schema.optionalKey(ProviderInstanceEnvironment),
129 enabled: Schema.optionalKey(Schema.Boolean),
130 config: Schema.optionalKey(Schema.Unknown),
131});
132export type ProviderInstanceConfig = typeof ProviderInstanceConfig.Type;
133
134/** Atomic mutation for one provider-instance map entry. */
135export const ProviderInstanceMutation = Schema.Union([
136 Schema.Struct({
137 operation: Schema.Literal("create"),
138 instanceId: ProviderInstanceId,
139 instance: ProviderInstanceConfig,
140 }),
141 Schema.Struct({
142 operation: Schema.Literal("upsert"),
143 instanceId: ProviderInstanceId,
144 instance: ProviderInstanceConfig,
145 }),
146 Schema.Struct({
147 operation: Schema.Literal("remove"),
148 instanceId: ProviderInstanceId,
149 }),
150]);
151export type ProviderInstanceMutation = typeof ProviderInstanceMutation.Type;
152
153/**
154 * Map shape for `ServerSettings.providerInstances`. Keyed by
155 * `ProviderInstanceId`, values are envelopes the registry feeds to drivers.
156 */
157export const ProviderInstanceConfigMap = Schema.Record(ProviderInstanceId, ProviderInstanceConfig);
158export type ProviderInstanceConfigMap = typeof ProviderInstanceConfigMap.Type;
159
160/**
161 * Construct the canonical `ProviderInstanceId` used as a back-compat default
162 * for a built-in driver. The legacy single-instance-per-driver world used
163 * the driver kind itself as the instance id; preserving that mapping keeps
164 * existing persisted threads, bindings, and cache files routable across the
165 * migration without rewriting their stored selection payloads.
166 */
167export const defaultInstanceIdForDriver = (driver: ProviderDriverKind): ProviderInstanceId =>
168 ProviderInstanceId.make(driver);