@to11ai/sdk 0.1.2 → 0.3.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.
- package/dist/behaviors/store.d.ts +62 -0
- package/dist/client.cjs +37 -4
- package/dist/client.d.ts +41 -2
- package/dist/client.js +45 -4
- package/dist/errors.cjs +90 -1
- package/dist/errors.d.ts +142 -0
- package/dist/errors.js +90 -1
- package/dist/gateway.cjs +12 -0
- package/dist/gateway.d.ts +26 -12
- package/dist/gateway.js +14 -0
- package/dist/generated/index.d.ts +1 -0
- package/dist/generated/types.gen.d.ts +22978 -0
- package/dist/generated/zod.gen.d.ts +13543 -0
- package/dist/projectEnvironments/resource.cjs +7 -2
- package/dist/projectEnvironments/resource.js +7 -2
- package/dist/projectEnvironments/types.d.ts +4 -75
- package/dist/prompts/cache.d.ts +24 -3
- package/dist/prompts/resource.cjs +101 -3
- package/dist/prompts/resource.d.ts +47 -3
- package/dist/prompts/resource.js +103 -5
- package/dist/prompts/types.d.ts +36 -177
- package/dist/routingRules/resource.cjs +9 -2
- package/dist/routingRules/resource.js +9 -2
- package/dist/routingRules/types.d.ts +12 -123
- package/dist/transport.d.ts +23 -0
- package/package.json +23 -12
- package/src/behaviors/store.ts +175 -0
- package/src/client.ts +115 -3
- package/src/errors.ts +261 -0
- package/src/gateway.ts +54 -12
- package/src/generated/index.ts +3 -0
- package/src/generated/types.gen.ts +24087 -0
- package/src/generated/zod.gen.ts +12660 -0
- package/src/projectEnvironments/resource.ts +10 -0
- package/src/projectEnvironments/types.ts +9 -91
- package/src/prompts/cache.ts +39 -4
- package/src/prompts/resource.ts +258 -5
- package/src/prompts/types.ts +56 -237
- package/src/routingRules/resource.ts +10 -1
- package/src/routingRules/types.ts +21 -155
- package/src/traceparent.ts +69 -4
- 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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|
package/src/prompts/cache.ts
CHANGED
|
@@ -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:
|
|
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):
|
|
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:
|
|
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;
|
package/src/prompts/resource.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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);
|