@sentrial/sdk 0.4.0 → 0.4.3

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.js CHANGED
@@ -68,7 +68,22 @@ var ValidationError = class extends SentrialError {
68
68
  };
69
69
 
70
70
  // src/redact.ts
71
- import { createHash } from "crypto";
71
+ var _createHash;
72
+ try {
73
+ const mod = eval("require")("crypto");
74
+ if (mod?.createHash) {
75
+ _createHash = mod.createHash;
76
+ }
77
+ } catch {
78
+ }
79
+ function getCreateHash() {
80
+ if (!_createHash) {
81
+ throw new Error(
82
+ 'Sentrial PII hash mode requires Node.js crypto module. Use mode "label" or "remove" in browser/edge environments.'
83
+ );
84
+ }
85
+ return _createHash;
86
+ }
72
87
  var DEFAULT_FIELDS = [
73
88
  "userInput",
74
89
  "assistantOutput",
@@ -95,7 +110,7 @@ var BUILTIN_PATTERNS = {
95
110
  ipAddresses: { pattern: IP_ADDRESS_PATTERN, label: "IP_ADDRESS" }
96
111
  };
97
112
  function hashValue(value) {
98
- return createHash("sha256").update(value).digest("hex").slice(0, 6);
113
+ return getCreateHash()("sha256").update(value).digest("hex").slice(0, 6);
99
114
  }
100
115
  function replaceMatch(match, label, mode) {
101
116
  switch (mode) {
@@ -157,6 +172,59 @@ function redactPayload(payload, config) {
157
172
  return result;
158
173
  }
159
174
 
175
+ // src/async-context.ts
176
+ var _AsyncLocalStorage = null;
177
+ try {
178
+ const mod = eval("require")("node:async_hooks");
179
+ if (mod?.AsyncLocalStorage) {
180
+ _AsyncLocalStorage = mod.AsyncLocalStorage;
181
+ }
182
+ } catch {
183
+ }
184
+ var NodeContextVar = class {
185
+ _storage;
186
+ _defaultValue;
187
+ constructor(defaultValue) {
188
+ this._storage = new _AsyncLocalStorage();
189
+ this._defaultValue = defaultValue;
190
+ }
191
+ get() {
192
+ const store = this._storage.getStore();
193
+ return store !== void 0 ? store : this._defaultValue;
194
+ }
195
+ set(value) {
196
+ const previous = this.get();
197
+ this._storage.enterWith(value);
198
+ return { _previous: previous };
199
+ }
200
+ reset(token) {
201
+ this._storage.enterWith(token._previous);
202
+ }
203
+ };
204
+ var SimpleContextVar = class {
205
+ _value;
206
+ constructor(defaultValue) {
207
+ this._value = defaultValue;
208
+ }
209
+ get() {
210
+ return this._value;
211
+ }
212
+ set(value) {
213
+ const previous = this._value;
214
+ this._value = value;
215
+ return { _previous: previous };
216
+ }
217
+ reset(token) {
218
+ this._value = token._previous;
219
+ }
220
+ };
221
+ function createContextVar(defaultValue) {
222
+ if (_AsyncLocalStorage) {
223
+ return new NodeContextVar(defaultValue);
224
+ }
225
+ return new SimpleContextVar(defaultValue);
226
+ }
227
+
160
228
  // src/cost.ts
161
229
  var OPENAI_PRICING = {
162
230
  "gpt-5.2": { input: 5, output: 15 },
@@ -208,7 +276,8 @@ var GOOGLE_PRICING = {
208
276
  "gemini-1.0-pro": { input: 0.5, output: 1.5 }
209
277
  };
210
278
  function findModelKey(model, pricing) {
211
- for (const key of Object.keys(pricing)) {
279
+ const keys = Object.keys(pricing).sort((a, b) => b.length - a.length);
280
+ for (const key of keys) {
212
281
  if (model.startsWith(key)) {
213
282
  return key;
214
283
  }
@@ -236,182 +305,832 @@ function calculateGoogleCost(params) {
236
305
  return calculateCost(inputTokens, outputTokens, GOOGLE_PRICING[modelKey]);
237
306
  }
238
307
 
239
- // src/types.ts
240
- var EventType = /* @__PURE__ */ ((EventType2) => {
241
- EventType2["TOOL_CALL"] = "tool_call";
242
- EventType2["LLM_DECISION"] = "llm_decision";
243
- EventType2["STATE_CHANGE"] = "state_change";
244
- EventType2["ERROR"] = "error";
245
- return EventType2;
246
- })(EventType || {});
247
-
248
- // src/client.ts
249
- var DEFAULT_API_URL = "https://api.sentrial.com";
250
- var MAX_RETRIES = 3;
251
- var INITIAL_BACKOFF_MS = 500;
252
- var MAX_BACKOFF_MS = 8e3;
253
- var BACKOFF_MULTIPLIER = 2;
254
- var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
255
- var REQUEST_TIMEOUT_MS = 1e4;
256
- var SentrialClient = class {
257
- apiUrl;
258
- apiKey;
259
- failSilently;
260
- piiConfig;
261
- piiConfigNeedsHydration = false;
262
- piiHydrationPromise;
263
- currentState = {};
264
- constructor(config = {}) {
265
- this.apiUrl = (config.apiUrl ?? (typeof process !== "undefined" ? process.env?.SENTRIAL_API_URL : void 0) ?? DEFAULT_API_URL).replace(/\/$/, "");
266
- this.apiKey = config.apiKey ?? (typeof process !== "undefined" ? process.env?.SENTRIAL_API_KEY : void 0);
267
- this.failSilently = config.failSilently ?? true;
268
- if (config.pii === true) {
269
- this.piiConfig = { enabled: true };
270
- this.piiConfigNeedsHydration = true;
271
- } else if (config.pii && typeof config.pii === "object") {
272
- this.piiConfig = config.pii;
273
- this.piiConfigNeedsHydration = false;
274
- }
308
+ // src/wrappers.ts
309
+ var _currentSessionId = createContextVar(null);
310
+ var _currentClient = createContextVar(null);
311
+ var _defaultClient = null;
312
+ function setSessionContext(sessionId, client) {
313
+ _currentSessionId.set(sessionId);
314
+ if (client) {
315
+ _currentClient.set(client);
275
316
  }
276
- /**
277
- * Fetch the organization's PII config from the server.
278
- *
279
- * Called lazily on the first request when `pii: true` was passed to the constructor.
280
- * Uses a single shared promise so concurrent requests don't trigger duplicate fetches.
281
- */
282
- async hydratePiiConfig() {
283
- if (!this.piiConfigNeedsHydration) return;
284
- if (this.piiHydrationPromise) {
285
- await this.piiHydrationPromise;
286
- return;
287
- }
288
- this.piiHydrationPromise = (async () => {
289
- try {
290
- const headers = {};
291
- if (this.apiKey) {
292
- headers["Authorization"] = `Bearer ${this.apiKey}`;
293
- }
294
- const controller = new AbortController();
295
- const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
296
- let response;
297
- try {
298
- response = await fetch(`${this.apiUrl}/api/sdk/pii-config`, {
299
- method: "GET",
300
- headers,
301
- signal: controller.signal
302
- });
303
- } finally {
304
- clearTimeout(timeoutId);
305
- }
306
- if (response.ok) {
307
- const data = await response.json();
308
- if (data.config) {
309
- this.piiConfig = {
310
- enabled: data.config.enabled,
311
- mode: data.config.mode,
312
- fields: data.config.fields,
313
- builtinPatterns: data.config.builtinPatterns,
314
- customPatterns: (data.config.customPatterns || []).map(
315
- (cp) => ({
316
- pattern: new RegExp(cp.pattern, "g"),
317
- label: cp.label
318
- })
319
- ),
320
- enhancedDetection: data.config.enhancedDetection
321
- };
322
- }
323
- }
324
- } catch {
325
- }
326
- this.piiConfigNeedsHydration = false;
327
- })();
328
- await this.piiHydrationPromise;
317
+ }
318
+ function clearSessionContext() {
319
+ _currentSessionId.set(null);
320
+ _currentClient.set(null);
321
+ }
322
+ function getSessionContext() {
323
+ return _currentSessionId.get();
324
+ }
325
+ function setDefaultClient(client) {
326
+ _defaultClient = client;
327
+ }
328
+ function _setSessionContextWithTokens(sessionId, client) {
329
+ const _sessionToken = _currentSessionId.set(sessionId);
330
+ const _clientToken = client ? _currentClient.set(client) : _currentClient.set(_currentClient.get());
331
+ return { _sessionToken, _clientToken };
332
+ }
333
+ function _restoreSessionContext(tokens) {
334
+ _currentSessionId.reset(tokens._sessionToken);
335
+ _currentClient.reset(tokens._clientToken);
336
+ }
337
+ function getTrackingClient() {
338
+ return _currentClient.get() ?? _defaultClient;
339
+ }
340
+ function wrapOpenAI(client, options = {}) {
341
+ const { trackWithoutSession = false } = options;
342
+ const chat = client.chat;
343
+ if (!chat?.completions?.create) {
344
+ console.warn("Sentrial: OpenAI client does not have chat.completions.create");
345
+ return client;
329
346
  }
330
- /**
331
- * Make an HTTP request with retry logic and exponential backoff.
332
- *
333
- * Retries on transient failures (network errors, timeouts, 429/5xx).
334
- * Up to MAX_RETRIES attempts with exponential backoff.
335
- */
336
- async safeRequest(method, url, body) {
337
- if (this.piiConfigNeedsHydration) {
338
- await this.hydratePiiConfig();
347
+ const originalCreate = chat.completions.create.bind(chat.completions);
348
+ chat.completions.create = async function(...args) {
349
+ const startTime = Date.now();
350
+ const params = args[0] ?? {};
351
+ const messages = params.messages ?? [];
352
+ const model = params.model ?? "unknown";
353
+ const isStreaming = params.stream === true;
354
+ if (isStreaming && !params.stream_options?.include_usage) {
355
+ args[0] = { ...params, stream_options: { ...params.stream_options, include_usage: true } };
339
356
  }
340
- let lastError;
341
- let backoff = INITIAL_BACKOFF_MS;
342
- for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
343
- try {
344
- const headers = {
345
- "Content-Type": "application/json"
346
- };
347
- if (this.apiKey) {
348
- headers["Authorization"] = `Bearer ${this.apiKey}`;
349
- }
350
- const finalBody = this.piiConfig && body && typeof body === "object" ? redactPayload(body, this.piiConfig) : body;
351
- const controller = new AbortController();
352
- const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
353
- let response;
354
- try {
355
- response = await fetch(url, {
356
- method,
357
- headers,
358
- body: finalBody ? JSON.stringify(finalBody) : void 0,
359
- signal: controller.signal
357
+ try {
358
+ const response = await originalCreate(...args);
359
+ if (isStreaming) {
360
+ return wrapOpenAIStream(response, { startTime, messages, model, trackWithoutSession });
361
+ }
362
+ const durationMs = Date.now() - startTime;
363
+ const promptTokens = response.usage?.prompt_tokens ?? 0;
364
+ const completionTokens = response.usage?.completion_tokens ?? 0;
365
+ const totalTokens = response.usage?.total_tokens ?? 0;
366
+ let outputContent = "";
367
+ const toolCalls = [];
368
+ const msg = response.choices?.[0]?.message;
369
+ if (msg?.content) {
370
+ outputContent = msg.content;
371
+ }
372
+ if (msg?.tool_calls) {
373
+ for (const tc of msg.tool_calls) {
374
+ toolCalls.push({
375
+ name: tc.function?.name ?? "unknown",
376
+ arguments: tc.function?.arguments ?? "{}"
360
377
  });
361
- } finally {
362
- clearTimeout(timeoutId);
363
- }
364
- if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < MAX_RETRIES) {
365
- await this.sleep(backoff);
366
- backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
367
- continue;
368
- }
369
- if (!response.ok) {
370
- const errorBody = await response.text();
371
- let errorData = {};
372
- try {
373
- errorData = JSON.parse(errorBody);
374
- } catch {
375
- }
376
- const error = new ApiError(
377
- errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
378
- response.status,
379
- errorData.error?.code
380
- );
381
- if (this.failSilently) {
382
- console.warn(`Sentrial: Request failed (${method} ${url}):`, error.message);
383
- return null;
384
- }
385
- throw error;
386
- }
387
- return await response.json();
388
- } catch (error) {
389
- if (error instanceof ApiError) {
390
- throw error;
391
- }
392
- lastError = error instanceof Error ? error : new Error(String(error));
393
- if (attempt < MAX_RETRIES) {
394
- await this.sleep(backoff);
395
- backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
396
- continue;
397
378
  }
398
379
  }
380
+ const cost = calculateOpenAICost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
381
+ trackLLMCall({
382
+ provider: "openai",
383
+ model,
384
+ messages,
385
+ output: outputContent,
386
+ toolCalls,
387
+ promptTokens,
388
+ completionTokens,
389
+ totalTokens,
390
+ cost,
391
+ durationMs,
392
+ trackWithoutSession
393
+ });
394
+ return response;
395
+ } catch (error) {
396
+ const durationMs = Date.now() - startTime;
397
+ trackLLMError({
398
+ provider: "openai",
399
+ model,
400
+ messages,
401
+ error,
402
+ durationMs,
403
+ trackWithoutSession
404
+ });
405
+ throw error;
399
406
  }
400
- const networkError = new NetworkError(
401
- lastError?.message ?? "Unknown network error",
402
- lastError
403
- );
404
- if (this.failSilently) {
405
- console.warn(`Sentrial: Request failed after ${MAX_RETRIES + 1} attempts (${method} ${url}):`, networkError.message);
406
- return null;
407
- }
408
- throw networkError;
409
- }
410
- sleep(ms) {
411
- return new Promise((resolve) => setTimeout(resolve, ms));
407
+ };
408
+ return client;
409
+ }
410
+ function wrapAnthropic(client, options = {}) {
411
+ const { trackWithoutSession = false } = options;
412
+ const messages = client.messages;
413
+ if (!messages?.create) {
414
+ console.warn("Sentrial: Anthropic client does not have messages.create");
415
+ return client;
412
416
  }
413
- /**
414
- * Create a new session
417
+ const originalCreate = messages.create.bind(messages);
418
+ messages.create = async function(...args) {
419
+ const startTime = Date.now();
420
+ const params = args[0] ?? {};
421
+ const inputMessages = params.messages ?? [];
422
+ const model = params.model ?? "unknown";
423
+ const system = params.system ?? "";
424
+ const isStreaming = params.stream === true;
425
+ try {
426
+ const response = await originalCreate(...args);
427
+ if (isStreaming) {
428
+ return wrapAnthropicStream(response, {
429
+ startTime,
430
+ messages: inputMessages,
431
+ model,
432
+ system,
433
+ trackWithoutSession
434
+ });
435
+ }
436
+ const durationMs = Date.now() - startTime;
437
+ const promptTokens = response.usage?.input_tokens ?? 0;
438
+ const completionTokens = response.usage?.output_tokens ?? 0;
439
+ const totalTokens = promptTokens + completionTokens;
440
+ let outputContent = "";
441
+ const toolCalls = [];
442
+ if (response.content) {
443
+ for (const block of response.content) {
444
+ if (block.type === "text") {
445
+ outputContent += block.text;
446
+ } else if (block.type === "tool_use") {
447
+ toolCalls.push({
448
+ name: block.name ?? "unknown",
449
+ arguments: JSON.stringify(block.input ?? {})
450
+ });
451
+ }
452
+ }
453
+ }
454
+ const cost = calculateAnthropicCost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
455
+ const fullMessages = system ? [{ role: "system", content: system }, ...inputMessages] : inputMessages;
456
+ trackLLMCall({
457
+ provider: "anthropic",
458
+ model,
459
+ messages: fullMessages,
460
+ output: outputContent,
461
+ toolCalls,
462
+ promptTokens,
463
+ completionTokens,
464
+ totalTokens,
465
+ cost,
466
+ durationMs,
467
+ trackWithoutSession
468
+ });
469
+ return response;
470
+ } catch (error) {
471
+ const durationMs = Date.now() - startTime;
472
+ trackLLMError({
473
+ provider: "anthropic",
474
+ model,
475
+ messages: inputMessages,
476
+ error,
477
+ durationMs,
478
+ trackWithoutSession
479
+ });
480
+ throw error;
481
+ }
482
+ };
483
+ return client;
484
+ }
485
+ function wrapGoogle(model, options = {}) {
486
+ const { trackWithoutSession = false } = options;
487
+ const originalGenerate = model.generateContent;
488
+ if (!originalGenerate) {
489
+ console.warn("Sentrial: Google model does not have generateContent");
490
+ return model;
491
+ }
492
+ model.generateContent = async function(...args) {
493
+ const startTime = Date.now();
494
+ const contents = args[0];
495
+ const modelName = model.model ?? "gemini-unknown";
496
+ const messages = googleContentsToMessages(contents);
497
+ try {
498
+ const response = await originalGenerate.apply(model, args);
499
+ const durationMs = Date.now() - startTime;
500
+ let promptTokens = 0;
501
+ let completionTokens = 0;
502
+ const usageMeta = response.response?.usageMetadata ?? response.usageMetadata;
503
+ if (usageMeta) {
504
+ promptTokens = usageMeta.promptTokenCount ?? 0;
505
+ completionTokens = usageMeta.candidatesTokenCount ?? 0;
506
+ }
507
+ const totalTokens = promptTokens + completionTokens;
508
+ let outputContent = "";
509
+ try {
510
+ outputContent = response.response?.text?.() ?? response.text?.() ?? "";
511
+ } catch {
512
+ }
513
+ const cost = calculateGoogleCost({ model: modelName, inputTokens: promptTokens, outputTokens: completionTokens });
514
+ trackLLMCall({
515
+ provider: "google",
516
+ model: modelName,
517
+ messages,
518
+ output: outputContent,
519
+ promptTokens,
520
+ completionTokens,
521
+ totalTokens,
522
+ cost,
523
+ durationMs,
524
+ trackWithoutSession
525
+ });
526
+ return response;
527
+ } catch (error) {
528
+ const durationMs = Date.now() - startTime;
529
+ trackLLMError({
530
+ provider: "google",
531
+ model: modelName,
532
+ messages,
533
+ error,
534
+ durationMs,
535
+ trackWithoutSession
536
+ });
537
+ throw error;
538
+ }
539
+ };
540
+ return model;
541
+ }
542
+ function googleContentsToMessages(contents) {
543
+ if (typeof contents === "string") {
544
+ return [{ role: "user", content: contents }];
545
+ }
546
+ if (Array.isArray(contents)) {
547
+ return contents.map((item) => {
548
+ if (typeof item === "string") {
549
+ return { role: "user", content: item };
550
+ }
551
+ if (item && typeof item === "object") {
552
+ return { role: item.role ?? "user", content: String(item.content ?? item) };
553
+ }
554
+ return { role: "user", content: String(item) };
555
+ });
556
+ }
557
+ return [{ role: "user", content: String(contents) }];
558
+ }
559
+ function wrapLLM(client, provider) {
560
+ if (provider === "openai" || client.chat?.completions?.create) {
561
+ return wrapOpenAI(client);
562
+ }
563
+ if (provider === "anthropic" || client.messages?.create) {
564
+ return wrapAnthropic(client);
565
+ }
566
+ if (provider === "google" || client.generateContent) {
567
+ return wrapGoogle(client);
568
+ }
569
+ console.warn("Sentrial: Unknown LLM client type. No auto-tracking applied.");
570
+ return client;
571
+ }
572
+ function wrapOpenAIStream(stream, ctx) {
573
+ let fullContent = "";
574
+ let usage = null;
575
+ const toolCallMap = /* @__PURE__ */ new Map();
576
+ let tracked = false;
577
+ const originalIterator = stream[Symbol.asyncIterator]?.bind(stream);
578
+ if (!originalIterator) return stream;
579
+ const trackResult = () => {
580
+ if (tracked) return;
581
+ tracked = true;
582
+ const durationMs = Date.now() - ctx.startTime;
583
+ const promptTokens = usage?.prompt_tokens ?? 0;
584
+ const completionTokens = usage?.completion_tokens ?? 0;
585
+ const totalTokens = usage?.total_tokens ?? promptTokens + completionTokens;
586
+ const cost = calculateOpenAICost({ model: ctx.model, inputTokens: promptTokens, outputTokens: completionTokens });
587
+ trackLLMCall({
588
+ provider: "openai",
589
+ model: ctx.model,
590
+ messages: ctx.messages,
591
+ output: fullContent,
592
+ toolCalls: Array.from(toolCallMap.values()),
593
+ promptTokens,
594
+ completionTokens,
595
+ totalTokens,
596
+ cost,
597
+ durationMs,
598
+ trackWithoutSession: ctx.trackWithoutSession
599
+ });
600
+ };
601
+ return new Proxy(stream, {
602
+ get(target, prop, receiver) {
603
+ if (prop === Symbol.asyncIterator) {
604
+ return function() {
605
+ const iter = originalIterator();
606
+ return {
607
+ async next() {
608
+ const result = await iter.next();
609
+ if (!result.done) {
610
+ const chunk = result.value;
611
+ const delta = chunk.choices?.[0]?.delta;
612
+ if (delta?.content) fullContent += delta.content;
613
+ if (delta?.tool_calls) {
614
+ for (const tc of delta.tool_calls) {
615
+ const idx = tc.index ?? 0;
616
+ const existing = toolCallMap.get(idx) ?? { name: "", arguments: "" };
617
+ if (tc.function?.name) existing.name = tc.function.name;
618
+ if (tc.function?.arguments) existing.arguments += tc.function.arguments;
619
+ toolCallMap.set(idx, existing);
620
+ }
621
+ }
622
+ if (chunk.usage) usage = chunk.usage;
623
+ } else {
624
+ trackResult();
625
+ }
626
+ return result;
627
+ },
628
+ async return(value) {
629
+ trackResult();
630
+ return iter.return?.(value) ?? { done: true, value: void 0 };
631
+ },
632
+ async throw(error) {
633
+ return iter.throw?.(error) ?? { done: true, value: void 0 };
634
+ }
635
+ };
636
+ };
637
+ }
638
+ return Reflect.get(target, prop, receiver);
639
+ }
640
+ });
641
+ }
642
+ function wrapAnthropicStream(stream, ctx) {
643
+ let fullContent = "";
644
+ let inputTokens = 0;
645
+ let outputTokens = 0;
646
+ const toolCallsById = /* @__PURE__ */ new Map();
647
+ let currentBlockIdx = -1;
648
+ let tracked = false;
649
+ const originalIterator = stream[Symbol.asyncIterator]?.bind(stream);
650
+ if (!originalIterator) return stream;
651
+ const trackResult = () => {
652
+ if (tracked) return;
653
+ tracked = true;
654
+ const durationMs = Date.now() - ctx.startTime;
655
+ const totalTokens = inputTokens + outputTokens;
656
+ const cost = calculateAnthropicCost({ model: ctx.model, inputTokens, outputTokens });
657
+ const fullMessages = ctx.system ? [{ role: "system", content: ctx.system }, ...ctx.messages] : ctx.messages;
658
+ trackLLMCall({
659
+ provider: "anthropic",
660
+ model: ctx.model,
661
+ messages: fullMessages,
662
+ output: fullContent,
663
+ toolCalls: Array.from(toolCallsById.values()),
664
+ promptTokens: inputTokens,
665
+ completionTokens: outputTokens,
666
+ totalTokens,
667
+ cost,
668
+ durationMs,
669
+ trackWithoutSession: ctx.trackWithoutSession
670
+ });
671
+ };
672
+ return new Proxy(stream, {
673
+ get(target, prop, receiver) {
674
+ if (prop === Symbol.asyncIterator) {
675
+ return function() {
676
+ const iter = originalIterator();
677
+ return {
678
+ async next() {
679
+ const result = await iter.next();
680
+ if (!result.done) {
681
+ const event = result.value;
682
+ if (event.type === "content_block_start") {
683
+ currentBlockIdx = event.index ?? currentBlockIdx + 1;
684
+ if (event.content_block?.type === "tool_use") {
685
+ toolCallsById.set(currentBlockIdx, {
686
+ name: event.content_block.name ?? "unknown",
687
+ arguments: ""
688
+ });
689
+ }
690
+ }
691
+ if (event.type === "content_block_delta") {
692
+ if (event.delta?.text) {
693
+ fullContent += event.delta.text;
694
+ }
695
+ if (event.delta?.type === "input_json_delta" && event.delta?.partial_json) {
696
+ const idx = event.index ?? currentBlockIdx;
697
+ const existing = toolCallsById.get(idx);
698
+ if (existing) {
699
+ existing.arguments += event.delta.partial_json;
700
+ }
701
+ }
702
+ }
703
+ if (event.type === "message_start" && event.message?.usage) {
704
+ inputTokens = event.message.usage.input_tokens ?? 0;
705
+ }
706
+ if (event.type === "message_delta" && event.usage) {
707
+ outputTokens = event.usage.output_tokens ?? 0;
708
+ }
709
+ } else {
710
+ trackResult();
711
+ }
712
+ return result;
713
+ },
714
+ async return(value) {
715
+ trackResult();
716
+ return iter.return?.(value) ?? { done: true, value: void 0 };
717
+ },
718
+ async throw(error) {
719
+ return iter.throw?.(error) ?? { done: true, value: void 0 };
720
+ }
721
+ };
722
+ };
723
+ }
724
+ return Reflect.get(target, prop, receiver);
725
+ }
726
+ });
727
+ }
728
+ function trackLLMCall(params) {
729
+ const client = getTrackingClient();
730
+ if (!client) return;
731
+ const sessionId = _currentSessionId.get();
732
+ if (!sessionId && !params.trackWithoutSession) {
733
+ return;
734
+ }
735
+ const toolOutput = {
736
+ content: params.output,
737
+ tokens: {
738
+ prompt: params.promptTokens,
739
+ completion: params.completionTokens,
740
+ total: params.totalTokens
741
+ },
742
+ cost_usd: params.cost
743
+ };
744
+ if (params.toolCalls && params.toolCalls.length > 0) {
745
+ toolOutput.tool_calls = params.toolCalls;
746
+ }
747
+ if (sessionId) {
748
+ client.trackToolCall({
749
+ sessionId,
750
+ toolName: `llm:${params.provider}:${params.model}`,
751
+ toolInput: {
752
+ messages: params.messages,
753
+ model: params.model,
754
+ provider: params.provider
755
+ },
756
+ toolOutput,
757
+ reasoning: `LLM call to ${params.provider} ${params.model}`,
758
+ estimatedCost: params.cost,
759
+ tokenCount: params.totalTokens,
760
+ metadata: {
761
+ provider: params.provider,
762
+ model: params.model,
763
+ duration_ms: params.durationMs,
764
+ prompt_tokens: params.promptTokens,
765
+ completion_tokens: params.completionTokens
766
+ }
767
+ }).catch((err) => {
768
+ console.warn("Sentrial: Failed to track LLM call:", err.message);
769
+ });
770
+ } else if (params.trackWithoutSession) {
771
+ client.createSession({
772
+ name: `LLM: ${params.provider}/${params.model}`,
773
+ agentName: `${params.provider}-wrapper`,
774
+ userId: "anonymous"
775
+ }).then((sid) => {
776
+ if (!sid) return;
777
+ return client.trackToolCall({
778
+ sessionId: sid,
779
+ toolName: `llm:${params.provider}:${params.model}`,
780
+ toolInput: {
781
+ messages: params.messages,
782
+ model: params.model,
783
+ provider: params.provider
784
+ },
785
+ toolOutput,
786
+ estimatedCost: params.cost,
787
+ tokenCount: params.totalTokens,
788
+ metadata: {
789
+ provider: params.provider,
790
+ model: params.model,
791
+ duration_ms: params.durationMs
792
+ }
793
+ }).then(() => {
794
+ return client.completeSession({
795
+ sessionId: sid,
796
+ success: true,
797
+ estimatedCost: params.cost,
798
+ promptTokens: params.promptTokens,
799
+ completionTokens: params.completionTokens,
800
+ totalTokens: params.totalTokens,
801
+ durationMs: params.durationMs
802
+ });
803
+ });
804
+ }).catch((err) => {
805
+ console.warn("Sentrial: Failed to track standalone LLM call:", err.message);
806
+ });
807
+ }
808
+ }
809
+ function trackLLMError(params) {
810
+ const client = getTrackingClient();
811
+ if (!client) return;
812
+ const sessionId = _currentSessionId.get();
813
+ if (!sessionId && !params.trackWithoutSession) {
814
+ return;
815
+ }
816
+ if (!sessionId) return;
817
+ client.trackError({
818
+ sessionId,
819
+ errorMessage: params.error.message,
820
+ errorType: params.error.name,
821
+ toolName: `llm:${params.provider}:${params.model}`,
822
+ metadata: {
823
+ provider: params.provider,
824
+ model: params.model,
825
+ duration_ms: params.durationMs
826
+ }
827
+ }).catch((err) => {
828
+ console.warn("Sentrial: Failed to track LLM error:", err.message);
829
+ });
830
+ }
831
+
832
+ // src/batcher.ts
833
+ var EventBatcher = class {
834
+ queue = [];
835
+ flushIntervalMs;
836
+ flushThreshold;
837
+ maxQueueSize;
838
+ timer = null;
839
+ sendFn;
840
+ flushing = false;
841
+ shutdownCalled = false;
842
+ exitHandler;
843
+ constructor(sendFn, config = {}) {
844
+ this.sendFn = sendFn;
845
+ this.flushIntervalMs = config.flushIntervalMs ?? 1e3;
846
+ this.flushThreshold = config.flushThreshold ?? 10;
847
+ this.maxQueueSize = config.maxQueueSize ?? 1e3;
848
+ this.timer = setInterval(() => {
849
+ void this.flush();
850
+ }, this.flushIntervalMs);
851
+ if (this.timer && typeof this.timer === "object" && "unref" in this.timer) {
852
+ this.timer.unref();
853
+ }
854
+ this.exitHandler = () => {
855
+ void this.shutdown();
856
+ };
857
+ if (typeof process !== "undefined" && process.on) {
858
+ process.on("beforeExit", this.exitHandler);
859
+ }
860
+ }
861
+ /**
862
+ * Enqueue an event for batched delivery.
863
+ *
864
+ * If the queue hits `flushThreshold`, an automatic flush is triggered.
865
+ * If the queue is full (`maxQueueSize`), the oldest event is dropped.
866
+ */
867
+ enqueue(method, url, body) {
868
+ if (this.shutdownCalled) return;
869
+ if (this.queue.length >= this.maxQueueSize) {
870
+ this.queue.shift();
871
+ if (typeof console !== "undefined") {
872
+ console.warn(
873
+ `Sentrial: Event queue full (${this.maxQueueSize}), dropping oldest event`
874
+ );
875
+ }
876
+ }
877
+ this.queue.push({ method, url, body });
878
+ if (this.queue.length >= this.flushThreshold) {
879
+ void this.flush();
880
+ }
881
+ }
882
+ /**
883
+ * Flush all queued events to the API.
884
+ *
885
+ * Drains the queue and fires all requests in parallel. Safe to call
886
+ * concurrently — only one flush runs at a time.
887
+ */
888
+ async flush() {
889
+ if (this.flushing || this.queue.length === 0) return;
890
+ this.flushing = true;
891
+ const batch = this.queue.splice(0, this.queue.length);
892
+ 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
+ );
902
+ } finally {
903
+ this.flushing = false;
904
+ }
905
+ }
906
+ /**
907
+ * Stop the batcher: clear the timer, flush remaining events, remove exit handler.
908
+ */
909
+ async shutdown() {
910
+ if (this.shutdownCalled) return;
911
+ this.shutdownCalled = true;
912
+ if (this.timer !== null) {
913
+ clearInterval(this.timer);
914
+ this.timer = null;
915
+ }
916
+ if (typeof process !== "undefined" && process.removeListener) {
917
+ process.removeListener("beforeExit", this.exitHandler);
918
+ }
919
+ this.flushing = false;
920
+ await this.flush();
921
+ }
922
+ /** Number of events currently queued. */
923
+ get size() {
924
+ return this.queue.length;
925
+ }
926
+ };
927
+
928
+ // src/types.ts
929
+ var EventType = /* @__PURE__ */ ((EventType2) => {
930
+ EventType2["TOOL_CALL"] = "tool_call";
931
+ EventType2["LLM_DECISION"] = "llm_decision";
932
+ EventType2["STATE_CHANGE"] = "state_change";
933
+ EventType2["ERROR"] = "error";
934
+ return EventType2;
935
+ })(EventType || {});
936
+
937
+ // src/client.ts
938
+ var DEFAULT_API_URL = "https://api.sentrial.com";
939
+ var MAX_RETRIES = 3;
940
+ var INITIAL_BACKOFF_MS = 500;
941
+ var MAX_BACKOFF_MS = 8e3;
942
+ var BACKOFF_MULTIPLIER = 2;
943
+ var RETRYABLE_STATUS_CODES = /* @__PURE__ */ new Set([408, 429, 500, 502, 503, 504]);
944
+ var REQUEST_TIMEOUT_MS = 1e4;
945
+ var SentrialClient = class {
946
+ apiUrl;
947
+ apiKey;
948
+ failSilently;
949
+ piiConfig;
950
+ piiConfigNeedsHydration = false;
951
+ piiHydrationPromise;
952
+ _stateVar = createContextVar({});
953
+ batcher;
954
+ /** Per-session cost/token accumulator — populated by trackToolCall/trackDecision */
955
+ sessionAccumulators = /* @__PURE__ */ new Map();
956
+ get currentState() {
957
+ return this._stateVar.get();
958
+ }
959
+ set currentState(value) {
960
+ this._stateVar.set(value);
961
+ }
962
+ constructor(config = {}) {
963
+ this.apiUrl = (config.apiUrl ?? (typeof process !== "undefined" ? process.env?.SENTRIAL_API_URL : void 0) ?? DEFAULT_API_URL).replace(/\/$/, "");
964
+ this.apiKey = config.apiKey ?? (typeof process !== "undefined" ? process.env?.SENTRIAL_API_KEY : void 0);
965
+ this.failSilently = config.failSilently ?? true;
966
+ if (config.pii === true) {
967
+ this.piiConfig = { enabled: true };
968
+ this.piiConfigNeedsHydration = true;
969
+ } else if (config.pii && typeof config.pii === "object") {
970
+ this.piiConfig = config.pii;
971
+ this.piiConfigNeedsHydration = false;
972
+ }
973
+ if (config.batching?.enabled) {
974
+ this.batcher = new EventBatcher(
975
+ (method, url, body) => this.safeRequest(method, url, body),
976
+ config.batching
977
+ );
978
+ }
979
+ }
980
+ /**
981
+ * Fetch the organization's PII config from the server.
982
+ *
983
+ * Called lazily on the first request when `pii: true` was passed to the constructor.
984
+ * Uses a single shared promise so concurrent requests don't trigger duplicate fetches.
985
+ */
986
+ async hydratePiiConfig() {
987
+ if (!this.piiConfigNeedsHydration) return;
988
+ if (this.piiHydrationPromise) {
989
+ await this.piiHydrationPromise;
990
+ return;
991
+ }
992
+ this.piiHydrationPromise = (async () => {
993
+ try {
994
+ const headers = {};
995
+ if (this.apiKey) {
996
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
997
+ }
998
+ const controller = new AbortController();
999
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
1000
+ let response;
1001
+ try {
1002
+ response = await fetch(`${this.apiUrl}/api/sdk/pii-config`, {
1003
+ method: "GET",
1004
+ headers,
1005
+ signal: controller.signal
1006
+ });
1007
+ } finally {
1008
+ clearTimeout(timeoutId);
1009
+ }
1010
+ if (response.ok) {
1011
+ const data = await response.json();
1012
+ if (data.config) {
1013
+ this.piiConfig = {
1014
+ enabled: data.config.enabled,
1015
+ mode: data.config.mode,
1016
+ fields: data.config.fields,
1017
+ builtinPatterns: data.config.builtinPatterns,
1018
+ customPatterns: (data.config.customPatterns || []).map(
1019
+ (cp) => ({
1020
+ pattern: new RegExp(cp.pattern, "g"),
1021
+ label: cp.label
1022
+ })
1023
+ ),
1024
+ enhancedDetection: data.config.enhancedDetection
1025
+ };
1026
+ }
1027
+ }
1028
+ } catch {
1029
+ }
1030
+ this.piiConfigNeedsHydration = false;
1031
+ })();
1032
+ await this.piiHydrationPromise;
1033
+ }
1034
+ /**
1035
+ * Make an HTTP request with retry logic and exponential backoff.
1036
+ *
1037
+ * Retries on transient failures (network errors, timeouts, 429/5xx).
1038
+ * Up to MAX_RETRIES attempts with exponential backoff.
1039
+ */
1040
+ async safeRequest(method, url, body) {
1041
+ if (this.piiConfigNeedsHydration) {
1042
+ await this.hydratePiiConfig();
1043
+ }
1044
+ let lastError;
1045
+ let backoff = INITIAL_BACKOFF_MS;
1046
+ for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
1047
+ try {
1048
+ const headers = {
1049
+ "Content-Type": "application/json"
1050
+ };
1051
+ if (this.apiKey) {
1052
+ headers["Authorization"] = `Bearer ${this.apiKey}`;
1053
+ }
1054
+ const finalBody = this.piiConfig && body && typeof body === "object" ? redactPayload(body, this.piiConfig) : body;
1055
+ const controller = new AbortController();
1056
+ const timeoutId = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);
1057
+ let response;
1058
+ try {
1059
+ response = await fetch(url, {
1060
+ method,
1061
+ headers,
1062
+ body: finalBody ? JSON.stringify(finalBody) : void 0,
1063
+ signal: controller.signal
1064
+ });
1065
+ } finally {
1066
+ clearTimeout(timeoutId);
1067
+ }
1068
+ if (RETRYABLE_STATUS_CODES.has(response.status) && attempt < MAX_RETRIES) {
1069
+ await this.sleep(backoff);
1070
+ backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
1071
+ continue;
1072
+ }
1073
+ if (!response.ok) {
1074
+ const errorBody = await response.text();
1075
+ let errorData = {};
1076
+ try {
1077
+ errorData = JSON.parse(errorBody);
1078
+ } catch {
1079
+ }
1080
+ const error = new ApiError(
1081
+ errorData.error?.message || `HTTP ${response.status}: ${response.statusText}`,
1082
+ response.status,
1083
+ errorData.error?.code
1084
+ );
1085
+ if (this.failSilently) {
1086
+ console.warn(`Sentrial: Request failed (${method} ${url}):`, error.message);
1087
+ return null;
1088
+ }
1089
+ throw error;
1090
+ }
1091
+ return await response.json();
1092
+ } catch (error) {
1093
+ if (error instanceof ApiError) {
1094
+ throw error;
1095
+ }
1096
+ lastError = error instanceof Error ? error : new Error(String(error));
1097
+ if (attempt < MAX_RETRIES) {
1098
+ await this.sleep(backoff);
1099
+ backoff = Math.min(backoff * BACKOFF_MULTIPLIER, MAX_BACKOFF_MS);
1100
+ continue;
1101
+ }
1102
+ }
1103
+ }
1104
+ const networkError = new NetworkError(
1105
+ lastError?.message ?? "Unknown network error",
1106
+ lastError
1107
+ );
1108
+ if (this.failSilently) {
1109
+ console.warn(`Sentrial: Request failed after ${MAX_RETRIES + 1} attempts (${method} ${url}):`, networkError.message);
1110
+ return null;
1111
+ }
1112
+ throw networkError;
1113
+ }
1114
+ sleep(ms) {
1115
+ return new Promise((resolve) => setTimeout(resolve, ms));
1116
+ }
1117
+ accumulate(sessionId, cost, tokenCount, toolOutput) {
1118
+ let acc = this.sessionAccumulators.get(sessionId);
1119
+ if (!acc) {
1120
+ acc = { cost: 0, promptTokens: 0, completionTokens: 0, totalTokens: 0 };
1121
+ this.sessionAccumulators.set(sessionId, acc);
1122
+ }
1123
+ if (cost != null) acc.cost += cost;
1124
+ if (tokenCount != null) acc.totalTokens += tokenCount;
1125
+ const rawTokens = toolOutput?.tokens;
1126
+ if (rawTokens && typeof rawTokens === "object" && !Array.isArray(rawTokens)) {
1127
+ const tokens = rawTokens;
1128
+ if (typeof tokens.prompt === "number") acc.promptTokens += tokens.prompt;
1129
+ if (typeof tokens.completion === "number") acc.completionTokens += tokens.completion;
1130
+ }
1131
+ }
1132
+ /**
1133
+ * Create a new session
415
1134
  *
416
1135
  * @param params - Session creation parameters
417
1136
  * @returns Session ID, or null if the request failed and failSilently is true
@@ -444,6 +1163,7 @@ var SentrialClient = class {
444
1163
  * @returns Event data
445
1164
  */
446
1165
  async trackToolCall(params) {
1166
+ this.accumulate(params.sessionId, params.estimatedCost, params.tokenCount, params.toolOutput);
447
1167
  const stateBefore = { ...this.currentState };
448
1168
  this.currentState[`${params.toolName}_result`] = params.toolOutput;
449
1169
  const payload = {
@@ -462,6 +1182,10 @@ var SentrialClient = class {
462
1182
  if (params.traceId !== void 0) payload.traceId = params.traceId;
463
1183
  if (params.spanId !== void 0) payload.spanId = params.spanId;
464
1184
  if (params.metadata !== void 0) payload.metadata = params.metadata;
1185
+ if (this.batcher) {
1186
+ this.batcher.enqueue("POST", `${this.apiUrl}/api/sdk/events`, payload);
1187
+ return null;
1188
+ }
465
1189
  return this.safeRequest("POST", `${this.apiUrl}/api/sdk/events`, payload);
466
1190
  }
467
1191
  /**
@@ -471,6 +1195,7 @@ var SentrialClient = class {
471
1195
  * @returns Event data
472
1196
  */
473
1197
  async trackDecision(params) {
1198
+ this.accumulate(params.sessionId, params.estimatedCost, params.tokenCount);
474
1199
  const stateBefore = { ...this.currentState };
475
1200
  const payload = {
476
1201
  sessionId: params.sessionId,
@@ -486,6 +1211,10 @@ var SentrialClient = class {
486
1211
  if (params.traceId !== void 0) payload.traceId = params.traceId;
487
1212
  if (params.spanId !== void 0) payload.spanId = params.spanId;
488
1213
  if (params.metadata !== void 0) payload.metadata = params.metadata;
1214
+ if (this.batcher) {
1215
+ this.batcher.enqueue("POST", `${this.apiUrl}/api/sdk/events`, payload);
1216
+ return null;
1217
+ }
489
1218
  return this.safeRequest("POST", `${this.apiUrl}/api/sdk/events`, payload);
490
1219
  }
491
1220
  /**
@@ -512,6 +1241,10 @@ var SentrialClient = class {
512
1241
  if (params.traceId !== void 0) payload.traceId = params.traceId;
513
1242
  if (params.spanId !== void 0) payload.spanId = params.spanId;
514
1243
  if (params.metadata !== void 0) payload.metadata = params.metadata;
1244
+ if (this.batcher) {
1245
+ this.batcher.enqueue("POST", `${this.apiUrl}/api/sdk/events`, payload);
1246
+ return null;
1247
+ }
515
1248
  return this.safeRequest("POST", `${this.apiUrl}/api/sdk/events`, payload);
516
1249
  }
517
1250
  /**
@@ -557,6 +1290,10 @@ var SentrialClient = class {
557
1290
  if (params.metadata) {
558
1291
  payload.metadata = params.metadata;
559
1292
  }
1293
+ if (this.batcher) {
1294
+ this.batcher.enqueue("POST", `${this.apiUrl}/api/sdk/events`, payload);
1295
+ return null;
1296
+ }
560
1297
  return this.safeRequest("POST", `${this.apiUrl}/api/sdk/events`, payload);
561
1298
  }
562
1299
  /**
@@ -586,6 +1323,17 @@ var SentrialClient = class {
586
1323
  * ```
587
1324
  */
588
1325
  async completeSession(params) {
1326
+ if (this.batcher) {
1327
+ await this.batcher.flush();
1328
+ }
1329
+ const acc = this.sessionAccumulators.get(params.sessionId);
1330
+ 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 };
1335
+ this.sessionAccumulators.delete(params.sessionId);
1336
+ }
589
1337
  const payload = {
590
1338
  status: params.success !== false ? "completed" : "failed",
591
1339
  success: params.success ?? true
@@ -606,6 +1354,27 @@ var SentrialClient = class {
606
1354
  payload
607
1355
  );
608
1356
  }
1357
+ /**
1358
+ * Flush any queued events immediately.
1359
+ *
1360
+ * No-op if batching is not enabled.
1361
+ */
1362
+ async flush() {
1363
+ if (this.batcher) {
1364
+ await this.batcher.flush();
1365
+ }
1366
+ }
1367
+ /**
1368
+ * Shut down the event batcher, flushing remaining events.
1369
+ *
1370
+ * Call this before your process exits for a clean shutdown.
1371
+ * No-op if batching is not enabled.
1372
+ */
1373
+ async shutdown() {
1374
+ if (this.batcher) {
1375
+ await this.batcher.shutdown();
1376
+ }
1377
+ }
609
1378
  /**
610
1379
  * Begin tracking an interaction (simplified API)
611
1380
  *
@@ -642,13 +1411,18 @@ var SentrialClient = class {
642
1411
  if (params.input) {
643
1412
  this.currentState.input = params.input;
644
1413
  }
1414
+ let sessionTokens;
1415
+ if (sessionId) {
1416
+ sessionTokens = _setSessionContextWithTokens(sessionId, this);
1417
+ }
645
1418
  return new Interaction({
646
1419
  client: this,
647
1420
  sessionId,
648
1421
  eventId,
649
1422
  userId: params.userId,
650
1423
  event: params.event,
651
- userInput: params.input
1424
+ userInput: params.input,
1425
+ sessionTokens
652
1426
  });
653
1427
  }
654
1428
  // Cost calculation static methods for convenience
@@ -665,12 +1439,15 @@ var Interaction = class {
665
1439
  userId;
666
1440
  /** Event name for this interaction */
667
1441
  event;
1442
+ startTime = Date.now();
668
1443
  finished = false;
669
1444
  success = true;
670
1445
  failureReason;
671
1446
  output;
672
1447
  userInput;
673
1448
  degraded;
1449
+ /** Context tokens for restoring previous session context on finish() */
1450
+ sessionTokens;
674
1451
  constructor(config) {
675
1452
  this.client = config.client;
676
1453
  this.sessionId = config.sessionId;
@@ -679,6 +1456,7 @@ var Interaction = class {
679
1456
  this.event = config.event;
680
1457
  this.userInput = config.userInput;
681
1458
  this.degraded = config.sessionId === null;
1459
+ this.sessionTokens = config.sessionTokens;
682
1460
  }
683
1461
  /**
684
1462
  * Set the output for this interaction
@@ -714,18 +1492,24 @@ var Interaction = class {
714
1492
  }
715
1493
  this.finished = true;
716
1494
  const finalOutput = params.output ?? this.output;
717
- return this.client.completeSession({
1495
+ const result = await this.client.completeSession({
718
1496
  sessionId: this.sessionId,
719
1497
  success: params.success ?? this.success,
720
1498
  failureReason: params.failureReason ?? this.failureReason,
721
1499
  estimatedCost: params.estimatedCost,
722
1500
  customMetrics: params.customMetrics,
1501
+ durationMs: params.durationMs ?? Date.now() - this.startTime,
723
1502
  promptTokens: params.promptTokens,
724
1503
  completionTokens: params.completionTokens,
725
1504
  totalTokens: params.totalTokens,
726
1505
  userInput: this.userInput,
727
1506
  assistantOutput: finalOutput
728
1507
  });
1508
+ if (this.sessionTokens) {
1509
+ _restoreSessionContext(this.sessionTokens);
1510
+ this.sessionTokens = void 0;
1511
+ }
1512
+ return result;
729
1513
  }
730
1514
  /**
731
1515
  * Track a tool call within this interaction
@@ -785,16 +1569,24 @@ function configure(config) {
785
1569
  function begin(params) {
786
1570
  return getClient().begin(params);
787
1571
  }
1572
+ async function flush() {
1573
+ if (defaultClient) await defaultClient.flush();
1574
+ }
1575
+ async function shutdown() {
1576
+ if (defaultClient) await defaultClient.shutdown();
1577
+ }
788
1578
  var sentrial = {
789
1579
  configure,
790
- begin
1580
+ begin,
1581
+ flush,
1582
+ shutdown
791
1583
  };
792
1584
 
793
1585
  // src/vercel.ts
794
- var _defaultClient = null;
1586
+ var _defaultClient2 = null;
795
1587
  var _globalConfig = {};
796
1588
  function configureVercel(config) {
797
- _defaultClient = new SentrialClient({
1589
+ _defaultClient2 = new SentrialClient({
798
1590
  apiKey: config.apiKey,
799
1591
  apiUrl: config.apiUrl,
800
1592
  failSilently: config.failSilently ?? true
@@ -806,19 +1598,20 @@ function configureVercel(config) {
806
1598
  };
807
1599
  }
808
1600
  function getClient2() {
809
- if (!_defaultClient) {
810
- _defaultClient = new SentrialClient();
1601
+ if (!_defaultClient2) {
1602
+ _defaultClient2 = new SentrialClient();
811
1603
  }
812
- return _defaultClient;
1604
+ return _defaultClient2;
813
1605
  }
814
1606
  function extractModelInfo(model) {
815
1607
  const modelId = model.modelId || model.id || "unknown";
816
- const provider = model.provider || guessProvider(modelId);
1608
+ const rawProvider = model.provider || "";
1609
+ const provider = rawProvider.split(".")[0] || guessProvider(modelId);
817
1610
  return { modelId, provider };
818
1611
  }
819
1612
  function guessProvider(modelId) {
820
1613
  const id = modelId.toLowerCase();
821
- if (id.includes("gpt") || id.includes("o1") || id.includes("o3") || id.includes("o4") || id.startsWith("chatgpt")) return "openai";
1614
+ if (id.includes("gpt") || id.startsWith("o1") || id.startsWith("o3") || id.startsWith("o4") || id.startsWith("chatgpt")) return "openai";
822
1615
  if (id.includes("claude")) return "anthropic";
823
1616
  if (id.includes("gemini")) return "google";
824
1617
  if (id.includes("mistral") || id.includes("mixtral") || id.includes("codestral") || id.includes("pixtral")) return "mistral";
@@ -844,7 +1637,7 @@ function calculateCostForCall(provider, modelId, promptTokens, completionTokens)
844
1637
  case "mistral":
845
1638
  return promptTokens / 1e6 * 2 + completionTokens / 1e6 * 6;
846
1639
  default:
847
- return promptTokens * 3e-6 + completionTokens * 6e-6;
1640
+ return 0;
848
1641
  }
849
1642
  }
850
1643
  function extractInput(params) {
@@ -858,6 +1651,15 @@ function extractInput(params) {
858
1651
  }
859
1652
  return "";
860
1653
  }
1654
+ function normalizeUsage(usage) {
1655
+ if (!usage) return void 0;
1656
+ const u = usage;
1657
+ const promptTokens = (u.inputTokens ?? u.promptTokens ?? 0) || 0;
1658
+ const completionTokens = (u.outputTokens ?? u.completionTokens ?? 0) || 0;
1659
+ const totalTokens = (u.totalTokens ?? promptTokens + completionTokens) || 0;
1660
+ if (promptTokens === 0 && completionTokens === 0 && totalTokens === 0) return void 0;
1661
+ return { promptTokens, completionTokens, totalTokens };
1662
+ }
861
1663
  function wrapTools(tools, sessionId, client) {
862
1664
  if (!tools) return void 0;
863
1665
  const wrappedTools = {};
@@ -910,295 +1712,126 @@ function wrapToolsAsync(tools, sessionPromise, client) {
910
1712
  ...tool,
911
1713
  execute: async (...args) => {
912
1714
  const startTime = Date.now();
913
- const sid = await sessionPromise;
914
1715
  try {
915
1716
  const result = await originalExecute(...args);
916
1717
  const durationMs = Date.now() - startTime;
917
- if (sid) {
918
- client.trackToolCall({
919
- sessionId: sid,
920
- toolName,
921
- toolInput: args[0],
922
- toolOutput: result,
923
- reasoning: `Tool executed in ${durationMs}ms`
924
- }).catch(() => {
925
- });
926
- }
927
- return result;
928
- } catch (error) {
929
- const durationMs = Date.now() - startTime;
930
- if (sid) {
931
- client.trackToolCall({
932
- sessionId: sid,
933
- toolName,
934
- toolInput: args[0],
935
- toolOutput: {},
936
- toolError: { message: error instanceof Error ? error.message : "Unknown error" },
937
- reasoning: `Tool failed after ${durationMs}ms`
938
- }).catch(() => {
939
- });
940
- }
941
- throw error;
942
- }
943
- }
944
- };
945
- } else {
946
- wrappedTools[toolName] = tool;
947
- }
948
- }
949
- return wrappedTools;
950
- }
951
- function wrapGenerateText(originalFn, client, config) {
952
- return async (params) => {
953
- const startTime = Date.now();
954
- const { modelId, provider } = extractModelInfo(params.model);
955
- const input = extractInput(params);
956
- const sessionId = await client.createSession({
957
- name: `generateText: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
958
- agentName: config.defaultAgent ?? "vercel-ai-sdk",
959
- userId: config.userId ?? "anonymous",
960
- convoId: config.convoId,
961
- metadata: {
962
- model: modelId,
963
- provider,
964
- function: "generateText",
965
- ...params.maxSteps ? { maxSteps: params.maxSteps } : {}
966
- }
967
- });
968
- if (!sessionId) {
969
- return originalFn(params);
970
- }
971
- await client.setInput(sessionId, input);
972
- const wrappedParams = {
973
- ...params,
974
- tools: wrapTools(params.tools, sessionId, client)
975
- };
976
- try {
977
- const result = await originalFn(wrappedParams);
978
- const durationMs = Date.now() - startTime;
979
- const resolvedModelId = result.response?.modelId || modelId;
980
- const promptTokens = result.usage?.promptTokens || 0;
981
- const completionTokens = result.usage?.completionTokens || 0;
982
- const totalTokens = result.usage?.totalTokens || promptTokens + completionTokens;
983
- const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
984
- const steps = result.steps;
985
- if (steps && steps.length >= 1) {
986
- for (let i = 0; i < steps.length; i++) {
987
- const step = steps[i];
988
- await client.trackEvent({
989
- sessionId,
990
- eventType: "llm_call",
991
- eventData: {
992
- model: resolvedModelId,
993
- provider,
994
- step: i + 1,
995
- total_steps: steps.length,
996
- prompt_tokens: step.usage?.promptTokens || 0,
997
- completion_tokens: step.usage?.completionTokens || 0,
998
- total_tokens: step.usage?.totalTokens || 0,
999
- finish_reason: step.finishReason,
1000
- tool_calls: step.toolCalls?.map((tc) => tc.toolName)
1001
- }
1002
- });
1003
- }
1004
- } else {
1005
- await client.trackEvent({
1006
- sessionId,
1007
- eventType: "llm_call",
1008
- eventData: {
1009
- model: resolvedModelId,
1010
- provider,
1011
- prompt_tokens: promptTokens,
1012
- completion_tokens: completionTokens,
1013
- total_tokens: totalTokens,
1014
- finish_reason: result.finishReason,
1015
- tool_calls: result.toolCalls?.map((tc) => tc.toolName)
1016
- }
1017
- });
1018
- }
1019
- await client.completeSession({
1020
- sessionId,
1021
- success: true,
1022
- output: result.text,
1023
- durationMs,
1024
- estimatedCost: cost,
1025
- promptTokens,
1026
- completionTokens,
1027
- totalTokens
1028
- });
1029
- return result;
1030
- } catch (error) {
1031
- const durationMs = Date.now() - startTime;
1032
- await client.trackError({
1033
- sessionId,
1034
- errorType: error instanceof Error ? error.name : "Error",
1035
- errorMessage: error instanceof Error ? error.message : "Unknown error"
1036
- });
1037
- await client.completeSession({
1038
- sessionId,
1039
- success: false,
1040
- failureReason: error instanceof Error ? error.message : "Unknown error",
1041
- durationMs
1042
- });
1043
- throw error;
1044
- }
1045
- };
1046
- }
1047
- function wrapStreamText(originalFn, client, config) {
1048
- return (params) => {
1049
- const startTime = Date.now();
1050
- const { modelId, provider } = extractModelInfo(params.model);
1051
- const input = extractInput(params);
1052
- let sessionId = null;
1053
- const sessionPromise = (async () => {
1054
- try {
1055
- const id = await client.createSession({
1056
- name: `streamText: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1057
- agentName: config.defaultAgent ?? "vercel-ai-sdk",
1058
- userId: config.userId ?? "anonymous",
1059
- convoId: config.convoId,
1060
- metadata: {
1061
- model: modelId,
1062
- provider,
1063
- function: "streamText"
1064
- }
1065
- });
1066
- sessionId = id;
1067
- if (id) {
1068
- client.setInput(id, input).catch(() => {
1069
- });
1070
- }
1071
- return id;
1072
- } catch {
1073
- return null;
1074
- }
1075
- })();
1076
- const wrappedParams = {
1077
- ...params,
1078
- tools: params.tools ? wrapToolsAsync(params.tools, sessionPromise, client) : void 0
1079
- };
1080
- const result = originalFn(wrappedParams);
1081
- let tracked = false;
1082
- async function trackCompletion(fullText, error) {
1083
- if (tracked) return;
1084
- tracked = true;
1085
- const durationMs = Date.now() - startTime;
1086
- const sid = sessionId || await sessionPromise;
1087
- if (!sid) return;
1088
- if (error) {
1089
- await client.trackError({
1090
- sessionId: sid,
1091
- errorType: error.name || "Error",
1092
- errorMessage: error.message || "Unknown error"
1093
- });
1094
- await client.completeSession({
1095
- sessionId: sid,
1096
- success: false,
1097
- failureReason: error.message || "Unknown error",
1098
- durationMs
1099
- });
1100
- return;
1101
- }
1102
- let usage;
1103
- try {
1104
- usage = result.usage ? await result.usage : void 0;
1105
- } catch {
1106
- }
1107
- const promptTokens = usage?.promptTokens || 0;
1108
- const completionTokens = usage?.completionTokens || 0;
1109
- const totalTokens = usage?.totalTokens || promptTokens + completionTokens;
1110
- const cost = calculateCostForCall(provider, modelId, promptTokens, completionTokens);
1111
- await client.trackEvent({
1112
- sessionId: sid,
1113
- eventType: "llm_call",
1114
- eventData: {
1115
- model: modelId,
1116
- provider,
1117
- prompt_tokens: promptTokens,
1118
- completion_tokens: completionTokens,
1119
- total_tokens: totalTokens
1120
- }
1121
- });
1122
- await client.completeSession({
1123
- sessionId: sid,
1124
- success: true,
1125
- output: fullText,
1126
- durationMs,
1127
- estimatedCost: cost,
1128
- promptTokens,
1129
- completionTokens,
1130
- totalTokens
1131
- });
1132
- }
1133
- const textProp = result.text;
1134
- if (typeof textProp === "string") {
1135
- trackCompletion(textProp).catch(() => {
1136
- });
1137
- } else if (textProp != null && typeof textProp.then === "function") {
1138
- const originalTextPromise = textProp;
1139
- result.text = originalTextPromise.then((text) => {
1140
- trackCompletion(text).catch(() => {
1141
- });
1142
- return text;
1143
- }).catch((err) => {
1144
- trackCompletion("", err instanceof Error ? err : new Error(String(err))).catch(() => {
1145
- });
1146
- throw err;
1147
- });
1148
- } else {
1149
- const originalTextStream = result.textStream;
1150
- let fullText = "";
1151
- result.textStream = (async function* () {
1152
- try {
1153
- for await (const chunk of originalTextStream) {
1154
- fullText += chunk;
1155
- yield chunk;
1718
+ sessionPromise.then((sid) => {
1719
+ if (sid) {
1720
+ client.trackToolCall({
1721
+ sessionId: sid,
1722
+ toolName,
1723
+ toolInput: args[0],
1724
+ toolOutput: result,
1725
+ reasoning: `Tool executed in ${durationMs}ms`
1726
+ }).catch(() => {
1727
+ });
1728
+ }
1729
+ });
1730
+ return result;
1731
+ } catch (error) {
1732
+ const durationMs = Date.now() - startTime;
1733
+ sessionPromise.then((sid) => {
1734
+ if (sid) {
1735
+ client.trackToolCall({
1736
+ sessionId: sid,
1737
+ toolName,
1738
+ toolInput: args[0],
1739
+ toolOutput: {},
1740
+ toolError: { message: error instanceof Error ? error.message : "Unknown error" },
1741
+ reasoning: `Tool failed after ${durationMs}ms`
1742
+ }).catch(() => {
1743
+ });
1744
+ }
1745
+ });
1746
+ throw error;
1156
1747
  }
1157
- await trackCompletion(fullText);
1158
- } catch (error) {
1159
- await trackCompletion(
1160
- "",
1161
- error instanceof Error ? error : new Error(String(error))
1162
- );
1163
- throw error;
1164
1748
  }
1165
- })();
1749
+ };
1750
+ } else {
1751
+ wrappedTools[toolName] = tool;
1166
1752
  }
1167
- return result;
1168
- };
1753
+ }
1754
+ return wrappedTools;
1169
1755
  }
1170
- function wrapGenerateObject(originalFn, client, config) {
1756
+ function wrapGenerateText(originalFn, client, config) {
1171
1757
  return async (params) => {
1172
1758
  const startTime = Date.now();
1173
1759
  const { modelId, provider } = extractModelInfo(params.model);
1174
1760
  const input = extractInput(params);
1175
1761
  const sessionId = await client.createSession({
1176
- name: `generateObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1762
+ name: `generateText: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1177
1763
  agentName: config.defaultAgent ?? "vercel-ai-sdk",
1178
1764
  userId: config.userId ?? "anonymous",
1179
1765
  convoId: config.convoId,
1180
1766
  metadata: {
1181
1767
  model: modelId,
1182
1768
  provider,
1183
- function: "generateObject"
1769
+ function: "generateText",
1770
+ ...params.maxSteps ? { maxSteps: params.maxSteps } : {}
1184
1771
  }
1185
1772
  });
1186
1773
  if (!sessionId) {
1187
1774
  return originalFn(params);
1188
1775
  }
1189
1776
  await client.setInput(sessionId, input);
1777
+ const wrappedParams = {
1778
+ ...params,
1779
+ tools: wrapTools(params.tools, sessionId, client)
1780
+ };
1190
1781
  try {
1191
- const result = await originalFn(params);
1782
+ const result = await originalFn(wrappedParams);
1192
1783
  const durationMs = Date.now() - startTime;
1193
1784
  const resolvedModelId = result.response?.modelId || modelId;
1194
- const promptTokens = result.usage?.promptTokens || 0;
1195
- const completionTokens = result.usage?.completionTokens || 0;
1196
- const totalTokens = result.usage?.totalTokens || promptTokens + completionTokens;
1785
+ const usage = normalizeUsage(result.usage);
1786
+ const promptTokens = usage?.promptTokens ?? 0;
1787
+ const completionTokens = usage?.completionTokens ?? 0;
1788
+ const totalTokens = usage?.totalTokens ?? 0;
1197
1789
  const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
1790
+ const steps = result.steps;
1791
+ if (steps && steps.length >= 1) {
1792
+ const stepPromises = steps.map((step) => {
1793
+ const su = normalizeUsage(step.usage);
1794
+ return client.trackToolCall({
1795
+ sessionId,
1796
+ toolName: `llm:${provider}:${resolvedModelId}`,
1797
+ toolInput: { finishReason: step.finishReason },
1798
+ toolOutput: {
1799
+ text: step.text?.slice(0, 500),
1800
+ tokens: {
1801
+ prompt: su?.promptTokens ?? 0,
1802
+ completion: su?.completionTokens ?? 0
1803
+ }
1804
+ },
1805
+ estimatedCost: calculateCostForCall(provider, resolvedModelId, su?.promptTokens ?? 0, su?.completionTokens ?? 0),
1806
+ tokenCount: su?.totalTokens,
1807
+ metadata: {
1808
+ tool_calls: step.toolCalls?.map((tc) => tc.toolName)
1809
+ }
1810
+ }).catch(() => {
1811
+ });
1812
+ });
1813
+ await Promise.all(stepPromises);
1814
+ } else {
1815
+ await client.trackToolCall({
1816
+ sessionId,
1817
+ toolName: `llm:${provider}:${resolvedModelId}`,
1818
+ toolInput: { finishReason: result.finishReason },
1819
+ toolOutput: {
1820
+ text: result.text?.slice(0, 500),
1821
+ tokens: { prompt: promptTokens, completion: completionTokens }
1822
+ },
1823
+ estimatedCost: cost,
1824
+ tokenCount: totalTokens,
1825
+ metadata: {
1826
+ tool_calls: result.toolCalls?.map((tc) => tc.toolName)
1827
+ }
1828
+ }).catch(() => {
1829
+ });
1830
+ }
1198
1831
  await client.completeSession({
1199
1832
  sessionId,
1200
1833
  success: true,
1201
- output: JSON.stringify(result.object),
1834
+ output: result.text,
1202
1835
  durationMs,
1203
1836
  estimatedCost: cost,
1204
1837
  promptTokens,
@@ -1223,393 +1856,524 @@ function wrapGenerateObject(originalFn, client, config) {
1223
1856
  }
1224
1857
  };
1225
1858
  }
1226
- function wrapStreamObject(originalFn, client, config) {
1859
+ function wrapStreamText(originalFn, client, config) {
1227
1860
  return (params) => {
1228
1861
  const startTime = Date.now();
1229
1862
  const { modelId, provider } = extractModelInfo(params.model);
1230
1863
  const input = extractInput(params);
1231
- const sessionPromise = (async () => {
1864
+ let sessionId = null;
1865
+ let resolveSessionReady;
1866
+ const sessionReady = new Promise((resolve) => {
1867
+ resolveSessionReady = resolve;
1868
+ });
1869
+ (async () => {
1232
1870
  try {
1233
1871
  const id = await client.createSession({
1234
- name: `streamObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1872
+ name: `streamText: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
1235
1873
  agentName: config.defaultAgent ?? "vercel-ai-sdk",
1236
1874
  userId: config.userId ?? "anonymous",
1237
1875
  convoId: config.convoId,
1238
1876
  metadata: {
1239
1877
  model: modelId,
1240
1878
  provider,
1241
- function: "streamObject"
1879
+ function: "streamText"
1242
1880
  }
1243
1881
  });
1882
+ sessionId = id;
1244
1883
  if (id) {
1245
1884
  client.setInput(id, input).catch(() => {
1246
1885
  });
1247
1886
  }
1248
- return id;
1249
1887
  } catch {
1250
- return null;
1888
+ sessionId = null;
1251
1889
  }
1890
+ resolveSessionReady();
1252
1891
  })();
1253
- const result = originalFn(params);
1254
- if (result.object) {
1255
- const originalObjectPromise = result.object;
1256
- result.object = originalObjectPromise.then(async (obj) => {
1257
- const durationMs = Date.now() - startTime;
1258
- const sid = await sessionPromise;
1259
- if (sid) {
1260
- let usage;
1892
+ const userOnStepFinish = params.onStepFinish;
1893
+ const userOnFinish = params.onFinish;
1894
+ const userOnError = params.onError;
1895
+ const wrappedParams = {
1896
+ ...params,
1897
+ tools: params.tools ? wrapToolsAsync(params.tools, sessionReady.then(() => sessionId), client) : void 0,
1898
+ onStepFinish: async (step) => {
1899
+ await sessionReady;
1900
+ if (sessionId) {
1901
+ const su = normalizeUsage(step.usage);
1902
+ const resolvedStepModel = step.response?.modelId ?? modelId;
1903
+ client.trackToolCall({
1904
+ sessionId,
1905
+ toolName: `llm:${provider}:${resolvedStepModel}`,
1906
+ toolInput: { finishReason: step.finishReason },
1907
+ toolOutput: {
1908
+ text: step.text?.slice(0, 500),
1909
+ tokens: {
1910
+ prompt: su?.promptTokens ?? 0,
1911
+ completion: su?.completionTokens ?? 0
1912
+ }
1913
+ },
1914
+ estimatedCost: calculateCostForCall(provider, resolvedStepModel, su?.promptTokens ?? 0, su?.completionTokens ?? 0),
1915
+ tokenCount: su?.totalTokens,
1916
+ metadata: {
1917
+ tool_calls: step.toolCalls?.map((tc) => tc.toolName)
1918
+ }
1919
+ }).catch(() => {
1920
+ });
1921
+ }
1922
+ if (userOnStepFinish) {
1261
1923
  try {
1262
- usage = result.usage ? await result.usage : void 0;
1924
+ userOnStepFinish(step);
1263
1925
  } catch {
1264
1926
  }
1265
- const promptTokens = usage?.promptTokens || 0;
1266
- const completionTokens = usage?.completionTokens || 0;
1267
- const totalTokens = usage?.totalTokens || promptTokens + completionTokens;
1268
- const cost = calculateCostForCall(provider, modelId, promptTokens, completionTokens);
1927
+ }
1928
+ },
1929
+ onFinish: async (event) => {
1930
+ await sessionReady;
1931
+ if (sessionId) {
1932
+ const durationMs = Date.now() - startTime;
1933
+ const usage = normalizeUsage(event.totalUsage ?? event.usage);
1934
+ const resolvedModelId = event.response?.modelId ?? modelId;
1935
+ const text = event.text ?? "";
1936
+ const promptTokens = usage?.promptTokens ?? 0;
1937
+ const completionTokens = usage?.completionTokens ?? 0;
1938
+ const totalTokens = usage?.totalTokens ?? promptTokens + completionTokens;
1939
+ const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
1269
1940
  await client.completeSession({
1270
- sessionId: sid,
1941
+ sessionId,
1271
1942
  success: true,
1272
- output: JSON.stringify(obj),
1943
+ output: text,
1273
1944
  durationMs,
1274
1945
  estimatedCost: cost,
1275
1946
  promptTokens,
1276
1947
  completionTokens,
1277
1948
  totalTokens
1949
+ }).catch(() => {
1278
1950
  });
1279
1951
  }
1280
- return obj;
1281
- }).catch(async (error) => {
1282
- const durationMs = Date.now() - startTime;
1283
- const sid = await sessionPromise;
1284
- if (sid) {
1952
+ if (userOnFinish) {
1953
+ try {
1954
+ userOnFinish(event);
1955
+ } catch {
1956
+ }
1957
+ }
1958
+ },
1959
+ onError: async (event) => {
1960
+ await sessionReady;
1961
+ if (sessionId) {
1962
+ const durationMs = Date.now() - startTime;
1963
+ const msg = event.error?.message ?? "Unknown error";
1285
1964
  await client.trackError({
1286
- sessionId: sid,
1287
- errorType: error instanceof Error ? error.name : "Error",
1288
- errorMessage: error instanceof Error ? error.message : "Unknown error"
1965
+ sessionId,
1966
+ errorType: event.error?.name ?? "Error",
1967
+ errorMessage: msg
1968
+ }).catch(() => {
1289
1969
  });
1290
1970
  await client.completeSession({
1291
- sessionId: sid,
1971
+ sessionId,
1292
1972
  success: false,
1293
- failureReason: error instanceof Error ? error.message : "Unknown error",
1973
+ failureReason: msg,
1294
1974
  durationMs
1975
+ }).catch(() => {
1295
1976
  });
1296
1977
  }
1297
- throw error;
1298
- });
1299
- }
1300
- return result;
1301
- };
1302
- }
1303
- function wrapAISDK(ai, options) {
1304
- const client = options?.client ?? getClient2();
1305
- const config = {
1306
- defaultAgent: options?.defaultAgent ?? _globalConfig.defaultAgent,
1307
- userId: options?.userId ?? _globalConfig.userId,
1308
- convoId: options?.convoId ?? _globalConfig.convoId
1309
- };
1310
- return {
1311
- generateText: ai.generateText ? wrapGenerateText(ai.generateText, client, config) : wrapGenerateText(
1312
- () => Promise.reject(new Error("generateText not available")),
1313
- client,
1314
- config
1315
- ),
1316
- streamText: ai.streamText ? wrapStreamText(ai.streamText, client, config) : wrapStreamText(() => ({ textStream: (async function* () {
1317
- })() }), client, config),
1318
- generateObject: ai.generateObject ? wrapGenerateObject(ai.generateObject, client, config) : wrapGenerateObject(
1319
- () => Promise.reject(new Error("generateObject not available")),
1320
- client,
1321
- config
1322
- ),
1323
- streamObject: ai.streamObject ? wrapStreamObject(ai.streamObject, client, config) : wrapStreamObject(() => ({}), client, config)
1324
- };
1325
- }
1326
-
1327
- // src/wrappers.ts
1328
- var _currentSessionId = null;
1329
- var _currentClient = null;
1330
- var _defaultClient2 = null;
1331
- function setSessionContext(sessionId, client) {
1332
- _currentSessionId = sessionId;
1333
- if (client) {
1334
- _currentClient = client;
1335
- }
1336
- }
1337
- function clearSessionContext() {
1338
- _currentSessionId = null;
1339
- _currentClient = null;
1340
- }
1341
- function getSessionContext() {
1342
- return _currentSessionId;
1343
- }
1344
- function setDefaultClient(client) {
1345
- _defaultClient2 = client;
1346
- }
1347
- function getTrackingClient() {
1348
- return _currentClient ?? _defaultClient2;
1349
- }
1350
- function wrapOpenAI(client, options = {}) {
1351
- const { trackWithoutSession = false } = options;
1352
- const chat = client.chat;
1353
- if (!chat?.completions?.create) {
1354
- console.warn("Sentrial: OpenAI client does not have chat.completions.create");
1355
- return client;
1356
- }
1357
- const originalCreate = chat.completions.create.bind(chat.completions);
1358
- chat.completions.create = async function(...args) {
1359
- const startTime = Date.now();
1360
- const params = args[0] ?? {};
1361
- const messages = params.messages ?? [];
1362
- const model = params.model ?? "unknown";
1363
- try {
1364
- const response = await originalCreate(...args);
1365
- const durationMs = Date.now() - startTime;
1366
- const promptTokens = response.usage?.prompt_tokens ?? 0;
1367
- const completionTokens = response.usage?.completion_tokens ?? 0;
1368
- const totalTokens = response.usage?.total_tokens ?? 0;
1369
- let outputContent = "";
1370
- if (response.choices?.[0]?.message?.content) {
1371
- outputContent = response.choices[0].message.content;
1978
+ if (userOnError) {
1979
+ try {
1980
+ userOnError(event);
1981
+ } catch {
1982
+ }
1983
+ }
1372
1984
  }
1373
- const cost = calculateOpenAICost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
1374
- trackLLMCall({
1375
- provider: "openai",
1376
- model,
1377
- messages,
1378
- output: outputContent,
1379
- promptTokens,
1380
- completionTokens,
1381
- totalTokens,
1382
- cost,
1383
- durationMs,
1384
- trackWithoutSession
1385
- });
1386
- return response;
1985
+ };
1986
+ try {
1987
+ return originalFn(wrappedParams);
1387
1988
  } catch (error) {
1388
- const durationMs = Date.now() - startTime;
1389
- trackLLMError({
1390
- provider: "openai",
1391
- model,
1392
- messages,
1393
- error,
1394
- durationMs,
1395
- trackWithoutSession
1989
+ sessionReady.then(() => {
1990
+ if (sessionId) {
1991
+ const durationMs = Date.now() - startTime;
1992
+ const msg = error instanceof Error ? error.message : String(error);
1993
+ client.trackError({ sessionId, errorType: error instanceof Error ? error.name : "Error", errorMessage: msg }).catch(() => {
1994
+ });
1995
+ client.completeSession({ sessionId, success: false, failureReason: msg, durationMs }).catch(() => {
1996
+ });
1997
+ }
1396
1998
  });
1397
1999
  throw error;
1398
2000
  }
1399
2001
  };
1400
- return client;
1401
2002
  }
1402
- function wrapAnthropic(client, options = {}) {
1403
- const { trackWithoutSession = false } = options;
1404
- const messages = client.messages;
1405
- if (!messages?.create) {
1406
- console.warn("Sentrial: Anthropic client does not have messages.create");
1407
- return client;
1408
- }
1409
- const originalCreate = messages.create.bind(messages);
1410
- messages.create = async function(...args) {
2003
+ function wrapGenerateObject(originalFn, client, config) {
2004
+ return async (params) => {
1411
2005
  const startTime = Date.now();
1412
- const params = args[0] ?? {};
1413
- const inputMessages = params.messages ?? [];
1414
- const model = params.model ?? "unknown";
1415
- const system = params.system ?? "";
2006
+ const { modelId, provider } = extractModelInfo(params.model);
2007
+ const input = extractInput(params);
2008
+ const sessionId = await client.createSession({
2009
+ name: `generateObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
2010
+ agentName: config.defaultAgent ?? "vercel-ai-sdk",
2011
+ userId: config.userId ?? "anonymous",
2012
+ convoId: config.convoId,
2013
+ metadata: {
2014
+ model: modelId,
2015
+ provider,
2016
+ function: "generateObject"
2017
+ }
2018
+ });
2019
+ if (!sessionId) {
2020
+ return originalFn(params);
2021
+ }
2022
+ await client.setInput(sessionId, input);
1416
2023
  try {
1417
- const response = await originalCreate(...args);
2024
+ const result = await originalFn(params);
1418
2025
  const durationMs = Date.now() - startTime;
1419
- const promptTokens = response.usage?.input_tokens ?? 0;
1420
- const completionTokens = response.usage?.output_tokens ?? 0;
1421
- const totalTokens = promptTokens + completionTokens;
1422
- let outputContent = "";
1423
- if (response.content) {
1424
- for (const block of response.content) {
1425
- if (block.type === "text") {
1426
- outputContent += block.text;
1427
- }
1428
- }
1429
- }
1430
- const cost = calculateAnthropicCost({ model, inputTokens: promptTokens, outputTokens: completionTokens });
1431
- const fullMessages = system ? [{ role: "system", content: system }, ...inputMessages] : inputMessages;
1432
- trackLLMCall({
1433
- provider: "anthropic",
1434
- model,
1435
- messages: fullMessages,
1436
- output: outputContent,
2026
+ const resolvedModelId = result.response?.modelId || modelId;
2027
+ const usage = normalizeUsage(result.usage);
2028
+ const promptTokens = usage?.promptTokens ?? 0;
2029
+ const completionTokens = usage?.completionTokens ?? 0;
2030
+ const totalTokens = usage?.totalTokens ?? 0;
2031
+ const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
2032
+ await client.trackToolCall({
2033
+ sessionId,
2034
+ toolName: `llm:${provider}:${resolvedModelId}`,
2035
+ toolInput: { function: "generateObject" },
2036
+ toolOutput: {
2037
+ object: result.object,
2038
+ tokens: { prompt: promptTokens, completion: completionTokens }
2039
+ },
2040
+ estimatedCost: cost,
2041
+ tokenCount: totalTokens
2042
+ }).catch(() => {
2043
+ });
2044
+ await client.completeSession({
2045
+ sessionId,
2046
+ success: true,
2047
+ output: JSON.stringify(result.object),
2048
+ durationMs,
2049
+ estimatedCost: cost,
1437
2050
  promptTokens,
1438
2051
  completionTokens,
1439
- totalTokens,
1440
- cost,
1441
- durationMs,
1442
- trackWithoutSession
2052
+ totalTokens
1443
2053
  });
1444
- return response;
2054
+ return result;
1445
2055
  } catch (error) {
1446
2056
  const durationMs = Date.now() - startTime;
1447
- trackLLMError({
1448
- provider: "anthropic",
1449
- model,
1450
- messages: inputMessages,
1451
- error,
1452
- durationMs,
1453
- trackWithoutSession
2057
+ await client.trackError({
2058
+ sessionId,
2059
+ errorType: error instanceof Error ? error.name : "Error",
2060
+ errorMessage: error instanceof Error ? error.message : "Unknown error"
2061
+ });
2062
+ await client.completeSession({
2063
+ sessionId,
2064
+ success: false,
2065
+ failureReason: error instanceof Error ? error.message : "Unknown error",
2066
+ durationMs
1454
2067
  });
1455
2068
  throw error;
1456
2069
  }
1457
2070
  };
1458
- return client;
1459
2071
  }
1460
- function wrapGoogle(model, options = {}) {
1461
- const { trackWithoutSession = false } = options;
1462
- const originalGenerate = model.generateContent;
1463
- if (!originalGenerate) {
1464
- console.warn("Sentrial: Google model does not have generateContent");
1465
- return model;
1466
- }
1467
- model.generateContent = async function(...args) {
2072
+ function wrapStreamObject(originalFn, client, config) {
2073
+ return (params) => {
1468
2074
  const startTime = Date.now();
1469
- const contents = args[0];
1470
- const modelName = model.model ?? "gemini-unknown";
1471
- const messages = googleContentsToMessages(contents);
1472
- try {
1473
- const response = await originalGenerate.apply(model, args);
1474
- const durationMs = Date.now() - startTime;
1475
- let promptTokens = 0;
1476
- let completionTokens = 0;
1477
- if (response.usageMetadata) {
1478
- promptTokens = response.usageMetadata.promptTokenCount ?? 0;
1479
- completionTokens = response.usageMetadata.candidatesTokenCount ?? 0;
1480
- }
1481
- const totalTokens = promptTokens + completionTokens;
1482
- let outputContent = "";
2075
+ const { modelId, provider } = extractModelInfo(params.model);
2076
+ const input = extractInput(params);
2077
+ let sessionId = null;
2078
+ let resolveSessionReady;
2079
+ const sessionReady = new Promise((resolve) => {
2080
+ resolveSessionReady = resolve;
2081
+ });
2082
+ (async () => {
1483
2083
  try {
1484
- outputContent = response.response?.text() ?? "";
2084
+ const id = await client.createSession({
2085
+ name: `streamObject: ${input.slice(0, 50)}${input.length > 50 ? "..." : ""}`,
2086
+ agentName: config.defaultAgent ?? "vercel-ai-sdk",
2087
+ userId: config.userId ?? "anonymous",
2088
+ convoId: config.convoId,
2089
+ metadata: {
2090
+ model: modelId,
2091
+ provider,
2092
+ function: "streamObject"
2093
+ }
2094
+ });
2095
+ sessionId = id;
2096
+ if (id) {
2097
+ client.setInput(id, input).catch(() => {
2098
+ });
2099
+ }
1485
2100
  } catch {
2101
+ sessionId = null;
1486
2102
  }
1487
- const cost = calculateGoogleCost({ model: modelName, inputTokens: promptTokens, outputTokens: completionTokens });
1488
- trackLLMCall({
1489
- provider: "google",
1490
- model: modelName,
1491
- messages,
1492
- output: outputContent,
1493
- promptTokens,
1494
- completionTokens,
1495
- totalTokens,
1496
- cost,
1497
- durationMs,
1498
- trackWithoutSession
1499
- });
1500
- return response;
2103
+ resolveSessionReady();
2104
+ })();
2105
+ const userOnFinish = params.onFinish;
2106
+ const userOnError = params.onError;
2107
+ const wrappedParams = {
2108
+ ...params,
2109
+ onFinish: async (event) => {
2110
+ await sessionReady;
2111
+ if (sessionId) {
2112
+ const durationMs = Date.now() - startTime;
2113
+ const usage = normalizeUsage(event.usage);
2114
+ const resolvedModelId = event.response?.modelId ?? modelId;
2115
+ const promptTokens = usage?.promptTokens ?? 0;
2116
+ const completionTokens = usage?.completionTokens ?? 0;
2117
+ const totalTokens = usage?.totalTokens ?? 0;
2118
+ const cost = calculateCostForCall(provider, resolvedModelId, promptTokens, completionTokens);
2119
+ if (event.error) {
2120
+ const errMsg = event.error instanceof Error ? event.error.message : String(event.error);
2121
+ await client.trackError({
2122
+ sessionId,
2123
+ errorType: "SchemaValidationError",
2124
+ errorMessage: errMsg
2125
+ }).catch(() => {
2126
+ });
2127
+ }
2128
+ await client.trackToolCall({
2129
+ sessionId,
2130
+ toolName: `llm:${provider}:${resolvedModelId}`,
2131
+ toolInput: { function: "streamObject" },
2132
+ toolOutput: {
2133
+ object: event.object,
2134
+ tokens: { prompt: promptTokens, completion: completionTokens }
2135
+ },
2136
+ estimatedCost: cost,
2137
+ tokenCount: totalTokens
2138
+ }).catch(() => {
2139
+ });
2140
+ await client.completeSession({
2141
+ sessionId,
2142
+ success: !event.error,
2143
+ output: event.object != null ? JSON.stringify(event.object) : void 0,
2144
+ failureReason: event.error ? String(event.error) : void 0,
2145
+ durationMs,
2146
+ estimatedCost: cost,
2147
+ promptTokens,
2148
+ completionTokens,
2149
+ totalTokens
2150
+ }).catch(() => {
2151
+ });
2152
+ }
2153
+ if (userOnFinish) {
2154
+ try {
2155
+ userOnFinish(event);
2156
+ } catch {
2157
+ }
2158
+ }
2159
+ },
2160
+ onError: async (event) => {
2161
+ await sessionReady;
2162
+ if (sessionId) {
2163
+ const durationMs = Date.now() - startTime;
2164
+ const msg = event.error?.message ?? "Unknown error";
2165
+ await client.trackError({
2166
+ sessionId,
2167
+ errorType: event.error?.name ?? "Error",
2168
+ errorMessage: msg
2169
+ }).catch(() => {
2170
+ });
2171
+ await client.completeSession({
2172
+ sessionId,
2173
+ success: false,
2174
+ failureReason: msg,
2175
+ durationMs
2176
+ }).catch(() => {
2177
+ });
2178
+ }
2179
+ if (userOnError) {
2180
+ try {
2181
+ userOnError(event);
2182
+ } catch {
2183
+ }
2184
+ }
2185
+ }
2186
+ };
2187
+ try {
2188
+ return originalFn(wrappedParams);
1501
2189
  } catch (error) {
1502
- const durationMs = Date.now() - startTime;
1503
- trackLLMError({
1504
- provider: "google",
1505
- model: modelName,
1506
- messages,
1507
- error,
1508
- durationMs,
1509
- trackWithoutSession
2190
+ sessionReady.then(() => {
2191
+ if (sessionId) {
2192
+ const durationMs = Date.now() - startTime;
2193
+ const msg = error instanceof Error ? error.message : String(error);
2194
+ client.trackError({ sessionId, errorType: error instanceof Error ? error.name : "Error", errorMessage: msg }).catch(() => {
2195
+ });
2196
+ client.completeSession({ sessionId, success: false, failureReason: msg, durationMs }).catch(() => {
2197
+ });
2198
+ }
1510
2199
  });
1511
2200
  throw error;
1512
2201
  }
1513
2202
  };
1514
- return model;
1515
- }
1516
- function googleContentsToMessages(contents) {
1517
- if (typeof contents === "string") {
1518
- return [{ role: "user", content: contents }];
1519
- }
1520
- if (Array.isArray(contents)) {
1521
- return contents.map((item) => {
1522
- if (typeof item === "string") {
1523
- return { role: "user", content: item };
1524
- }
1525
- if (item && typeof item === "object") {
1526
- return { role: item.role ?? "user", content: String(item.content ?? item) };
1527
- }
1528
- return { role: "user", content: String(item) };
1529
- });
1530
- }
1531
- return [{ role: "user", content: String(contents) }];
1532
2203
  }
1533
- function wrapLLM(client, provider) {
1534
- if (provider === "openai" || client.chat?.completions?.create) {
1535
- return wrapOpenAI(client);
1536
- }
1537
- if (provider === "anthropic" || client.messages?.create) {
1538
- return wrapAnthropic(client);
1539
- }
1540
- if (provider === "google" || client.generateContent) {
1541
- return wrapGoogle(client);
1542
- }
1543
- console.warn("Sentrial: Unknown LLM client type. No auto-tracking applied.");
1544
- return client;
2204
+ function wrapAISDK(ai, options) {
2205
+ const client = options?.client ?? getClient2();
2206
+ const config = {
2207
+ defaultAgent: options?.defaultAgent ?? _globalConfig.defaultAgent,
2208
+ userId: options?.userId ?? _globalConfig.userId,
2209
+ convoId: options?.convoId ?? _globalConfig.convoId
2210
+ };
2211
+ return {
2212
+ generateText: ai.generateText ? wrapGenerateText(ai.generateText, client, config) : wrapGenerateText(
2213
+ () => Promise.reject(new Error("generateText not available")),
2214
+ client,
2215
+ config
2216
+ ),
2217
+ streamText: ai.streamText ? wrapStreamText(ai.streamText, client, config) : wrapStreamText(() => ({ textStream: (async function* () {
2218
+ })() }), client, config),
2219
+ generateObject: ai.generateObject ? wrapGenerateObject(ai.generateObject, client, config) : wrapGenerateObject(
2220
+ () => Promise.reject(new Error("generateObject not available")),
2221
+ client,
2222
+ config
2223
+ ),
2224
+ streamObject: ai.streamObject ? wrapStreamObject(ai.streamObject, client, config) : wrapStreamObject(() => ({}), client, config)
2225
+ };
1545
2226
  }
1546
- function trackLLMCall(params) {
1547
- const client = getTrackingClient();
1548
- if (!client) return;
1549
- const sessionId = _currentSessionId;
1550
- if (!sessionId && !params.trackWithoutSession) {
1551
- return;
1552
- }
1553
- if (sessionId) {
1554
- client.trackToolCall({
1555
- sessionId,
1556
- toolName: `llm:${params.provider}:${params.model}`,
1557
- toolInput: {
1558
- messages: params.messages,
1559
- model: params.model,
1560
- provider: params.provider
1561
- },
1562
- toolOutput: {
1563
- content: params.output,
1564
- tokens: {
1565
- prompt: params.promptTokens,
1566
- completion: params.completionTokens,
1567
- total: params.totalTokens
1568
- },
1569
- cost_usd: params.cost
1570
- },
1571
- reasoning: `LLM call to ${params.provider} ${params.model}`,
1572
- estimatedCost: params.cost,
1573
- tokenCount: params.totalTokens,
1574
- metadata: {
1575
- provider: params.provider,
1576
- model: params.model,
1577
- duration_ms: params.durationMs,
1578
- prompt_tokens: params.promptTokens,
1579
- completion_tokens: params.completionTokens
1580
- }
1581
- }).catch((err) => {
1582
- console.warn("Sentrial: Failed to track LLM call:", err.message);
2227
+
2228
+ // src/claude-code.ts
2229
+ function wrapClaudeAgent(queryFn, wrapOptions) {
2230
+ const {
2231
+ client,
2232
+ defaultAgent = "claude-agent",
2233
+ userId = "anonymous",
2234
+ convoId,
2235
+ extraMetadata
2236
+ } = wrapOptions;
2237
+ return function wrappedQuery(params) {
2238
+ const { prompt, options = {} } = params;
2239
+ const startTime = Date.now();
2240
+ let sessionId = null;
2241
+ let resolveSessionReady;
2242
+ const sessionReady = new Promise((resolve) => {
2243
+ resolveSessionReady = resolve;
1583
2244
  });
1584
- }
1585
- }
1586
- function trackLLMError(params) {
1587
- const client = getTrackingClient();
1588
- if (!client) return;
1589
- const sessionId = _currentSessionId;
1590
- if (!sessionId && !params.trackWithoutSession) {
1591
- return;
1592
- }
1593
- if (sessionId) {
1594
- client.trackError({
1595
- sessionId,
1596
- errorMessage: params.error.message,
1597
- errorType: params.error.name,
1598
- toolName: `llm:${params.provider}:${params.model}`,
1599
- metadata: {
1600
- provider: params.provider,
1601
- model: params.model,
1602
- duration_ms: params.durationMs
2245
+ const sessionName = typeof prompt === "string" ? `${defaultAgent}: ${prompt.slice(0, 100)}` : `${defaultAgent} session`;
2246
+ const pendingToolCalls = [];
2247
+ const sentrialToolHook = {
2248
+ hooks: [
2249
+ async (input, toolUseID, _opts) => {
2250
+ await sessionReady;
2251
+ if (!sessionId) return;
2252
+ const toolOutput = input?.tool_response && typeof input.tool_response === "object" ? input.tool_response : { response: input?.tool_response ?? null };
2253
+ const p = client.trackToolCall({
2254
+ sessionId,
2255
+ toolName: input?.tool_name ?? "unknown",
2256
+ toolInput: input?.tool_input ?? {},
2257
+ toolOutput,
2258
+ metadata: { tool_use_id: toolUseID }
2259
+ }).catch(() => {
2260
+ });
2261
+ pendingToolCalls.push(p);
2262
+ }
2263
+ ]
2264
+ };
2265
+ const sentrialToolFailureHook = {
2266
+ hooks: [
2267
+ async (input, toolUseID, _opts) => {
2268
+ await sessionReady;
2269
+ if (!sessionId) return;
2270
+ const p = client.trackToolCall({
2271
+ sessionId,
2272
+ toolName: input?.tool_name ?? "unknown",
2273
+ toolInput: input?.tool_input ?? {},
2274
+ toolOutput: {},
2275
+ toolError: { message: input?.error ?? "unknown error" },
2276
+ metadata: { tool_use_id: toolUseID }
2277
+ }).catch(() => {
2278
+ });
2279
+ pendingToolCalls.push(p);
2280
+ }
2281
+ ]
2282
+ };
2283
+ const mergedHooks = {
2284
+ ...options.hooks ?? {}
2285
+ };
2286
+ const existingPostToolUse = mergedHooks.PostToolUse ?? [];
2287
+ mergedHooks.PostToolUse = [...existingPostToolUse, sentrialToolHook];
2288
+ const existingPostToolUseFailure = mergedHooks.PostToolUseFailure ?? [];
2289
+ mergedHooks.PostToolUseFailure = [...existingPostToolUseFailure, sentrialToolFailureHook];
2290
+ const mergedOptions = {
2291
+ ...options,
2292
+ hooks: mergedHooks
2293
+ };
2294
+ const generator = queryFn({ prompt, options: mergedOptions });
2295
+ return (async function* () {
2296
+ try {
2297
+ for await (const message of generator) {
2298
+ if (message.type === "system" && message.subtype === "init") {
2299
+ const metadata = {
2300
+ model: message.model,
2301
+ tools: message.tools,
2302
+ cwd: message.cwd,
2303
+ mcp_servers: message.mcp_servers,
2304
+ sdk_session_id: message.session_id,
2305
+ ...extraMetadata ?? {}
2306
+ };
2307
+ try {
2308
+ sessionId = await client.createSession({
2309
+ name: sessionName,
2310
+ agentName: defaultAgent,
2311
+ userId,
2312
+ convoId,
2313
+ metadata
2314
+ });
2315
+ } catch {
2316
+ sessionId = null;
2317
+ }
2318
+ resolveSessionReady();
2319
+ }
2320
+ if (message.type === "result" && sessionId) {
2321
+ const isError = !!message.is_error;
2322
+ const inputTokens = message.usage?.input_tokens ?? 0;
2323
+ const outputTokens = message.usage?.output_tokens ?? 0;
2324
+ let failureReason;
2325
+ if (isError) {
2326
+ if (message.errors && message.errors.length > 0) {
2327
+ failureReason = message.errors.join("; ");
2328
+ } else {
2329
+ failureReason = message.subtype;
2330
+ }
2331
+ }
2332
+ await Promise.allSettled(pendingToolCalls);
2333
+ try {
2334
+ await client.completeSession({
2335
+ sessionId,
2336
+ success: !isError,
2337
+ failureReason,
2338
+ estimatedCost: message.total_cost_usd,
2339
+ promptTokens: inputTokens,
2340
+ completionTokens: outputTokens,
2341
+ totalTokens: inputTokens + outputTokens,
2342
+ durationMs: message.duration_ms ?? Date.now() - startTime,
2343
+ userInput: typeof prompt === "string" ? prompt : void 0,
2344
+ output: message.result,
2345
+ customMetrics: {
2346
+ num_turns: message.num_turns ?? 0,
2347
+ duration_api_ms: message.duration_api_ms ?? 0
2348
+ }
2349
+ });
2350
+ } catch {
2351
+ }
2352
+ }
2353
+ yield message;
2354
+ }
2355
+ } catch (error) {
2356
+ if (sessionId) {
2357
+ await Promise.allSettled(pendingToolCalls);
2358
+ try {
2359
+ await client.completeSession({
2360
+ sessionId,
2361
+ success: false,
2362
+ failureReason: error instanceof Error ? error.message : String(error),
2363
+ durationMs: Date.now() - startTime
2364
+ });
2365
+ } catch {
2366
+ }
2367
+ }
2368
+ throw error;
1603
2369
  }
1604
- }).catch((err) => {
1605
- console.warn("Sentrial: Failed to track LLM error:", err.message);
1606
- });
1607
- }
2370
+ })();
2371
+ };
1608
2372
  }
1609
2373
 
1610
2374
  // src/decorators.ts
1611
2375
  var _defaultClient3 = null;
1612
- var _currentInteraction = null;
2376
+ var _currentInteraction = createContextVar(null);
1613
2377
  function getClient3() {
1614
2378
  if (!_defaultClient3) {
1615
2379
  try {
@@ -1629,7 +2393,7 @@ function getCurrentSessionId() {
1629
2393
  return getSessionContext();
1630
2394
  }
1631
2395
  function getCurrentInteraction() {
1632
- return _currentInteraction;
2396
+ return _currentInteraction.get();
1633
2397
  }
1634
2398
  function withTool(name, fn) {
1635
2399
  const isAsync = fn.constructor.name === "AsyncFunction";
@@ -1730,10 +2494,11 @@ function withSession(agentName, fn, options = {}) {
1730
2494
  input: userInput
1731
2495
  });
1732
2496
  const sessionId = interaction.getSessionId();
2497
+ let sessionTokens;
1733
2498
  if (sessionId) {
1734
- setSessionContext(sessionId, client);
2499
+ sessionTokens = _setSessionContextWithTokens(sessionId, client);
1735
2500
  }
1736
- _currentInteraction = interaction;
2501
+ const interactionToken = _currentInteraction.set(interaction);
1737
2502
  try {
1738
2503
  const result = await fn(...args);
1739
2504
  let output;
@@ -1758,8 +2523,10 @@ function withSession(agentName, fn, options = {}) {
1758
2523
  });
1759
2524
  throw error;
1760
2525
  } finally {
1761
- clearSessionContext();
1762
- _currentInteraction = null;
2526
+ if (sessionTokens) {
2527
+ _restoreSessionContext(sessionTokens);
2528
+ }
2529
+ _currentInteraction.reset(interactionToken);
1763
2530
  }
1764
2531
  };
1765
2532
  }
@@ -1845,10 +2612,11 @@ function TrackSession(agentName, options) {
1845
2612
  input: userInput
1846
2613
  });
1847
2614
  const sessionId = interaction.getSessionId();
2615
+ let sessionTokens;
1848
2616
  if (sessionId) {
1849
- setSessionContext(sessionId, client);
2617
+ sessionTokens = _setSessionContextWithTokens(sessionId, client);
1850
2618
  }
1851
- _currentInteraction = interaction;
2619
+ const interactionToken = _currentInteraction.set(interaction);
1852
2620
  try {
1853
2621
  const result = await originalMethod.apply(this, args);
1854
2622
  let output;
@@ -1873,8 +2641,10 @@ function TrackSession(agentName, options) {
1873
2641
  });
1874
2642
  throw error;
1875
2643
  } finally {
1876
- clearSessionContext();
1877
- _currentInteraction = null;
2644
+ if (sessionTokens) {
2645
+ _restoreSessionContext(sessionTokens);
2646
+ }
2647
+ _currentInteraction.reset(interactionToken);
1878
2648
  }
1879
2649
  };
1880
2650
  return descriptor;
@@ -1887,6 +2657,8 @@ var SessionContext = class {
1887
2657
  client;
1888
2658
  interaction = null;
1889
2659
  output;
2660
+ sessionTokens;
2661
+ interactionToken;
1890
2662
  constructor(options) {
1891
2663
  this.userId = options.userId;
1892
2664
  this.agent = options.agent;
@@ -1905,9 +2677,9 @@ var SessionContext = class {
1905
2677
  });
1906
2678
  const sessionId = this.interaction.getSessionId();
1907
2679
  if (sessionId) {
1908
- setSessionContext(sessionId, this.client);
2680
+ this.sessionTokens = _setSessionContextWithTokens(sessionId, this.client);
1909
2681
  }
1910
- _currentInteraction = this.interaction;
2682
+ this.interactionToken = _currentInteraction.set(this.interaction);
1911
2683
  return this;
1912
2684
  }
1913
2685
  /**
@@ -1927,8 +2699,12 @@ var SessionContext = class {
1927
2699
  failureReason: options?.error
1928
2700
  });
1929
2701
  }
1930
- clearSessionContext();
1931
- _currentInteraction = null;
2702
+ if (this.sessionTokens) {
2703
+ _restoreSessionContext(this.sessionTokens);
2704
+ }
2705
+ if (this.interactionToken) {
2706
+ _currentInteraction.reset(this.interactionToken);
2707
+ }
1932
2708
  }
1933
2709
  /**
1934
2710
  * Get the session ID
@@ -1982,30 +2758,31 @@ function serializeOutput(value) {
1982
2758
  }
1983
2759
 
1984
2760
  // src/context.ts
1985
- var _experimentContext = null;
2761
+ var _experimentContext = createContextVar(null);
1986
2762
  function getSystemPrompt(defaultPrompt) {
1987
- if (_experimentContext?.systemPrompt) {
1988
- return _experimentContext.systemPrompt;
2763
+ const ctx = _experimentContext.get();
2764
+ if (ctx?.systemPrompt) {
2765
+ return ctx.systemPrompt;
1989
2766
  }
1990
2767
  return defaultPrompt ?? "";
1991
2768
  }
1992
2769
  function getExperimentContext() {
1993
- return _experimentContext;
2770
+ return _experimentContext.get();
1994
2771
  }
1995
2772
  function isExperimentMode() {
1996
- return _experimentContext !== null;
2773
+ return _experimentContext.get() !== null;
1997
2774
  }
1998
2775
  function getVariantName() {
1999
- return _experimentContext?.variantName ?? null;
2776
+ return _experimentContext.get()?.variantName ?? null;
2000
2777
  }
2001
2778
  function getExperimentId() {
2002
- return _experimentContext?.experimentId ?? null;
2779
+ return _experimentContext.get()?.experimentId ?? null;
2003
2780
  }
2004
2781
  function setExperimentContext(context) {
2005
- _experimentContext = context;
2782
+ _experimentContext.set(context);
2006
2783
  }
2007
2784
  function clearExperimentContext() {
2008
- _experimentContext = null;
2785
+ _experimentContext.set(null);
2009
2786
  }
2010
2787
 
2011
2788
  // src/experiment.ts
@@ -2338,6 +3115,7 @@ var Experiment = class {
2338
3115
  };
2339
3116
  export {
2340
3117
  ApiError,
3118
+ EventBatcher,
2341
3119
  EventType,
2342
3120
  Experiment,
2343
3121
  ExperimentRunTracker,
@@ -2357,6 +3135,7 @@ export {
2357
3135
  clearSessionContext,
2358
3136
  configure,
2359
3137
  configureVercel,
3138
+ createContextVar,
2360
3139
  getCurrentInteraction,
2361
3140
  getCurrentSessionId,
2362
3141
  getExperimentContext,
@@ -2379,6 +3158,7 @@ export {
2379
3158
  withTool,
2380
3159
  wrapAISDK,
2381
3160
  wrapAnthropic,
3161
+ wrapClaudeAgent,
2382
3162
  wrapGoogle,
2383
3163
  wrapLLM,
2384
3164
  wrapOpenAI