@sentrial/sdk 0.4.3 → 0.4.5

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/index.d.cts CHANGED
@@ -270,6 +270,9 @@ declare function redactPayload(payload: Record<string, unknown>, config: PiiConf
270
270
  * calls (createSession, completeSession) bypass the batcher entirely
271
271
  * because they need synchronous responses.
272
272
  *
273
+ * Events are delivered in enqueue order so server-assigned sequence numbers
274
+ * match the order an agent actually executed steps.
275
+ *
273
276
  * Flush triggers:
274
277
  * 1. Timer — every `flushIntervalMs` (default 1 000 ms)
275
278
  * 2. Queue size — when queue reaches `flushThreshold` (default 10)
@@ -312,7 +315,7 @@ declare class EventBatcher {
312
315
  /**
313
316
  * Flush all queued events to the API.
314
317
  *
315
- * Drains the queue and fires all requests in parallel. Safe to call
318
+ * Drains the queue and sends requests in enqueue order. Safe to call
316
319
  * concurrently — only one flush runs at a time.
317
320
  */
318
321
  flush(): Promise<void>;
@@ -385,6 +388,8 @@ interface CreateSessionParams {
385
388
  convoId?: string;
386
389
  /** Optional metadata */
387
390
  metadata?: Record<string, unknown>;
391
+ /** Production experiment assignment ids to link to this session */
392
+ experimentAssignmentIds?: string[];
388
393
  }
389
394
  /**
390
395
  * Parameters for completing a session
@@ -582,6 +587,50 @@ interface FinishParams {
582
587
  /** Duration in milliseconds (auto-calculated from begin/finish if not provided) */
583
588
  durationMs?: number;
584
589
  }
590
+ interface ProductionVariantConfig {
591
+ name: string;
592
+ weight?: number;
593
+ prompt?: string;
594
+ systemPrompt?: string;
595
+ model?: string;
596
+ promptVersion?: string;
597
+ toolConfig?: Record<string, unknown>;
598
+ metadata?: Record<string, unknown>;
599
+ }
600
+ interface GetVariantParams {
601
+ /** Production experiment name configured in Sentrial */
602
+ experimentName: string;
603
+ /** Agent name this variant is for */
604
+ agentName?: string;
605
+ /** External user id for sticky bucketing */
606
+ userId?: string;
607
+ /** Conversation id for sticky multi-turn bucketing */
608
+ convoId?: string;
609
+ /** Stable anonymous id if no user/conversation id exists */
610
+ anonymousId?: string;
611
+ /** Optional existing Sentrial session id */
612
+ sessionId?: string;
613
+ /** Variant to use when no active live experiment matches */
614
+ defaultVariant?: string;
615
+ /** Prompt version currently running in the customer app */
616
+ promptVersion?: string;
617
+ /** Model currently running in the customer app */
618
+ model?: string;
619
+ /** Extra assignment metadata */
620
+ metadata?: Record<string, unknown>;
621
+ }
622
+ interface VariantAssignment {
623
+ active: boolean;
624
+ reason: 'assigned' | 'holdout' | 'not_found';
625
+ experimentId?: string;
626
+ assignmentId?: string;
627
+ variant: string;
628
+ variantConfig?: ProductionVariantConfig;
629
+ prompt?: string;
630
+ systemPrompt?: string;
631
+ model?: string;
632
+ promptVersion?: string;
633
+ }
585
634
  /**
586
635
  * Parameters for cost calculation
587
636
  */
@@ -719,6 +768,7 @@ declare class SentrialClient {
719
768
  private piiHydrationPromise?;
720
769
  private readonly _stateVar;
721
770
  private readonly batcher?;
771
+ private pendingExperimentAssignmentIds;
722
772
  /** Per-session cost/token accumulator — populated by trackToolCall/trackDecision */
723
773
  private sessionAccumulators;
724
774
  private get currentState();
@@ -747,6 +797,13 @@ declare class SentrialClient {
747
797
  * @returns Session ID, or null if the request failed and failSilently is true
748
798
  */
749
799
  createSession(params: CreateSessionParams): Promise<string | null>;
800
+ /**
801
+ * Get a sticky production experiment variant for this user/conversation.
802
+ *
803
+ * The returned assignment id is automatically linked to the next session
804
+ * created by this client. You can also pass `sessionId` to link immediately.
805
+ */
806
+ getVariant(params: GetVariantParams): Promise<VariantAssignment>;
750
807
  /**
751
808
  * Track a tool call event
752
809
  *
@@ -986,6 +1043,7 @@ declare function configure(config: SentrialClientConfig): void;
986
1043
  * ```
987
1044
  */
988
1045
  declare function begin(params: BeginParams): Promise<Interaction>;
1046
+ declare function getVariant(params: GetVariantParams): Promise<VariantAssignment>;
989
1047
  /**
990
1048
  * Simple API namespace for module-level usage
991
1049
  */
@@ -994,6 +1052,7 @@ declare function shutdown(): Promise<void>;
994
1052
  declare const sentrial: {
995
1053
  configure: typeof configure;
996
1054
  begin: typeof begin;
1055
+ getVariant: typeof getVariant;
997
1056
  flush: typeof flush;
998
1057
  shutdown: typeof shutdown;
999
1058
  };
@@ -1187,11 +1246,13 @@ type StreamTextResult = {
1187
1246
  }>;
1188
1247
  [key: string]: unknown;
1189
1248
  };
1249
+ /** String or function that returns a string (for dynamic per-request values) */
1250
+ type StringOrFn = string | (() => string | undefined);
1190
1251
  /** Per-instance config passed through closures (not global) */
1191
1252
  type WrapperConfig = {
1192
1253
  defaultAgent?: string;
1193
- userId?: string;
1194
- convoId?: string;
1254
+ userId?: StringOrFn;
1255
+ convoId?: StringOrFn;
1195
1256
  };
1196
1257
  /**
1197
1258
  * Configure the Sentrial SDK for Vercel AI SDK integration.
@@ -1211,8 +1272,8 @@ declare function configureVercel(config: {
1211
1272
  apiKey?: string;
1212
1273
  apiUrl?: string;
1213
1274
  defaultAgent?: string;
1214
- userId?: string;
1215
- convoId?: string;
1275
+ userId?: StringOrFn;
1276
+ convoId?: StringOrFn;
1216
1277
  failSilently?: boolean;
1217
1278
  }): void;
1218
1279
  declare function wrapGenerateText(originalFn: Function, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => Promise<GenerateTextResult>;
@@ -1246,8 +1307,8 @@ declare function wrapStreamObject(originalFn: Function, client: SentrialClient,
1246
1307
  declare function wrapAISDK(ai: AIModule, options?: {
1247
1308
  client?: SentrialClient;
1248
1309
  defaultAgent?: string;
1249
- userId?: string;
1250
- convoId?: string;
1310
+ userId?: StringOrFn;
1311
+ convoId?: StringOrFn;
1251
1312
  }): {
1252
1313
  generateText: ReturnType<typeof wrapGenerateText>;
1253
1314
  streamText: ReturnType<typeof wrapStreamText>;
@@ -1887,4 +1948,4 @@ declare class Experiment {
1887
1948
  getResults(): Promise<ExperimentResults | null>;
1888
1949
  }
1889
1950
 
1890
- export { ApiError, type ApiResponse, type BatcherConfig, type BeginParams, type CompleteSessionParams, type ContextToken, type ContextVar, type CostParams, type CreateSessionParams, type Event, EventBatcher, EventType, Experiment, type ExperimentContext, type ExperimentResults, type ExperimentRunResult, ExperimentRunTracker, type ExperimentTestCase, type ExperimentVariant, type FinishParams, type GenerateTextParams, type GenerateTextResult, Interaction, NetworkError, type PiiBuiltinPatterns, type PiiConfig, type PiiCustomPattern, type PiiField, type PiiMode, SentrialClient, type SentrialClientConfig, SentrialError, type Session, SessionContext, type SessionStatus, type StreamTextResult, Tool, type TrackDecisionParams, type TrackErrorParams, TrackSession, type TrackToolCallParams, ValidationError, type WrapClaudeAgentOptions, begin, calculateAnthropicCost, calculateGoogleCost, calculateOpenAICost, clearExperimentContext, clearSessionContext, configure, configureVercel, createContextVar, getCurrentInteraction, getCurrentSessionId, getExperimentContext, getExperimentId, getSessionContext, getSystemPrompt, getVariantName, hashValue, isExperimentMode, redactPayload, redactString, redactValue, replaceMatch, sentrial, setClient, setDefaultClient, setExperimentContext, setSessionContext, withSession, withTool, wrapAISDK, wrapAnthropic, wrapClaudeAgent, wrapGoogle, wrapLLM, wrapOpenAI };
1951
+ export { ApiError, type ApiResponse, type BatcherConfig, type BeginParams, type CompleteSessionParams, type ContextToken, type ContextVar, type CostParams, type CreateSessionParams, type Event, EventBatcher, EventType, Experiment, type ExperimentContext, type ExperimentResults, type ExperimentRunResult, ExperimentRunTracker, type ExperimentTestCase, type ExperimentVariant, type FinishParams, type GenerateTextParams, type GenerateTextResult, type GetVariantParams, Interaction, NetworkError, type PiiBuiltinPatterns, type PiiConfig, type PiiCustomPattern, type PiiField, type PiiMode, type ProductionVariantConfig, SentrialClient, type SentrialClientConfig, SentrialError, type Session, SessionContext, type SessionStatus, type StreamTextResult, Tool, type TrackDecisionParams, type TrackErrorParams, TrackSession, type TrackToolCallParams, ValidationError, type VariantAssignment, type WrapClaudeAgentOptions, begin, calculateAnthropicCost, calculateGoogleCost, calculateOpenAICost, clearExperimentContext, clearSessionContext, configure, configureVercel, createContextVar, getCurrentInteraction, getCurrentSessionId, getExperimentContext, getExperimentId, getSessionContext, getSystemPrompt, getVariant, getVariantName, hashValue, isExperimentMode, redactPayload, redactString, redactValue, replaceMatch, sentrial, setClient, setDefaultClient, setExperimentContext, setSessionContext, withSession, withTool, wrapAISDK, wrapAnthropic, wrapClaudeAgent, wrapGoogle, wrapLLM, wrapOpenAI };
package/dist/index.d.ts CHANGED
@@ -270,6 +270,9 @@ declare function redactPayload(payload: Record<string, unknown>, config: PiiConf
270
270
  * calls (createSession, completeSession) bypass the batcher entirely
271
271
  * because they need synchronous responses.
272
272
  *
273
+ * Events are delivered in enqueue order so server-assigned sequence numbers
274
+ * match the order an agent actually executed steps.
275
+ *
273
276
  * Flush triggers:
274
277
  * 1. Timer — every `flushIntervalMs` (default 1 000 ms)
275
278
  * 2. Queue size — when queue reaches `flushThreshold` (default 10)
@@ -312,7 +315,7 @@ declare class EventBatcher {
312
315
  /**
313
316
  * Flush all queued events to the API.
314
317
  *
315
- * Drains the queue and fires all requests in parallel. Safe to call
318
+ * Drains the queue and sends requests in enqueue order. Safe to call
316
319
  * concurrently — only one flush runs at a time.
317
320
  */
318
321
  flush(): Promise<void>;
@@ -385,6 +388,8 @@ interface CreateSessionParams {
385
388
  convoId?: string;
386
389
  /** Optional metadata */
387
390
  metadata?: Record<string, unknown>;
391
+ /** Production experiment assignment ids to link to this session */
392
+ experimentAssignmentIds?: string[];
388
393
  }
389
394
  /**
390
395
  * Parameters for completing a session
@@ -582,6 +587,50 @@ interface FinishParams {
582
587
  /** Duration in milliseconds (auto-calculated from begin/finish if not provided) */
583
588
  durationMs?: number;
584
589
  }
590
+ interface ProductionVariantConfig {
591
+ name: string;
592
+ weight?: number;
593
+ prompt?: string;
594
+ systemPrompt?: string;
595
+ model?: string;
596
+ promptVersion?: string;
597
+ toolConfig?: Record<string, unknown>;
598
+ metadata?: Record<string, unknown>;
599
+ }
600
+ interface GetVariantParams {
601
+ /** Production experiment name configured in Sentrial */
602
+ experimentName: string;
603
+ /** Agent name this variant is for */
604
+ agentName?: string;
605
+ /** External user id for sticky bucketing */
606
+ userId?: string;
607
+ /** Conversation id for sticky multi-turn bucketing */
608
+ convoId?: string;
609
+ /** Stable anonymous id if no user/conversation id exists */
610
+ anonymousId?: string;
611
+ /** Optional existing Sentrial session id */
612
+ sessionId?: string;
613
+ /** Variant to use when no active live experiment matches */
614
+ defaultVariant?: string;
615
+ /** Prompt version currently running in the customer app */
616
+ promptVersion?: string;
617
+ /** Model currently running in the customer app */
618
+ model?: string;
619
+ /** Extra assignment metadata */
620
+ metadata?: Record<string, unknown>;
621
+ }
622
+ interface VariantAssignment {
623
+ active: boolean;
624
+ reason: 'assigned' | 'holdout' | 'not_found';
625
+ experimentId?: string;
626
+ assignmentId?: string;
627
+ variant: string;
628
+ variantConfig?: ProductionVariantConfig;
629
+ prompt?: string;
630
+ systemPrompt?: string;
631
+ model?: string;
632
+ promptVersion?: string;
633
+ }
585
634
  /**
586
635
  * Parameters for cost calculation
587
636
  */
@@ -719,6 +768,7 @@ declare class SentrialClient {
719
768
  private piiHydrationPromise?;
720
769
  private readonly _stateVar;
721
770
  private readonly batcher?;
771
+ private pendingExperimentAssignmentIds;
722
772
  /** Per-session cost/token accumulator — populated by trackToolCall/trackDecision */
723
773
  private sessionAccumulators;
724
774
  private get currentState();
@@ -747,6 +797,13 @@ declare class SentrialClient {
747
797
  * @returns Session ID, or null if the request failed and failSilently is true
748
798
  */
749
799
  createSession(params: CreateSessionParams): Promise<string | null>;
800
+ /**
801
+ * Get a sticky production experiment variant for this user/conversation.
802
+ *
803
+ * The returned assignment id is automatically linked to the next session
804
+ * created by this client. You can also pass `sessionId` to link immediately.
805
+ */
806
+ getVariant(params: GetVariantParams): Promise<VariantAssignment>;
750
807
  /**
751
808
  * Track a tool call event
752
809
  *
@@ -986,6 +1043,7 @@ declare function configure(config: SentrialClientConfig): void;
986
1043
  * ```
987
1044
  */
988
1045
  declare function begin(params: BeginParams): Promise<Interaction>;
1046
+ declare function getVariant(params: GetVariantParams): Promise<VariantAssignment>;
989
1047
  /**
990
1048
  * Simple API namespace for module-level usage
991
1049
  */
@@ -994,6 +1052,7 @@ declare function shutdown(): Promise<void>;
994
1052
  declare const sentrial: {
995
1053
  configure: typeof configure;
996
1054
  begin: typeof begin;
1055
+ getVariant: typeof getVariant;
997
1056
  flush: typeof flush;
998
1057
  shutdown: typeof shutdown;
999
1058
  };
@@ -1187,11 +1246,13 @@ type StreamTextResult = {
1187
1246
  }>;
1188
1247
  [key: string]: unknown;
1189
1248
  };
1249
+ /** String or function that returns a string (for dynamic per-request values) */
1250
+ type StringOrFn = string | (() => string | undefined);
1190
1251
  /** Per-instance config passed through closures (not global) */
1191
1252
  type WrapperConfig = {
1192
1253
  defaultAgent?: string;
1193
- userId?: string;
1194
- convoId?: string;
1254
+ userId?: StringOrFn;
1255
+ convoId?: StringOrFn;
1195
1256
  };
1196
1257
  /**
1197
1258
  * Configure the Sentrial SDK for Vercel AI SDK integration.
@@ -1211,8 +1272,8 @@ declare function configureVercel(config: {
1211
1272
  apiKey?: string;
1212
1273
  apiUrl?: string;
1213
1274
  defaultAgent?: string;
1214
- userId?: string;
1215
- convoId?: string;
1275
+ userId?: StringOrFn;
1276
+ convoId?: StringOrFn;
1216
1277
  failSilently?: boolean;
1217
1278
  }): void;
1218
1279
  declare function wrapGenerateText(originalFn: Function, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => Promise<GenerateTextResult>;
@@ -1246,8 +1307,8 @@ declare function wrapStreamObject(originalFn: Function, client: SentrialClient,
1246
1307
  declare function wrapAISDK(ai: AIModule, options?: {
1247
1308
  client?: SentrialClient;
1248
1309
  defaultAgent?: string;
1249
- userId?: string;
1250
- convoId?: string;
1310
+ userId?: StringOrFn;
1311
+ convoId?: StringOrFn;
1251
1312
  }): {
1252
1313
  generateText: ReturnType<typeof wrapGenerateText>;
1253
1314
  streamText: ReturnType<typeof wrapStreamText>;
@@ -1887,4 +1948,4 @@ declare class Experiment {
1887
1948
  getResults(): Promise<ExperimentResults | null>;
1888
1949
  }
1889
1950
 
1890
- export { ApiError, type ApiResponse, type BatcherConfig, type BeginParams, type CompleteSessionParams, type ContextToken, type ContextVar, type CostParams, type CreateSessionParams, type Event, EventBatcher, EventType, Experiment, type ExperimentContext, type ExperimentResults, type ExperimentRunResult, ExperimentRunTracker, type ExperimentTestCase, type ExperimentVariant, type FinishParams, type GenerateTextParams, type GenerateTextResult, Interaction, NetworkError, type PiiBuiltinPatterns, type PiiConfig, type PiiCustomPattern, type PiiField, type PiiMode, SentrialClient, type SentrialClientConfig, SentrialError, type Session, SessionContext, type SessionStatus, type StreamTextResult, Tool, type TrackDecisionParams, type TrackErrorParams, TrackSession, type TrackToolCallParams, ValidationError, type WrapClaudeAgentOptions, begin, calculateAnthropicCost, calculateGoogleCost, calculateOpenAICost, clearExperimentContext, clearSessionContext, configure, configureVercel, createContextVar, getCurrentInteraction, getCurrentSessionId, getExperimentContext, getExperimentId, getSessionContext, getSystemPrompt, getVariantName, hashValue, isExperimentMode, redactPayload, redactString, redactValue, replaceMatch, sentrial, setClient, setDefaultClient, setExperimentContext, setSessionContext, withSession, withTool, wrapAISDK, wrapAnthropic, wrapClaudeAgent, wrapGoogle, wrapLLM, wrapOpenAI };
1951
+ export { ApiError, type ApiResponse, type BatcherConfig, type BeginParams, type CompleteSessionParams, type ContextToken, type ContextVar, type CostParams, type CreateSessionParams, type Event, EventBatcher, EventType, Experiment, type ExperimentContext, type ExperimentResults, type ExperimentRunResult, ExperimentRunTracker, type ExperimentTestCase, type ExperimentVariant, type FinishParams, type GenerateTextParams, type GenerateTextResult, type GetVariantParams, Interaction, NetworkError, type PiiBuiltinPatterns, type PiiConfig, type PiiCustomPattern, type PiiField, type PiiMode, type ProductionVariantConfig, SentrialClient, type SentrialClientConfig, SentrialError, type Session, SessionContext, type SessionStatus, type StreamTextResult, Tool, type TrackDecisionParams, type TrackErrorParams, TrackSession, type TrackToolCallParams, ValidationError, type VariantAssignment, type WrapClaudeAgentOptions, begin, calculateAnthropicCost, calculateGoogleCost, calculateOpenAICost, clearExperimentContext, clearSessionContext, configure, configureVercel, createContextVar, getCurrentInteraction, getCurrentSessionId, getExperimentContext, getExperimentId, getSessionContext, getSystemPrompt, getVariant, getVariantName, hashValue, isExperimentMode, redactPayload, redactString, redactValue, replaceMatch, sentrial, setClient, setDefaultClient, setExperimentContext, setSessionContext, withSession, withTool, wrapAISDK, wrapAnthropic, wrapClaudeAgent, wrapGoogle, wrapLLM, wrapOpenAI };
package/dist/index.js CHANGED
@@ -70,7 +70,7 @@ var ValidationError = class extends SentrialError {
70
70
  // src/redact.ts
71
71
  var _createHash;
72
72
  try {
73
- const mod = eval("require")("crypto");
73
+ const mod = new Function('return require("crypto")')();
74
74
  if (mod?.createHash) {
75
75
  _createHash = mod.createHash;
76
76
  }
@@ -175,7 +175,7 @@ function redactPayload(payload, config) {
175
175
  // src/async-context.ts
176
176
  var _AsyncLocalStorage = null;
177
177
  try {
178
- const mod = eval("require")("node:async_hooks");
178
+ const mod = new Function('return require("node:async_hooks")')();
179
179
  if (mod?.AsyncLocalStorage) {
180
180
  _AsyncLocalStorage = mod.AsyncLocalStorage;
181
181
  }
@@ -325,6 +325,9 @@ function getSessionContext() {
325
325
  function setDefaultClient(client) {
326
326
  _defaultClient = client;
327
327
  }
328
+ function getDefaultClient() {
329
+ return _defaultClient;
330
+ }
328
331
  function _setSessionContextWithTokens(sessionId, client) {
329
332
  const _sessionToken = _currentSessionId.set(sessionId);
330
333
  const _clientToken = client ? _currentClient.set(client) : _currentClient.set(_currentClient.get());
@@ -882,7 +885,7 @@ var EventBatcher = class {
882
885
  /**
883
886
  * Flush all queued events to the API.
884
887
  *
885
- * Drains the queue and fires all requests in parallel. Safe to call
888
+ * Drains the queue and sends requests in enqueue order. Safe to call
886
889
  * concurrently — only one flush runs at a time.
887
890
  */
888
891
  async flush() {
@@ -890,15 +893,15 @@ var EventBatcher = class {
890
893
  this.flushing = true;
891
894
  const batch = this.queue.splice(0, this.queue.length);
892
895
  try {
893
- await Promise.all(
894
- batch.map(
895
- (event) => this.sendFn(event.method, event.url, event.body).catch((err) => {
896
- if (typeof console !== "undefined") {
897
- console.warn("Sentrial: Batched event failed:", err);
898
- }
899
- })
900
- )
901
- );
896
+ for (const event of batch) {
897
+ try {
898
+ await this.sendFn(event.method, event.url, event.body);
899
+ } catch (err) {
900
+ if (typeof console !== "undefined") {
901
+ console.warn("Sentrial: Batched event failed:", err);
902
+ }
903
+ }
904
+ }
902
905
  } finally {
903
906
  this.flushing = false;
904
907
  }
@@ -951,6 +954,7 @@ var SentrialClient = class {
951
954
  piiHydrationPromise;
952
955
  _stateVar = createContextVar({});
953
956
  batcher;
957
+ pendingExperimentAssignmentIds = [];
954
958
  /** Per-session cost/token accumulator — populated by trackToolCall/trackDecision */
955
959
  sessionAccumulators = /* @__PURE__ */ new Map();
956
960
  get currentState() {
@@ -1101,12 +1105,12 @@ var SentrialClient = class {
1101
1105
  }
1102
1106
  }
1103
1107
  }
1104
- const networkError = new NetworkError(
1105
- lastError?.message ?? "Unknown network error",
1106
- lastError
1107
- );
1108
+ const networkError = new NetworkError(lastError?.message ?? "Unknown network error", lastError);
1108
1109
  if (this.failSilently) {
1109
- console.warn(`Sentrial: Request failed after ${MAX_RETRIES + 1} attempts (${method} ${url}):`, networkError.message);
1110
+ console.warn(
1111
+ `Sentrial: Request failed after ${MAX_RETRIES + 1} attempts (${method} ${url}):`,
1112
+ networkError.message
1113
+ );
1110
1114
  return null;
1111
1115
  }
1112
1116
  throw networkError;
@@ -1149,13 +1153,53 @@ var SentrialClient = class {
1149
1153
  if (params.convoId) {
1150
1154
  payload.convoId = params.convoId;
1151
1155
  }
1156
+ const experimentAssignmentIds = [
1157
+ ...params.experimentAssignmentIds || [],
1158
+ ...this.pendingExperimentAssignmentIds
1159
+ ];
1160
+ if (experimentAssignmentIds.length > 0) {
1161
+ payload.experimentAssignmentIds = Array.from(new Set(experimentAssignmentIds));
1162
+ payload.metadata = {
1163
+ ...params.metadata || {},
1164
+ sentrial_experiment_assignment_ids: payload.experimentAssignmentIds
1165
+ };
1166
+ }
1152
1167
  const response = await this.safeRequest(
1153
1168
  "POST",
1154
1169
  `${this.apiUrl}/api/sdk/sessions`,
1155
1170
  payload
1156
1171
  );
1172
+ if (response?.id && experimentAssignmentIds.length > 0) {
1173
+ this.pendingExperimentAssignmentIds = [];
1174
+ }
1157
1175
  return response?.id ?? null;
1158
1176
  }
1177
+ /**
1178
+ * Get a sticky production experiment variant for this user/conversation.
1179
+ *
1180
+ * The returned assignment id is automatically linked to the next session
1181
+ * created by this client. You can also pass `sessionId` to link immediately.
1182
+ */
1183
+ async getVariant(params) {
1184
+ const fallback = {
1185
+ active: false,
1186
+ reason: "not_found",
1187
+ variant: params.defaultVariant || "control"
1188
+ };
1189
+ const response = await this.safeRequest(
1190
+ "POST",
1191
+ `${this.apiUrl}/api/sdk/experiments/variant`,
1192
+ {
1193
+ ...params,
1194
+ sdkName: "@sentrial/sdk"
1195
+ }
1196
+ );
1197
+ if (!response) return fallback;
1198
+ if (response.active && response.assignmentId && !params.sessionId) {
1199
+ this.pendingExperimentAssignmentIds.push(response.assignmentId);
1200
+ }
1201
+ return response;
1202
+ }
1159
1203
  /**
1160
1204
  * Track a tool call event
1161
1205
  *
@@ -1264,11 +1308,9 @@ var SentrialClient = class {
1264
1308
  * @returns Updated session or null on error
1265
1309
  */
1266
1310
  async setInput(sessionId, input) {
1267
- return this.safeRequest(
1268
- "PATCH",
1269
- `${this.apiUrl}/api/sdk/sessions/${sessionId}`,
1270
- { userInput: input }
1271
- );
1311
+ return this.safeRequest("PATCH", `${this.apiUrl}/api/sdk/sessions/${sessionId}`, {
1312
+ userInput: input
1313
+ });
1272
1314
  }
1273
1315
  /**
1274
1316
  * Track a generic event
@@ -1328,10 +1370,14 @@ var SentrialClient = class {
1328
1370
  }
1329
1371
  const acc = this.sessionAccumulators.get(params.sessionId);
1330
1372
  if (acc) {
1331
- if (params.estimatedCost === void 0 && acc.cost > 0) params = { ...params, estimatedCost: acc.cost };
1332
- if (params.promptTokens === void 0 && acc.promptTokens > 0) params = { ...params, promptTokens: acc.promptTokens };
1333
- if (params.completionTokens === void 0 && acc.completionTokens > 0) params = { ...params, completionTokens: acc.completionTokens };
1334
- if (params.totalTokens === void 0 && acc.totalTokens > 0) params = { ...params, totalTokens: acc.totalTokens };
1373
+ if (params.estimatedCost === void 0 && acc.cost > 0)
1374
+ params = { ...params, estimatedCost: acc.cost };
1375
+ if (params.promptTokens === void 0 && acc.promptTokens > 0)
1376
+ params = { ...params, promptTokens: acc.promptTokens };
1377
+ if (params.completionTokens === void 0 && acc.completionTokens > 0)
1378
+ params = { ...params, completionTokens: acc.completionTokens };
1379
+ if (params.totalTokens === void 0 && acc.totalTokens > 0)
1380
+ params = { ...params, totalTokens: acc.totalTokens };
1335
1381
  this.sessionAccumulators.delete(params.sessionId);
1336
1382
  }
1337
1383
  const payload = {
@@ -1565,10 +1611,14 @@ function getClient() {
1565
1611
  }
1566
1612
  function configure(config) {
1567
1613
  defaultClient = new SentrialClient(config);
1614
+ setDefaultClient(defaultClient);
1568
1615
  }
1569
1616
  function begin(params) {
1570
1617
  return getClient().begin(params);
1571
1618
  }
1619
+ function getVariant(params) {
1620
+ return getClient().getVariant(params);
1621
+ }
1572
1622
  async function flush() {
1573
1623
  if (defaultClient) await defaultClient.flush();
1574
1624
  }
@@ -1578,11 +1628,16 @@ async function shutdown() {
1578
1628
  var sentrial = {
1579
1629
  configure,
1580
1630
  begin,
1631
+ getVariant,
1581
1632
  flush,
1582
1633
  shutdown
1583
1634
  };
1584
1635
 
1585
1636
  // src/vercel.ts
1637
+ function resolveStringOrFn(value, fallback) {
1638
+ if (typeof value === "function") return value() ?? fallback;
1639
+ return value ?? fallback;
1640
+ }
1586
1641
  var _defaultClient2 = null;
1587
1642
  var _globalConfig = {};
1588
1643
  function configureVercel(config) {
@@ -1591,6 +1646,7 @@ function configureVercel(config) {
1591
1646
  apiUrl: config.apiUrl,
1592
1647
  failSilently: config.failSilently ?? true
1593
1648
  });
1649
+ setDefaultClient(_defaultClient2);
1594
1650
  _globalConfig = {
1595
1651
  defaultAgent: config.defaultAgent,
1596
1652
  userId: config.userId,
@@ -1640,14 +1696,26 @@ function calculateCostForCall(provider, modelId, promptTokens, completionTokens)
1640
1696
  return 0;
1641
1697
  }
1642
1698
  }
1699
+ function extractContentText(content) {
1700
+ if (typeof content === "string") return content;
1701
+ if (Array.isArray(content)) {
1702
+ const textParts = content.filter((part) => part && part.type === "text" && typeof part.text === "string").map((part) => part.text);
1703
+ if (textParts.length > 0) return textParts.join("\n");
1704
+ }
1705
+ return JSON.stringify(content);
1706
+ }
1643
1707
  function extractInput(params) {
1644
1708
  if (params.prompt) return params.prompt;
1645
1709
  if (params.messages && params.messages.length > 0) {
1646
1710
  const lastUserMessage = [...params.messages].reverse().find((m) => m.role === "user");
1647
1711
  if (lastUserMessage) {
1648
- return typeof lastUserMessage.content === "string" ? lastUserMessage.content : JSON.stringify(lastUserMessage.content);
1712
+ return extractContentText(lastUserMessage.content);
1649
1713
  }
1650
- return JSON.stringify(params.messages);
1714
+ const lastNonSystem = [...params.messages].reverse().find((m) => m.role !== "system");
1715
+ if (lastNonSystem) {
1716
+ return extractContentText(lastNonSystem.content);
1717
+ }
1718
+ return "";
1651
1719
  }
1652
1720
  return "";
1653
1721
  }
@@ -1761,8 +1829,8 @@ function wrapGenerateText(originalFn, client, config) {
1761
1829
  const sessionId = await client.createSession({
1762
1830
  name: `generateText: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1763
1831
  agentName: config.defaultAgent ?? "vercel-ai-sdk",
1764
- userId: config.userId ?? "anonymous",
1765
- convoId: config.convoId,
1832
+ userId: resolveStringOrFn(config.userId, "anonymous"),
1833
+ convoId: resolveStringOrFn(config.convoId),
1766
1834
  metadata: {
1767
1835
  model: modelId,
1768
1836
  provider,
@@ -1862,6 +1930,7 @@ function wrapStreamText(originalFn, client, config) {
1862
1930
  const { modelId, provider } = extractModelInfo(params.model);
1863
1931
  const input = extractInput(params);
1864
1932
  let sessionId = null;
1933
+ let sessionCompleted = false;
1865
1934
  let resolveSessionReady;
1866
1935
  const sessionReady = new Promise((resolve) => {
1867
1936
  resolveSessionReady = resolve;
@@ -1871,8 +1940,8 @@ function wrapStreamText(originalFn, client, config) {
1871
1940
  const id = await client.createSession({
1872
1941
  name: `streamText: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1873
1942
  agentName: config.defaultAgent ?? "vercel-ai-sdk",
1874
- userId: config.userId ?? "anonymous",
1875
- convoId: config.convoId,
1943
+ userId: resolveStringOrFn(config.userId, "anonymous"),
1944
+ convoId: resolveStringOrFn(config.convoId),
1876
1945
  metadata: {
1877
1946
  model: modelId,
1878
1947
  provider,
@@ -1928,7 +1997,8 @@ function wrapStreamText(originalFn, client, config) {
1928
1997
  },
1929
1998
  onFinish: async (event) => {
1930
1999
  await sessionReady;
1931
- if (sessionId) {
2000
+ if (sessionId && !sessionCompleted) {
2001
+ sessionCompleted = true;
1932
2002
  const durationMs = Date.now() - startTime;
1933
2003
  const usage = normalizeUsage(event.totalUsage ?? event.usage);
1934
2004
  const resolvedModelId = event.response?.modelId ?? modelId;
@@ -1958,7 +2028,8 @@ function wrapStreamText(originalFn, client, config) {
1958
2028
  },
1959
2029
  onError: async (event) => {
1960
2030
  await sessionReady;
1961
- if (sessionId) {
2031
+ if (sessionId && !sessionCompleted) {
2032
+ sessionCompleted = true;
1962
2033
  const durationMs = Date.now() - startTime;
1963
2034
  const msg = event.error?.message ?? "Unknown error";
1964
2035
  await client.trackError({
@@ -2008,8 +2079,8 @@ function wrapGenerateObject(originalFn, client, config) {
2008
2079
  const sessionId = await client.createSession({
2009
2080
  name: `generateObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
2010
2081
  agentName: config.defaultAgent ?? "vercel-ai-sdk",
2011
- userId: config.userId ?? "anonymous",
2012
- convoId: config.convoId,
2082
+ userId: resolveStringOrFn(config.userId, "anonymous"),
2083
+ convoId: resolveStringOrFn(config.convoId),
2013
2084
  metadata: {
2014
2085
  model: modelId,
2015
2086
  provider,
@@ -2084,8 +2155,8 @@ function wrapStreamObject(originalFn, client, config) {
2084
2155
  const id = await client.createSession({
2085
2156
  name: `streamObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
2086
2157
  agentName: config.defaultAgent ?? "vercel-ai-sdk",
2087
- userId: config.userId ?? "anonymous",
2088
- convoId: config.convoId,
2158
+ userId: resolveStringOrFn(config.userId, "anonymous"),
2159
+ convoId: resolveStringOrFn(config.convoId),
2089
2160
  metadata: {
2090
2161
  model: modelId,
2091
2162
  provider,
@@ -2375,6 +2446,11 @@ function wrapClaudeAgent(queryFn, wrapOptions) {
2375
2446
  var _defaultClient3 = null;
2376
2447
  var _currentInteraction = createContextVar(null);
2377
2448
  function getClient3() {
2449
+ const configuredClient = getDefaultClient();
2450
+ if (configuredClient) {
2451
+ _defaultClient3 = configuredClient;
2452
+ return configuredClient;
2453
+ }
2378
2454
  if (!_defaultClient3) {
2379
2455
  try {
2380
2456
  _defaultClient3 = new SentrialClient();
@@ -3142,6 +3218,7 @@ export {
3142
3218
  getExperimentId,
3143
3219
  getSessionContext,
3144
3220
  getSystemPrompt,
3221
+ getVariant,
3145
3222
  getVariantName,
3146
3223
  hashValue,
3147
3224
  isExperimentMode,