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