@yanlinglabs/winter-agent-sdk 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,535 @@
1
+ import type { HookSource } from "../permissions/types.js";
2
+ import type { SettingSource } from "../settings/types.js";
3
+ import type { BrandProfile } from "../brand.js";
4
+ /**
5
+ * `sdk.d.ts:4597-4610`. THREE fields — WS-11 §4 and the plan both name only the first two.
6
+ * `skipMcpDiscovery` (`4609`) loads the plugin's skills/hooks/agents/commands but does NOT read its
7
+ * `.mcp.json` or manifest `mcpServers`, for hosts that own the plugin's MCP connections themselves.
8
+ * `type: 'local'` is a closed one-member literal on the pin (remote/marketplace plugins must first
9
+ * exist locally, `1846`).
10
+ */
11
+ export interface SdkPluginConfig {
12
+ type: "local";
13
+ path: string;
14
+ skipMcpDiscovery?: boolean;
15
+ }
16
+ /**
17
+ * `sdk.d.ts:2159-2164` verbatim (R5-9 as amended after Task 1). THREE arms: a replacement string, a
18
+ * block array split by `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` (options.ts), or the preset object — whose
19
+ * `excludeDynamicSections` is NESTED (`2163`) and doc-asserted inert for a string prompt (`2124`).
20
+ * `preset: 'claude_code'` is the pinned closed literal; Winter's own native preset spelling (BrandProfile.presetName) is
21
+ * an ALIAS Lane C resolves, deliberately not a widening of this union (a caller passing it to the
22
+ * pinned SDK would not typecheck, and this type exists to stay drop-in).
23
+ */
24
+ export type SystemPromptPreset = {
25
+ type: "preset";
26
+ preset: "claude_code";
27
+ append?: string;
28
+ excludeDynamicSections?: boolean;
29
+ };
30
+ export type SystemPromptOption = string | string[] | SystemPromptPreset;
31
+ /** `sdk.d.ts:963-966`; `OutputFormat = JsonSchemaOutputFormat` (`2207`) is currently a one-member union. */
32
+ export interface JsonSchemaOutputFormat {
33
+ type: "json_schema";
34
+ schema: Record<string, unknown>;
35
+ }
36
+ export type OutputFormat = JsonSchemaOutputFormat;
37
+ /**
38
+ * The main-session skill filter. DISCLOSED: the artifact records this option's doc lines but not its
39
+ * declared type; capture (4) shows the running engine accepts `'all'` (and that omission is not
40
+ * "skills off"). `string[] | "all"` is Winter's reading of those two facts — see options.ts.
41
+ */
42
+ export type SkillsOption = string[] | "all";
43
+ /**
44
+ * `sdk.d.ts:2848-2858` (R5-11 as amended). The typed result of `Query.rewindFiles(userMessageId,
45
+ * { dryRun? })` — the pinned parameter is `userMessageId` (`2641`, its own `@param` doc says the
46
+ * VALUE is a uuid), and both the second parameter and this result are absent from WS-11 §9.
47
+ *
48
+ * `skippedLinks` (`2857`) carries a behavioural rule, not just a count (doc `2855`): a tracked path
49
+ * that resolves to a symlink/hard link/non-regular file, or whose parent no longer resolves where it
50
+ * did at checkpoint time, or whose backup cannot be safely read, is REFUSED rather than restored —
51
+ * and the counter is populated on real rewinds only, never on a `dryRun`.
52
+ *
53
+ * Types only at Task 2: the control wiring is Task 3's and the mechanism is Lane K's.
54
+ */
55
+ export interface RewindFilesResult {
56
+ canRewind: boolean;
57
+ error?: string;
58
+ filesChanged?: string[];
59
+ insertions?: number;
60
+ deletions?: number;
61
+ skippedLinks?: number;
62
+ }
63
+ /** The wire form of a rewind request (`sdk.d.ts:4146-4150`): snake_case, and it drops the result. */
64
+ export interface RewindFilesRequest {
65
+ subtype: "rewind_files";
66
+ user_message_id: string;
67
+ dry_run?: boolean;
68
+ }
69
+ /** `sdk.d.ts:4881-4889`: one entry of `system/init.plugins`. `version` is plugin-author-controlled and doc-marked "validate before trusting". */
70
+ export interface InitPluginInfo {
71
+ name: string;
72
+ path: string;
73
+ version?: string;
74
+ }
75
+ export interface RuntimeHookMatcherGroup {
76
+ matcher?: string;
77
+ hookCount: number;
78
+ timeoutSec?: number;
79
+ source: HookSource;
80
+ hookNames?: Array<string | null>;
81
+ }
82
+ export type RuntimeHooksConfig = Partial<Record<string, RuntimeHookMatcherGroup[]>>;
83
+ export interface SandboxSettingsConfig {
84
+ enabled?: boolean;
85
+ autoAllowBashIfSandboxed?: boolean;
86
+ excludedCommands?: string[];
87
+ allowUnsandboxedCommands?: boolean;
88
+ filesystem?: {
89
+ allowWrite?: string[];
90
+ denyWrite?: string[];
91
+ denyRead?: string[];
92
+ };
93
+ network?: {
94
+ allowedDomains?: string[];
95
+ deniedDomains?: string[];
96
+ [key: string]: unknown;
97
+ };
98
+ }
99
+ export interface McpServerToolPolicy {
100
+ name: string;
101
+ permission_policy?: "always_allow" | "always_ask" | "always_deny";
102
+ org_max_permission?: "allow" | "ask" | "blocked";
103
+ }
104
+ export interface McpStdioServerConfig {
105
+ type?: "stdio";
106
+ command: string;
107
+ args?: string[];
108
+ env?: Record<string, string>;
109
+ timeout?: number;
110
+ alwaysLoad?: boolean;
111
+ }
112
+ export interface McpHttpServerConfig {
113
+ type: "http";
114
+ url: string;
115
+ headers?: Record<string, string>;
116
+ tools?: McpServerToolPolicy[];
117
+ timeout?: number;
118
+ alwaysLoad?: boolean;
119
+ }
120
+ export interface McpSSEServerConfig {
121
+ type: "sse";
122
+ url: string;
123
+ headers?: Record<string, string>;
124
+ tools?: McpServerToolPolicy[];
125
+ timeout?: number;
126
+ alwaysLoad?: boolean;
127
+ }
128
+ export interface WireMcpToolDefinition {
129
+ name: string;
130
+ description?: string;
131
+ inputSchema: Record<string, unknown>;
132
+ outputSchema?: Record<string, unknown>;
133
+ annotations?: {
134
+ readOnlyHint?: boolean;
135
+ destructiveHint?: boolean;
136
+ openWorldHint?: boolean;
137
+ title?: string;
138
+ idempotentHint?: boolean;
139
+ };
140
+ _meta?: Record<string, unknown>;
141
+ }
142
+ export interface McpSdkServerConfig {
143
+ type: "sdk";
144
+ name: string;
145
+ timeout?: number;
146
+ tools?: WireMcpToolDefinition[];
147
+ }
148
+ export type McpServerConfigForProcessTransport = McpStdioServerConfig | McpHttpServerConfig | McpSSEServerConfig | McpSdkServerConfig;
149
+ export type AgentMcpServerSpec = string | Record<string, McpServerConfigForProcessTransport>;
150
+ export interface RuntimeAgentDefinition {
151
+ description: string;
152
+ prompt: string;
153
+ tools?: string[];
154
+ disallowedTools?: string[];
155
+ model?: string;
156
+ mcpServers?: AgentMcpServerSpec[];
157
+ criticalSystemReminder_EXPERIMENTAL?: string;
158
+ skills?: string[];
159
+ initialPrompt?: string;
160
+ maxTurns?: number;
161
+ background?: boolean;
162
+ memory?: "user" | "project" | "local";
163
+ effort?: "low" | "medium" | "high" | "xhigh" | "max" | number;
164
+ permissionMode?: string;
165
+ observer?: string;
166
+ observerMessage?: string;
167
+ }
168
+ export interface RuntimeConfig {
169
+ sessionId: string;
170
+ cwd: string;
171
+ model: string;
172
+ permissionMode?: string;
173
+ maxTurns?: number;
174
+ resume?: string;
175
+ continue?: boolean;
176
+ forkSession?: boolean;
177
+ resumeSessionAt?: string;
178
+ resumeDropsTurn?: boolean;
179
+ persistSession?: boolean;
180
+ winterHome?: string;
181
+ allowedTools?: string[];
182
+ disallowedTools?: string[];
183
+ permissions?: {
184
+ allow?: string[];
185
+ ask?: string[];
186
+ deny?: string[];
187
+ disableBypassPermissionsMode?: boolean;
188
+ };
189
+ settingSources?: SettingSource[];
190
+ managedSettings?: Record<string, unknown>;
191
+ serverManagedSettings?: Record<string, unknown>;
192
+ allowDangerouslySkipPermissions?: boolean;
193
+ hooks?: RuntimeHooksConfig;
194
+ includeHookEvents?: boolean;
195
+ permissionPromptToolName?: string;
196
+ additionalDirectories?: string[];
197
+ sandbox?: SandboxSettingsConfig;
198
+ outputsDir?: string;
199
+ capabilities?: string[];
200
+ toolSearchEnabled?: boolean;
201
+ insideSubagent?: boolean;
202
+ familyMetadata?: {
203
+ taskNative?: boolean;
204
+ };
205
+ mcpServers?: Record<string, McpServerConfigForProcessTransport>;
206
+ strictMcpConfig?: boolean;
207
+ toolAliases?: Record<string, string>;
208
+ agents?: Record<string, RuntimeAgentDefinition>;
209
+ forwardSubagentText?: boolean;
210
+ agentId?: string;
211
+ isolationPinnedCwd?: boolean;
212
+ systemPrompt?: SystemPromptOption;
213
+ plugins?: SdkPluginConfig[];
214
+ skills?: SkillsOption;
215
+ outputFormat?: OutputFormat;
216
+ enableFileCheckpointing?: boolean;
217
+ contextWindowTokens?: number;
218
+ compactionThreshold?: number;
219
+ trustedWorkspace?: boolean;
220
+ plansDirectory?: string;
221
+ outputStyle?: string;
222
+ provider?: ProviderSelection;
223
+ fallbackModel?: string;
224
+ thinking?: ThinkingConfig;
225
+ effort?: EffortLevel;
226
+ /**
227
+ * @deprecated Use `thinking` instead.
228
+ *
229
+ * CONSUMER AND RULE, named because this field is inert until something applies it and a mapping
230
+ * with no owner is how a deprecated option quietly keeps its old semantics.
231
+ *
232
+ * OWNER: `packages/runtime/src/provider/selection.ts` (T3), at the point where a session's
233
+ * effective `ThinkingConfig` is resolved and handed to an adapter — NOT the adapter itself, so
234
+ * every family sees one already-resolved shape rather than each re-deriving it.
235
+ *
236
+ * RULE (R6-E, from the pinned deprecation text at `sdk.d.ts:1750-1757`):
237
+ * 1. `thinking`, when present, WINS outright — the pin states that precedence twice (`1732`,
238
+ * `8215`) — and `maxThinkingTokens` is then ignored entirely, not merged.
239
+ * 2. Otherwise `0` maps to `{ type: "disabled" }`.
240
+ * 3. Otherwise ANY other value maps to `{ type: "adaptive" }` — deliberately NOT
241
+ * `{ type: "enabled", budgetTokens: N }`. This is the trap the pin calls out: on a modern
242
+ * model the field is reinterpreted as on/off, so forwarding `maxThinkingTokens: 8000` as a
243
+ * budget of 8000 would be a different request from the one the pinned runtime makes.
244
+ * 4. An adapter MAY re-resolve `enabled` to `adaptive` for a model whose evidence says
245
+ * adaptive-only (capture (F) observed exactly that), recorded in the descriptor's `reasoning`
246
+ * evidence — that is a per-model adapter rule, downstream of this mapping.
247
+ */
248
+ maxThinkingTokens?: number;
249
+ includePartialMessages?: boolean;
250
+ maxBudgetUsd?: number;
251
+ providerStallTimeoutMs?: number;
252
+ keychainService?: string;
253
+ autoClassifier?: AutoClassifierConfig;
254
+ advisor?: AdvisorConfig;
255
+ /**
256
+ * P7a (D19): the RESOLVED brand profile — every Winter-owned name this session runs under.
257
+ *
258
+ * ALWAYS the full profile, never a partial and never absent from a config `query()` built: the
259
+ * WRAPPER resolves (`resolveBrand(Options.brand)`, validated at construction), and the runtime
260
+ * NEVER defaults. That asymmetry is the whole design — a runtime that filled in Winter's own
261
+ * names when the field was missing would silently un-brand a reuser's session on any path that
262
+ * forgot to thread it, which is exactly the failure the profile exists to make impossible.
263
+ *
264
+ * Optional on the TYPE only, for the configs this repository hand-builds in tests and for a
265
+ * pre-P7a wire message; a runtime reader that finds it absent is reading a config no `query()`
266
+ * produced and should say so rather than substituting.
267
+ *
268
+ * `keychainService` above is the DEPRECATED standalone alias for `brand.keychainService` (P6's
269
+ * own R6-10 field). The wrapper folds it: when the host sets it, its value is what lands in
270
+ * `brand.keychainService` too, so the two can never disagree on the wire.
271
+ */
272
+ brand?: BrandProfile;
273
+ }
274
+ /**
275
+ * The pinned effort vocabulary, `sdk.d.ts:586` (derived-shapes-p6.md item (c)).
276
+ *
277
+ * **No numeric member, deliberately (R6-E).** The numeric form exists in the pin on exactly one
278
+ * surface — `AgentDefinition.effort` (`sdk.d.ts:87`), a per-subagent field — and `Options.effort`
279
+ * (`1749`) is the bare union. Widening this to `| number` would be an Options-parity divergence.
280
+ * provider-runtime's `TurnRequest.effort` DOES keep `number`, because a child carrying a numeric
281
+ * effort reaches an adapter through that seam; the adapter maps it to the nearest verified tier of
282
+ * the model's own `reasoning.efforts` (the pin states no unit or mapping — a documented absence,
283
+ * OQ-P6-2 — so this is gap-filling, not divergence).
284
+ */
285
+ export type EffortLevel = "low" | "medium" | "high" | "xhigh" | "max";
286
+ /**
287
+ * `sdk.d.ts:8216` — three arms, exactly these spellings (derived-shapes-p6.md item (c)).
288
+ *
289
+ * Two declaration facts the option's own JSDoc contradicts, resolved in the declaration's favour:
290
+ * `budgetTokens` is OPTIONAL on the `enabled` arm (`8230`), and `display` exists on `adaptive` and
291
+ * `enabled` but NOT on `disabled`. The clearing form (`display: null`) exists only on the imperative
292
+ * surfaces (`setMaxThinkingTokens`), never in `ThinkingConfig` — so it is absent here too.
293
+ */
294
+ export type ThinkingConfig = {
295
+ type: "adaptive";
296
+ display?: "summarized" | "omitted";
297
+ } | {
298
+ type: "enabled";
299
+ budgetTokens?: number;
300
+ display?: "summarized" | "omitted";
301
+ } | {
302
+ type: "disabled";
303
+ };
304
+ /**
305
+ * R6-10: an OPAQUE reference to credential material, never the material itself. Descriptors and
306
+ * config alike carry references; the store resolves one at the last responsible moment.
307
+ *
308
+ * - `keychain` — one record per provider/account, `account = "<providerId>:<accountId>"`, service
309
+ * from `Options.keychainService ?? WINTER_BRAND.keychainService`. Retires the single fixed secret name.
310
+ * - `env` — a host NAMES the variable explicitly. Ambient keys are NEVER scanned implicitly: an
311
+ * `ANTHROPIC_API_KEY` sitting in the environment does not become a credential by existing.
312
+ * - `file` — a shared-credentials file, a GCP service-account JSON, or a raw single-value file.
313
+ * - `inline` — a host responsibility: never persisted by the SDK, redacted in every frame and log.
314
+ * - `aws-default-chain` — env + shared-credentials file only this phase (R6-16: no IMDS/STS).
315
+ * - `none` — a local endpoint that needs no credential. NOT "unauthenticated by accident".
316
+ */
317
+ export type CredentialRef = {
318
+ kind: "keychain";
319
+ account: string;
320
+ service?: string;
321
+ } | {
322
+ kind: "env";
323
+ name: string;
324
+ } | {
325
+ kind: "file";
326
+ path: string;
327
+ format: "aws-shared-credentials" | "gcp-service-account-json" | "raw";
328
+ profile?: string;
329
+ } | {
330
+ kind: "inline";
331
+ value: string;
332
+ } | {
333
+ kind: "aws-default-chain";
334
+ } | {
335
+ kind: "none";
336
+ };
337
+ /**
338
+ * Non-secret connection metadata (WS-13 §6: "the selected account/region/project/deployment is
339
+ * recorded as non-secret connection metadata — never forced through one bearer-token abstraction").
340
+ *
341
+ * The wire twin of provider-runtime's `ConnectionProfile`, minus its `providerId` (which is
342
+ * `ProviderSelection.providerId` here). `baseUrl` is a USER endpoint and goes through the endpoint
343
+ * policy — generated descriptor endpoints are immutable and never come from this shape (R6-11).
344
+ */
345
+ export interface ProviderConnectionConfig {
346
+ baseUrl?: string;
347
+ headers?: Record<string, string>;
348
+ region?: string;
349
+ project?: string;
350
+ location?: string;
351
+ deployment?: string;
352
+ apiVersion?: string;
353
+ local?: boolean;
354
+ /**
355
+ * P7a carry (Lane D): WHERE this connection's `baseUrl` came from, which is what the endpoint
356
+ * policy actually needs to know.
357
+ *
358
+ * `"reviewed"` — the catalog's own generated/reviewed endpoint, COPIED into the profile by the
359
+ * runtime's `connectionFrom`. `"user"` — a host- or user-entered endpoint.
360
+ *
361
+ * WHY IT EXISTS. The two are byte-identical strings by the time they reach the profile, and the
362
+ * policy has been forced to treat both as user endpoints because it could not tell them apart —
363
+ * which silently downgrades every multi-provider row (whose reviewed endpoint is always copied)
364
+ * out of the privileged-header path in production while adapter fixtures, which pass a generated
365
+ * base URL directly, keep passing. Recording the ORIGIN is what lets the policy answer honestly.
366
+ *
367
+ * ABSENT means unknown, and unknown must be treated as `"user"` — the conservative reading. The
368
+ * evaluation rules are Lane D's (`provider-runtime/src/endpoint-policy.ts`); the spine declares
369
+ * the field so producer and consumer can land in either order.
370
+ */
371
+ endpointOrigin?: "reviewed" | "user";
372
+ }
373
+ /**
374
+ * R6-9: which provider a bare model id resolves against, plus how to reach and authenticate it.
375
+ *
376
+ * `allowUnlisted` sits HERE rather than on `ConnectionProfile` (whose shape T2's brief pins
377
+ * verbatim): it is a selection-policy bit, not connection metadata. R6-F admits it only for a
378
+ * local/gateway provider whose `liveCatalogAuthority` is not `authoritative`; anywhere else the
379
+ * registry ignores it and an unlisted id is still a typed `unknown-model` refusal, because WS-13
380
+ * §8.3's model-id validation is the whole point of the catalog.
381
+ */
382
+ export interface ProviderSelection {
383
+ providerId: string;
384
+ authRef?: CredentialRef;
385
+ connection?: ProviderConnectionConfig;
386
+ allowUnlisted?: boolean;
387
+ }
388
+ /** R6-14: the classifier's own route — resolved through the SAME selection path as the session model. */
389
+ export interface AutoClassifierConfig {
390
+ model: string;
391
+ authRef?: CredentialRef;
392
+ }
393
+ /**
394
+ * The advisor/reviewer backend's own model selection (P2 carry, wired in T10). Same selection path.
395
+ *
396
+ * `authRef` (P6 fix wave, Ruling E-1) mirrors `AutoClassifierConfig.authRef`: the advisor's OWN
397
+ * credential, for a reviewer on another provider than the session's. A cross-provider advisor never
398
+ * inherits the session's credential -- without a ref here, and without a keychain record for the
399
+ * advisor's provider, the advisor's provider refuses with a typed `no-credential-for-provider`.
400
+ */
401
+ export interface AdvisorConfig {
402
+ model: string;
403
+ authRef?: CredentialRef;
404
+ }
405
+ /**
406
+ * The pinned `ModelInfo` (`sdk.d.ts:1261-1300`): three required fields, six optional.
407
+ *
408
+ * `description` being REQUIRED is the notable one — a catalog row generated from an upstream
409
+ * extraction with no description cannot satisfy this shape without inventing text. The four
410
+ * `supports*` booleans are the pin's ENTIRE capability vocabulary for a model, and each is OMITTED
411
+ * when unknown rather than reported `false` (R6-I): absent means unknown, and capture (J) observed
412
+ * the omission varying per capability within one response.
413
+ */
414
+ export interface ModelInfo {
415
+ value: string;
416
+ resolvedModel?: string;
417
+ displayName: string;
418
+ description: string;
419
+ supportsEffort?: boolean;
420
+ supportedEffortLevels?: Array<"low" | "medium" | "high" | "xhigh" | "max">;
421
+ supportsAdaptiveThinking?: boolean;
422
+ supportsFastMode?: boolean;
423
+ supportsAutoMode?: boolean;
424
+ }
425
+ /**
426
+ * The pinned `AccountInfo` (`sdk.d.ts:23-33`). EVERY field optional; an empty object is valid, and
427
+ * capture (J) observed exactly three keys present under API-key auth.
428
+ *
429
+ * `apiKeySource` here is a BARE STRING while `system/init.apiKeySource` is the closed `ApiKeySource`
430
+ * union — the same concept typed twice, differently, in one pinned declaration. Reproduced rather
431
+ * than reconciled: a Winter consumer that narrowed this one would be narrowing something the pin
432
+ * left open.
433
+ */
434
+ export interface AccountInfo {
435
+ email?: string;
436
+ organization?: string;
437
+ subscriptionType?: string;
438
+ tokenSource?: string;
439
+ apiKeySource?: string;
440
+ apiProvider?: "firstParty" | "bedrock" | "vertex" | "foundry" | "anthropicAws" | "anthropicGoogleCloud" | "mantle" | "gateway";
441
+ }
442
+ /**
443
+ * One option as a host renders it (WS-13c §7).
444
+ *
445
+ * `resolvesTo` is present only when the slot actually resolves for THIS session — a credential is
446
+ * configured and the provider is enabled. Its absence is the honest "this option exists in the
447
+ * lineup but nothing here can serve it", which is what lets a switcher grey an entry rather than
448
+ * offer it and fail at turn time.
449
+ */
450
+ export interface SlotView {
451
+ name: string;
452
+ canonicalModelId: string;
453
+ description: string;
454
+ reason: string;
455
+ resolvesTo?: {
456
+ providerId: string;
457
+ key: string;
458
+ };
459
+ }
460
+ /**
461
+ * The slots a session is CURRENTLY offering, and why they are those slots (WS-13c §3).
462
+ *
463
+ * `source` is the whole provenance story in one field, and every member is a distinct, observable
464
+ * state a host may want to explain:
465
+ * `family-default` the effective main model's family has curated slots, and these are they.
466
+ * `custom` `settings.modelSlots` replaced the set (D27).
467
+ * `claude-pinned` the effective main model is a Claude model, so the set is the pinned four and
468
+ * any custom slots were IGNORED and recorded (D25, "for now").
469
+ * `own-model` the family has no curated slots (or is `other`), so the session renders its
470
+ * own effective model as the single slot (D26's minimum of one).
471
+ */
472
+ export interface ActiveSlotSet {
473
+ family: string;
474
+ source: "family-default" | "custom" | "claude-pinned" | "own-model";
475
+ slots: SlotView[];
476
+ }
477
+ /**
478
+ * P7a carry (Lane D): whether this session can currently SERVE a catalog row — a TRI-STATE, not a
479
+ * boolean.
480
+ *
481
+ * `"present"` a credential is configured for the row's provider and the provider is not disabled.
482
+ * `"absent"` we know there is none.
483
+ * `"unknown"` nobody has probed this provider yet, so the honest answer is "we do not know".
484
+ *
485
+ * WHY THE BOOLEAN WAS WRONG. A boolean has to collapse `unknown` onto one of the other two, and
486
+ * both collapses lie: `false` tells a model switcher a perfectly usable row is unavailable, and
487
+ * `true` (the shape the cold first paint originally shipped) claimed every row in a 604-model
488
+ * catalog was servable against an empty credential store. A host renders three states differently —
489
+ * available, unavailable, and "sign in to find out" — so the type carries three.
490
+ *
491
+ * PUBLIC-SHAPE CHANGE, declared at the spine and made TRUTHFUL by Lane D: until Lane D's
492
+ * credential-view work lands, `family-listing.ts` maps its existing boolean to `"present"`/
493
+ * `"absent"` and never emits `"unknown"`. Consumers must therefore already handle all three.
494
+ */
495
+ export type ModelRowServable = "present" | "absent" | "unknown";
496
+ /**
497
+ * `Query.listModelFamilies()`'s answer (WS-13c §7): the active set first, then everything behind
498
+ * "more options".
499
+ *
500
+ * `active` is `undefined` ONLY for a session running a scripted test double, which has no effective
501
+ * model to derive a family from. It is not a "no families" signal — that is `families: []`.
502
+ */
503
+ export interface ModelFamilyListing {
504
+ active: ActiveSlotSet | undefined;
505
+ families: Array<{
506
+ id: string;
507
+ displayName: string;
508
+ vendor: string;
509
+ slots: SlotView[];
510
+ models: Array<{
511
+ canonicalModelId: string;
512
+ displayName: string;
513
+ rows: Array<{
514
+ key: string;
515
+ providerId: string;
516
+ status: string;
517
+ pricingBasis: string;
518
+ servable: ModelRowServable;
519
+ }>;
520
+ }>;
521
+ }>;
522
+ }
523
+ /**
524
+ * One entry of `settings.modelSlots` (WS-13c §5, D27): the user's own facing name for a model.
525
+ *
526
+ * `model` is a `canonicalModelId` OR a catalog key — both spellings are things a user reasonably has
527
+ * to hand, and validation resolves either. `description` overrides Winter's own slot text; absent
528
+ * means "use Winter's description for that canonical model, else the row's displayName".
529
+ */
530
+ export interface ModelSlotSetting {
531
+ name: string;
532
+ model: string;
533
+ provider?: string;
534
+ description?: string;
535
+ }