@sentrial/sdk 0.4.5 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,74 @@
1
+ import { C as CostParams, a as SentrialClientConfig, b as CreateSessionParams, G as GetVariantParams, V as VariantAssignment, R as RecordExperimentExposureParams, c as ExperimentExposureReceipt, T as TrackToolCallParams, d as Event, e as TrackDecisionParams, f as TrackErrorParams, g as Session, h as CompleteSessionParams, D as DeliveryDiagnostics, B as BeginParams, F as FinishParams } from './types-BEf8OATP.js';
2
+ export { A as ApiResponse, i as BatcherConfig, j as EventBatcher, k as EventType, L as LargeToolContentConfig, P as PiiBuiltinPatterns, l as PiiConfig, m as PiiCustomPattern, n as PiiField, o as PiiMode, p as ProductionVariantConfig, q as SessionStatus, r as hashValue, s as redactPayload, t as redactString, u as redactValue, v as replaceMatch } from './types-BEf8OATP.js';
3
+
4
+ /** The five builtin pattern categories, in their canonical (load-bearing) order. */
5
+ type PiiPatternKey = 'emails' | 'creditCards' | 'ssns' | 'phones' | 'ipAddresses';
6
+ /**
7
+ * Map of builtin pattern keys to their regex and label. ORDER MATTERS: consumers apply these in
8
+ * insertion order, replacing each match before the next pattern runs. Longer/more-specific number
9
+ * patterns MUST come before shorter ones — creditCards + ssns BEFORE phones — otherwise the PHONE
10
+ * pattern's separator alternative can eat a phone-shaped tail of a separator-formatted card (e.g.
11
+ * "411-111-111-1111" -> "411-[PHONE]"), leaking the leading digits; and for a bare 16-digit card
12
+ * the PHONE pattern would eat the trailing 10 digits, leaking the leading 6 (e.g.
13
+ * "4111111111111111" -> "411111[PHONE]"). Bare digit runs are already immune via PHONE_PATTERN's
14
+ * (?<!\d) guard. Kept in lockstep with the Python `_BUILTIN_PATTERNS`.
15
+ */
16
+ declare const BUILTIN_PATTERNS: Record<PiiPatternKey, {
17
+ pattern: RegExp;
18
+ label: string;
19
+ validate?: (match: string) => boolean;
20
+ }>;
21
+
22
+ /** Process-local OTLP transport observations. No URLs, bodies, IDs or credentials retained. */
23
+ declare const SERVER_STAGES: readonly ["body", "decode", "ingest", "resolve", "transaction", "postprocess", "total"];
24
+ type Stage = (typeof SERVER_STAGES)[number];
25
+ type AttemptOutcome = 'httpSuccess' | 'httpError' | 'transportError' | 'timeout' | 'cancelled' | 'bodyError';
26
+ type DurationSummary = {
27
+ measured: number;
28
+ unavailable: number;
29
+ totalMs: number;
30
+ minMs: number | null;
31
+ maxMs: number | null;
32
+ };
33
+ type ServerDurations = Record<Stage, number | null>;
34
+ type TransportAttempt = {
35
+ retry: boolean;
36
+ outcome: AttemptOutcome;
37
+ status: number | null;
38
+ elapsedMs: number | null;
39
+ serverTiming: ServerDurations;
40
+ acknowledgment: {
41
+ acceptedSpans: number | null;
42
+ rejectedSpans: number | null;
43
+ } | null;
44
+ };
45
+ type TransportDiagnosticsSnapshot = {
46
+ scope: 'otlp';
47
+ recentLimit: 32;
48
+ requests: {
49
+ started: number;
50
+ inFlight: number;
51
+ returned: number;
52
+ failed: number;
53
+ cancelled: number;
54
+ duration: DurationSummary;
55
+ };
56
+ attempts: {
57
+ started: number;
58
+ inFlight: number;
59
+ retries: number;
60
+ outcomes: Record<AttemptOutcome, number>;
61
+ duration: DurationSummary;
62
+ };
63
+ acknowledgments: {
64
+ complete: number;
65
+ partial: number;
66
+ unknown: number;
67
+ };
68
+ serverTiming: Record<Stage, DurationSummary>;
69
+ recent: TransportAttempt[];
70
+ };
71
+
1
72
  /**
2
73
  * Async-safe context variables for the Sentrial SDK.
3
74
  *
@@ -99,28 +170,6 @@ declare function getSessionContext(): string | null;
99
170
  * Set the default client for wrappers.
100
171
  */
101
172
  declare function setDefaultClient(client: SentrialClient): void;
102
- /**
103
- * Wrap an OpenAI client to automatically track all LLM calls.
104
- *
105
- * @param client - OpenAI client instance
106
- * @param options - Wrapper options
107
- * @returns The same client, now with auto-tracking enabled
108
- *
109
- * @example
110
- * ```ts
111
- * import OpenAI from 'openai';
112
- * import { wrapOpenAI, configure } from '@sentrial/sdk';
113
- *
114
- * configure({ apiKey: 'sentrial_live_xxx' });
115
- * const openai = wrapOpenAI(new OpenAI());
116
- *
117
- * // Now use client normally - all calls are tracked!
118
- * const response = await openai.chat.completions.create({
119
- * model: 'gpt-4o',
120
- * messages: [{ role: 'user', content: 'Hello' }],
121
- * });
122
- * ```
123
- */
124
173
  declare function wrapOpenAI<T extends object>(client: T, options?: {
125
174
  trackWithoutSession?: boolean;
126
175
  }): T;
@@ -189,470 +238,36 @@ declare function wrapGoogle<T extends object>(model: T, options?: {
189
238
  */
190
239
  declare function wrapLLM<T extends object>(client: T, provider?: 'openai' | 'anthropic' | 'google'): T;
191
240
 
192
- /**
193
- * PII Redaction Module
194
- *
195
- * Automatically detects and redacts personally identifiable information (PII)
196
- * from SDK payloads before they are sent to the Sentrial API.
197
- */
198
- /** How redacted values are replaced */
199
- type PiiMode = 'label' | 'hash' | 'remove';
200
- /** Fields that can be redacted in SDK payloads */
201
- type PiiField = 'userInput' | 'assistantOutput' | 'toolInput' | 'toolOutput' | 'metadata' | 'reasoning' | 'failureReason';
202
- /** A custom regex pattern with a human-readable label */
203
- interface PiiCustomPattern {
204
- pattern: RegExp;
205
- label: string;
206
- }
207
- /** Toggle individual builtin pattern categories */
208
- interface PiiBuiltinPatterns {
209
- emails?: boolean;
210
- phones?: boolean;
211
- ssns?: boolean;
212
- creditCards?: boolean;
213
- ipAddresses?: boolean;
214
- }
215
- /** Full PII redaction configuration */
216
- interface PiiConfig {
217
- /** Enable PII redaction (default: false) */
218
- enabled: boolean;
219
- /** Which payload fields to redact (default: DEFAULT_FIELDS) */
220
- fields?: PiiField[];
221
- /** Replacement mode (default: 'label') */
222
- mode?: PiiMode;
223
- /** Enable enhanced detection heuristics (reserved for future use) */
224
- enhancedDetection?: boolean;
225
- /** Toggle builtin patterns (default: all enabled) */
226
- builtinPatterns?: PiiBuiltinPatterns;
227
- /** Additional custom patterns to apply */
228
- customPatterns?: PiiCustomPattern[];
229
- }
230
- /**
231
- * Hash a value using SHA-256 and return the first 6 hex characters.
232
- * Used in 'hash' mode to produce a short, deterministic pseudonym.
233
- */
234
- declare function hashValue(value: string): string;
235
- /**
236
- * Produce the replacement string for a matched PII value.
237
- *
238
- * - 'label' mode: [LABEL]
239
- * - 'hash' mode: [PII:abcdef]
240
- * - 'remove' mode: '' (empty string)
241
- */
242
- declare function replaceMatch(match: string, label: string, mode: PiiMode): string;
243
- /**
244
- * Run all enabled builtin and custom patterns against a single string,
245
- * replacing every match according to the specified mode.
246
- */
247
- declare function redactString(value: string, mode: PiiMode, builtinPatterns: Required<PiiBuiltinPatterns>, customPatterns: PiiCustomPattern[]): string;
248
- /**
249
- * Recursively redact PII from a value. Handles strings, arrays, plain objects,
250
- * and passes through primitives (numbers, booleans, null, undefined) unchanged.
251
- */
252
- declare function redactValue(value: unknown, mode: PiiMode, builtinPatterns: Required<PiiBuiltinPatterns>, customPatterns: PiiCustomPattern[]): unknown;
253
- /**
254
- * Main entry point: redact configured fields in a payload object.
255
- *
256
- * Only fields listed in the PiiConfig (or DEFAULT_FIELDS) are redacted.
257
- * Other fields are returned unchanged.
258
- *
259
- * @param payload - The data payload (e.g., session params, tool call params)
260
- * @param config - PII configuration from the client
261
- * @returns A new payload object with PII redacted from the configured fields
262
- */
263
- declare function redactPayload(payload: Record<string, unknown>, config: PiiConfig): Record<string, unknown>;
264
-
265
- /**
266
- * EventBatcher — fire-and-forget event queue with periodic flushing.
267
- *
268
- * Buffers tracking events (tool calls, decisions, errors, generic events)
269
- * and flushes them in batches to reduce HTTP overhead. Session lifecycle
270
- * calls (createSession, completeSession) bypass the batcher entirely
271
- * because they need synchronous responses.
272
- *
273
- * Events are delivered in enqueue order so server-assigned sequence numbers
274
- * match the order an agent actually executed steps.
275
- *
276
- * Flush triggers:
277
- * 1. Timer — every `flushIntervalMs` (default 1 000 ms)
278
- * 2. Queue size — when queue reaches `flushThreshold` (default 10)
279
- * 3. Manual — `flush()` or `shutdown()`
280
- * 4. Process exit — `beforeExit` handler
281
- *
282
- * Back-pressure: queue is capped at `maxQueueSize` (default 1 000).
283
- * When full, oldest events are dropped and a warning is logged.
284
- */
285
- interface BatcherConfig {
286
- /** Whether batching is enabled (default: false — all calls are immediate) */
287
- enabled?: boolean;
288
- /** Milliseconds between automatic flushes (default: 1000) */
289
- flushIntervalMs?: number;
290
- /** Flush when the queue reaches this many items (default: 10) */
291
- flushThreshold?: number;
292
- /** Maximum queue size before dropping oldest events (default: 1000) */
293
- maxQueueSize?: number;
294
- }
295
- /** Signature for the function that actually sends an event to the API. */
296
- type SendFn = (method: string, url: string, body: unknown) => Promise<unknown>;
297
- declare class EventBatcher {
298
- private readonly queue;
299
- private readonly flushIntervalMs;
300
- private readonly flushThreshold;
301
- private readonly maxQueueSize;
302
- private timer;
303
- private sendFn;
304
- private flushing;
305
- private shutdownCalled;
306
- private readonly exitHandler;
307
- constructor(sendFn: SendFn, config?: BatcherConfig);
308
- /**
309
- * Enqueue an event for batched delivery.
310
- *
311
- * If the queue hits `flushThreshold`, an automatic flush is triggered.
312
- * If the queue is full (`maxQueueSize`), the oldest event is dropped.
313
- */
314
- enqueue(method: string, url: string, body: unknown): void;
315
- /**
316
- * Flush all queued events to the API.
317
- *
318
- * Drains the queue and sends requests in enqueue order. Safe to call
319
- * concurrently — only one flush runs at a time.
320
- */
321
- flush(): Promise<void>;
322
- /**
323
- * Stop the batcher: clear the timer, flush remaining events, remove exit handler.
324
- */
325
- shutdown(): Promise<void>;
326
- /** Number of events currently queued. */
327
- get size(): number;
328
- }
329
-
330
- /**
331
- * Type definitions for the Sentrial TypeScript SDK
332
- */
333
-
334
- /**
335
- * Event types that can be tracked in a session
336
- */
337
- declare enum EventType {
338
- TOOL_CALL = "tool_call",
339
- LLM_DECISION = "llm_decision",
340
- STATE_CHANGE = "state_change",
341
- ERROR = "error"
241
+ /** Portable, payload-free context for a separately instrumented worker session.
242
+ * This is correlation metadata, never an authorization token or proof of outcome. */
243
+ interface ActionIdentity {
244
+ actionId: string;
245
+ attemptId?: string;
246
+ retryOfAttemptId?: string;
342
247
  }
343
- /**
344
- * Session status values
345
- */
346
- type SessionStatus = 'running' | 'completed' | 'failed' | 'cancelled';
347
- /**
348
- * Configuration options for SentrialClient
349
- */
350
- interface SentrialClientConfig {
351
- /** API key for authentication (defaults to SENTRIAL_API_KEY env var) */
352
- apiKey?: string;
353
- /** URL of the Sentrial API server (defaults to SENTRIAL_API_URL env var or production) */
354
- apiUrl?: string;
355
- /**
356
- * If true (default), SDK errors are logged but won't crash your app.
357
- * Set to false during development to see full errors.
358
- */
359
- failSilently?: boolean;
360
- /**
361
- * PII redaction configuration.
362
- * - Pass a PiiConfig object for full control.
363
- * - Pass `true` to fetch the org's PII config from the server automatically.
364
- */
365
- pii?: PiiConfig | boolean;
366
- /**
367
- * Event batching configuration.
368
- * When enabled, fire-and-forget tracking calls (trackToolCall, trackDecision,
369
- * trackError, trackEvent) are queued and flushed periodically instead of
370
- * being sent immediately. Session lifecycle calls (createSession,
371
- * completeSession) always bypass the batcher.
372
- */
373
- batching?: BatcherConfig;
248
+ interface HandoffContext extends ActionIdentity {
249
+ version: 'sentrial-handoff-v1';
250
+ parentSessionId: string;
374
251
  }
375
- /**
376
- * Parameters for creating a new session
377
- */
378
- interface CreateSessionParams {
379
- /** Name of the session */
380
- name: string;
381
- /** Identifier for the agent type (used for grouping) */
382
- agentName: string;
383
- /** External user ID (for grouping sessions by end-user) */
384
- userId: string;
385
- /** Optional parent session ID for multi-agent hierarchies */
386
- parentSessionId?: string;
387
- /** Optional conversation ID to group related sessions into a thread */
388
- convoId?: string;
389
- /** Optional metadata */
390
- metadata?: Record<string, unknown>;
391
- /** Production experiment assignment ids to link to this session */
392
- experimentAssignmentIds?: string[];
393
- }
394
- /**
395
- * Parameters for completing a session
396
- */
397
- interface CompleteSessionParams {
398
- /** Session ID */
399
- sessionId: string;
400
- /** Whether the session successfully completed its goal (default: true) */
401
- success?: boolean;
402
- /** If success=false, why did it fail? */
403
- failureReason?: string;
404
- /** Total estimated cost in USD for this session */
405
- estimatedCost?: number;
406
- /** Custom metrics (e.g., { customer_satisfaction: 4.5, steps_taken: 3 }) */
407
- customMetrics?: Record<string, number>;
408
- /** Duration in milliseconds (auto-calculated if not provided) */
409
- durationMs?: number;
410
- /** Number of prompt/input tokens used */
411
- promptTokens?: number;
412
- /** Number of completion/output tokens used */
413
- completionTokens?: number;
414
- /** Total tokens used (prompt + completion) */
415
- totalTokens?: number;
416
- /** The user's original query/input for this session */
417
- userInput?: string;
418
- /** The final assistant response for this session */
419
- assistantOutput?: string;
420
- /** Alias for assistantOutput - the final output text */
421
- output?: string;
422
- }
423
- /**
424
- * Session data returned from the API
425
- */
426
- interface Session {
427
- id: string;
428
- agentId?: string;
429
- parentSessionId?: string;
430
- convoId?: string;
431
- name: string;
432
- agentName: string;
433
- userId: string;
434
- status: SessionStatus;
435
- totalEvents: number;
436
- userInput?: string;
437
- assistantOutput?: string;
438
- success?: boolean;
439
- failureReason?: string;
440
- estimatedCost?: number;
441
- customMetrics?: Record<string, unknown>;
442
- score?: number;
443
- metricScores?: Record<string, unknown>;
444
- metricReasonings?: Record<string, string>;
445
- promptTokens?: number;
446
- completionTokens?: number;
447
- totalTokens?: number;
448
- durationMs?: number;
449
- metadata?: Record<string, unknown>;
450
- createdAt: Date;
451
- updatedAt: Date;
452
- completedAt?: Date;
453
- }
454
- /**
455
- * Parameters for tracking a tool call event
456
- */
457
- interface TrackToolCallParams {
458
- /** Session ID */
459
- sessionId: string;
460
- /** Name of the tool */
461
- toolName: string;
462
- /** Tool input data */
463
- toolInput: Record<string, unknown>;
464
- /** Tool output data */
465
- toolOutput: Record<string, unknown>;
466
- /** Optional reasoning for why this tool was called */
467
- reasoning?: string;
468
- /** Estimated cost in USD for this tool call */
469
- estimatedCost?: number;
470
- /** Error details if the tool failed */
471
- toolError?: Record<string, unknown>;
472
- /** Number of tokens used by this tool call (for LLM-based tools) */
473
- tokenCount?: number;
474
- /** OpenTelemetry trace ID for distributed tracing */
475
- traceId?: string;
476
- /** OpenTelemetry span ID for distributed tracing */
477
- spanId?: string;
478
- /** Additional metadata */
479
- metadata?: Record<string, unknown>;
480
- }
481
- /**
482
- * Parameters for tracking an LLM decision event
483
- */
484
- interface TrackDecisionParams {
485
- /** Session ID */
486
- sessionId: string;
487
- /** Decision reasoning */
488
- reasoning: string;
489
- /** Alternative options considered */
490
- alternatives?: string[];
491
- /** Confidence score (0.0 to 1.0) */
492
- confidence?: number;
493
- /** Estimated cost in USD for this decision */
494
- estimatedCost?: number;
495
- /** Number of tokens used */
496
- tokenCount?: number;
497
- /** OpenTelemetry trace ID for distributed tracing */
498
- traceId?: string;
499
- /** OpenTelemetry span ID for distributed tracing */
500
- spanId?: string;
501
- /** Additional metadata */
502
- metadata?: Record<string, unknown>;
503
- }
504
- /**
505
- * Parameters for tracking an error event
506
- */
507
- interface TrackErrorParams {
508
- /** Session ID */
509
- sessionId: string;
510
- /** Error message */
511
- errorMessage: string;
512
- /** Type of error (e.g., "ValueError", "APIError") */
513
- errorType?: string;
514
- /** Name of the tool that caused the error (if applicable) */
515
- toolName?: string;
516
- /** Stack trace for debugging */
517
- stackTrace?: string;
518
- /** OpenTelemetry trace ID for distributed tracing */
519
- traceId?: string;
520
- /** OpenTelemetry span ID for distributed tracing */
521
- spanId?: string;
522
- /** Additional metadata */
523
- metadata?: Record<string, unknown>;
524
- }
525
- /**
526
- * Event data returned from the API
527
- */
528
- interface Event {
529
- id: string;
530
- sessionId: string;
531
- eventType: EventType;
532
- timestamp: Date;
533
- sequenceNumber: number;
534
- toolName?: string;
535
- toolInput?: Record<string, unknown>;
536
- toolOutput?: Record<string, unknown>;
537
- toolError?: Record<string, unknown>;
538
- reasoning?: string;
539
- alternativesConsidered?: string[];
540
- confidence?: number;
541
- stateBefore?: Record<string, unknown>;
542
- stateAfter?: Record<string, unknown>;
543
- stateDiff?: Record<string, unknown>;
544
- estimatedCost?: number;
545
- tokenCount?: number;
546
- metadata?: Record<string, unknown>;
547
- traceId?: string;
548
- spanId?: string;
549
- }
550
- /**
551
- * Parameters for beginning an interaction
552
- */
553
- interface BeginParams {
554
- /** External user ID for grouping sessions */
555
- userId: string;
556
- /** Event type/name (e.g., "chat_message", "search_query") */
557
- event: string;
558
- /** Optional input data for the interaction */
559
- input?: string;
560
- /** Optional custom event ID (auto-generated UUID if not provided) */
561
- eventId?: string;
562
- /** Optional conversation ID to group related interactions */
563
- convoId?: string;
564
- /** Optional additional metadata */
565
- metadata?: Record<string, unknown>;
566
- }
567
- /**
568
- * Parameters for finishing an interaction
569
- */
570
- interface FinishParams {
571
- /** Output/response from the interaction */
572
- output?: string;
573
- /** Whether the interaction succeeded (default: true) */
574
- success?: boolean;
575
- /** Reason for failure if success=false */
576
- failureReason?: string;
577
- /** Total estimated cost in USD */
578
- estimatedCost?: number;
579
- /** Custom metrics */
580
- customMetrics?: Record<string, number>;
581
- /** Number of prompt/input tokens used */
582
- promptTokens?: number;
583
- /** Number of completion/output tokens used */
584
- completionTokens?: number;
585
- /** Total tokens used */
586
- totalTokens?: number;
587
- /** Duration in milliseconds (auto-calculated from begin/finish if not provided) */
588
- durationMs?: number;
589
- }
590
- interface ProductionVariantConfig {
591
- name: string;
592
- weight?: number;
593
- prompt?: string;
594
- systemPrompt?: string;
595
- model?: string;
596
- promptVersion?: string;
597
- toolConfig?: Record<string, unknown>;
598
- metadata?: Record<string, unknown>;
599
- }
600
- interface GetVariantParams {
601
- /** Production experiment name configured in Sentrial */
602
- experimentName: string;
603
- /** Agent name this variant is for */
604
- agentName?: string;
605
- /** External user id for sticky bucketing */
606
- userId?: string;
607
- /** Conversation id for sticky multi-turn bucketing */
608
- convoId?: string;
609
- /** Stable anonymous id if no user/conversation id exists */
610
- anonymousId?: string;
611
- /** Optional existing Sentrial session id */
612
- sessionId?: string;
613
- /** Variant to use when no active live experiment matches */
614
- defaultVariant?: string;
615
- /** Prompt version currently running in the customer app */
616
- promptVersion?: string;
617
- /** Model currently running in the customer app */
618
- model?: string;
619
- /** Extra assignment metadata */
620
- metadata?: Record<string, unknown>;
621
- }
622
- interface VariantAssignment {
623
- active: boolean;
624
- reason: 'assigned' | 'holdout' | 'not_found';
625
- experimentId?: string;
626
- assignmentId?: string;
627
- variant: string;
628
- variantConfig?: ProductionVariantConfig;
629
- prompt?: string;
630
- systemPrompt?: string;
631
- model?: string;
632
- promptVersion?: string;
633
- }
634
- /**
635
- * Parameters for cost calculation
636
- */
637
- interface CostParams {
638
- /** Model name (e.g., "gpt-4", "claude-3-sonnet") */
639
- model: string;
640
- /** Number of input tokens */
641
- inputTokens: number;
642
- /** Number of output tokens */
643
- outputTokens: number;
644
- }
645
- /**
646
- * Standard API response wrapper
647
- */
648
- interface ApiResponse<T> {
649
- data?: T;
650
- error?: {
651
- code: string;
652
- message: string;
653
- details?: Record<string, unknown>;
654
- };
252
+ interface DispatchContext extends ActionIdentity {
253
+ version: 'sentrial-handoff-v2';
254
+ parentSessionId: string;
255
+ dispatchId: string;
655
256
  }
257
+ type HandoffOutcome = 'completed' | 'failed' | 'cancelled';
258
+ declare function actionMetadata(identity: ActionIdentity, metadata?: Record<string, unknown>): Record<string, unknown>;
259
+ declare function createHandoffContext(input: ActionIdentity & {
260
+ parentSessionId: string;
261
+ }): string;
262
+ declare function resumeHandoffContext(carrier: string, metadata?: Record<string, unknown>): {
263
+ parentSessionId: string;
264
+ metadata: Record<string, unknown>;
265
+ };
266
+ /** Use a new dispatchId for each execution; keep it stable only for transport retries. */
267
+ declare function createDispatchContext(input: ActionIdentity & {
268
+ parentSessionId: string;
269
+ dispatchId: string;
270
+ }): string;
656
271
 
657
272
  /**
658
273
  * Cost calculation utilities for various LLM providers
@@ -709,15 +324,140 @@ declare function calculateAnthropicCost(params: CostParams): number;
709
324
  * ```
710
325
  */
711
326
  declare function calculateGoogleCost(params: CostParams): number;
712
-
713
327
  /**
714
- * Sentrial Client - Main SDK interface
715
- *
716
- * Captures agent sessions, tool calls, and metrics. This data powers:
717
- * - Signal detection (auto-detect patterns/anomalies)
718
- * - Root cause analysis (understand WHY agents fail)
719
- * - Code fixer (AI-suggested fixes with GitHub PRs)
720
- */
328
+ * Provider → cost dispatch. SINGLE source shared by the Vercel AI SDK wrapper (vercel.ts) and the
329
+ * Mastra SDK so a provider is billed identically across integrations — previously each carried its
330
+ * own switch and the Mastra copy dropped flat-rate providers to $0 (#488/#559).
331
+ *
332
+ * UNIFIED CROSS-SDK FALLBACK POLICY (lockstep with the Python SDK's `resolve_provider_cost` in
333
+ * packages/python-sdk/sentrial/pricing.py, guarded by tests/test_pricing_sync.py). The chain,
334
+ * identical in both SDKs:
335
+ * 1. Resolve the provider HINT tolerantly, by SUBSTRING — OTel-semconv spellings like
336
+ * 'az.ai.openai' or 'mistral_ai' resolve to the correct table (openai/anthropic/google first,
337
+ * then the flat-rate providers). A hint that resolves nothing falls back to inferring the
338
+ * provider from the model id (guessProvider), so an empty/'unknown'/'gateway' hint never
339
+ * forces the default block when the model id names its family.
340
+ * 2. A gateway 'creator/model' id whose creator is a FLAT-rate provider bills that flat rate
341
+ * regardless of the hint (an Anthropic-compatible gateway may carry a keyset hint).
342
+ * 3. openai/anthropic/google bill on their per-model tables; a gateway creator naming one of
343
+ * them wins the table choice and the slash prefix is always stripped for the key match. An
344
+ * UNKNOWN model within a known provider bills the provider's cheap-modern
345
+ * `fallback.{provider}` key (never an expensive legacy flagship).
346
+ * 4. Flat-rate providers (deepseek/cohere/mistral) bill their canonical `flatProviders` rate.
347
+ * 5. A GENUINELY unknown provider (including recognized-but-unpriced families like meta/llama
348
+ * and alibaba/qwen) bills the shared cheap `fallback.default` block — NEVER a silent $0,
349
+ * which would understate real spend. (Unified Jul 2026; this dispatch previously returned $0
350
+ * here while Python billed the default block.)
351
+ * All rates come from the canonical pricing.json (fallback.default included).
352
+ */
353
+ declare function calculateCostByProvider(provider: string, model: string, inputTokens: number, outputTokens: number): number;
354
+ /**
355
+ * Usage shape accepted by {@link normalizeUsage} — supports both AI SDK v3/v4
356
+ * (promptTokens/completionTokens, OpenAI naming) and v5/v6 (inputTokens/outputTokens,
357
+ * generic naming). All fields optional; a `totalTokens` may be present on either.
358
+ */
359
+ type NormalizableUsage = {
360
+ inputTokens?: number;
361
+ outputTokens?: number;
362
+ promptTokens?: number;
363
+ completionTokens?: number;
364
+ totalTokens?: number;
365
+ };
366
+ type NormalizedUsage = {
367
+ promptTokens: number;
368
+ completionTokens: number;
369
+ totalTokens: number;
370
+ };
371
+ /**
372
+ * Normalize LLM usage from any Vercel AI SDK version (and Mastra, which follows AI SDK naming)
373
+ * into a consistent { promptTokens, completionTokens, totalTokens } shape; returns undefined when
374
+ * there is no usage or every count is zero.
375
+ *
376
+ * AI SDK v3/v4 uses promptTokens/completionTokens (OpenAI naming); v5/v6 uses
377
+ * inputTokens/outputTokens (generic naming). This is the SINGLE canonical implementation shared by
378
+ * the Vercel AI SDK wrapper (vercel.ts) and the Mastra SDK, which previously each carried a
379
+ * divergent copy: vercel.ts coerced with `|| 0` (NaN/non-numeric -> 0) while mastra used `?? 0`
380
+ * (nullish-only, so a NaN or a non-numeric field could leak straight through). We adopt the SAFER
381
+ * `|| 0`-style semantics here via `firstTokenCount` (an explicit Number.isFinite guard applied to
382
+ * BOTH the field selection and the final value) so a malformed usage object can never surface
383
+ * NaN/garbage token counts or costs, and a NaN primary field (e.g. inputTokens) falls through to a
384
+ * valid secondary (promptTokens) rather than masking it — this does not change behavior for real
385
+ * numeric inputs, and only tightens the previously looser mastra path.
386
+ */
387
+ declare function normalizeUsage(usage: NormalizableUsage | Record<string, unknown> | undefined): NormalizedUsage | undefined;
388
+ declare function guessProvider(modelId: string): string;
389
+ /**
390
+ * Canonical "normalize raw AI-SDK usage → prompt/completion/total token counts → cost via
391
+ * calculateCostByProvider(provider, model, ...)" derivation, shared by BOTH AI-SDK wrappers
392
+ * (Vercel `resolveTokensAndCost` call sites and the Mastra integration). Lives here — beside
393
+ * normalizeUsage + calculateCostByProvider — so the two wrappers can never drift on how they
394
+ * fold provider-extra token fields; previously only the Vercel copy folded Anthropic
395
+ * cachedInputTokens / Google reasoningTokens, so @sentrial/mastra silently under-billed
396
+ * cache-read-heavy Anthropic and thinking-heavy Google calls.
397
+ *
398
+ * totalTokens comes from normalizeUsage, which already falls back to promptTokens+completionTokens,
399
+ * so this preserves the stricter sum-fallback everywhere (no caller regresses to `?? 0`). The
400
+ * containment arithmetic gates each fold so subset-reporting providers (OpenAI) and AI-SDK v7
401
+ * (fields moved into inputTokenDetails/outputTokenDetails) can never double-count.
402
+ */
403
+ /**
404
+ * Fold AI-SDK provider-extra token fields into flat prompt/completion/total counts.
405
+ *
406
+ * AI SDK v5/v6 report provider extras as separate top-level fields that our flat reads drop:
407
+ * - Anthropic: inputTokens EXCLUDES cache reads (cachedInputTokens is EXTRA; total = in+out)
408
+ * - Google: outputTokens EXCLUDES thinking (reasoningTokens; total = in+out+reasoning)
409
+ * OpenAI reports both as SUBSETS (already inside the flat counts), and v7 moved the fields into
410
+ * inputTokenDetails/outputTokenDetails (absent here) — so each fold is gated on the containment
411
+ * arithmetic so subset-reporting providers and v7 can never double-count. Shared by BOTH AI-SDK
412
+ * wrappers (resolveTokensAndCost + the Mastra per-step aggregator) so they can never drift.
413
+ */
414
+ declare function foldProviderExtras(rawUsage: NormalizableUsage | Record<string, unknown> | undefined, provider: string, base: {
415
+ promptTokens: number;
416
+ completionTokens: number;
417
+ totalTokens: number;
418
+ }): {
419
+ promptTokens: number;
420
+ completionTokens: number;
421
+ totalTokens: number;
422
+ /**
423
+ * The Anthropic cache-READ tokens folded into `promptTokens` (0 when no fold applied). Reported
424
+ * token COUNTS stay cache-inclusive, but cost must weight this slice at
425
+ * {@link ANTHROPIC_CACHE_READ_MULTIPLIER} (0.1x), matching the direct wrapAnthropic path — so
426
+ * callers computing cost subtract 0.9x of this back out. See resolveTokensAndCost.
427
+ */
428
+ foldedCachedInputTokens: number;
429
+ };
430
+ declare function resolveTokensAndCost(rawUsage: NormalizableUsage | Record<string, unknown> | undefined, provider: string, model: string,
431
+ /**
432
+ * Anthropic cache-READ tokens already folded into `rawUsage`'s prompt count by an UPSTREAM
433
+ * per-step pass (the Mastra aggregator folds each step before summing, then discards the raw
434
+ * `cachedInputTokens` field so foldProviderExtras here can't re-derive it). Pass the summed
435
+ * folded count so the 0.1x cache-read discount is still applied to the cost basis. Omit (0)
436
+ * for the normal single-usage path, where foldProviderExtras below computes the fold itself.
437
+ */
438
+ preFoldedCachedInputTokens?: number): {
439
+ promptTokens: number;
440
+ completionTokens: number;
441
+ totalTokens: number;
442
+ cost: number;
443
+ };
444
+
445
+ declare const AGENT_DIAGNOSTIC_CATEGORIES: readonly ["missing_context", "tool_problem", "missing_capability", "uncertain_completion"];
446
+ interface AgentDiagnosticInput {
447
+ /** Opaque identifier reused for subsequent reports within this session. */
448
+ reportId: string;
449
+ category: (typeof AGENT_DIAGNOSTIC_CATEGORIES)[number];
450
+ /** Both states are assertions by the agent, including claimed recovery. */
451
+ state: 'reported' | 'recovered';
452
+ description: string;
453
+ /** Optional related OTel span IDs in this trace; these are unverified references. */
454
+ relatedSpanIds?: readonly string[];
455
+ }
456
+ interface AgentDiagnostic extends Omit<AgentDiagnosticInput, 'relatedSpanIds'> {
457
+ revision: 'agent-diagnostic-v1';
458
+ origin: 'agent_report';
459
+ relatedSpanIds: readonly string[];
460
+ }
721
461
 
722
462
  /**
723
463
  * Sentrial Client for agent observability.
@@ -758,6 +498,18 @@ declare function calculateGoogleCost(params: CostParams): number;
758
498
  * By default, the SDK fails silently (failSilently=true) to ensure
759
499
  * monitoring never crashes your application. Set failSilently=false
760
500
  * during development to see errors.
501
+ *
502
+ * Containment seam (the never-crash contract's enforcement point): every public tracking
503
+ * entrypoint on this client (and on Interaction) is wrapped by `containMethod` (see
504
+ * contained.ts and the application block at the bottom of this file) — the ENTIRE method
505
+ * body runs inside one try honoring `failSilently`. Any exception escaping a method body
506
+ * is caught at the method boundary: with failSilently=true (default) it is logged once per
507
+ * method and the method's degraded value is returned (null / undefined / a degraded
508
+ * Interaction); with failSilently=false it rethrows/rejects unchanged. Interior guards
509
+ * (toText, safeJsonStringify, capStateSnapshot, accumulate's safeNum, the toolError field
510
+ * accessors) remain for DATA QUALITY: they let a tracking call still succeed with degraded
511
+ * values instead of the seam dropping the whole call. Mirrors the Python SDK's
512
+ * `_wire.contained` decorations.
761
513
  */
762
514
  declare class SentrialClient {
763
515
  private readonly apiUrl;
@@ -768,9 +520,26 @@ declare class SentrialClient {
768
520
  private piiHydrationPromise?;
769
521
  private readonly _stateVar;
770
522
  private readonly batcher?;
523
+ private readonly contentUploader?;
524
+ private readonly transportDiagnostics;
525
+ private readonly otlpSessions;
526
+ /** Every ambient OTel traceId this client has EVER consumed for a session — permanent (bounded,
527
+ * FIFO) because the server's otel-sess:<traceId> fallback key is permanent; see claimTraceId. */
528
+ private readonly consumedAmbientTraceIds;
771
529
  private pendingExperimentAssignmentIds;
772
530
  /** Per-session cost/token accumulator — populated by trackToolCall/trackDecision */
773
531
  private sessionAccumulators;
532
+ /**
533
+ * Accumulators for sessions that never call completeSession() — e.g. a mastra stream the caller
534
+ * abandons without consuming, so its onFinish never fires — are evicted after this TTL so their
535
+ * bookkeeping can't linger. The TTL is measured from LAST ACTIVITY (accumulate() refreshes the
536
+ * entry's timestamp), so an actively-tracking long-running session is never dropped — only
537
+ * sessions idle for 2h expire. The accumulator is only a fallback for callers that don't pass
538
+ * explicit cost/tokens to finish (#561).
539
+ */
540
+ private static readonly ACCUMULATOR_TTL_MS;
541
+ /** Hard cap on live accumulator entries — backstop for the TTL sweep (#561). */
542
+ private static readonly MAX_ACCUMULATORS;
774
543
  private get currentState();
775
544
  private set currentState(value);
776
545
  constructor(config?: SentrialClientConfig);
@@ -787,7 +556,14 @@ declare class SentrialClient {
787
556
  * Retries on transient failures (network errors, timeouts, 429/5xx).
788
557
  * Up to MAX_RETRIES attempts with exponential backoff.
789
558
  */
559
+ /**
560
+ * Public wrapper around the retrying transport (exponential backoff, 10s timeout,
561
+ * PII redaction, structured ApiError/NetworkError) so callers like Experiment reuse
562
+ * it instead of hand-rolling a bare fetch (#482).
563
+ */
564
+ apiRequest<T>(method: string, url: string, body?: unknown): Promise<T | null>;
790
565
  private safeRequest;
566
+ private requestWithRetries;
791
567
  private sleep;
792
568
  private accumulate;
793
569
  /**
@@ -796,6 +572,8 @@ declare class SentrialClient {
796
572
  * @param params - Session creation parameters
797
573
  * @returns Session ID, or null if the request failed and failSilently is true
798
574
  */
575
+ /** Build the dependency bundle the OTLP transport needs (private members accessed here). */
576
+ private otlpDeps;
799
577
  createSession(params: CreateSessionParams): Promise<string | null>;
800
578
  /**
801
579
  * Get a sticky production experiment variant for this user/conversation.
@@ -804,25 +582,28 @@ declare class SentrialClient {
804
582
  * created by this client. You can also pass `sessionId` to link immediately.
805
583
  */
806
584
  getVariant(params: GetVariantParams): Promise<VariantAssignment>;
585
+ /** Report only after using this configuration. getVariant/createSession never imply exposure.
586
+ * Returns null on fail-silent delivery failure; safe to retry with the same fields. */
587
+ recordExperimentExposure(params: RecordExperimentExposureParams): Promise<ExperimentExposureReceipt | null>;
807
588
  /**
808
589
  * Track a tool call event
809
590
  *
810
591
  * @param params - Tool call parameters
811
- * @returns Event data
592
+ * @returns `null` — the OTLP transport emits a span and does not return a synchronous event
812
593
  */
813
594
  trackToolCall(params: TrackToolCallParams): Promise<Event | null>;
814
595
  /**
815
596
  * Track an LLM decision event
816
597
  *
817
598
  * @param params - Decision parameters
818
- * @returns Event data
599
+ * @returns `null` — the OTLP transport emits a span and does not return a synchronous event
819
600
  */
820
601
  trackDecision(params: TrackDecisionParams): Promise<Event | null>;
821
602
  /**
822
603
  * Track an error event
823
604
  *
824
605
  * @param params - Error parameters
825
- * @returns Event data
606
+ * @returns `null` — the OTLP transport emits a span and does not return a synchronous event
826
607
  */
827
608
  trackError(params: TrackErrorParams): Promise<Event | null>;
828
609
  /**
@@ -835,17 +616,47 @@ declare class SentrialClient {
835
616
  /**
836
617
  * Set the user input for a session
837
618
  *
619
+ * Re-posts the session's root span (start-only, no end time) with the updated
620
+ * `gen_ai.input.messages` so the server refreshes `sessions.userInput` WITHOUT finalizing the
621
+ * session — it stays running. Always resolves `null` under the OTLP transport (no synchronous session row).
622
+ *
838
623
  * @param sessionId - Session ID
839
624
  * @param input - User input text
840
- * @returns Updated session or null on error
625
+ * @returns `null` — the OTLP transport does not return a synchronous session row
841
626
  */
842
627
  setInput(sessionId: string, input: string): Promise<Session | null>;
843
628
  /**
844
- * Track a generic event
629
+ * Merge additional metadata into a running session (post-begin).
630
+ *
631
+ * No network call: the merged metadata is carried on the finalize re-post, where the server
632
+ * merges it into the session's metadata (user keys top-level, later values win on a key
633
+ * collision; the reserved `otel` diagnostics key is preserved server-side). String values are
634
+ * redacted like begin()'s metadata.
635
+ *
636
+ * @param sessionId - Session ID
637
+ * @param metadata - Metadata keys to merge into the session metadata
638
+ */
639
+ setSessionMetadata(sessionId: string, metadata: Record<string, unknown>): void;
640
+ /**
641
+ * Set/override the session-level model for a running session (post-begin).
642
+ *
643
+ * No network call: carried on the finalize re-post as gen_ai.request.model and under the
644
+ * stable `model` key in the session metadata (the sessions table has no model column).
845
645
  *
846
- * @param params - Event parameters
847
- * @returns Event data or null on error
646
+ * @param sessionId - Session ID
647
+ * @param model - Model identifier (e.g. 'claude-sonnet-4-5')
648
+ */
649
+ setSessionModel(sessionId: string, model: string): void;
650
+ /**
651
+ * Record an optional agent assertion, including claimed recovery. No model call,
652
+ * automatic error status, state snapshot, or verified failure label is created.
653
+ * Reuse reportId within this session for later observations; each is retained.
848
654
  */
655
+ reportDiagnostic(params: {
656
+ sessionId: string;
657
+ report: AgentDiagnosticInput;
658
+ }): Promise<null>;
659
+ /** Track a generic event. */
849
660
  trackEvent(params: {
850
661
  sessionId: string;
851
662
  eventType: string;
@@ -858,7 +669,8 @@ declare class SentrialClient {
858
669
  * This is the recommended way to close sessions for performance monitoring.
859
670
  *
860
671
  * @param params - Session completion parameters
861
- * @returns Updated session data
672
+ * @returns `null` — the OTLP transport re-posts the final root span and the server finalizes
673
+ * the session asynchronously, so no session row is returned synchronously.
862
674
  *
863
675
  * @example
864
676
  * ```ts
@@ -882,14 +694,41 @@ declare class SentrialClient {
882
694
  /**
883
695
  * Flush any queued events immediately.
884
696
  *
885
- * No-op if batching is not enabled.
697
+ * Waits for pending content uploads before draining queued telemetry.
886
698
  */
887
699
  flush(): Promise<void>;
700
+ /** Queue delivery health; null when batching is disabled. Contains no payloads. */
701
+ getDeliveryDiagnostics(): Readonly<DeliveryDiagnostics> | null;
702
+ /** Content upload counts only; submitted bytes may still await server verification. */
703
+ getContentUploadDiagnostics(): {
704
+ attempted: number;
705
+ submitted: number;
706
+ missing: number;
707
+ active: number;
708
+ pendingEvents: number;
709
+ } | null;
710
+ /** Bounded process-local OTLP request/response diagnostics, independent of batching. */
711
+ getTransportDiagnostics(): TransportDiagnosticsSnapshot;
712
+ /** Record delegation before dispatch, then pass the returned carrier to the worker. */
713
+ handoff(identity: ActionIdentity & {
714
+ sessionId: string;
715
+ }): Promise<string>;
716
+ /** Emit before queueing work. This records intent, not queue delivery acknowledgment. */
717
+ dispatchHandoff(identity: ActionIdentity & {
718
+ sessionId: string;
719
+ dispatchId: string;
720
+ }): Promise<string>;
721
+ acceptHandoff(sessionId: string, carrier: string): Promise<void>;
722
+ /** Call before finishing the worker session. Outcome describes execution, not business success. */
723
+ finishHandoff(sessionId: string, carrier: string, outcome: HandoffOutcome): Promise<void>;
724
+ /** Call in the parent only after receiving this worker's terminal result. */
725
+ receiveHandoff(sessionId: string, carrier: string, workerSessionId: string, outcome: HandoffOutcome): Promise<void>;
726
+ private recordHandoff;
888
727
  /**
889
728
  * Shut down the event batcher, flushing remaining events.
890
729
  *
891
730
  * Call this before your process exits for a clean shutdown.
892
- * No-op if batching is not enabled.
731
+ * Cancels pending content uploads, then drains their fallback events and queued telemetry.
893
732
  */
894
733
  shutdown(): Promise<void>;
895
734
  /**
@@ -944,7 +783,6 @@ declare class Interaction {
944
783
  readonly userId: string;
945
784
  /** Event name for this interaction */
946
785
  readonly event: string;
947
- private readonly startTime;
948
786
  private finished;
949
787
  private success;
950
788
  private failureReason?;
@@ -963,11 +801,30 @@ declare class Interaction {
963
801
  * @param output - The output/response text
964
802
  */
965
803
  setOutput(output: string): void;
804
+ /**
805
+ * Merge additional metadata into this interaction's session (post-begin).
806
+ *
807
+ * Carried on the finish() re-post: the server merges the keys into the session's metadata
808
+ * top-level (later values win; the reserved `otel` diagnostics key is preserved).
809
+ *
810
+ * @param metadata - Metadata keys to merge into the session metadata
811
+ */
812
+ setMetadata(metadata: Record<string, unknown>): void;
813
+ /**
814
+ * Set/override the session-level model for this interaction (post-begin).
815
+ *
816
+ * Carried on the finish() re-post as gen_ai.request.model and under the stable `model` key in
817
+ * the session metadata.
818
+ *
819
+ * @param model - Model identifier (e.g. 'claude-sonnet-4-5')
820
+ */
821
+ setModel(model: string): void;
966
822
  /**
967
823
  * Finish the interaction and record metrics
968
824
  *
969
825
  * @param params - Finish parameters
970
- * @returns Updated session data, or null if degraded/already finished
826
+ * @returns `null` — the OTLP transport finalizes asynchronously server-side (also `null`
827
+ * if degraded/already finished).
971
828
  *
972
829
  * @example
973
830
  * ```ts
@@ -983,6 +840,8 @@ declare class Interaction {
983
840
  * Track a tool call within this interaction
984
841
  */
985
842
  trackToolCall(params: Omit<TrackToolCallParams, 'sessionId'>): Promise<Event | null>;
843
+ /** Report an agent observation without changing this interaction's success or failure. */
844
+ reportDiagnostic(report: AgentDiagnosticInput): Promise<null>;
986
845
  /**
987
846
  * Track an LLM decision within this interaction
988
847
  */
@@ -1044,6 +903,7 @@ declare function configure(config: SentrialClientConfig): void;
1044
903
  */
1045
904
  declare function begin(params: BeginParams): Promise<Interaction>;
1046
905
  declare function getVariant(params: GetVariantParams): Promise<VariantAssignment>;
906
+ declare function recordExperimentExposure(params: RecordExperimentExposureParams): Promise<ExperimentExposureReceipt | null>;
1047
907
  /**
1048
908
  * Simple API namespace for module-level usage
1049
909
  */
@@ -1053,6 +913,7 @@ declare const sentrial: {
1053
913
  configure: typeof configure;
1054
914
  begin: typeof begin;
1055
915
  getVariant: typeof getVariant;
916
+ recordExperimentExposure: typeof recordExperimentExposure;
1056
917
  flush: typeof flush;
1057
918
  shutdown: typeof shutdown;
1058
919
  };
@@ -1106,7 +967,11 @@ declare class NetworkError extends SentrialError {
1106
967
  constructor(message: string, cause?: Error);
1107
968
  }
1108
969
  /**
1109
- * Error thrown when validation fails
970
+ * Error thrown when validation fails.
971
+ *
972
+ * @deprecated The SDK no longer throws this internally. Retained as a public export for backwards
973
+ * compatibility with downstream consumers that import it; slated for removal in a future MINOR/MAJOR
974
+ * release (not a patch), per the SDK compatibility policy.
1110
975
  */
1111
976
  declare class ValidationError extends SentrialError {
1112
977
  constructor(message: string, details?: Record<string, unknown>);
@@ -1116,7 +981,7 @@ declare class ValidationError extends SentrialError {
1116
981
  * Vercel AI SDK Integration for Sentrial
1117
982
  *
1118
983
  * Automatically traces AI SDK calls with full input/output logging, metrics, and tool execution.
1119
- * Supports Vercel AI SDK v3, v4, v5, and v6.
984
+ * Supports Vercel AI SDK v3 through v7.
1120
985
  *
1121
986
  * @example Simple Usage
1122
987
  * ```ts
@@ -1152,13 +1017,33 @@ declare class ValidationError extends SentrialError {
1152
1017
  * @packageDocumentation
1153
1018
  */
1154
1019
 
1020
+ type AnyFunction = (...args: unknown[]) => unknown;
1021
+ type AISDKFunction = (...args: never[]) => unknown;
1155
1022
  type AIModule = {
1156
- generateText?: Function;
1157
- streamText?: Function;
1158
- generateObject?: Function;
1159
- streamObject?: Function;
1160
- experimental_generateText?: Function;
1161
- experimental_streamText?: Function;
1023
+ generateText?: AISDKFunction;
1024
+ streamText?: AISDKFunction;
1025
+ generateObject?: AISDKFunction;
1026
+ streamObject?: AISDKFunction;
1027
+ experimental_generateText?: AISDKFunction;
1028
+ experimental_streamText?: AISDKFunction;
1029
+ experimental_generateObject?: AISDKFunction;
1030
+ experimental_streamObject?: AISDKFunction;
1031
+ };
1032
+ type WrappedFunction<T, Stable extends PropertyKey, Experimental extends PropertyKey, Fallback> = T extends Record<Stable, infer F extends AISDKFunction> ? F : T extends Record<Experimental, infer F extends AISDKFunction> ? F : Fallback;
1033
+ type WrappedAISDK<T extends AIModule> = {
1034
+ generateText: WrappedFunction<T, 'generateText', 'experimental_generateText', ReturnType<typeof wrapGenerateText>>;
1035
+ streamText: WrappedFunction<T, 'streamText', 'experimental_streamText', ReturnType<typeof wrapStreamText>>;
1036
+ generateObject: WrappedFunction<T, 'generateObject', 'experimental_generateObject', ReturnType<typeof wrapGenerateObject>>;
1037
+ streamObject: WrappedFunction<T, 'streamObject', 'experimental_streamObject', ReturnType<typeof wrapStreamObject>>;
1038
+ };
1039
+ type WrapAISDKOptions = {
1040
+ client?: SentrialClient;
1041
+ defaultAgent?: string;
1042
+ userId?: StringOrFn;
1043
+ convoId?: StringOrFn;
1044
+ /** Session metadata, including explicit signalActionId / signalWorkflow when known.
1045
+ * Create a separate wrapper for each business action; no identity is inferred. */
1046
+ metadata?: Record<string, unknown>;
1162
1047
  };
1163
1048
  type GenerateTextParams = {
1164
1049
  model: {
@@ -1172,7 +1057,7 @@ type GenerateTextParams = {
1172
1057
  }>;
1173
1058
  system?: string;
1174
1059
  tools?: Record<string, {
1175
- execute?: Function;
1060
+ execute?: AnyFunction;
1176
1061
  } & Record<string, unknown>>;
1177
1062
  maxTokens?: number;
1178
1063
  maxSteps?: number;
@@ -1202,6 +1087,16 @@ type StepResult = {
1202
1087
  }>;
1203
1088
  usage?: UsageInfo;
1204
1089
  finishReason?: string;
1090
+ /** Per-step response metadata — v5+ modelId, v3/v4 model. */
1091
+ response?: {
1092
+ modelId?: string;
1093
+ model?: string;
1094
+ };
1095
+ /** v6+: the model that produced this step (prepareStep can switch mid-call). */
1096
+ model?: {
1097
+ provider?: string;
1098
+ modelId?: string;
1099
+ };
1205
1100
  };
1206
1101
  type GenerateTextResult = {
1207
1102
  text: string;
@@ -1253,6 +1148,7 @@ type WrapperConfig = {
1253
1148
  defaultAgent?: string;
1254
1149
  userId?: StringOrFn;
1255
1150
  convoId?: StringOrFn;
1151
+ metadata?: Record<string, unknown>;
1256
1152
  };
1257
1153
  /**
1258
1154
  * Configure the Sentrial SDK for Vercel AI SDK integration.
@@ -1276,10 +1172,10 @@ declare function configureVercel(config: {
1276
1172
  convoId?: StringOrFn;
1277
1173
  failSilently?: boolean;
1278
1174
  }): void;
1279
- declare function wrapGenerateText(originalFn: Function, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => Promise<GenerateTextResult>;
1280
- declare function wrapStreamText(originalFn: Function, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => StreamTextResult;
1281
- declare function wrapGenerateObject(originalFn: Function, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => Promise<unknown>;
1282
- declare function wrapStreamObject(originalFn: Function, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => unknown;
1175
+ declare function wrapGenerateText(originalFn: AnyFunction, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => Promise<GenerateTextResult>;
1176
+ declare function wrapStreamText(originalFn: AnyFunction, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => StreamTextResult;
1177
+ declare function wrapGenerateObject(originalFn: AnyFunction, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => Promise<unknown>;
1178
+ declare function wrapStreamObject(originalFn: AnyFunction, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => unknown;
1283
1179
  /**
1284
1180
  * Wrap the Vercel AI SDK to automatically trace all AI calls.
1285
1181
  *
@@ -1304,17 +1200,62 @@ declare function wrapStreamObject(originalFn: Function, client: SentrialClient,
1304
1200
  * const { generateText } = wrapAISDK(ai, { client, defaultAgent: 'my-agent' });
1305
1201
  * ```
1306
1202
  */
1307
- declare function wrapAISDK(ai: AIModule, options?: {
1308
- client?: SentrialClient;
1309
- defaultAgent?: string;
1310
- userId?: StringOrFn;
1311
- convoId?: StringOrFn;
1312
- }): {
1313
- generateText: ReturnType<typeof wrapGenerateText>;
1314
- streamText: ReturnType<typeof wrapStreamText>;
1315
- generateObject: ReturnType<typeof wrapGenerateObject>;
1316
- streamObject: ReturnType<typeof wrapStreamObject>;
1203
+ declare function wrapAISDK<T extends AIModule>(ai: T, options?: WrapAISDKOptions): WrappedAISDK<T>;
1204
+
1205
+ /**
1206
+ * Options shape accepted by {@link getVariantForAISDK} — a permissive subset of
1207
+ * an AI SDK `generateText` / `streamText` call. Extra keys are preserved, so it
1208
+ * composes with whatever options you already build.
1209
+ */
1210
+ type AISDKVariantOptions = {
1211
+ model?: unknown;
1212
+ system?: string;
1213
+ prompt?: string;
1214
+ messages?: unknown;
1215
+ experimental_telemetry?: {
1216
+ isEnabled?: boolean;
1217
+ metadata?: Record<string, unknown>;
1218
+ };
1219
+ [key: string]: unknown;
1317
1220
  };
1221
+ /**
1222
+ * Resolve a production A/B variant and merge it into Vercel AI SDK call options.
1223
+ *
1224
+ * Pairs `client.getVariant()` with a `generateText` / `streamText` call: it
1225
+ * fetches the sticky assignment for this user/convo, applies the variant's
1226
+ * `systemPrompt`, `prompt`, and (via `modelResolver`, since the AI SDK needs a
1227
+ * model *object*, not a string) `model`, and tags the call's
1228
+ * `experimental_telemetry.metadata` with the experiment + variant so the
1229
+ * exposure is visible on the resulting session. Returns the merged options
1230
+ * (spread straight into your `generateText` call) and the raw assignment so you
1231
+ * can branch on `assignment.variant`.
1232
+ *
1233
+ * The assignment is recorded server-side by `getVariant` regardless of whether a
1234
+ * variant config is applied, so a `reason: 'holdout'` / `'not_found'` result is
1235
+ * safe — the base options pass through unchanged (sticky control).
1236
+ *
1237
+ * @example
1238
+ * ```ts
1239
+ * import { openai } from '@ai-sdk/openai';
1240
+ * import { wrapAISDK, getVariantForAISDK } from '@sentrial/sdk';
1241
+ * import * as ai from 'ai';
1242
+ *
1243
+ * const { generateText } = wrapAISDK(ai);
1244
+ * const { options, assignment } = await getVariantForAISDK(
1245
+ * { experimentName: 'support-prompt-v2', userId, convoId },
1246
+ * { model: openai('gpt-4o-mini'), system: 'You are a support agent.', prompt: userQuery },
1247
+ * { modelResolver: (id) => openai(id) }
1248
+ * );
1249
+ * const { text } = await generateText(options); // runs the assigned variant, tracked
1250
+ * ```
1251
+ */
1252
+ declare function getVariantForAISDK<T extends AISDKVariantOptions>(variantParams: GetVariantParams, baseOptions: T, opts?: {
1253
+ modelResolver?: (modelId: string) => unknown;
1254
+ client?: SentrialClient;
1255
+ }): Promise<{
1256
+ options: T;
1257
+ assignment: VariantAssignment;
1258
+ }>;
1318
1259
 
1319
1260
  /**
1320
1261
  * Sentrial wrapper for the Claude Agent SDK (Claude Code).
@@ -1380,6 +1321,487 @@ interface WrapClaudeAgentOptions {
1380
1321
  */
1381
1322
  declare function wrapClaudeAgent(queryFn: QueryFunction, wrapOptions: WrapClaudeAgentOptions): QueryFunction;
1382
1323
 
1324
+ /**
1325
+ * OpenAI Agents SDK Integration for Sentrial
1326
+ *
1327
+ * Observes the OpenAI Agents SDK's (`@openai/agents`) tracing pipeline and maps
1328
+ * every trace onto a Sentrial session via the `begin()`/Interaction API:
1329
+ *
1330
+ * - **trace start** → `begin()` (agent/event name from the root agent span,
1331
+ * falling back to the trace's workflow name; session title from the trace name)
1332
+ * - **generation / response spans** → per-call LLM events (`llm:{model}` tool name
1333
+ * convention, prompt/completion tokens from usage, cost via the pricing tables,
1334
+ * duration from the span's start/end timestamps)
1335
+ * - **function spans** → `trackToolCall` (name, parsed args, result, errors)
1336
+ * - **handoff spans** → `trackDecision` ("Handoff from X to Y")
1337
+ * - **guardrail spans** → `trackError` when triggered, `trackDecision` when passed
1338
+ * - **trace end** → `finish()` (aggregated tokens/cost, last LLM output as the
1339
+ * session output, failure state from root agent span errors / triggered guardrails)
1340
+ *
1341
+ * Distinction from {@link wrapOpenAI}:
1342
+ * `wrapOpenAI` wraps a RAW `openai` client instance so each LLM call is tracked
1343
+ * inside a session YOU manage with `begin()`. This module instead observes the
1344
+ * OpenAI **Agents SDK** runtime (`Agent` + `run`) from the outside via its
1345
+ * documented tracing-processor hook — each agent run becomes a full Sentrial
1346
+ * session automatically, including tool calls, handoffs, and guardrails. If you
1347
+ * use the Agents SDK, prefer this module; don't combine both on the same run or
1348
+ * LLM calls will be double-counted.
1349
+ *
1350
+ * Verified against the official OpenAI Agents JS SDK
1351
+ * (https://openai.github.io/openai-agents-js/ and the `@openai/agents-core`
1352
+ * tracing sources): `TracingProcessor` is `onTraceStart/onTraceEnd/onSpanStart/
1353
+ * onSpanEnd(…): Promise<void>` + `shutdown(timeout?)`/`forceFlush()`; `Trace`
1354
+ * exposes camelCase `traceId/name/groupId/metadata`; `Span` exposes
1355
+ * `traceId/spanId/parentId/startedAt/endedAt/error/spanData` with ISO-string
1356
+ * timestamps and a `SpanError { message, data? }`; `SpanData` fields are
1357
+ * snake_case (`from_agent`, `to_agent`, `mcp_data`, usage `input_tokens`/
1358
+ * `output_tokens`; `ResponseSpanData` carries the raw Responses API payload on
1359
+ * `_response`). Registration is `addTraceProcessor(processor)` (additive — the
1360
+ * default OpenAI-dashboard exporter keeps running alongside).
1361
+ *
1362
+ * The `@openai/agents` package stays an optional peer dependency: like
1363
+ * `wrapAISDK(ai)`, the module never imports it — you hand `addTraceProcessor`
1364
+ * (or the module namespace that carries it) to {@link instrumentOpenAIAgents}.
1365
+ *
1366
+ * @example
1367
+ * ```ts
1368
+ * import { Agent, run, addTraceProcessor } from '@openai/agents';
1369
+ * import { sentrial, instrumentOpenAIAgents } from '@sentrial/sdk';
1370
+ *
1371
+ * sentrial.configure({ apiKey: process.env.SENTRIAL_API_KEY });
1372
+ * instrumentOpenAIAgents({ addTraceProcessor }, { userId: 'user_123' });
1373
+ *
1374
+ * const agent = new Agent({ name: 'Assistant', instructions: 'Be helpful.' });
1375
+ * const result = await run(agent, 'Write a haiku about recursion.');
1376
+ * // The run is now a Sentrial session with LLM events, tool calls, handoffs.
1377
+ * ```
1378
+ *
1379
+ * @packageDocumentation
1380
+ */
1381
+
1382
+ /** The subset of `Trace` this processor reads (camelCase, per agents-core). */
1383
+ type AgentsTrace = {
1384
+ traceId?: string;
1385
+ name?: string;
1386
+ groupId?: string | null;
1387
+ metadata?: Record<string, unknown>;
1388
+ };
1389
+ /** `SpanError` shape (`span.error`). */
1390
+ type AgentsSpanError = {
1391
+ message?: string;
1392
+ data?: Record<string, unknown>;
1393
+ };
1394
+ /** The subset of `Span` this processor reads (camelCase getters, ISO timestamps). */
1395
+ type AgentsSpan = {
1396
+ traceId?: string;
1397
+ spanId?: string;
1398
+ parentId?: string | null;
1399
+ startedAt?: string | null;
1400
+ endedAt?: string | null;
1401
+ error?: AgentsSpanError | null;
1402
+ spanData?: Record<string, unknown> & {
1403
+ type?: string;
1404
+ };
1405
+ };
1406
+ /** The one entry point we need from `@openai/agents` / `@openai/agents-core`. */
1407
+ type AgentsModuleLike = {
1408
+ addTraceProcessor: (processor: unknown) => void;
1409
+ };
1410
+ interface OpenAIAgentsProcessorOptions {
1411
+ /** Explicit SentrialClient. Defaults to a client from the environment (SENTRIAL_API_KEY). */
1412
+ client?: SentrialClient;
1413
+ /** Explicit agent/event name override (defaults to the root agent span's name per run). */
1414
+ defaultAgent?: string;
1415
+ /** User ID to associate with sessions (default 'anonymous'). */
1416
+ userId?: string;
1417
+ /** Extra metadata merged into every session. */
1418
+ extraMetadata?: Record<string, unknown>;
1419
+ }
1420
+ /**
1421
+ * Duck-typed `TracingProcessor` (from `@openai/agents-core`) that mirrors OpenAI
1422
+ * Agents SDK runs into Sentrial sessions.
1423
+ *
1424
+ * Register it via {@link instrumentOpenAIAgents}, or directly:
1425
+ * ```ts
1426
+ * import { addTraceProcessor } from '@openai/agents';
1427
+ * addTraceProcessor(new SentrialOpenAIAgentsProcessor({ client }));
1428
+ * ```
1429
+ *
1430
+ * Every callback body is containment-wrapped: the Agents SDK awaits these
1431
+ * methods inline on the user's run path, so a tracking failure must never
1432
+ * reject into the host agent — it is swallowed instead (the SentrialClient/
1433
+ * Interaction methods additionally carry their own containment seam).
1434
+ *
1435
+ * Performance tip: construct the client with `batching: { enabled: true }` so
1436
+ * `track*` calls enqueue instead of awaiting a network round-trip per span.
1437
+ * (The TS BatcherConfig takes an object — a bare `batching: true` is silently
1438
+ * ignored; the Python SDK's `batching=True` bool shorthand has no TS twin.)
1439
+ */
1440
+ declare class SentrialOpenAIAgentsProcessor {
1441
+ private clientRef?;
1442
+ private readonly defaultAgent?;
1443
+ private readonly userId;
1444
+ private readonly extraMetadata;
1445
+ private readonly traces;
1446
+ private tracesCapWarned;
1447
+ constructor(options?: OpenAIAgentsProcessorOptions);
1448
+ /** Optional TracingProcessor lifecycle hook — nothing to start. */
1449
+ start(): void;
1450
+ onTraceStart(trace: AgentsTrace): Promise<void>;
1451
+ onTraceEnd(trace: AgentsTrace): Promise<void>;
1452
+ onSpanStart(span: AgentsSpan): Promise<void>;
1453
+ onSpanEnd(span: AgentsSpan): Promise<void>;
1454
+ /**
1455
+ * Called at application stop: finishes any still-open sessions as interrupted
1456
+ * and flushes the client's event queue (if batching).
1457
+ */
1458
+ shutdown(_timeout?: number): Promise<void>;
1459
+ /** Forces immediate processing of queued events (no-op without batching). */
1460
+ forceFlush(): Promise<void>;
1461
+ private resolveClient;
1462
+ private flushClient;
1463
+ private handleTraceStart;
1464
+ private stateFor;
1465
+ /**
1466
+ * Begin the Sentrial session for this trace exactly once. Event/agent name
1467
+ * preference: explicit `defaultAgent` > root agent span name > trace workflow
1468
+ * name > 'openai-agents'. The session title is the trace's workflow name.
1469
+ *
1470
+ * DELIBERATE cross-SDK divergence on begin failure: the rejected promise is
1471
+ * cached, so a trace whose begin() failed never retries (avoids a partial
1472
+ * session whose earlier spans were lost). The Python twin leaves
1473
+ * `interaction=None` and retries at the next span (favoring late recovery).
1474
+ * Both are documented choices, not drift.
1475
+ */
1476
+ private ensureInteraction;
1477
+ private handleSpanStart;
1478
+ private handleSpanEnd;
1479
+ private trackLLMEvent;
1480
+ private trackFunction;
1481
+ private trackHandoff;
1482
+ private trackGuardrail;
1483
+ private handleTraceEnd;
1484
+ private finishState;
1485
+ }
1486
+ /**
1487
+ * Construct a {@link SentrialOpenAIAgentsProcessor} and register it with the
1488
+ * OpenAI Agents SDK. Pass the SDK's `addTraceProcessor` (or any object carrying
1489
+ * it, e.g. the whole `@openai/agents` module namespace) — the peer dependency is
1490
+ * never imported here, mirroring `wrapAISDK(ai)`.
1491
+ *
1492
+ * Idempotent per (agents module, client) pair: calling this again with the same
1493
+ * `addTraceProcessor` and the same `options.client` (or again with no client —
1494
+ * the env-default) returns the FIRST registered processor instead of registering
1495
+ * a duplicate, which would mirror every run into two sessions. The first
1496
+ * duplicate call emits a console warning (silent thereafter).
1497
+ *
1498
+ * @example
1499
+ * ```ts
1500
+ * import * as agents from '@openai/agents';
1501
+ * import { instrumentOpenAIAgents } from '@sentrial/sdk';
1502
+ *
1503
+ * instrumentOpenAIAgents(agents, { userId: 'user_123' });
1504
+ * ```
1505
+ *
1506
+ * @param agents - The `@openai/agents` module namespace (or `{ addTraceProcessor }`).
1507
+ * @param options - Processor options (client, defaultAgent, userId, extraMetadata).
1508
+ * @returns The registered processor (keep it to call `shutdown()`/`forceFlush()`).
1509
+ */
1510
+ declare function instrumentOpenAIAgents(agents: AgentsModuleLike, options?: OpenAIAgentsProcessorOptions): SentrialOpenAIAgentsProcessor;
1511
+
1512
+ /**
1513
+ * Google Agent Development Kit (ADK) Integration for Sentrial — TypeScript twin
1514
+ * of the Python `sentrial.google_adk` module, ported for `adk-js` (`@google/adk`).
1515
+ *
1516
+ * Maps a Google ADK **run** (one `Runner.runAsync` invocation) onto a single
1517
+ * Sentrial session via the `begin()`/Interaction API. The attachment surface is a
1518
+ * Runner-level **Plugin** — ADK's `BasePlugin` fires globally for the *entire*
1519
+ * invocation across *every* agent, tool, and model call, so one run → one Sentrial
1520
+ * session automatically (the ADK equivalent of the OpenAI Agents SDK's global
1521
+ * trace processor in {@link SentrialOpenAIAgentsProcessor}).
1522
+ *
1523
+ * Mapping (mirrors the Python google_adk.py):
1524
+ *
1525
+ * - **run start** (`beforeRunCallback`) → `begin()` — agent/event name from the
1526
+ * root agent, `convoId` from the ADK session id, `userId` from the ADK session
1527
+ * (overridable), input from the run's `userContent`.
1528
+ * - **model calls** (`beforeModelCallback` → `afterModelCallback`) → per-call LLM
1529
+ * event via `trackToolCall` (`llm:{model}` convention, model from
1530
+ * `LlmRequest.model`, prompt/completion tokens from `LlmResponse.usageMetadata`
1531
+ * — `promptTokenCount` / `candidatesTokenCount` — cost via the pricing tables,
1532
+ * duration from the before/after timestamps). Streaming *partial* responses are
1533
+ * skipped so tokens aren't double-counted; the final response carries the usage.
1534
+ * - **tool calls** (`beforeToolCallback` → `afterToolCallback`) → `trackToolCall`
1535
+ * (tool name/args/result, duration from before/after).
1536
+ * - **errors** (`onToolErrorCallback` / `onModelErrorCallback` and any
1537
+ * `LlmResponse.errorCode`) → `trackError` (the session finishes `success=false`).
1538
+ * - **run end** (`afterRunCallback`) → `finish()` with rolled-up tokens/cost and
1539
+ * the last model output; custom metrics `llm_calls` / `tool_calls`.
1540
+ *
1541
+ * Distinction from {@link wrapGoogle}:
1542
+ * `wrapGoogle` wraps a RAW `@google/genai` model so each `generateContent` call is
1543
+ * tracked inside a session YOU manage with `begin()`. This module instead observes
1544
+ * the ADK **runtime** (`Agent` + `Runner`) from the outside via its documented
1545
+ * Plugin hooks — each ADK run becomes a full Sentrial session automatically. If you
1546
+ * use ADK, prefer this module; don't combine both on the same run (LLM calls would
1547
+ * be double-counted).
1548
+ *
1549
+ * The `@google/adk` package stays an OPTIONAL peer dependency: this module NEVER
1550
+ * imports it. ADK's `PluginManager` dispatches to plugin callbacks BY NAME and
1551
+ * dedups by `.name`, and a plain object structurally satisfies `BasePlugin`, so a
1552
+ * duck-typed class needs no `BasePlugin` subclass — the module imports cleanly with
1553
+ * or without ADK installed. You attach it via `plugins: [new SentrialAdkPlugin()]`
1554
+ * at Runner construction, or after the fact with {@link instrumentAdk}.
1555
+ *
1556
+ * Verified against the official ADK-JS source (github.com/google/adk-js, package
1557
+ * `@google/adk`):
1558
+ * - `core/src/plugins/base_plugin.ts` — plugin callbacks take a SINGLE camelCase
1559
+ * params object (NOT Python's keyword args): `beforeRunCallback({invocationContext})`,
1560
+ * `afterRunCallback({invocationContext})`, `beforeModelCallback({callbackContext, llmRequest})`,
1561
+ * `afterModelCallback({callbackContext, llmResponse})`,
1562
+ * `onModelErrorCallback({callbackContext, llmRequest, error})`,
1563
+ * `beforeToolCallback({tool, toolArgs, toolContext})`,
1564
+ * `afterToolCallback({tool, toolArgs, toolContext, result})`,
1565
+ * `onToolErrorCallback({tool, toolArgs, toolContext, error})`. Returning a
1566
+ * non-`undefined` value SHORT-CIRCUITS the run, so every callback returns
1567
+ * `undefined` (pure observer).
1568
+ * - `core/src/plugins/plugin_manager.ts` — `runCallbacks` CATCHES a throwing
1569
+ * callback and RE-THROWS a wrapped error that HALTS the agent run; containment
1570
+ * inside every callback body is therefore mandatory. `registerPlugin` throws on a
1571
+ * duplicate `.name`; `getPlugin(name)` retrieves by name.
1572
+ * - `core/src/models/llm_response.ts` — `content?: Content` (`.parts[].text`),
1573
+ * `usageMetadata` (`promptTokenCount` / `candidatesTokenCount`), `partial`,
1574
+ * `errorCode` / `errorMessage`.
1575
+ * - `core/src/models/llm_request.ts` — `model?: string`.
1576
+ * - `core/src/agents/invocation_context.ts` — `invocationId`, `agent` (`.name`),
1577
+ * `session` (`.id` / `.appName` / `.userId`), `appName`/`userId` getters, `userContent`.
1578
+ * - `core/src/agents/readonly_context.ts` — the callback `Context` exposes getters
1579
+ * `invocationId` / `agentName` / `sessionId` / `userId` / `userContent` and the
1580
+ * public `invocationContext`.
1581
+ * - `core/src/runner/runner.ts` — `plugins?: BasePlugin[]` at construction;
1582
+ * `runner.pluginManager.registerPlugin(plugin)`.
1583
+ *
1584
+ * @example
1585
+ * ```ts
1586
+ * import { InMemoryRunner } from '@google/adk';
1587
+ * import { LlmAgent } from '@google/adk';
1588
+ * import { sentrial, SentrialAdkPlugin } from '@sentrial/sdk';
1589
+ *
1590
+ * sentrial.configure({ apiKey: process.env.SENTRIAL_API_KEY });
1591
+ *
1592
+ * const agent = new LlmAgent({ name: 'assistant', model: 'gemini-2.0-flash' });
1593
+ * const runner = new InMemoryRunner({ agent, plugins: [new SentrialAdkPlugin()] });
1594
+ * // Every runner.runAsync(...) is now a Sentrial session with LLM events,
1595
+ * // tool calls, and token/cost rollups.
1596
+ * ```
1597
+ *
1598
+ * @packageDocumentation
1599
+ */
1600
+
1601
+ /** The subset of ADK `Content` (`@google/genai`) this plugin reads. */
1602
+ type AdkContent = {
1603
+ parts?: Array<{
1604
+ text?: unknown;
1605
+ } | unknown>;
1606
+ } | string | unknown;
1607
+ /** The subset of ADK `InvocationContext` this plugin reads (run lifecycle). */
1608
+ type AdkInvocationContext = {
1609
+ invocationId?: string;
1610
+ agent?: {
1611
+ name?: string;
1612
+ };
1613
+ session?: {
1614
+ id?: string;
1615
+ appName?: string;
1616
+ userId?: string;
1617
+ };
1618
+ appName?: string;
1619
+ userId?: string;
1620
+ userContent?: AdkContent;
1621
+ };
1622
+ /** The subset of the ADK callback `Context` this plugin reads (model/tool calls). */
1623
+ type AdkCallbackContext = {
1624
+ invocationId?: string;
1625
+ agentName?: string;
1626
+ sessionId?: string;
1627
+ userId?: string;
1628
+ userContent?: AdkContent;
1629
+ invocationContext?: AdkInvocationContext;
1630
+ };
1631
+ /** The subset of ADK `LlmRequest` this plugin reads. */
1632
+ type AdkLlmRequest = {
1633
+ model?: string;
1634
+ };
1635
+ /** The subset of ADK `LlmResponse` this plugin reads. */
1636
+ type AdkLlmResponse = {
1637
+ content?: AdkContent;
1638
+ usageMetadata?: {
1639
+ promptTokenCount?: unknown;
1640
+ candidatesTokenCount?: unknown;
1641
+ };
1642
+ partial?: unknown;
1643
+ errorCode?: unknown;
1644
+ errorMessage?: unknown;
1645
+ };
1646
+ /** The subset of ADK `BaseTool` this plugin reads. */
1647
+ type AdkTool = {
1648
+ name?: string;
1649
+ };
1650
+ /** The Runner surface {@link instrumentAdk} needs (`runner.pluginManager.registerPlugin`). */
1651
+ type AdkRunnerLike = {
1652
+ pluginManager?: {
1653
+ registerPlugin?: (plugin: unknown) => void;
1654
+ getPlugin?: (name: string) => unknown;
1655
+ };
1656
+ };
1657
+ interface AdkPluginOptions {
1658
+ /** Explicit SentrialClient. Defaults to a client from the environment (SENTRIAL_API_KEY). */
1659
+ client?: SentrialClient;
1660
+ /** ADK plugin name (default 'sentrial'); ADK requires unique plugin names. */
1661
+ name?: string;
1662
+ /** Explicit agent/event name override (defaults to the root agent's name per run). */
1663
+ defaultAgent?: string;
1664
+ /** User ID override (defaults to the ADK session's userId, then 'anonymous'). */
1665
+ userId?: string;
1666
+ /** Extra metadata merged into every session. */
1667
+ extraMetadata?: Record<string, unknown>;
1668
+ }
1669
+ /**
1670
+ * Duck-typed Google ADK `BasePlugin` that mirrors ADK runs into Sentrial sessions.
1671
+ *
1672
+ * Register it on a `Runner`:
1673
+ * ```ts
1674
+ * const runner = new InMemoryRunner({ agent, plugins: [new SentrialAdkPlugin()] });
1675
+ * ```
1676
+ * or attach it to an existing runner with {@link instrumentAdk}.
1677
+ *
1678
+ * This is a *plain* class implementing the `BasePlugin` callback protocol — it is
1679
+ * intentionally NOT a `BasePlugin` subclass, because ADK's `PluginManager`
1680
+ * dispatches to callbacks by name and dedups by `.name`, so subclassing would force
1681
+ * a hard `@google/adk` import (breaking the "optional peer" contract and
1682
+ * import-without-ADK). Every callback body is containment-wrapped: ADK's
1683
+ * PluginManager re-throws a callback error and HALTS the run, so a tracking failure
1684
+ * must never escape — it is swallowed instead. Every callback returns `undefined`
1685
+ * (a pure observer never short-circuits the model/tool call).
1686
+ *
1687
+ * Performance tip: construct the client with `batching: { enabled: true }` so
1688
+ * `track*` calls enqueue instead of awaiting a network round-trip per event. ADK
1689
+ * invokes these callbacks inline on the run's event loop.
1690
+ */
1691
+ declare class SentrialAdkPlugin {
1692
+ /** PluginManager reads `.name` for dedup/logging — a real string, always present. */
1693
+ readonly name: string;
1694
+ private clientRef?;
1695
+ private readonly defaultAgent?;
1696
+ private readonly userIdOption?;
1697
+ private readonly extraMetadata;
1698
+ private readonly runs;
1699
+ /** One-time guard so the runs-cap eviction warns at most once (like the duplicate-instrument warn). */
1700
+ private runsCapWarned;
1701
+ constructor(options?: AdkPluginOptions);
1702
+ beforeRunCallback(params: {
1703
+ invocationContext: AdkInvocationContext;
1704
+ }): Promise<undefined>;
1705
+ afterRunCallback(params: {
1706
+ invocationContext: AdkInvocationContext;
1707
+ }): Promise<void>;
1708
+ onRunErrorCallback(params: {
1709
+ invocationContext: AdkInvocationContext;
1710
+ error: unknown;
1711
+ }): Promise<void>;
1712
+ beforeModelCallback(params: {
1713
+ callbackContext: AdkCallbackContext;
1714
+ llmRequest: AdkLlmRequest;
1715
+ }): Promise<undefined>;
1716
+ afterModelCallback(params: {
1717
+ callbackContext: AdkCallbackContext;
1718
+ llmResponse: AdkLlmResponse;
1719
+ }): Promise<undefined>;
1720
+ onModelErrorCallback(params: {
1721
+ callbackContext: AdkCallbackContext;
1722
+ llmRequest: AdkLlmRequest;
1723
+ error: unknown;
1724
+ }): Promise<undefined>;
1725
+ beforeToolCallback(params: {
1726
+ tool: AdkTool;
1727
+ toolArgs: Record<string, unknown>;
1728
+ toolContext: AdkCallbackContext;
1729
+ }): Promise<undefined>;
1730
+ afterToolCallback(params: {
1731
+ tool: AdkTool;
1732
+ toolArgs: Record<string, unknown>;
1733
+ toolContext: AdkCallbackContext;
1734
+ result: unknown;
1735
+ }): Promise<undefined>;
1736
+ onToolErrorCallback(params: {
1737
+ tool: AdkTool;
1738
+ toolArgs: Record<string, unknown>;
1739
+ toolContext: AdkCallbackContext;
1740
+ error: unknown;
1741
+ }): Promise<undefined>;
1742
+ private resolveClient;
1743
+ /**
1744
+ * Fetch (or lazily create) the per-run state for an invocation id, back-filling
1745
+ * any run-identity fields not known when it was first created (a model/tool
1746
+ * callback can reach a run before beforeRun in some ADK flows).
1747
+ */
1748
+ private getOrCreateState;
1749
+ private stateFromInvocationContext;
1750
+ private stateFromCallbackContext;
1751
+ /**
1752
+ * Begin the Sentrial session for this run exactly once. Event/agent name
1753
+ * preference: explicit `defaultAgent` > root agent name > 'google-adk'. The ADK
1754
+ * session id becomes `convoId`; the ADK session `userId` (or the configured
1755
+ * override) becomes the Sentrial user.
1756
+ *
1757
+ * DELIBERATE (matches the openai-agents.ts TS twin): the interaction promise is
1758
+ * cached, so a run whose `begin()` REJECTED never retries (avoids a partial
1759
+ * session whose earlier events were lost). The Python google_adk leaves the
1760
+ * interaction unset and retries at the next callback — both are documented,
1761
+ * per-SDK choices, not drift.
1762
+ */
1763
+ private ensureInteraction;
1764
+ private handleRunStart;
1765
+ private handleRunEnd;
1766
+ private handleRunError;
1767
+ private handleModelStart;
1768
+ private handleModelEnd;
1769
+ private handleModelError;
1770
+ private emitLlmEvent;
1771
+ private handleToolStart;
1772
+ private popToolTiming;
1773
+ private handleToolEnd;
1774
+ private handleToolError;
1775
+ private finishState;
1776
+ }
1777
+ /**
1778
+ * Register a {@link SentrialAdkPlugin} on an already-constructed ADK `Runner`.
1779
+ *
1780
+ * Convenience for when you didn't pass `plugins: [new SentrialAdkPlugin()]` at
1781
+ * Runner construction. Pass the runner (its `pluginManager.registerPlugin` is the
1782
+ * only surface used) — the `@google/adk` peer is never imported here, mirroring
1783
+ * `instrumentOpenAIAgents(agents)`.
1784
+ *
1785
+ * Idempotent per runner: calling it again on the same runner returns the FIRST
1786
+ * registered plugin instead of registering a duplicate (which ADK would reject on
1787
+ * the duplicate name anyway, and which would mirror every run into two sessions).
1788
+ * The first duplicate call emits a console warning (silent thereafter).
1789
+ *
1790
+ * @example
1791
+ * ```ts
1792
+ * import { InMemoryRunner } from '@google/adk';
1793
+ * import { instrumentAdk } from '@sentrial/sdk';
1794
+ *
1795
+ * const runner = new InMemoryRunner({ agent });
1796
+ * instrumentAdk(runner, { userId: 'user_123' });
1797
+ * ```
1798
+ *
1799
+ * @param runner - An ADK `Runner` exposing `pluginManager.registerPlugin`.
1800
+ * @param options - Plugin options (client, name, defaultAgent, userId, extraMetadata).
1801
+ * @returns The registered plugin.
1802
+ */
1803
+ declare function instrumentAdk(runner: AdkRunnerLike, options?: AdkPluginOptions): SentrialAdkPlugin;
1804
+
1383
1805
  /**
1384
1806
  * Sentrial Decorators - Easy instrumentation for AI agents
1385
1807
  *
@@ -1948,4 +2370,4 @@ declare class Experiment {
1948
2370
  getResults(): Promise<ExperimentResults | null>;
1949
2371
  }
1950
2372
 
1951
- export { ApiError, type ApiResponse, type BatcherConfig, type BeginParams, type CompleteSessionParams, type ContextToken, type ContextVar, type CostParams, type CreateSessionParams, type Event, EventBatcher, EventType, Experiment, type ExperimentContext, type ExperimentResults, type ExperimentRunResult, ExperimentRunTracker, type ExperimentTestCase, type ExperimentVariant, type FinishParams, type GenerateTextParams, type GenerateTextResult, type GetVariantParams, Interaction, NetworkError, type PiiBuiltinPatterns, type PiiConfig, type PiiCustomPattern, type PiiField, type PiiMode, type ProductionVariantConfig, SentrialClient, type SentrialClientConfig, SentrialError, type Session, SessionContext, type SessionStatus, type StreamTextResult, Tool, type TrackDecisionParams, type TrackErrorParams, TrackSession, type TrackToolCallParams, ValidationError, type VariantAssignment, type WrapClaudeAgentOptions, begin, calculateAnthropicCost, calculateGoogleCost, calculateOpenAICost, clearExperimentContext, clearSessionContext, configure, configureVercel, createContextVar, getCurrentInteraction, getCurrentSessionId, getExperimentContext, getExperimentId, getSessionContext, getSystemPrompt, getVariant, getVariantName, hashValue, isExperimentMode, redactPayload, redactString, redactValue, replaceMatch, sentrial, setClient, setDefaultClient, setExperimentContext, setSessionContext, withSession, withTool, wrapAISDK, wrapAnthropic, wrapClaudeAgent, wrapGoogle, wrapLLM, wrapOpenAI };
2373
+ export { type AISDKVariantOptions, type ActionIdentity, type AdkCallbackContext, type AdkContent, type AdkInvocationContext, type AdkLlmRequest, type AdkLlmResponse, type AdkPluginOptions, type AdkRunnerLike, type AdkTool, type AgentDiagnostic, type AgentDiagnosticInput, type AgentsModuleLike, type AgentsSpan, type AgentsSpanError, type AgentsTrace, ApiError, BUILTIN_PATTERNS, BeginParams, CompleteSessionParams, type ContextToken, type ContextVar, CostParams, CreateSessionParams, DeliveryDiagnostics, type DispatchContext, type DurationSummary, Event, Experiment, type ExperimentContext, ExperimentExposureReceipt, type ExperimentResults, type ExperimentRunResult, ExperimentRunTracker, type ExperimentTestCase, type ExperimentVariant, FinishParams, type GenerateTextParams, type GenerateTextResult, GetVariantParams, type HandoffContext, type HandoffOutcome, Interaction, NetworkError, type NormalizableUsage, type NormalizedUsage, type OpenAIAgentsProcessorOptions, RecordExperimentExposureParams, SentrialAdkPlugin, SentrialClient, SentrialClientConfig, SentrialError, SentrialOpenAIAgentsProcessor, type ServerDurations, Session, SessionContext, type StreamTextResult, Tool, TrackDecisionParams, TrackErrorParams, TrackSession, TrackToolCallParams, type TransportAttempt, type TransportDiagnosticsSnapshot, ValidationError, VariantAssignment, type WrapClaudeAgentOptions, actionMetadata, begin, calculateAnthropicCost, calculateCostByProvider, calculateGoogleCost, calculateOpenAICost, clearExperimentContext, clearSessionContext, configure, configureVercel, createContextVar, createDispatchContext, createHandoffContext, foldProviderExtras, getCurrentInteraction, getCurrentSessionId, getExperimentContext, getExperimentId, getSessionContext, getSystemPrompt, getVariant, getVariantForAISDK, getVariantName, guessProvider, instrumentAdk, instrumentOpenAIAgents, isExperimentMode, normalizeUsage, recordExperimentExposure, resolveTokensAndCost, resumeHandoffContext, sentrial, setClient, setDefaultClient, setExperimentContext, setSessionContext, withSession, withTool, wrapAISDK, wrapAnthropic, wrapClaudeAgent, wrapGoogle, wrapLLM, wrapOpenAI };