@copilotkit/shared 1.69.3 → 1.70.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.
Files changed (63) hide show
  1. package/dist/index.cjs +31 -13
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.d.cts +19 -17
  4. package/dist/index.d.cts.map +1 -1
  5. package/dist/index.d.mts +19 -17
  6. package/dist/index.d.mts.map +1 -1
  7. package/dist/index.mjs +29 -15
  8. package/dist/index.mjs.map +1 -1
  9. package/dist/index.umd.js +162 -34
  10. package/dist/index.umd.js.map +1 -1
  11. package/dist/package.cjs +1 -1
  12. package/dist/package.mjs +1 -1
  13. package/dist/telemetry/index.d.mts +3 -2
  14. package/dist/telemetry/lambda-client.cjs +25 -4
  15. package/dist/telemetry/lambda-client.cjs.map +1 -1
  16. package/dist/telemetry/lambda-client.d.cts +14 -1
  17. package/dist/telemetry/lambda-client.d.cts.map +1 -1
  18. package/dist/telemetry/lambda-client.d.mts +14 -1
  19. package/dist/telemetry/lambda-client.d.mts.map +1 -1
  20. package/dist/telemetry/lambda-client.mjs +25 -5
  21. package/dist/telemetry/lambda-client.mjs.map +1 -1
  22. package/dist/telemetry/sampling.cjs +28 -0
  23. package/dist/telemetry/sampling.cjs.map +1 -0
  24. package/dist/telemetry/sampling.d.cts +37 -0
  25. package/dist/telemetry/sampling.d.cts.map +1 -0
  26. package/dist/telemetry/sampling.d.mts +37 -0
  27. package/dist/telemetry/sampling.d.mts.map +1 -0
  28. package/dist/telemetry/sampling.mjs +25 -0
  29. package/dist/telemetry/sampling.mjs.map +1 -0
  30. package/dist/telemetry/telemetry-client.cjs +72 -11
  31. package/dist/telemetry/telemetry-client.cjs.map +1 -1
  32. package/dist/telemetry/telemetry-client.d.cts +43 -1
  33. package/dist/telemetry/telemetry-client.d.cts.map +1 -1
  34. package/dist/telemetry/telemetry-client.d.mts +43 -1
  35. package/dist/telemetry/telemetry-client.d.mts.map +1 -1
  36. package/dist/telemetry/telemetry-client.mjs +73 -12
  37. package/dist/telemetry/telemetry-client.mjs.map +1 -1
  38. package/dist/utils/console-styling.cjs +3 -3
  39. package/dist/utils/console-styling.cjs.map +1 -1
  40. package/dist/utils/console-styling.mjs +3 -3
  41. package/dist/utils/console-styling.mjs.map +1 -1
  42. package/dist/utils/index.d.cts +1 -1
  43. package/dist/utils/index.d.mts +1 -1
  44. package/dist/utils/types.cjs.map +1 -1
  45. package/dist/utils/types.d.cts +46 -1
  46. package/dist/utils/types.d.cts.map +1 -1
  47. package/dist/utils/types.d.mts +46 -1
  48. package/dist/utils/types.d.mts.map +1 -1
  49. package/dist/utils/types.mjs.map +1 -1
  50. package/package.json +2 -2
  51. package/src/__tests__/license-context.test.ts +224 -25
  52. package/src/index.ts +72 -16
  53. package/src/telemetry/index.ts +2 -0
  54. package/src/telemetry/lambda-client.test.ts +336 -1
  55. package/src/telemetry/lambda-client.ts +56 -15
  56. package/src/telemetry/sampling.test.ts +65 -0
  57. package/src/telemetry/sampling.ts +70 -0
  58. package/src/telemetry/telemetry-blank-license-identity.test.ts +121 -0
  59. package/src/telemetry/telemetry-client.test.ts +438 -15
  60. package/src/telemetry/telemetry-client.ts +145 -30
  61. package/src/utils/__tests__/conditions.test.ts +161 -0
  62. package/src/utils/console-styling.ts +3 -3
  63. package/src/utils/types.ts +52 -0
@@ -2,7 +2,12 @@ import { Analytics } from "@segment/analytics-node";
2
2
  import type { AnalyticsEvents } from "./events";
3
3
  import { flattenObject } from "./utils";
4
4
  import { v4 as uuidv4 } from "uuid";
5
- import { lambdaClient, parseAndWarnTelemetryId } from "./lambda-client";
5
+ import {
6
+ firstNonBlankTelemetryId,
7
+ lambdaClient,
8
+ parseAndWarnTelemetryId,
9
+ } from "./lambda-client";
10
+ import { computeSamplingMeta, TELEMETRY_EMITTER_V1 } from "./sampling";
6
11
 
7
12
  /**
8
13
  * Checks if telemetry is disabled via environment variables.
@@ -22,6 +27,26 @@ export function isTelemetryDisabled(): boolean {
22
27
  );
23
28
  }
24
29
 
30
+ /** Transport identity and sampling authority resolved for one runtime. */
31
+ export interface TelemetryIdentity {
32
+ telemetryId?: string;
33
+ licenseToken?: string;
34
+ }
35
+
36
+ /** Capture-only telemetry client bound to one runtime identity. */
37
+ export interface TelemetryCapture {
38
+ capture<K extends keyof AnalyticsEvents>(
39
+ event: K,
40
+ properties: AnalyticsEvents[K],
41
+ ): Promise<void>;
42
+ }
43
+
44
+ interface ResolvedTelemetryIdentity {
45
+ telemetryId: string | null;
46
+ licenseToken: string | null;
47
+ licenseTelemetryId: string | null;
48
+ }
49
+
25
50
  export class TelemetryClient {
26
51
  segment: Analytics | undefined;
27
52
  globalProperties: Record<string, any> = {};
@@ -30,11 +55,13 @@ export class TelemetryClient {
30
55
  // client decodes its payload to extract telemetry_id. Customer API
31
56
  // keys are NOT used here — they flow only into Segment.
32
57
  private licenseToken: string | null = null;
33
- // Parsed telemetry_id from the license-token JWT payload. Cached at
34
- // setLicenseToken time so `capture()` can branch on identified vs
35
- // anonymous without re-parsing per event. Null when the token is
36
- // absent or yielded no telemetry_id.
58
+ // Standalone analytics identity. This stays separate from the effective
59
+ // identity so legacy callers continue sending only their license token to
60
+ // the Lambda transport.
37
61
  private telemetryId: string | null = null;
62
+ // License-derived identity used only as sampling authority. A standalone
63
+ // telemetry id remains a transport claim and does not bypass sampleRate.
64
+ private licenseTelemetryId: string | null = null;
38
65
  packageName: string;
39
66
  packageVersion: string;
40
67
  private telemetryDisabled: boolean = false;
@@ -91,35 +118,55 @@ export class TelemetryClient {
91
118
  async capture<K extends keyof AnalyticsEvents>(
92
119
  event: K,
93
120
  properties: AnalyticsEvents[K],
94
- ) {
121
+ ): Promise<void> {
122
+ return this.captureWithIdentity(event, properties, {
123
+ telemetryId: this.telemetryId,
124
+ licenseToken: this.licenseToken,
125
+ licenseTelemetryId: this.licenseTelemetryId,
126
+ });
127
+ }
128
+
129
+ private async captureWithIdentity<K extends keyof AnalyticsEvents>(
130
+ event: K,
131
+ properties: AnalyticsEvents[K],
132
+ identity: ResolvedTelemetryIdentity,
133
+ ): Promise<void> {
95
134
  if (this.telemetryDisabled) {
96
135
  return;
97
136
  }
98
137
 
99
- // Anonymous callers (no telemetry_id) are gated by sampleRate.
100
- // Identified callers (license token with telemetry_id) always send —
138
+ // Callers without license-derived sampling authority are gated by
139
+ // sampleRate. Legacy license tokens with telemetry_id always send —
101
140
  // the volume is bounded by paying-customer count and full fidelity
102
141
  // per identified customer is worth the marginal cost.
103
- if (!this.telemetryId && !this.shouldSendEvent()) {
142
+ if (!identity.licenseTelemetryId && !this.shouldSendEvent()) {
104
143
  return;
105
144
  }
106
145
 
107
- // Identified events ship at 100% effective rate, anonymous events at
108
- // sampleRate. Compute per-event so downstream weight-based extrapolation
109
- // (sampleWeight = 1 / effectiveRate) is correct for both populations;
110
- // a single global sampleWeight would overweight identified-customer
111
- // counts by 1/sampleRate.
112
- const effectiveSampleRate = this.telemetryId ? 1 : this.sampleRate;
113
- const samplingMeta = {
114
- sampleRate: effectiveSampleRate,
115
- sampleRateAdjustmentFactor: 1 - effectiveSampleRate,
116
- sampleWeight: 1 / effectiveSampleRate,
146
+ // Sampling metadata is computed in ./sampling so this client and the
147
+ // v2 runtime client can't drift apart again — see the note there.
148
+ const samplingMeta = computeSamplingMeta({
149
+ telemetryId: identity.licenseTelemetryId,
150
+ sampleRate: this.sampleRate,
151
+ });
152
+
153
+ // Everything below travels identically on both copies of this event.
154
+ // The event id is what makes the dual-write dedupable downstream:
155
+ // one capture() produces one id, stamped on the lambda copy and the
156
+ // Segment copy alike, so consumers no longer have to infer the
157
+ // duplication from $lib or from which fields happen to be present
158
+ // (OSS-1019).
159
+ const eventMeta = {
160
+ ...samplingMeta,
161
+ telemetry_emitter: TELEMETRY_EMITTER_V1,
162
+ telemetry_event_id: uuidv4(),
117
163
  };
118
164
 
119
165
  const flattenedProperties = flattenObject(properties);
120
166
  const propertiesWithGlobal: Record<string, any> = {
121
167
  ...this.globalProperties,
122
- ...samplingMeta,
168
+ ...eventMeta,
169
+ telemetry_transport: "segment",
123
170
  ...flattenedProperties,
124
171
  };
125
172
  const orderedPropertiesWithGlobal = Object.keys(propertiesWithGlobal)
@@ -135,10 +182,15 @@ export class TelemetryClient {
135
182
  await lambdaClient.send({
136
183
  event,
137
184
  properties: flattenedProperties,
138
- globalProperties: { ...this.globalProperties, ...samplingMeta },
185
+ globalProperties: {
186
+ ...this.globalProperties,
187
+ ...eventMeta,
188
+ telemetry_transport: "lambda",
189
+ },
139
190
  packageName: this.packageName,
140
191
  packageVersion: this.packageVersion,
141
- licenseToken: this.licenseToken ?? undefined,
192
+ telemetryId: identity.telemetryId ?? undefined,
193
+ licenseToken: identity.licenseToken ?? undefined,
142
194
  });
143
195
 
144
196
  if (this.segment) {
@@ -169,12 +221,75 @@ export class TelemetryClient {
169
221
  });
170
222
  }
171
223
 
172
- // The license token isn't added to globalProperties — we don't want
173
- // the JWT itself shipped on every event. Only its decoded telemetry_id
174
- // travels, in the X-CopilotKit-Telemetry-Id header set by lambda-client.
224
+ /**
225
+ * Atomically configure standalone, legacy, or anonymous telemetry identity.
226
+ *
227
+ * A standalone id takes transport precedence over a supplied legacy license
228
+ * token, but only a license-derived id grants sampling authority. Neither
229
+ * value is added to event properties.
230
+ *
231
+ * @param identity - One standalone id, one legacy license token, or neither.
232
+ */
233
+ setTelemetryIdentity(identity: {
234
+ telemetryId?: string;
235
+ licenseToken?: string;
236
+ }): void {
237
+ const resolvedIdentity = this.resolveTelemetryIdentity(identity);
238
+ this.telemetryId = resolvedIdentity.telemetryId;
239
+ this.licenseToken = resolvedIdentity.licenseToken;
240
+ this.licenseTelemetryId = resolvedIdentity.licenseTelemetryId;
241
+ }
242
+
243
+ /**
244
+ * Configure legacy license-derived telemetry identity.
245
+ *
246
+ * @param licenseToken - License token whose telemetry claim identifies sends.
247
+ */
175
248
  setLicenseToken(licenseToken: string) {
176
- this.licenseToken = licenseToken;
177
- this.telemetryId = parseAndWarnTelemetryId(licenseToken);
249
+ this.setTelemetryIdentity({ licenseToken });
250
+ }
251
+
252
+ /**
253
+ * Create an immutable capture scope for one runtime.
254
+ *
255
+ * The scope shares this client's sinks, process-wide opt-out, global
256
+ * properties, and sampling settings, but snapshots transport identity and
257
+ * license-derived sampling authority. Constructing another runtime cannot
258
+ * rewrite an existing scope.
259
+ *
260
+ * @param identity - The runtime's construction-time telemetry identity.
261
+ * @returns A capture-only client bound to that identity.
262
+ */
263
+ createScope(identity: TelemetryIdentity): TelemetryCapture {
264
+ const resolvedIdentity = this.resolveTelemetryIdentity(identity);
265
+
266
+ return {
267
+ capture: <K extends keyof AnalyticsEvents>(
268
+ event: K,
269
+ properties: AnalyticsEvents[K],
270
+ ) => this.captureWithIdentity(event, properties, resolvedIdentity),
271
+ };
272
+ }
273
+
274
+ private resolveTelemetryIdentity(
275
+ identity: TelemetryIdentity,
276
+ ): ResolvedTelemetryIdentity {
277
+ const telemetryId = firstNonBlankTelemetryId(identity.telemetryId);
278
+ if (telemetryId !== undefined) {
279
+ return {
280
+ telemetryId,
281
+ licenseToken: null,
282
+ licenseTelemetryId: null,
283
+ };
284
+ }
285
+
286
+ return {
287
+ telemetryId: null,
288
+ licenseToken: identity.licenseToken ?? null,
289
+ licenseTelemetryId: identity.licenseToken
290
+ ? parseAndWarnTelemetryId(identity.licenseToken)
291
+ : null,
292
+ };
178
293
  }
179
294
 
180
295
  private setSampleRate(sampleRate: number | undefined) {
@@ -198,8 +313,8 @@ export class TelemetryClient {
198
313
 
199
314
  this.sampleRate = _sampleRate;
200
315
  // Per-event sampling metadata (sampleRate/sampleRateAdjustmentFactor/
201
- // sampleWeight) is computed in capture() so identified events get
202
- // their own effectiveSampleRate=1 weight instead of the anonymous
203
- // population's 1/sampleRate.
316
+ // sampleWeight) is computed per capture() in ./sampling. Only license-
317
+ // authorized events get effectiveSampleRate=1; standalone transport
318
+ // identity stays in the sampled population.
204
319
  }
205
320
  }
@@ -0,0 +1,161 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ executeConditions,
4
+ Condition,
5
+ ComparisonCondition,
6
+ LogicalCondition,
7
+ ExistenceCondition,
8
+ } from "../conditions";
9
+
10
+ describe("executeConditions", () => {
11
+ it("returns true when conditions is empty or undefined", () => {
12
+ expect(executeConditions({ value: { a: 1 } })).toBe(true);
13
+ expect(executeConditions({ conditions: [], value: { a: 1 } })).toBe(true);
14
+ });
15
+
16
+ it("treats multiple conditions as an implicit AND", () => {
17
+ const conditions: Condition[] = [
18
+ { rule: "EQUALS", path: "a", value: 1 },
19
+ { rule: "EQUALS", path: "b", value: 2 },
20
+ ];
21
+ expect(executeConditions({ conditions, value: { a: 1, b: 2 } })).toBe(true);
22
+ expect(executeConditions({ conditions, value: { a: 1, b: 3 } })).toBe(false);
23
+ });
24
+
25
+ describe("comparison rules", () => {
26
+ it("EQUALS and NOT_EQUALS", () => {
27
+ const eq: ComparisonCondition = { rule: "EQUALS", path: "a", value: 5 };
28
+ const neq: ComparisonCondition = { rule: "NOT_EQUALS", path: "a", value: 5 };
29
+ expect(executeConditions({ conditions: [eq], value: { a: 5 } })).toBe(true);
30
+ expect(executeConditions({ conditions: [eq], value: { a: 6 } })).toBe(false);
31
+ expect(executeConditions({ conditions: [neq], value: { a: 6 } })).toBe(true);
32
+ expect(executeConditions({ conditions: [neq], value: { a: 5 } })).toBe(false);
33
+ });
34
+
35
+ it("GREATER_THAN and LESS_THAN", () => {
36
+ const gt: ComparisonCondition = { rule: "GREATER_THAN", path: "n", value: 10 };
37
+ const lt: ComparisonCondition = { rule: "LESS_THAN", path: "n", value: 10 };
38
+ expect(executeConditions({ conditions: [gt], value: { n: 11 } })).toBe(true);
39
+ expect(executeConditions({ conditions: [gt], value: { n: 10 } })).toBe(false);
40
+ expect(executeConditions({ conditions: [lt], value: { n: 9 } })).toBe(true);
41
+ expect(executeConditions({ conditions: [lt], value: { n: 10 } })).toBe(false);
42
+ });
43
+
44
+ it("CONTAINS and NOT_CONTAINS only apply to arrays", () => {
45
+ const contains: ComparisonCondition = { rule: "CONTAINS", path: "tags", value: "ts" };
46
+ const notContains: ComparisonCondition = { rule: "NOT_CONTAINS", path: "tags", value: "ts" };
47
+ expect(
48
+ executeConditions({ conditions: [contains], value: { tags: ["js", "ts"] } }),
49
+ ).toBe(true);
50
+ expect(
51
+ executeConditions({ conditions: [contains], value: { tags: ["js"] } }),
52
+ ).toBe(false);
53
+ expect(
54
+ executeConditions({ conditions: [notContains], value: { tags: ["js"] } }),
55
+ ).toBe(true);
56
+ // non-array target always fails both CONTAINS and NOT_CONTAINS
57
+ expect(
58
+ executeConditions({ conditions: [contains], value: { tags: "ts" } }),
59
+ ).toBe(false);
60
+ expect(
61
+ executeConditions({ conditions: [notContains], value: { tags: "ts" } }),
62
+ ).toBe(false);
63
+ });
64
+
65
+ it("MATCHES uses the value as a regular expression", () => {
66
+ const matches: ComparisonCondition = { rule: "MATCHES", path: "code", value: "^CK-[0-9]+$" };
67
+ expect(executeConditions({ conditions: [matches], value: { code: "CK-123" } })).toBe(true);
68
+ expect(executeConditions({ conditions: [matches], value: { code: "nope" } })).toBe(false);
69
+ });
70
+
71
+ it("STARTS_WITH and ENDS_WITH", () => {
72
+ const sw: ComparisonCondition = { rule: "STARTS_WITH", path: "s", value: "foo" };
73
+ const ew: ComparisonCondition = { rule: "ENDS_WITH", path: "s", value: "bar" };
74
+ expect(executeConditions({ conditions: [sw], value: { s: "foobar" } })).toBe(true);
75
+ expect(executeConditions({ conditions: [sw], value: { s: "barfoo" } })).toBe(false);
76
+ expect(executeConditions({ conditions: [ew], value: { s: "foobar" } })).toBe(true);
77
+ expect(executeConditions({ conditions: [ew], value: { s: "barfoo" } })).toBe(false);
78
+ });
79
+ });
80
+
81
+ describe("existence rules", () => {
82
+ it("EXISTS passes only for defined non-null values", () => {
83
+ const exists: ExistenceCondition = { rule: "EXISTS", path: "a" };
84
+ expect(executeConditions({ conditions: [exists], value: { a: 0 } })).toBe(true);
85
+ expect(executeConditions({ conditions: [exists], value: { a: "" } })).toBe(true);
86
+ expect(executeConditions({ conditions: [exists], value: { a: false } })).toBe(true);
87
+ expect(executeConditions({ conditions: [exists], value: { a: null } })).toBe(false);
88
+ expect(executeConditions({ conditions: [exists], value: {} })).toBe(false);
89
+ });
90
+
91
+ it("NOT_EXISTS is the inverse of EXISTS", () => {
92
+ const notExists: ExistenceCondition = { rule: "NOT_EXISTS", path: "a" };
93
+ expect(executeConditions({ conditions: [notExists], value: { a: 1 } })).toBe(false);
94
+ expect(executeConditions({ conditions: [notExists], value: { a: null } })).toBe(true);
95
+ expect(executeConditions({ conditions: [notExists], value: {} })).toBe(true);
96
+ });
97
+ });
98
+
99
+ describe("logical rules", () => {
100
+ it("AND requires all nested conditions to pass", () => {
101
+ const and: LogicalCondition = {
102
+ rule: "AND",
103
+ conditions: [
104
+ { rule: "EQUALS", path: "a", value: 1 },
105
+ { rule: "EQUALS", path: "b", value: 2 },
106
+ ],
107
+ };
108
+ expect(executeConditions({ conditions: [and], value: { a: 1, b: 2 } })).toBe(true);
109
+ expect(executeConditions({ conditions: [and], value: { a: 1, b: 3 } })).toBe(false);
110
+ });
111
+
112
+ it("OR passes when any nested condition passes", () => {
113
+ const or: LogicalCondition = {
114
+ rule: "OR",
115
+ conditions: [
116
+ { rule: "EQUALS", path: "a", value: 1 },
117
+ { rule: "EQUALS", path: "b", value: 2 },
118
+ ],
119
+ };
120
+ expect(executeConditions({ conditions: [or], value: { a: 9, b: 2 } })).toBe(true);
121
+ expect(executeConditions({ conditions: [or], value: { a: 9, b: 9 } })).toBe(false);
122
+ });
123
+
124
+ it("NOT inverts the AND of nested conditions", () => {
125
+ const notAll: LogicalCondition = {
126
+ rule: "NOT",
127
+ conditions: [
128
+ { rule: "EQUALS", path: "a", value: 1 },
129
+ { rule: "EQUALS", path: "b", value: 2 },
130
+ ],
131
+ };
132
+ // both true → NOT(AND) = false
133
+ expect(executeConditions({ conditions: [notAll], value: { a: 1, b: 2 } })).toBe(false);
134
+ // one false → NOT(AND) = true
135
+ expect(executeConditions({ conditions: [notAll], value: { a: 1, b: 3 } })).toBe(true);
136
+ });
137
+ });
138
+
139
+ describe("path resolution", () => {
140
+ it("resolves nested dot paths", () => {
141
+ const cond: ComparisonCondition = { rule: "EQUALS", path: "user.profile.age", value: 30 };
142
+ expect(
143
+ executeConditions({ conditions: [cond], value: { user: { profile: { age: 30 } } } }),
144
+ ).toBe(true);
145
+ expect(
146
+ executeConditions({ conditions: [cond], value: { user: { profile: { age: 31 } } } }),
147
+ ).toBe(false);
148
+ });
149
+
150
+ it("returns false for a missing nested path (undefined target)", () => {
151
+ const eq: ComparisonCondition = { rule: "EQUALS", path: "a.b.c", value: 1 };
152
+ expect(executeConditions({ conditions: [eq], value: {} })).toBe(false);
153
+ });
154
+
155
+ it("uses the whole value when no path is given", () => {
156
+ const eq: ComparisonCondition = { rule: "EQUALS", value: 42 };
157
+ expect(executeConditions({ conditions: [eq], value: 42 })).toBe(true);
158
+ expect(executeConditions({ conditions: [eq], value: 43 })).toBe(false);
159
+ });
160
+ });
161
+ });
@@ -53,15 +53,15 @@ export function logCopilotKitPlatformMessage() {
53
53
  console.log(
54
54
  `%cCopilotKit Warning%c
55
55
 
56
- useCopilotChatHeadless_c provides full compatibility with CopilotKit's newly released Headless UI feature set. To enable this premium feature, add your public license key, available for free at:
56
+ useCopilotChatHeadless_c provides full compatibility with CopilotKit's newly released Headless UI feature set. Headless UI requires a CopilotKit Intelligence license key, available for free at:
57
57
 
58
58
  %chttps://dashboard.operations.copilotkit.ai%c
59
59
 
60
60
  Alternatively, useCopilotChat is available for basic programmatic control, and does not require a license key.
61
61
 
62
- To learn more about premium features, read the documentation here:
62
+ To learn more about CopilotKit Intelligence, read the documentation here:
63
63
 
64
- %chttps://docs.copilotkit.ai/premium/overview%c`,
64
+ %chttps://docs.copilotkit.ai/intelligence/overview%c`,
65
65
  ConsoleStyles.header,
66
66
  ConsoleStyles.body,
67
67
  ConsoleStyles.cta,
@@ -46,6 +46,55 @@ export type RuntimeLicenseStatus =
46
46
  | "invalid"
47
47
  | "unknown";
48
48
 
49
+ /** Runtime entitlement authority resolved by a managed or self-hosted backend. */
50
+ interface RuntimeEntitlement {
51
+ /** Whether the resolved entitlement currently grants product access. */
52
+ active: boolean;
53
+ /** Deployment authority that produced this entitlement. */
54
+ source: "managedOrgSubscription" | "selfHostedDeploymentLicense";
55
+ /** Boolean feature grants keyed by stable feature id. */
56
+ features: Record<string, boolean>;
57
+ /** Numeric limits keyed by stable feature id. */
58
+ limits: Record<string, number>;
59
+ /** Optional catalog plan code supplied by the entitlement authority. */
60
+ planCode?: string;
61
+ /** Optional lower-level source metadata supplied by the authority. */
62
+ entitlementSource?: string;
63
+ }
64
+
65
+ /** Public diagnostic returned when Runtime entitlement resolution is not ready. */
66
+ interface RuntimeEntitlementError {
67
+ /** Stable backend or SDK error code. */
68
+ code: string;
69
+ /** Safe human-readable diagnostic. */
70
+ message: string;
71
+ /** Whether a later resolution attempt may succeed without reconfiguration. */
72
+ retryable: boolean;
73
+ /** Optional originating request correlation id. */
74
+ requestId?: string;
75
+ /** Optional originating trace correlation id. */
76
+ traceId?: string;
77
+ }
78
+
79
+ /** Successfully resolved Runtime entitlement response. */
80
+ interface RuntimeEntitlementReadyResponse {
81
+ status: "ready";
82
+ entitlement: RuntimeEntitlement;
83
+ error?: never;
84
+ }
85
+
86
+ /** Structured non-ready Runtime entitlement response. */
87
+ interface RuntimeEntitlementErrorResponse {
88
+ status: "degraded" | "misconfigured" | "unavailable";
89
+ entitlement?: never;
90
+ error: RuntimeEntitlementError;
91
+ }
92
+
93
+ /** Final structured Runtime entitlement response exposed through `/info`. */
94
+ export type RuntimeEntitlementResponse =
95
+ | RuntimeEntitlementReadyResponse
96
+ | RuntimeEntitlementErrorResponse;
97
+
49
98
  export interface A2UIRuntimeInfo {
50
99
  enabled: boolean;
51
100
  /**
@@ -77,6 +126,9 @@ export interface RuntimeInfo {
77
126
  a2uiEnabled?: boolean;
78
127
  a2ui?: A2UIRuntimeInfo;
79
128
  openGenerativeUIEnabled?: boolean;
129
+ /** Structured Runtime-level entitlement authority, when advertised. */
130
+ runtimeEntitlements?: RuntimeEntitlementResponse;
131
+ /** Legacy compatibility diagnostic retained for older Core/Inspector clients. */
80
132
  licenseStatus?: RuntimeLicenseStatus;
81
133
  telemetryDisabled?: boolean;
82
134
  }