@to11ai/sdk 0.1.1 → 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
@@ -19,6 +19,16 @@
19
19
  * @see docs/product-specs/2026-05-20-to11-1016-sdk-traceparent-propagation.md
20
20
  */
21
21
 
22
+ // NOTE: deliberately NO top-level `import ... from "node:module"`. With the
23
+ // SDK's `--external '*'` build that import is emitted verbatim into the ESM
24
+ // `dist/client.js`, and a static `node:module` import is resolved at module
25
+ // initialization — *before* any try/catch in `resolveRequire()` can run. In
26
+ // edge runtimes that lack `node:module` (Cloudflare Workers without
27
+ // `nodejs_compat`, bare Service Workers) that is a hard module-load failure,
28
+ // not the spec-R2 graceful no-op. We instead reach `node:module` lazily and
29
+ // optionally via `process.getBuiltinModule`, which is absent (not throwing) in
30
+ // those runtimes.
31
+
22
32
  // Minimal structural typing of the OTel API surface we actually use. Keeping
23
33
  // this local — rather than importing types from `@opentelemetry/api` — is
24
34
  // what keeps the SDK zero-dep at the TypeScript layer too. Source types:
@@ -52,16 +62,71 @@ let loader: OtelLoader = defaultLoader;
52
62
 
53
63
  function defaultLoader(): OtelApi | null {
54
64
  try {
55
- // biome-ignore lint/suspicious/noExplicitAny: dynamic require shim — see header comment
56
- const g = globalThis as any;
57
- // biome-ignore lint/security/noGlobalEval: runtime-detected optional peer dep; bundlers cannot statically resolve
58
- const r = (g.require ?? eval("require")) as (id: string) => unknown;
65
+ const r = resolveRequire();
66
+ if (r === null) return null;
59
67
  return r("@opentelemetry/api") as OtelApi;
60
68
  } catch {
61
69
  return null;
62
70
  }
63
71
  }
64
72
 
73
+ /**
74
+ * Resolve a CommonJS `require` usable for optionally loading `@opentelemetry/api`,
75
+ * across every runtime this package ships to. Order matters — it is the whole
76
+ * point of the TO11-1380 fix:
77
+ *
78
+ * 1. `globalThis.require` — present in some bundled / global contexts.
79
+ * 2. `eval("require")` — resolves the module-local `require` in the
80
+ * `dist/client.cjs` (CommonJS) build. In a native Node ESM module this
81
+ * throws `ReferenceError: require is not defined`, so we fall through.
82
+ * 3. `process.getBuiltinModule("node:module").createRequire(import.meta.url)`
83
+ * — the ESM-correct path. The published `dist/client.js` is loaded by Node
84
+ * via `import`, where `require` is not defined and not on `globalThis`. The
85
+ * previous loader stopped at step 2, cached `null` forever, and made
86
+ * `activeTraceContext()` a permanent no-op for the primary import build
87
+ * (TO11-1380). `import.meta` is only referenced here, in the ESM branch, so
88
+ * the CJS build never evaluates it.
89
+ *
90
+ * `node:module` is reached lazily via `process.getBuiltinModule` (not a static
91
+ * `import`) precisely so non-Node runtimes never resolve the builtin at module
92
+ * load. In a browser / edge runtime (Cloudflare Workers, Vercel Edge)
93
+ * `globalThis.require` is absent, `eval("require")` throws, and
94
+ * `process.getBuiltinModule` is absent — every step degrades to a strict no-op,
95
+ * preserving the zero-dep edge behavior (spec R2). `process.getBuiltinModule`
96
+ * is available on Node ≥ 20.16 / ≥ 22.3; older Node ESM (no global `require`,
97
+ * no `getBuiltinModule`) degrades to no-op rather than crashing.
98
+ */
99
+ function resolveRequire(): ((id: string) => unknown) | null {
100
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic require shim — see header comment
101
+ const g = globalThis as any;
102
+ if (typeof g.require === "function") {
103
+ return g.require as (id: string) => unknown;
104
+ }
105
+ try {
106
+ // biome-ignore lint/security/noGlobalEval: CJS build only; throws in native ESM and we fall through.
107
+ const cjsRequire = eval("require") as (id: string) => unknown;
108
+ if (typeof cjsRequire === "function") return cjsRequire;
109
+ } catch {
110
+ // Not a CommonJS context (native ESM or edge runtime). Try ESM next.
111
+ }
112
+ try {
113
+ // `getBuiltinModule` is optional: absent (returns undefined / not a
114
+ // function) in edge runtimes, so this never throws at module load.
115
+ const getBuiltinModule = g.process?.getBuiltinModule as
116
+ | ((id: string) => unknown)
117
+ | undefined;
118
+ if (typeof getBuiltinModule !== "function") return null;
119
+ const nodeModule = getBuiltinModule("node:module") as {
120
+ createRequire?: (path: string | URL) => (id: string) => unknown;
121
+ };
122
+ if (typeof nodeModule?.createRequire !== "function") return null;
123
+ return nodeModule.createRequire(import.meta.url);
124
+ } catch {
125
+ // No usable `node:module` (edge runtime) — strict no-op.
126
+ return null;
127
+ }
128
+ }
129
+
65
130
  /**
66
131
  * Return the W3C `traceparent` (and optional `tracestate`) string pair for
67
132
  * the currently active OpenTelemetry span in the host process, or `null` if
package/src/transport.ts CHANGED
@@ -1,13 +1,20 @@
1
+ import type { BehaviorStore } from "./behaviors/store";
1
2
  import {
3
+ AmbiguousEnvLabelsError,
2
4
  EnvNotEnabledForProviderError,
5
+ EnvNotFoundError,
6
+ NoActiveReleaseError,
3
7
  NoApplicableRouteError,
4
8
  PromptLabelNotRegisteredError,
9
+ PromptNotFoundError,
10
+ PromptPolicyViolationError,
5
11
  RuleNotFoundError,
6
12
  RuleVersionInvalidSnapshotError,
7
13
  To11aiApiError,
8
14
  To11aiAuthError,
9
15
  To11aiNetworkError,
10
16
  To11aiRateLimitError,
17
+ To11aiResponseValidationError,
11
18
  To11aiValidationError,
12
19
  } from "./errors";
13
20
  import { activeTraceContext } from "./traceparent";
@@ -28,6 +35,111 @@ const RULE_NOT_FOUND_DETAILS_ERRORS = new Set([
28
35
  "rule_not_found_in_env",
29
36
  ]);
30
37
 
38
+ /**
39
+ * TO11-980 (S9) prompt-resolution canonical error codes.
40
+ *
41
+ * WIRE NOTE: the server (TO11-1925/1926) emits the generic HTTP class
42
+ * (`not_found` / `conflict` / `unprocessable_entity`) at the envelope's
43
+ * top-level `error.code`, and the canonical SCREAMING_SNAKE code below in
44
+ * `error.details.code` (matching the existing `PROMPT_NOT_FOUND` convention).
45
+ * The four prompt-resolve branches in `mapHttpError` therefore read
46
+ * `details.code`, NOT top-level `errorCode`.
47
+ *
48
+ * Two codes fold into `PromptNotFoundError`: `PROMPT_NOT_FOUND` (no prompt)
49
+ * and `PROMPT_LABELS_NOT_FOUND` (no version matched the env-label).
50
+ */
51
+ const PROMPT_NOT_FOUND_DETAILS_CODES = new Set([
52
+ "PROMPT_NOT_FOUND",
53
+ "PROMPT_LABELS_NOT_FOUND",
54
+ ]);
55
+
56
+ /**
57
+ * Prompt-resolution policy violation codes (S5 / TO11-976). The generic
58
+ * `POLICY_VIOLATION` plus the concrete `ENV_POLICY_*` family the server
59
+ * emits. Any of these maps to `PromptPolicyViolationError`. This set is
60
+ * matched on `details.code` BEFORE the generic `401 || 403 → auth` guard so a
61
+ * 403 policy violation is not swallowed as an auth error (D3, AC6).
62
+ */
63
+ const PROMPT_POLICY_VIOLATION_DETAILS_CODES = new Set([
64
+ "POLICY_VIOLATION",
65
+ "ENV_POLICY_REQUIRED_SOURCE_ENV",
66
+ "ENV_POLICY_MINIMUM_DWELL_TIME",
67
+ "ENV_POLICY_NO_ROLLBACK_IN_SOURCE",
68
+ "ENV_POLICY_REQUIRES_APPROVAL",
69
+ "ENV_POLICY_SEPARATION_OF_DUTIES",
70
+ "ENV_POLICY_SOURCE_ENV_UNAVAILABLE",
71
+ "ENV_POLICY_ENV_NOT_FOUND",
72
+ "ENV_POLICY_INVALID_CONFIG",
73
+ ]);
74
+
75
+ /**
76
+ * Map a prompt-resolution envelope (keyed on `details.code`) to a typed S9
77
+ * subclass, or `undefined` when the code is not one of the four S9 families.
78
+ *
79
+ * Returning `undefined` lets the caller fall through to the existing
80
+ * status-keyed dispatch (so unmapped codes stay base `To11aiApiError`, per
81
+ * D3). This function reads ONLY `details.code` — never top-level `error.code`
82
+ * — because the server puts the canonical code in `details.code` (see above).
83
+ */
84
+ function mapPromptResolveError(
85
+ status: number,
86
+ message: string,
87
+ details: unknown,
88
+ ): To11aiApiError | undefined {
89
+ const d = (details && typeof details === "object" ? details : {}) as Record<
90
+ string,
91
+ unknown
92
+ >;
93
+ const detailsCode = typeof d.code === "string" ? d.code : undefined;
94
+ if (detailsCode === undefined) return undefined;
95
+
96
+ const slug = typeof d.slug === "string" ? d.slug : undefined;
97
+ const projectId = typeof d.projectId === "string" ? d.projectId : undefined;
98
+ const env = typeof d.env === "string" ? d.env : undefined;
99
+ const promptId = typeof d.promptId === "string" ? d.promptId : undefined;
100
+
101
+ if (PROMPT_NOT_FOUND_DETAILS_CODES.has(detailsCode)) {
102
+ return new PromptNotFoundError(
103
+ message,
104
+ detailsCode,
105
+ { slug, projectId },
106
+ details,
107
+ status,
108
+ );
109
+ }
110
+ if (detailsCode === "ENV_NOT_FOUND") {
111
+ // Server-side unknown-env. (The client-eager "missing" variant is
112
+ // raised in resource.ts before any request — never here.)
113
+ return new EnvNotFoundError(
114
+ message,
115
+ { env, source: "unknown" },
116
+ details,
117
+ status,
118
+ );
119
+ }
120
+ if (detailsCode === "NO_ACTIVE_RELEASE") {
121
+ return new NoActiveReleaseError(
122
+ message,
123
+ { slug, promptId, env },
124
+ details,
125
+ status,
126
+ );
127
+ }
128
+ if (detailsCode === "AMBIGUOUS_ENV_LABELS") {
129
+ return new AmbiguousEnvLabelsError(message, { env, slug }, details, status);
130
+ }
131
+ if (PROMPT_POLICY_VIOLATION_DETAILS_CODES.has(detailsCode)) {
132
+ const policy = typeof d.policy === "string" ? d.policy : undefined;
133
+ return new PromptPolicyViolationError(
134
+ message,
135
+ { code: detailsCode, policy, detail: d.detail },
136
+ details,
137
+ status,
138
+ );
139
+ }
140
+ return undefined;
141
+ }
142
+
31
143
  export type AuthConfig =
32
144
  | { readonly apiKey: string; readonly getAccessToken?: never }
33
145
  | { readonly getAccessToken: () => Promise<string>; readonly apiKey?: never };
@@ -44,6 +156,12 @@ export interface TransportConfig {
44
156
  * attempt. Default: 3
45
157
  */
46
158
  readonly maxRetries?: number;
159
+ /**
160
+ * Optional behavior store updated from each successful response's
161
+ * `sdkBehavior` field (TO11-2100). When omitted, server-delivered behavior
162
+ * values are simply not captured.
163
+ */
164
+ readonly behaviorStore?: BehaviorStore;
47
165
  }
48
166
 
49
167
  /** Standard API envelope returned by the to11ai server. */
@@ -58,6 +176,15 @@ export interface ApiEnvelope<T = unknown> {
58
176
  readonly total: number;
59
177
  readonly totalPages: number;
60
178
  } | null;
179
+ /**
180
+ * Optional, additively-versioned server-evaluated SDK-behavior values
181
+ * (TO11-2100). Present only when the server has behavior flags to deliver;
182
+ * an unknown `v` is ignored by the SDK. See `behaviors/store.ts`.
183
+ */
184
+ readonly sdkBehavior?: {
185
+ readonly v: number;
186
+ readonly flags: Record<string, unknown>;
187
+ };
61
188
  }
62
189
 
63
190
  const DEFAULT_TIMEOUT_MS = 30_000;
@@ -99,6 +226,18 @@ function mapHttpError(
99
226
  const errorCode = envelope?.code;
100
227
  const details = envelope?.details;
101
228
 
229
+ // TO11-980 (S9): prompt-resolution typed errors are keyed on
230
+ // `details.code` (the canonical SCREAMING_SNAKE code), NOT top-level
231
+ // `error.code`. This MUST run BEFORE the generic `401 || 403 → auth`
232
+ // guard below so a 403 `POLICY_VIOLATION` is mapped to
233
+ // `PromptPolicyViolationError` rather than swallowed as `To11aiAuthError`
234
+ // (D3 branch-ordering, AC6). Unmapped `details.code` values return
235
+ // `undefined` and fall through to the existing status-keyed dispatch.
236
+ const promptResolveError = mapPromptResolveError(status, message, details);
237
+ if (promptResolveError) {
238
+ return promptResolveError;
239
+ }
240
+
102
241
  if (status === 401 || status === 403) {
103
242
  return new To11aiAuthError(
104
243
  message,
@@ -253,12 +392,14 @@ export class Transport {
253
392
  private readonly auth: AuthConfig;
254
393
  private readonly timeoutMs: number;
255
394
  private readonly maxRetries: number;
395
+ private readonly behaviorStore: BehaviorStore | undefined;
256
396
 
257
397
  constructor(config: TransportConfig) {
258
398
  this.baseUrl = config.baseUrl.replace(/\/$/, "");
259
399
  this.auth = config.auth;
260
400
  this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
261
401
  this.maxRetries = config.maxRetries ?? DEFAULT_MAX_RETRIES;
402
+ this.behaviorStore = config.behaviorStore;
262
403
  }
263
404
 
264
405
  private async buildHeaders(): Promise<Record<string, string>> {
@@ -289,6 +430,12 @@ export class Transport {
289
430
  options?: {
290
431
  query?: Record<string, string | number | boolean | undefined>;
291
432
  body?: unknown;
433
+ /**
434
+ * Runtime validator for the envelope `data` on a 2xx response. When
435
+ * supplied, the decoded value replaces `data`; a thrown decoder error
436
+ * surfaces as `To11aiResponseValidationError`.
437
+ */
438
+ decode?: (data: unknown) => T;
292
439
  },
293
440
  ): Promise<ApiEnvelope<T>> {
294
441
  const url = new URL(this.baseUrl + path);
@@ -320,7 +467,15 @@ export class Transport {
320
467
  signal: controller.signal,
321
468
  });
322
469
 
323
- clearTimeout(timeoutId);
470
+ // NOTE: the timeout is intentionally NOT cleared here. A server can
471
+ // send headers promptly (resolving `fetch`) but then stall the
472
+ // response body forever. The timeout must stay armed across
473
+ // `response.text()` and JSON parsing so a stalled body still aborts
474
+ // after `timeoutMs`; an abort mid-body surfaces as an AbortError that
475
+ // the catch block maps to `To11aiNetworkError`. Cleanup lives in the
476
+ // `finally` below so every exit path (success, retry `continue`,
477
+ // throw) clears the timer exactly once before the next attempt. See
478
+ // TO11-1402.
324
479
 
325
480
  // 5xx — retry only if the method is idempotent (RFC 9110 §9.2.2).
326
481
  // POST/PATCH may have already committed server-side, so replaying
@@ -361,10 +516,32 @@ export class Transport {
361
516
  );
362
517
  }
363
518
 
519
+ // Capture any server-delivered SDK-behavior values from the
520
+ // success envelope (TO11-2100). Behavior is delivered on success
521
+ // responses only — gate on the envelope's `success` flag, not just
522
+ // the 2xx status, so an unusual 2xx-with-`success:false` body does
523
+ // not update the store. An absent field leaves prior values intact.
524
+ // Done before any caller-supplied decode so the store reflects the
525
+ // raw envelope regardless of how the data is post-processed.
526
+ if (json.success === true) {
527
+ this.behaviorStore?.updateFromEnvelope(json.sdkBehavior);
528
+ }
529
+
530
+ if (options?.decode && json.data !== null && json.data !== undefined) {
531
+ try {
532
+ const decoded = options.decode(json.data);
533
+ return { ...json, data: decoded };
534
+ } catch (validationErr) {
535
+ throw new To11aiResponseValidationError(
536
+ path,
537
+ validationErr,
538
+ validationErr,
539
+ );
540
+ }
541
+ }
542
+
364
543
  return json;
365
544
  } catch (err) {
366
- clearTimeout(timeoutId);
367
-
368
545
  // Re-throw typed SDK errors immediately (no retry)
369
546
  if (err instanceof To11aiApiError) {
370
547
  throw err;
@@ -403,6 +580,14 @@ export class Transport {
403
580
 
404
581
  // Re-throw unexpected errors (e.g. auth callback failures) as-is
405
582
  throw err;
583
+ } finally {
584
+ // Clear the per-attempt timeout exactly once on every exit path:
585
+ // success (`return`), retry (`continue`), or throw. Running here —
586
+ // rather than after `fetch` resolves — keeps the timer armed across
587
+ // the response-body read so a stalled body still aborts (TO11-1402),
588
+ // while still freeing the timer before the loop starts a fresh
589
+ // attempt with a new AbortController.
590
+ clearTimeout(timeoutId);
406
591
  }
407
592
  }
408
593
  }