@to11ai/sdk 0.1.2 → 0.2.0

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.
Files changed (42) hide show
  1. package/dist/behaviors/store.d.ts +62 -0
  2. package/dist/client.cjs +37 -4
  3. package/dist/client.d.ts +41 -2
  4. package/dist/client.js +45 -4
  5. package/dist/errors.cjs +90 -1
  6. package/dist/errors.d.ts +142 -0
  7. package/dist/errors.js +90 -1
  8. package/dist/gateway.cjs +12 -0
  9. package/dist/gateway.d.ts +26 -12
  10. package/dist/gateway.js +14 -0
  11. package/dist/generated/index.d.ts +1 -0
  12. package/dist/generated/types.gen.d.ts +22974 -0
  13. package/dist/generated/zod.gen.d.ts +13531 -0
  14. package/dist/projectEnvironments/resource.cjs +7 -2
  15. package/dist/projectEnvironments/resource.js +7 -2
  16. package/dist/projectEnvironments/types.d.ts +4 -75
  17. package/dist/prompts/cache.d.ts +24 -3
  18. package/dist/prompts/resource.cjs +101 -3
  19. package/dist/prompts/resource.d.ts +47 -3
  20. package/dist/prompts/resource.js +103 -5
  21. package/dist/prompts/types.d.ts +36 -177
  22. package/dist/routingRules/resource.cjs +9 -2
  23. package/dist/routingRules/resource.js +9 -2
  24. package/dist/routingRules/types.d.ts +12 -123
  25. package/dist/transport.d.ts +23 -0
  26. package/package.json +23 -12
  27. package/src/behaviors/store.ts +175 -0
  28. package/src/client.ts +115 -3
  29. package/src/errors.ts +261 -0
  30. package/src/gateway.ts +54 -12
  31. package/src/generated/index.ts +3 -0
  32. package/src/generated/types.gen.ts +24083 -0
  33. package/src/generated/zod.gen.ts +12648 -0
  34. package/src/projectEnvironments/resource.ts +10 -0
  35. package/src/projectEnvironments/types.ts +9 -91
  36. package/src/prompts/cache.ts +39 -4
  37. package/src/prompts/resource.ts +258 -5
  38. package/src/prompts/types.ts +56 -237
  39. package/src/routingRules/resource.ts +10 -1
  40. package/src/routingRules/types.ts +21 -155
  41. package/src/traceparent.ts +69 -4
  42. package/src/transport.ts +188 -3
@@ -9,6 +9,8 @@
9
9
  // envelope, and returns the resource shape verbatim.
10
10
  // ---------------------------------------------------------------------------
11
11
 
12
+ import { z } from "zod";
13
+ import { zProjectEnvironment } from "../generated/zod.gen";
12
14
  import type { ApiEnvelope, Transport } from "../transport";
13
15
  import type {
14
16
  ArchiveProjectEnvironmentInput,
@@ -87,6 +89,10 @@ export class ProjectEnvironmentResource {
87
89
  query: input.includeArchived
88
90
  ? { includeArchived: "true" }
89
91
  : undefined,
92
+ decode: (data) =>
93
+ z
94
+ .array(zProjectEnvironment)
95
+ .parse(data) as ListProjectEnvironmentsResult,
90
96
  },
91
97
  );
92
98
  return unwrap(envelope);
@@ -100,6 +106,10 @@ export class ProjectEnvironmentResource {
100
106
  const envelope = await this.transport.request<GetProjectEnvironmentResult>(
101
107
  "GET",
102
108
  projectEnvironmentPath(input.projectId, input.slug),
109
+ {
110
+ decode: (data) =>
111
+ zProjectEnvironment.parse(data) as GetProjectEnvironmentResult,
112
+ },
103
113
  );
104
114
  return unwrap(envelope);
105
115
  }
@@ -1,82 +1,18 @@
1
- // ---------------------------------------------------------------------------
2
- // Project Environments wire types (TO11-793 / env-registry §S21).
3
- // ---------------------------------------------------------------------------
4
- // Spec: docs/product-specs/2026-05-14-environments-registry.md
5
- // Plan: docs/exec-plans/active/2026-05-14-env-registry-plan.md §S21
6
- //
7
- // Hand-rolled TS mirrors of the Effect Schema types defined in
8
- // `apps/api/src/domains/tenancy/projects/model/project-environment.ts`. The
9
- // published `@to11ai/sdk` package intentionally does NOT depend on the
10
- // internal `@to11/api` workspace package — keep these in lockstep with the
11
- // API model when adding fields.
12
- // ---------------------------------------------------------------------------
1
+ import type {
2
+ ArchiveProjectEnvironmentResult as GenArchiveProjectEnvironmentResult,
3
+ LivePointerCounts as GenLivePointerCounts,
4
+ ProjectEnvironment as GenProjectEnvironment,
5
+ } from "../generated/types.gen";
13
6
 
14
- // ─── Wire shape ─────────────────────────────────────────────────────────────
7
+ export type ProjectEnvironment = GenProjectEnvironment;
15
8
 
16
- /**
17
- * Project-scoped environment row as returned by
18
- * `GET /v1/projects/:projectId/environments/:slug`.
19
- *
20
- * Notes:
21
- * - `isSystem: true` for the seeded `prod` / `staging` rows. Those rows
22
- * cannot be renamed, archived, or have their `slug`/`isSystem` mutated.
23
- * - `isGatewayFallback: true` marks the env the gateway routes to when no
24
- * `x-to11-env` header is supplied. At most one env per project may carry
25
- * this flag; clearing the only fallback is rejected with 409
26
- * `CANNOT_CLEAR_ONLY_GATEWAY_FALLBACK`.
27
- * - `archivedAt` is `null` for live envs and an ISO-8601 timestamp for
28
- * soft-archived ones.
29
- */
30
- export interface ProjectEnvironment {
31
- id: string;
32
- projectId: string;
33
- slug: string;
34
- name: string;
35
- color: string | null;
36
- description: string | null;
37
- isPointer: boolean;
38
- requiresApproval: boolean;
39
- isDefault: boolean;
40
- isGatewayFallback: boolean;
41
- sortOrder: number;
42
- isSystem: boolean;
43
- archivedAt: string | null;
44
- createdBy: string;
45
- updatedBy: string;
46
- createdAt: string;
47
- updatedAt: string;
48
- }
49
-
50
- /**
51
- * Pointer-count payload attached to archive responses. `prompts` is real
52
- * today; `rules` / `providers` are 0 in v1 (reserved for Wave 6+ specs to
53
- * keep the response shape stable when those domains land).
54
- */
55
- export interface LivePointerCounts {
56
- prompts: number;
57
- rules: number;
58
- providers: number;
59
- }
60
-
61
- /**
62
- * Response body for `POST /v1/projects/:projectId/environments/:slug/archive`.
63
- * Carries the soft-archived environment plus the count of live pointers
64
- * cascade-cleared by the archive operation (informational, for the
65
- * confirmation modal in the web UI).
66
- */
67
- export interface ArchiveProjectEnvironmentResult {
68
- environment: ProjectEnvironment;
69
- livePointerCounts: LivePointerCounts;
70
- }
9
+ export type LivePointerCounts = GenLivePointerCounts;
71
10
 
72
- // ─── Inputs ─────────────────────────────────────────────────────────────────
11
+ export type ArchiveProjectEnvironmentResult =
12
+ GenArchiveProjectEnvironmentResult;
73
13
 
74
14
  export interface ListProjectEnvironmentsInput {
75
15
  projectId: string;
76
- /**
77
- * When `true`, the response includes archived envs (those with a
78
- * non-null `archivedAt`). Default behavior excludes them.
79
- */
80
16
  includeArchived?: boolean;
81
17
  }
82
18
 
@@ -89,15 +25,6 @@ export interface GetProjectEnvironmentInput {
89
25
 
90
26
  export type GetProjectEnvironmentResult = ProjectEnvironment;
91
27
 
92
- /**
93
- * Create input — must specify a unique `slug` matching lowercase-kebab
94
- * (a-z, 0-9, hyphen, 1..63 chars). Reserved system slugs (`prod`,
95
- * `staging`) are rejected by the API with `ENVIRONMENT_SLUG_TAKEN`.
96
- *
97
- * Setting `isGatewayFallback: true` here additionally requires the
98
- * `project:manage_default_environment` capability; the create use case
99
- * atomically clears the flag on any other env in the same project.
100
- */
101
28
  export interface CreateProjectEnvironmentInput {
102
29
  projectId: string;
103
30
  slug: string;
@@ -113,15 +40,6 @@ export interface CreateProjectEnvironmentInput {
113
40
 
114
41
  export type CreateProjectEnvironmentResult = ProjectEnvironment;
115
42
 
116
- /**
117
- * PATCH input — every field is optional. `slug` rename triggers a 7-day
118
- * alias row server-side. System envs (`isSystem: true`) reject `slug`,
119
- * `isSystem`, and other immutable-field mutations with
120
- * `SYSTEM_ENV_IMMUTABLE`.
121
- *
122
- * As with `create`, mutating `isGatewayFallback` requires the additional
123
- * `project:manage_default_environment` capability.
124
- */
125
43
  export interface UpdateProjectEnvironmentInput {
126
44
  projectId: string;
127
45
  slug: string;
@@ -1,4 +1,4 @@
1
- import type { ResolvedPrompt } from "./types";
1
+ import type { FetchedPrompt, PromptContext, ResolvedPrompt } from "./types";
2
2
 
3
3
  export interface CacheConfig {
4
4
  /** Time-to-live in seconds. Default: 300 (5 minutes) */
@@ -7,8 +7,14 @@ export interface CacheConfig {
7
7
  maxEntries?: number;
8
8
  }
9
9
 
10
+ /**
11
+ * The cache stores either a legacy `resolve()` result or an enriched
12
+ * `fetch()` result. Both are JSON-cloneable; the cache is shape-agnostic.
13
+ */
14
+ export type CacheableResolved = ResolvedPrompt | FetchedPrompt;
15
+
10
16
  interface CacheEntry {
11
- value: ResolvedPrompt;
17
+ value: CacheableResolved;
12
18
  expiresAt: number;
13
19
  }
14
20
 
@@ -37,6 +43,35 @@ export function buildCacheKey(
37
43
  return `${JSON.stringify(projectId)}|${JSON.stringify(slug)}|${JSON.stringify(sortedLabels)}|${JSON.stringify(varsPart)}`;
38
44
  }
39
45
 
46
+ /**
47
+ * Build a cache key for an env-aware `fetch()` call.
48
+ *
49
+ * Folds in `env`, `context`, and `subject` ON TOP of the base
50
+ * project/slug/labels/variables key so two calls that differ only by
51
+ * env/context/subject do not return a stale cross-env / cross-cohort hit
52
+ * (spec §3 "Caching key changes"). The env slug is also forwarded as the
53
+ * single resolution label (env→label mapping, correction #3), so it appears
54
+ * in the `labels` segment too; including it explicitly here keeps the key
55
+ * unambiguous and self-documenting.
56
+ *
57
+ * `subject` participates in the key so a `subject`-keyed weighted assignment
58
+ * caches as the same deterministic variant on repeat (D8); the
59
+ * weighted-no-subject bypass is handled in the resource, not here.
60
+ */
61
+ export function buildFetchCacheKey(
62
+ projectId: string,
63
+ slug: string,
64
+ env: string,
65
+ context?: PromptContext,
66
+ subject?: string,
67
+ variables?: Record<string, unknown>,
68
+ ): string {
69
+ const base = buildCacheKey(projectId, slug, [env], variables);
70
+ const ctxPart =
71
+ context !== undefined ? JSON.stringify(sortObjectKeys(context)) : null;
72
+ return `fetch|${base}|${JSON.stringify(env)}|${JSON.stringify(ctxPart)}|${JSON.stringify(subject ?? null)}`;
73
+ }
74
+
40
75
  function sortObjectKeys(obj: unknown): unknown {
41
76
  if (obj === null || typeof obj !== "object") return obj;
42
77
  if (Array.isArray(obj)) return obj.map(sortObjectKeys);
@@ -65,7 +100,7 @@ export class PromptCache {
65
100
  this.store = new Map();
66
101
  }
67
102
 
68
- get(key: string): ResolvedPrompt | undefined {
103
+ get(key: string): CacheableResolved | undefined {
69
104
  const entry = this.store.get(key);
70
105
  if (!entry) return undefined;
71
106
 
@@ -81,7 +116,7 @@ export class PromptCache {
81
116
  return entry.value;
82
117
  }
83
118
 
84
- set(key: string, value: ResolvedPrompt): void {
119
+ set(key: string, value: CacheableResolved): void {
85
120
  // maxEntries: 0 (or negative) means caching is disabled — drop the entry.
86
121
  if (this.maxEntries <= 0) {
87
122
  return;
@@ -1,7 +1,9 @@
1
- import { To11aiNetworkError } from "../errors";
1
+ import { z } from "zod";
2
+ import { EnvNotFoundError, To11aiNetworkError } from "../errors";
3
+ import { zResolvedPrompt } from "../generated/zod.gen";
2
4
  import type { ApiEnvelope, Transport } from "../transport";
3
5
  import type { CacheConfig } from "./cache";
4
- import { buildCacheKey, PromptCache } from "./cache";
6
+ import { buildCacheKey, buildFetchCacheKey, PromptCache } from "./cache";
5
7
  import type {
6
8
  ArchivePromptInput,
7
9
  ArchivePromptResult,
@@ -13,6 +15,8 @@ import type {
13
15
  CreatePromptVersionResult,
14
16
  DeleteProjectPromptLabelInput,
15
17
  DeleteProjectPromptLabelResult,
18
+ FetchedPrompt,
19
+ FetchPromptOptions,
16
20
  GetProjectPromptLabelInput,
17
21
  GetProjectPromptLabelResult,
18
22
  GetPromptInput,
@@ -34,6 +38,10 @@ import type {
34
38
  MovePromptLabelInput,
35
39
  MovePromptLabelResult,
36
40
  PaginatedResult,
41
+ RenderedChatMessage,
42
+ RenderedPrompt,
43
+ ResolvedPrompt,
44
+ ResolvedPromptEnvelope,
37
45
  ResolvePromptInput,
38
46
  ResolvePromptResult,
39
47
  UpdateProjectPromptLabelInput,
@@ -41,12 +49,14 @@ import type {
41
49
  UpdatePromptInput,
42
50
  UpdatePromptResult,
43
51
  } from "./types";
52
+ import { ENV_SLUG_REGEX } from "./types";
44
53
 
45
54
  // Re-export CacheConfig so callers can import from this module
46
55
  export type { CacheConfig } from "./cache";
47
56
  export type {
48
57
  ArchivePromptInput,
49
58
  ArchivePromptResult,
59
+ BlockRenderRecord,
50
60
  ChatMessage,
51
61
  CreateProjectPromptLabelInput,
52
62
  CreateProjectPromptLabelResult,
@@ -56,6 +66,8 @@ export type {
56
66
  CreatePromptVersionResult,
57
67
  DeleteProjectPromptLabelInput,
58
68
  DeleteProjectPromptLabelResult,
69
+ FetchedPrompt,
70
+ FetchPromptOptions,
59
71
  GetProjectPromptLabelInput,
60
72
  GetProjectPromptLabelResult,
61
73
  GetPromptInput,
@@ -82,6 +94,7 @@ export type {
82
94
  ProjectPromptLabel,
83
95
  ProjectPromptLabelUsage,
84
96
  Prompt,
97
+ PromptContext,
85
98
  PromptFormat,
86
99
  PromptLabelEvent,
87
100
  PromptLabelPointer,
@@ -90,6 +103,7 @@ export type {
90
103
  RenderedChatMessage,
91
104
  RenderedPrompt,
92
105
  ResolvedPrompt,
106
+ ResolvedPromptEnvelope,
93
107
  ResolvePromptInput,
94
108
  ResolvePromptResult,
95
109
  RowError,
@@ -102,6 +116,19 @@ export type {
102
116
  UpdatePromptResult,
103
117
  } from "./types";
104
118
 
119
+ const zResolvedPromptLegacy = zResolvedPrompt.pick({
120
+ promptId: true,
121
+ versionId: true,
122
+ version: true,
123
+ labels: true,
124
+ contentHash: true,
125
+ rendered: true,
126
+ });
127
+
128
+ const zResolvedPromptEnvelope = zResolvedPrompt
129
+ .partial({ releaseId: true })
130
+ .extend({ strategy: z.enum(["single", "weighted"]).optional() });
131
+
105
132
  function unwrap<T>(envelope: ApiEnvelope<T>): T {
106
133
  if (envelope.data === null || envelope.data === undefined) {
107
134
  throw new Error("API returned success but data is null");
@@ -113,6 +140,75 @@ function getProjectPromptsPath(projectId: string): string {
113
140
  return `/v1/projects/${encodeURIComponent(projectId)}/prompts`;
114
141
  }
115
142
 
143
+ /**
144
+ * Project a server `rendered` payload into the `messages` array of the R5
145
+ * `FetchedPrompt` (D5):
146
+ * - `format: "chat"` → the message array directly.
147
+ * - `format: "text"` → a single synthesized `{ role: "user", content }`.
148
+ */
149
+ function renderedToMessages(rendered: RenderedPrompt): RenderedChatMessage[] {
150
+ if (rendered.format === "chat") {
151
+ return rendered.messages;
152
+ }
153
+ return [{ role: "user", content: rendered.text }];
154
+ }
155
+
156
+ /**
157
+ * Normalize the server resolve envelope into the enriched R5 `FetchedPrompt`.
158
+ *
159
+ * - `messages` is synthesized from `rendered` via the D5 projection.
160
+ * - `releaseId` is ALWAYS a key: a string when the server sends one, else
161
+ * explicit `null` (R5 `string | null`).
162
+ * - `variantName` is present IFF the server sent one (i.e. weighted release);
163
+ * absent for `single` — never `null`/`""`.
164
+ * - `blockRenderRecord` defaults to `{}` when the server omitted it.
165
+ */
166
+ function normalizeResolvedEnvelope(
167
+ raw: ResolvedPrompt | ResolvedPromptEnvelope,
168
+ ): FetchedPrompt {
169
+ const provenance = raw as ResolvedPromptEnvelope;
170
+ const result: FetchedPrompt = {
171
+ messages: renderedToMessages(raw.rendered),
172
+ version: raw.version,
173
+ releaseId: provenance.releaseId ?? null,
174
+ blockRenderRecord: provenance.blockRenderRecord ?? {},
175
+ promptId: raw.promptId,
176
+ versionId: raw.versionId,
177
+ contentHash: raw.contentHash,
178
+ labels: raw.labels,
179
+ rendered: raw.rendered,
180
+ };
181
+ // Set `variantName` only when it is a non-empty string, so the key is
182
+ // genuinely absent for `single`-strategy releases (R5: present IFF weighted —
183
+ // never `null`, never `""`). A `null`/`""` from the wire is normalized to
184
+ // absent, which also keeps the D8 `variantName !== undefined` weighted
185
+ // inference honest (Cursor).
186
+ if (
187
+ typeof provenance.variantName === "string" &&
188
+ provenance.variantName.length > 0
189
+ ) {
190
+ result.variantName = provenance.variantName;
191
+ }
192
+ return result;
193
+ }
194
+
195
+ /**
196
+ * Lift a caller-supplied offline `fallback` into the enriched `FetchedPrompt`
197
+ * shape (D7), so `fetch()` returns an identical shape on the online and
198
+ * offline paths. An already-enriched `FetchedPrompt` is returned as-is; a
199
+ * legacy `ResolvedPrompt` is lifted (messages synthesized; `releaseId` → its
200
+ * own value or explicit `null`; `blockRenderRecord` → `{}`; `variantName`
201
+ * stays absent).
202
+ */
203
+ function liftFallbackToFetched(
204
+ fallback: ResolvedPrompt | FetchedPrompt,
205
+ ): FetchedPrompt {
206
+ if ("messages" in fallback) {
207
+ return structuredClone(fallback);
208
+ }
209
+ return normalizeResolvedEnvelope(fallback);
210
+ }
211
+
116
212
  function getProjectPromptPath(projectId: string, promptId: string): string {
117
213
  return `${getProjectPromptsPath(projectId)}/${encodeURIComponent(promptId)}`;
118
214
  }
@@ -149,13 +245,35 @@ function buildLabelEventsQuery(input: ListLabelEventsInput): string {
149
245
  return params.toString();
150
246
  }
151
247
 
248
+ /**
249
+ * Init-time config for the env-aware `fetch()` surface (TO11-980 / S9).
250
+ *
251
+ * `projectId` and `env` come from `createClient`. `projectId` is required for
252
+ * `fetch()` (it builds the project-scoped resolve path); `env` is the default
253
+ * serving environment inherited by every `fetch(slug)` call.
254
+ */
255
+ export interface PromptResourceConfig {
256
+ /** Project the client is bound to. Required for `fetch()`; validated at createClient. */
257
+ projectId?: string;
258
+ /** Default serving env for `fetch()`. Validated against ENV_SLUG_REGEX at createClient. */
259
+ env?: string;
260
+ }
261
+
152
262
  export class PromptResource {
153
263
  private readonly transport: Transport;
154
264
  private readonly cache: PromptCache;
155
-
156
- constructor(transport: Transport, cacheConfig?: CacheConfig) {
265
+ private readonly projectId: string | undefined;
266
+ private readonly env: string | undefined;
267
+
268
+ constructor(
269
+ transport: Transport,
270
+ cacheConfig?: CacheConfig,
271
+ config?: PromptResourceConfig,
272
+ ) {
157
273
  this.transport = transport;
158
274
  this.cache = new PromptCache(cacheConfig);
275
+ this.projectId = config?.projectId;
276
+ this.env = config?.env;
159
277
  }
160
278
 
161
279
  // ─── Create ──────────────────────────────────────────────────────────────
@@ -303,8 +421,138 @@ export class PromptResource {
303
421
  return unwrap(envelope);
304
422
  }
305
423
 
424
+ // ─── Fetch (env-aware resolution, TO11-980 / S9) ──────────────────────────
425
+
426
+ /**
427
+ * Resolve a prompt for the client's configured `projectId` + `env`
428
+ * (Style-A). The ergonomic slug-first surface: env is configured once at
429
+ * `createClient` and inherited here, with an optional per-call override.
430
+ *
431
+ * - **env→label wire mapping (correction #3):** the resolve route is
432
+ * label-based (`/v1/projects/:projectId/prompts:resolve`, body
433
+ * `labels[]`); it does NOT accept a top-level `env` field. The resolved
434
+ * env slug (lowercase-kebab — a valid label name) is therefore sent as
435
+ * the single resolution label: body `{ slug, labels: [env], context?,
436
+ * subject?, variables? }`.
437
+ * - **No silent fallback (R4):** if no env is resolvable (none at init AND
438
+ * none per call) this throws `EnvNotFoundError` (`reason.source =
439
+ * "missing"`) CLIENT-side, eagerly, before any transport call.
440
+ * - **Cache (D8):** caches `single`-strategy releases and weighted releases
441
+ * WITH a `subject`; BYPASSES the cache for weighted releases with NO
442
+ * `subject` (a uniform-random assignment must not be pinned for the TTL).
443
+ *
444
+ * Prefer this over the deprecated `resolve()`.
445
+ */
446
+ async fetch(
447
+ slug: string,
448
+ options: FetchPromptOptions = {},
449
+ ): Promise<FetchedPrompt> {
450
+ if (this.projectId === undefined || this.projectId.trim() === "") {
451
+ throw new Error(
452
+ "[to11ai/sdk] `prompts.fetch()` requires a `projectId` on the client — pass it to createClient({ projectId, ... }).",
453
+ );
454
+ }
455
+
456
+ // R2 precedence: per-call env > init-time env. No third source.
457
+ const env = options.env ?? this.env;
458
+
459
+ // R4 / AC4: no env resolvable → throw eagerly, ZERO transport calls.
460
+ if (env === undefined) {
461
+ throw EnvNotFoundError.missing();
462
+ }
463
+
464
+ // Validate the RESOLVED env (per-call OR init). createClient validates the
465
+ // init-time env, but PromptResource has a public constructor that can
466
+ // bypass createClient, so re-validate here so a malformed env (e.g.
467
+ // "Prod") never reaches the wire as a resolution label (CodeRabbit).
468
+ if (!ENV_SLUG_REGEX.test(env)) {
469
+ throw new Error(
470
+ `[to11ai/sdk] \`env\` must match ${ENV_SLUG_REGEX} (lowercase-kebab, 1-63 chars). Received: ${JSON.stringify(env)}`,
471
+ );
472
+ }
473
+
474
+ const {
475
+ context,
476
+ subject,
477
+ variables,
478
+ cache: useCache = true,
479
+ fallback,
480
+ } = options;
481
+
482
+ const cacheKey = buildFetchCacheKey(
483
+ this.projectId,
484
+ slug,
485
+ env,
486
+ context,
487
+ subject,
488
+ variables,
489
+ );
490
+
491
+ if (useCache) {
492
+ const cached = this.cache.get(cacheKey);
493
+ // Only enriched (fetch) entries are valid hits here; the fetch key
494
+ // namespace ("fetch|…") never collides with the legacy resolve key.
495
+ if (cached && "messages" in cached) return structuredClone(cached);
496
+ }
497
+
498
+ // env→label mapping: the env slug is the single resolution label.
499
+ const body: Record<string, unknown> = { slug, labels: [env] };
500
+ if (context !== undefined) body.context = context;
501
+ if (subject !== undefined) body.subject = subject;
502
+ if (variables !== undefined) body.variables = variables;
503
+
504
+ try {
505
+ const envelope = await this.transport.request<ResolvedPromptEnvelope>(
506
+ "POST",
507
+ `${getProjectPromptsPath(this.projectId)}:resolve`,
508
+ {
509
+ body,
510
+ decode: (data) =>
511
+ zResolvedPromptEnvelope.parse(data) as ResolvedPromptEnvelope,
512
+ },
513
+ );
514
+ const raw = unwrap(envelope);
515
+ const result = normalizeResolvedEnvelope(raw);
516
+
517
+ // D8: bypass the cache for a weighted release with NO subject —
518
+ // caching a uniform-random assignment would pin one variant for the
519
+ // whole TTL. Cache `single` releases and `subject`-keyed weighted
520
+ // releases (deterministic, safe to cache). Weighted-ness is inferred
521
+ // from `strategy === "weighted"` OR the presence of `variantName`
522
+ // (which the server stamps iff weighted) — so a result that carries a
523
+ // variant but omits `strategy` is still correctly bypassed rather than
524
+ // pinning one random variant for the TTL (Cursor / Greptile).
525
+ const isWeighted =
526
+ raw.strategy === "weighted" || result.variantName !== undefined;
527
+ const cacheable = !(isWeighted && subject === undefined);
528
+ if (useCache && cacheable) {
529
+ this.cache.set(cacheKey, structuredClone(result));
530
+ }
531
+
532
+ return result;
533
+ } catch (err) {
534
+ // D7: offline fallback is consulted ONLY on a network outage with an
535
+ // empty cache. A server `ENV_NOT_FOUND` is a typed application error
536
+ // (not a To11aiNetworkError), so it bypasses this branch. R4's
537
+ // client-eager throw already happened above the try, so there is no
538
+ // network error to trigger it on the env-missing path.
539
+ if (err instanceof To11aiNetworkError && fallback !== undefined) {
540
+ return liftFallbackToFetched(fallback);
541
+ }
542
+ throw err;
543
+ }
544
+ }
545
+
306
546
  // ─── Resolve ──────────────────────────────────────────────────────────────
307
547
 
548
+ /**
549
+ * @deprecated Use {@link PromptResource.fetch | `prompts.fetch(slug,
550
+ * options?)`} instead. `resolve()` is env-agnostic (no `env` / `context` /
551
+ * `subject`, no enriched release/variant provenance, no typed-env errors)
552
+ * and is retained only for back-compat. It is NOT subject to the R4
553
+ * no-silent-fallback env gate (it has no env concept). New code should
554
+ * configure `projectId` + `env` at `createClient` and call `fetch()`.
555
+ */
308
556
  async resolve(input: ResolvePromptInput): Promise<ResolvePromptResult> {
309
557
  const {
310
558
  projectId,
@@ -319,7 +567,10 @@ export class PromptResource {
319
567
 
320
568
  if (useCache) {
321
569
  const cached = this.cache.get(cacheKey);
322
- if (cached) return structuredClone(cached);
570
+ // The legacy resolve key namespace only ever stores ResolvedPrompt
571
+ // entries (the fetch namespace is "fetch|…"); the `messages` guard
572
+ // keeps the static return type as ResolvedPrompt.
573
+ if (cached && !("messages" in cached)) return structuredClone(cached);
323
574
  }
324
575
 
325
576
  try {
@@ -328,6 +579,8 @@ export class PromptResource {
328
579
  `${getProjectPromptsPath(projectId)}:resolve`,
329
580
  {
330
581
  body: { slug, labels, variables },
582
+ decode: (data) =>
583
+ zResolvedPromptLegacy.parse(data) as ResolvePromptResult,
331
584
  },
332
585
  );
333
586
  const result = unwrap(envelope);