@sentrial/sdk 0.4.5 → 0.5.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;
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;
206
247
  }
207
- /** Toggle individual builtin pattern categories */
208
- interface PiiBuiltinPatterns {
209
- emails?: boolean;
210
- phones?: boolean;
211
- ssns?: boolean;
212
- creditCards?: boolean;
213
- ipAddresses?: boolean;
248
+ interface HandoffContext extends ActionIdentity {
249
+ version: 'sentrial-handoff-v1';
250
+ parentSessionId: string;
214
251
  }
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"
342
- }
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;
374
- }
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).
845
630
  *
846
- * @param params - Event parameters
847
- * @returns Event data or null on error
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
848
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).
645
+ *
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.
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,16 @@ declare class ValidationError extends SentrialError {
1152
1017
  * @packageDocumentation
1153
1018
  */
1154
1019
 
1020
+ type AnyFunction = (...args: unknown[]) => unknown;
1155
1021
  type AIModule = {
1156
- generateText?: Function;
1157
- streamText?: Function;
1158
- generateObject?: Function;
1159
- streamObject?: Function;
1160
- experimental_generateText?: Function;
1161
- experimental_streamText?: Function;
1022
+ generateText?: AnyFunction;
1023
+ streamText?: AnyFunction;
1024
+ generateObject?: AnyFunction;
1025
+ streamObject?: AnyFunction;
1026
+ experimental_generateText?: AnyFunction;
1027
+ experimental_streamText?: AnyFunction;
1028
+ experimental_generateObject?: AnyFunction;
1029
+ experimental_streamObject?: AnyFunction;
1162
1030
  };
1163
1031
  type GenerateTextParams = {
1164
1032
  model: {
@@ -1172,7 +1040,7 @@ type GenerateTextParams = {
1172
1040
  }>;
1173
1041
  system?: string;
1174
1042
  tools?: Record<string, {
1175
- execute?: Function;
1043
+ execute?: AnyFunction;
1176
1044
  } & Record<string, unknown>>;
1177
1045
  maxTokens?: number;
1178
1046
  maxSteps?: number;
@@ -1202,6 +1070,16 @@ type StepResult = {
1202
1070
  }>;
1203
1071
  usage?: UsageInfo;
1204
1072
  finishReason?: string;
1073
+ /** Per-step response metadata — v5+ modelId, v3/v4 model. */
1074
+ response?: {
1075
+ modelId?: string;
1076
+ model?: string;
1077
+ };
1078
+ /** v6+: the model that produced this step (prepareStep can switch mid-call). */
1079
+ model?: {
1080
+ provider?: string;
1081
+ modelId?: string;
1082
+ };
1205
1083
  };
1206
1084
  type GenerateTextResult = {
1207
1085
  text: string;
@@ -1276,10 +1154,10 @@ declare function configureVercel(config: {
1276
1154
  convoId?: StringOrFn;
1277
1155
  failSilently?: boolean;
1278
1156
  }): 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;
1157
+ declare function wrapGenerateText(originalFn: AnyFunction, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => Promise<GenerateTextResult>;
1158
+ declare function wrapStreamText(originalFn: AnyFunction, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => StreamTextResult;
1159
+ declare function wrapGenerateObject(originalFn: AnyFunction, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => Promise<unknown>;
1160
+ declare function wrapStreamObject(originalFn: AnyFunction, client: SentrialClient, config: WrapperConfig): (params: GenerateTextParams) => unknown;
1283
1161
  /**
1284
1162
  * Wrap the Vercel AI SDK to automatically trace all AI calls.
1285
1163
  *
@@ -1316,6 +1194,61 @@ declare function wrapAISDK(ai: AIModule, options?: {
1316
1194
  streamObject: ReturnType<typeof wrapStreamObject>;
1317
1195
  };
1318
1196
 
1197
+ /**
1198
+ * Options shape accepted by {@link getVariantForAISDK} — a permissive subset of
1199
+ * an AI SDK `generateText` / `streamText` call. Extra keys are preserved, so it
1200
+ * composes with whatever options you already build.
1201
+ */
1202
+ type AISDKVariantOptions = {
1203
+ model?: unknown;
1204
+ system?: string;
1205
+ prompt?: string;
1206
+ messages?: unknown;
1207
+ experimental_telemetry?: {
1208
+ isEnabled?: boolean;
1209
+ metadata?: Record<string, unknown>;
1210
+ };
1211
+ [key: string]: unknown;
1212
+ };
1213
+ /**
1214
+ * Resolve a production A/B variant and merge it into Vercel AI SDK call options.
1215
+ *
1216
+ * Pairs `client.getVariant()` with a `generateText` / `streamText` call: it
1217
+ * fetches the sticky assignment for this user/convo, applies the variant's
1218
+ * `systemPrompt`, `prompt`, and (via `modelResolver`, since the AI SDK needs a
1219
+ * model *object*, not a string) `model`, and tags the call's
1220
+ * `experimental_telemetry.metadata` with the experiment + variant so the
1221
+ * exposure is visible on the resulting session. Returns the merged options
1222
+ * (spread straight into your `generateText` call) and the raw assignment so you
1223
+ * can branch on `assignment.variant`.
1224
+ *
1225
+ * The assignment is recorded server-side by `getVariant` regardless of whether a
1226
+ * variant config is applied, so a `reason: 'holdout'` / `'not_found'` result is
1227
+ * safe — the base options pass through unchanged (sticky control).
1228
+ *
1229
+ * @example
1230
+ * ```ts
1231
+ * import { openai } from '@ai-sdk/openai';
1232
+ * import { wrapAISDK, getVariantForAISDK } from '@sentrial/sdk';
1233
+ * import * as ai from 'ai';
1234
+ *
1235
+ * const { generateText } = wrapAISDK(ai);
1236
+ * const { options, assignment } = await getVariantForAISDK(
1237
+ * { experimentName: 'support-prompt-v2', userId, convoId },
1238
+ * { model: openai('gpt-4o-mini'), system: 'You are a support agent.', prompt: userQuery },
1239
+ * { modelResolver: (id) => openai(id) }
1240
+ * );
1241
+ * const { text } = await generateText(options); // runs the assigned variant, tracked
1242
+ * ```
1243
+ */
1244
+ declare function getVariantForAISDK<T extends AISDKVariantOptions>(variantParams: GetVariantParams, baseOptions: T, opts?: {
1245
+ modelResolver?: (modelId: string) => unknown;
1246
+ client?: SentrialClient;
1247
+ }): Promise<{
1248
+ options: T;
1249
+ assignment: VariantAssignment;
1250
+ }>;
1251
+
1319
1252
  /**
1320
1253
  * Sentrial wrapper for the Claude Agent SDK (Claude Code).
1321
1254
  *
@@ -1380,6 +1313,487 @@ interface WrapClaudeAgentOptions {
1380
1313
  */
1381
1314
  declare function wrapClaudeAgent(queryFn: QueryFunction, wrapOptions: WrapClaudeAgentOptions): QueryFunction;
1382
1315
 
1316
+ /**
1317
+ * OpenAI Agents SDK Integration for Sentrial
1318
+ *
1319
+ * Observes the OpenAI Agents SDK's (`@openai/agents`) tracing pipeline and maps
1320
+ * every trace onto a Sentrial session via the `begin()`/Interaction API:
1321
+ *
1322
+ * - **trace start** → `begin()` (agent/event name from the root agent span,
1323
+ * falling back to the trace's workflow name; session title from the trace name)
1324
+ * - **generation / response spans** → per-call LLM events (`llm:{model}` tool name
1325
+ * convention, prompt/completion tokens from usage, cost via the pricing tables,
1326
+ * duration from the span's start/end timestamps)
1327
+ * - **function spans** → `trackToolCall` (name, parsed args, result, errors)
1328
+ * - **handoff spans** → `trackDecision` ("Handoff from X to Y")
1329
+ * - **guardrail spans** → `trackError` when triggered, `trackDecision` when passed
1330
+ * - **trace end** → `finish()` (aggregated tokens/cost, last LLM output as the
1331
+ * session output, failure state from root agent span errors / triggered guardrails)
1332
+ *
1333
+ * Distinction from {@link wrapOpenAI}:
1334
+ * `wrapOpenAI` wraps a RAW `openai` client instance so each LLM call is tracked
1335
+ * inside a session YOU manage with `begin()`. This module instead observes the
1336
+ * OpenAI **Agents SDK** runtime (`Agent` + `run`) from the outside via its
1337
+ * documented tracing-processor hook — each agent run becomes a full Sentrial
1338
+ * session automatically, including tool calls, handoffs, and guardrails. If you
1339
+ * use the Agents SDK, prefer this module; don't combine both on the same run or
1340
+ * LLM calls will be double-counted.
1341
+ *
1342
+ * Verified against the official OpenAI Agents JS SDK
1343
+ * (https://openai.github.io/openai-agents-js/ and the `@openai/agents-core`
1344
+ * tracing sources): `TracingProcessor` is `onTraceStart/onTraceEnd/onSpanStart/
1345
+ * onSpanEnd(…): Promise<void>` + `shutdown(timeout?)`/`forceFlush()`; `Trace`
1346
+ * exposes camelCase `traceId/name/groupId/metadata`; `Span` exposes
1347
+ * `traceId/spanId/parentId/startedAt/endedAt/error/spanData` with ISO-string
1348
+ * timestamps and a `SpanError { message, data? }`; `SpanData` fields are
1349
+ * snake_case (`from_agent`, `to_agent`, `mcp_data`, usage `input_tokens`/
1350
+ * `output_tokens`; `ResponseSpanData` carries the raw Responses API payload on
1351
+ * `_response`). Registration is `addTraceProcessor(processor)` (additive — the
1352
+ * default OpenAI-dashboard exporter keeps running alongside).
1353
+ *
1354
+ * The `@openai/agents` package stays an optional peer dependency: like
1355
+ * `wrapAISDK(ai)`, the module never imports it — you hand `addTraceProcessor`
1356
+ * (or the module namespace that carries it) to {@link instrumentOpenAIAgents}.
1357
+ *
1358
+ * @example
1359
+ * ```ts
1360
+ * import { Agent, run, addTraceProcessor } from '@openai/agents';
1361
+ * import { sentrial, instrumentOpenAIAgents } from '@sentrial/sdk';
1362
+ *
1363
+ * sentrial.configure({ apiKey: process.env.SENTRIAL_API_KEY });
1364
+ * instrumentOpenAIAgents({ addTraceProcessor }, { userId: 'user_123' });
1365
+ *
1366
+ * const agent = new Agent({ name: 'Assistant', instructions: 'Be helpful.' });
1367
+ * const result = await run(agent, 'Write a haiku about recursion.');
1368
+ * // The run is now a Sentrial session with LLM events, tool calls, handoffs.
1369
+ * ```
1370
+ *
1371
+ * @packageDocumentation
1372
+ */
1373
+
1374
+ /** The subset of `Trace` this processor reads (camelCase, per agents-core). */
1375
+ type AgentsTrace = {
1376
+ traceId?: string;
1377
+ name?: string;
1378
+ groupId?: string | null;
1379
+ metadata?: Record<string, unknown>;
1380
+ };
1381
+ /** `SpanError` shape (`span.error`). */
1382
+ type AgentsSpanError = {
1383
+ message?: string;
1384
+ data?: Record<string, unknown>;
1385
+ };
1386
+ /** The subset of `Span` this processor reads (camelCase getters, ISO timestamps). */
1387
+ type AgentsSpan = {
1388
+ traceId?: string;
1389
+ spanId?: string;
1390
+ parentId?: string | null;
1391
+ startedAt?: string | null;
1392
+ endedAt?: string | null;
1393
+ error?: AgentsSpanError | null;
1394
+ spanData?: Record<string, unknown> & {
1395
+ type?: string;
1396
+ };
1397
+ };
1398
+ /** The one entry point we need from `@openai/agents` / `@openai/agents-core`. */
1399
+ type AgentsModuleLike = {
1400
+ addTraceProcessor: (processor: unknown) => void;
1401
+ };
1402
+ interface OpenAIAgentsProcessorOptions {
1403
+ /** Explicit SentrialClient. Defaults to a client from the environment (SENTRIAL_API_KEY). */
1404
+ client?: SentrialClient;
1405
+ /** Explicit agent/event name override (defaults to the root agent span's name per run). */
1406
+ defaultAgent?: string;
1407
+ /** User ID to associate with sessions (default 'anonymous'). */
1408
+ userId?: string;
1409
+ /** Extra metadata merged into every session. */
1410
+ extraMetadata?: Record<string, unknown>;
1411
+ }
1412
+ /**
1413
+ * Duck-typed `TracingProcessor` (from `@openai/agents-core`) that mirrors OpenAI
1414
+ * Agents SDK runs into Sentrial sessions.
1415
+ *
1416
+ * Register it via {@link instrumentOpenAIAgents}, or directly:
1417
+ * ```ts
1418
+ * import { addTraceProcessor } from '@openai/agents';
1419
+ * addTraceProcessor(new SentrialOpenAIAgentsProcessor({ client }));
1420
+ * ```
1421
+ *
1422
+ * Every callback body is containment-wrapped: the Agents SDK awaits these
1423
+ * methods inline on the user's run path, so a tracking failure must never
1424
+ * reject into the host agent — it is swallowed instead (the SentrialClient/
1425
+ * Interaction methods additionally carry their own containment seam).
1426
+ *
1427
+ * Performance tip: construct the client with `batching: { enabled: true }` so
1428
+ * `track*` calls enqueue instead of awaiting a network round-trip per span.
1429
+ * (The TS BatcherConfig takes an object — a bare `batching: true` is silently
1430
+ * ignored; the Python SDK's `batching=True` bool shorthand has no TS twin.)
1431
+ */
1432
+ declare class SentrialOpenAIAgentsProcessor {
1433
+ private clientRef?;
1434
+ private readonly defaultAgent?;
1435
+ private readonly userId;
1436
+ private readonly extraMetadata;
1437
+ private readonly traces;
1438
+ private tracesCapWarned;
1439
+ constructor(options?: OpenAIAgentsProcessorOptions);
1440
+ /** Optional TracingProcessor lifecycle hook — nothing to start. */
1441
+ start(): void;
1442
+ onTraceStart(trace: AgentsTrace): Promise<void>;
1443
+ onTraceEnd(trace: AgentsTrace): Promise<void>;
1444
+ onSpanStart(span: AgentsSpan): Promise<void>;
1445
+ onSpanEnd(span: AgentsSpan): Promise<void>;
1446
+ /**
1447
+ * Called at application stop: finishes any still-open sessions as interrupted
1448
+ * and flushes the client's event queue (if batching).
1449
+ */
1450
+ shutdown(_timeout?: number): Promise<void>;
1451
+ /** Forces immediate processing of queued events (no-op without batching). */
1452
+ forceFlush(): Promise<void>;
1453
+ private resolveClient;
1454
+ private flushClient;
1455
+ private handleTraceStart;
1456
+ private stateFor;
1457
+ /**
1458
+ * Begin the Sentrial session for this trace exactly once. Event/agent name
1459
+ * preference: explicit `defaultAgent` > root agent span name > trace workflow
1460
+ * name > 'openai-agents'. The session title is the trace's workflow name.
1461
+ *
1462
+ * DELIBERATE cross-SDK divergence on begin failure: the rejected promise is
1463
+ * cached, so a trace whose begin() failed never retries (avoids a partial
1464
+ * session whose earlier spans were lost). The Python twin leaves
1465
+ * `interaction=None` and retries at the next span (favoring late recovery).
1466
+ * Both are documented choices, not drift.
1467
+ */
1468
+ private ensureInteraction;
1469
+ private handleSpanStart;
1470
+ private handleSpanEnd;
1471
+ private trackLLMEvent;
1472
+ private trackFunction;
1473
+ private trackHandoff;
1474
+ private trackGuardrail;
1475
+ private handleTraceEnd;
1476
+ private finishState;
1477
+ }
1478
+ /**
1479
+ * Construct a {@link SentrialOpenAIAgentsProcessor} and register it with the
1480
+ * OpenAI Agents SDK. Pass the SDK's `addTraceProcessor` (or any object carrying
1481
+ * it, e.g. the whole `@openai/agents` module namespace) — the peer dependency is
1482
+ * never imported here, mirroring `wrapAISDK(ai)`.
1483
+ *
1484
+ * Idempotent per (agents module, client) pair: calling this again with the same
1485
+ * `addTraceProcessor` and the same `options.client` (or again with no client —
1486
+ * the env-default) returns the FIRST registered processor instead of registering
1487
+ * a duplicate, which would mirror every run into two sessions. The first
1488
+ * duplicate call emits a console warning (silent thereafter).
1489
+ *
1490
+ * @example
1491
+ * ```ts
1492
+ * import * as agents from '@openai/agents';
1493
+ * import { instrumentOpenAIAgents } from '@sentrial/sdk';
1494
+ *
1495
+ * instrumentOpenAIAgents(agents, { userId: 'user_123' });
1496
+ * ```
1497
+ *
1498
+ * @param agents - The `@openai/agents` module namespace (or `{ addTraceProcessor }`).
1499
+ * @param options - Processor options (client, defaultAgent, userId, extraMetadata).
1500
+ * @returns The registered processor (keep it to call `shutdown()`/`forceFlush()`).
1501
+ */
1502
+ declare function instrumentOpenAIAgents(agents: AgentsModuleLike, options?: OpenAIAgentsProcessorOptions): SentrialOpenAIAgentsProcessor;
1503
+
1504
+ /**
1505
+ * Google Agent Development Kit (ADK) Integration for Sentrial — TypeScript twin
1506
+ * of the Python `sentrial.google_adk` module, ported for `adk-js` (`@google/adk`).
1507
+ *
1508
+ * Maps a Google ADK **run** (one `Runner.runAsync` invocation) onto a single
1509
+ * Sentrial session via the `begin()`/Interaction API. The attachment surface is a
1510
+ * Runner-level **Plugin** — ADK's `BasePlugin` fires globally for the *entire*
1511
+ * invocation across *every* agent, tool, and model call, so one run → one Sentrial
1512
+ * session automatically (the ADK equivalent of the OpenAI Agents SDK's global
1513
+ * trace processor in {@link SentrialOpenAIAgentsProcessor}).
1514
+ *
1515
+ * Mapping (mirrors the Python google_adk.py):
1516
+ *
1517
+ * - **run start** (`beforeRunCallback`) → `begin()` — agent/event name from the
1518
+ * root agent, `convoId` from the ADK session id, `userId` from the ADK session
1519
+ * (overridable), input from the run's `userContent`.
1520
+ * - **model calls** (`beforeModelCallback` → `afterModelCallback`) → per-call LLM
1521
+ * event via `trackToolCall` (`llm:{model}` convention, model from
1522
+ * `LlmRequest.model`, prompt/completion tokens from `LlmResponse.usageMetadata`
1523
+ * — `promptTokenCount` / `candidatesTokenCount` — cost via the pricing tables,
1524
+ * duration from the before/after timestamps). Streaming *partial* responses are
1525
+ * skipped so tokens aren't double-counted; the final response carries the usage.
1526
+ * - **tool calls** (`beforeToolCallback` → `afterToolCallback`) → `trackToolCall`
1527
+ * (tool name/args/result, duration from before/after).
1528
+ * - **errors** (`onToolErrorCallback` / `onModelErrorCallback` and any
1529
+ * `LlmResponse.errorCode`) → `trackError` (the session finishes `success=false`).
1530
+ * - **run end** (`afterRunCallback`) → `finish()` with rolled-up tokens/cost and
1531
+ * the last model output; custom metrics `llm_calls` / `tool_calls`.
1532
+ *
1533
+ * Distinction from {@link wrapGoogle}:
1534
+ * `wrapGoogle` wraps a RAW `@google/genai` model so each `generateContent` call is
1535
+ * tracked inside a session YOU manage with `begin()`. This module instead observes
1536
+ * the ADK **runtime** (`Agent` + `Runner`) from the outside via its documented
1537
+ * Plugin hooks — each ADK run becomes a full Sentrial session automatically. If you
1538
+ * use ADK, prefer this module; don't combine both on the same run (LLM calls would
1539
+ * be double-counted).
1540
+ *
1541
+ * The `@google/adk` package stays an OPTIONAL peer dependency: this module NEVER
1542
+ * imports it. ADK's `PluginManager` dispatches to plugin callbacks BY NAME and
1543
+ * dedups by `.name`, and a plain object structurally satisfies `BasePlugin`, so a
1544
+ * duck-typed class needs no `BasePlugin` subclass — the module imports cleanly with
1545
+ * or without ADK installed. You attach it via `plugins: [new SentrialAdkPlugin()]`
1546
+ * at Runner construction, or after the fact with {@link instrumentAdk}.
1547
+ *
1548
+ * Verified against the official ADK-JS source (github.com/google/adk-js, package
1549
+ * `@google/adk`):
1550
+ * - `core/src/plugins/base_plugin.ts` — plugin callbacks take a SINGLE camelCase
1551
+ * params object (NOT Python's keyword args): `beforeRunCallback({invocationContext})`,
1552
+ * `afterRunCallback({invocationContext})`, `beforeModelCallback({callbackContext, llmRequest})`,
1553
+ * `afterModelCallback({callbackContext, llmResponse})`,
1554
+ * `onModelErrorCallback({callbackContext, llmRequest, error})`,
1555
+ * `beforeToolCallback({tool, toolArgs, toolContext})`,
1556
+ * `afterToolCallback({tool, toolArgs, toolContext, result})`,
1557
+ * `onToolErrorCallback({tool, toolArgs, toolContext, error})`. Returning a
1558
+ * non-`undefined` value SHORT-CIRCUITS the run, so every callback returns
1559
+ * `undefined` (pure observer).
1560
+ * - `core/src/plugins/plugin_manager.ts` — `runCallbacks` CATCHES a throwing
1561
+ * callback and RE-THROWS a wrapped error that HALTS the agent run; containment
1562
+ * inside every callback body is therefore mandatory. `registerPlugin` throws on a
1563
+ * duplicate `.name`; `getPlugin(name)` retrieves by name.
1564
+ * - `core/src/models/llm_response.ts` — `content?: Content` (`.parts[].text`),
1565
+ * `usageMetadata` (`promptTokenCount` / `candidatesTokenCount`), `partial`,
1566
+ * `errorCode` / `errorMessage`.
1567
+ * - `core/src/models/llm_request.ts` — `model?: string`.
1568
+ * - `core/src/agents/invocation_context.ts` — `invocationId`, `agent` (`.name`),
1569
+ * `session` (`.id` / `.appName` / `.userId`), `appName`/`userId` getters, `userContent`.
1570
+ * - `core/src/agents/readonly_context.ts` — the callback `Context` exposes getters
1571
+ * `invocationId` / `agentName` / `sessionId` / `userId` / `userContent` and the
1572
+ * public `invocationContext`.
1573
+ * - `core/src/runner/runner.ts` — `plugins?: BasePlugin[]` at construction;
1574
+ * `runner.pluginManager.registerPlugin(plugin)`.
1575
+ *
1576
+ * @example
1577
+ * ```ts
1578
+ * import { InMemoryRunner } from '@google/adk';
1579
+ * import { LlmAgent } from '@google/adk';
1580
+ * import { sentrial, SentrialAdkPlugin } from '@sentrial/sdk';
1581
+ *
1582
+ * sentrial.configure({ apiKey: process.env.SENTRIAL_API_KEY });
1583
+ *
1584
+ * const agent = new LlmAgent({ name: 'assistant', model: 'gemini-2.0-flash' });
1585
+ * const runner = new InMemoryRunner({ agent, plugins: [new SentrialAdkPlugin()] });
1586
+ * // Every runner.runAsync(...) is now a Sentrial session with LLM events,
1587
+ * // tool calls, and token/cost rollups.
1588
+ * ```
1589
+ *
1590
+ * @packageDocumentation
1591
+ */
1592
+
1593
+ /** The subset of ADK `Content` (`@google/genai`) this plugin reads. */
1594
+ type AdkContent = {
1595
+ parts?: Array<{
1596
+ text?: unknown;
1597
+ } | unknown>;
1598
+ } | string | unknown;
1599
+ /** The subset of ADK `InvocationContext` this plugin reads (run lifecycle). */
1600
+ type AdkInvocationContext = {
1601
+ invocationId?: string;
1602
+ agent?: {
1603
+ name?: string;
1604
+ };
1605
+ session?: {
1606
+ id?: string;
1607
+ appName?: string;
1608
+ userId?: string;
1609
+ };
1610
+ appName?: string;
1611
+ userId?: string;
1612
+ userContent?: AdkContent;
1613
+ };
1614
+ /** The subset of the ADK callback `Context` this plugin reads (model/tool calls). */
1615
+ type AdkCallbackContext = {
1616
+ invocationId?: string;
1617
+ agentName?: string;
1618
+ sessionId?: string;
1619
+ userId?: string;
1620
+ userContent?: AdkContent;
1621
+ invocationContext?: AdkInvocationContext;
1622
+ };
1623
+ /** The subset of ADK `LlmRequest` this plugin reads. */
1624
+ type AdkLlmRequest = {
1625
+ model?: string;
1626
+ };
1627
+ /** The subset of ADK `LlmResponse` this plugin reads. */
1628
+ type AdkLlmResponse = {
1629
+ content?: AdkContent;
1630
+ usageMetadata?: {
1631
+ promptTokenCount?: unknown;
1632
+ candidatesTokenCount?: unknown;
1633
+ };
1634
+ partial?: unknown;
1635
+ errorCode?: unknown;
1636
+ errorMessage?: unknown;
1637
+ };
1638
+ /** The subset of ADK `BaseTool` this plugin reads. */
1639
+ type AdkTool = {
1640
+ name?: string;
1641
+ };
1642
+ /** The Runner surface {@link instrumentAdk} needs (`runner.pluginManager.registerPlugin`). */
1643
+ type AdkRunnerLike = {
1644
+ pluginManager?: {
1645
+ registerPlugin?: (plugin: unknown) => void;
1646
+ getPlugin?: (name: string) => unknown;
1647
+ };
1648
+ };
1649
+ interface AdkPluginOptions {
1650
+ /** Explicit SentrialClient. Defaults to a client from the environment (SENTRIAL_API_KEY). */
1651
+ client?: SentrialClient;
1652
+ /** ADK plugin name (default 'sentrial'); ADK requires unique plugin names. */
1653
+ name?: string;
1654
+ /** Explicit agent/event name override (defaults to the root agent's name per run). */
1655
+ defaultAgent?: string;
1656
+ /** User ID override (defaults to the ADK session's userId, then 'anonymous'). */
1657
+ userId?: string;
1658
+ /** Extra metadata merged into every session. */
1659
+ extraMetadata?: Record<string, unknown>;
1660
+ }
1661
+ /**
1662
+ * Duck-typed Google ADK `BasePlugin` that mirrors ADK runs into Sentrial sessions.
1663
+ *
1664
+ * Register it on a `Runner`:
1665
+ * ```ts
1666
+ * const runner = new InMemoryRunner({ agent, plugins: [new SentrialAdkPlugin()] });
1667
+ * ```
1668
+ * or attach it to an existing runner with {@link instrumentAdk}.
1669
+ *
1670
+ * This is a *plain* class implementing the `BasePlugin` callback protocol — it is
1671
+ * intentionally NOT a `BasePlugin` subclass, because ADK's `PluginManager`
1672
+ * dispatches to callbacks by name and dedups by `.name`, so subclassing would force
1673
+ * a hard `@google/adk` import (breaking the "optional peer" contract and
1674
+ * import-without-ADK). Every callback body is containment-wrapped: ADK's
1675
+ * PluginManager re-throws a callback error and HALTS the run, so a tracking failure
1676
+ * must never escape — it is swallowed instead. Every callback returns `undefined`
1677
+ * (a pure observer never short-circuits the model/tool call).
1678
+ *
1679
+ * Performance tip: construct the client with `batching: { enabled: true }` so
1680
+ * `track*` calls enqueue instead of awaiting a network round-trip per event. ADK
1681
+ * invokes these callbacks inline on the run's event loop.
1682
+ */
1683
+ declare class SentrialAdkPlugin {
1684
+ /** PluginManager reads `.name` for dedup/logging — a real string, always present. */
1685
+ readonly name: string;
1686
+ private clientRef?;
1687
+ private readonly defaultAgent?;
1688
+ private readonly userIdOption?;
1689
+ private readonly extraMetadata;
1690
+ private readonly runs;
1691
+ /** One-time guard so the runs-cap eviction warns at most once (like the duplicate-instrument warn). */
1692
+ private runsCapWarned;
1693
+ constructor(options?: AdkPluginOptions);
1694
+ beforeRunCallback(params: {
1695
+ invocationContext: AdkInvocationContext;
1696
+ }): Promise<undefined>;
1697
+ afterRunCallback(params: {
1698
+ invocationContext: AdkInvocationContext;
1699
+ }): Promise<void>;
1700
+ onRunErrorCallback(params: {
1701
+ invocationContext: AdkInvocationContext;
1702
+ error: unknown;
1703
+ }): Promise<void>;
1704
+ beforeModelCallback(params: {
1705
+ callbackContext: AdkCallbackContext;
1706
+ llmRequest: AdkLlmRequest;
1707
+ }): Promise<undefined>;
1708
+ afterModelCallback(params: {
1709
+ callbackContext: AdkCallbackContext;
1710
+ llmResponse: AdkLlmResponse;
1711
+ }): Promise<undefined>;
1712
+ onModelErrorCallback(params: {
1713
+ callbackContext: AdkCallbackContext;
1714
+ llmRequest: AdkLlmRequest;
1715
+ error: unknown;
1716
+ }): Promise<undefined>;
1717
+ beforeToolCallback(params: {
1718
+ tool: AdkTool;
1719
+ toolArgs: Record<string, unknown>;
1720
+ toolContext: AdkCallbackContext;
1721
+ }): Promise<undefined>;
1722
+ afterToolCallback(params: {
1723
+ tool: AdkTool;
1724
+ toolArgs: Record<string, unknown>;
1725
+ toolContext: AdkCallbackContext;
1726
+ result: unknown;
1727
+ }): Promise<undefined>;
1728
+ onToolErrorCallback(params: {
1729
+ tool: AdkTool;
1730
+ toolArgs: Record<string, unknown>;
1731
+ toolContext: AdkCallbackContext;
1732
+ error: unknown;
1733
+ }): Promise<undefined>;
1734
+ private resolveClient;
1735
+ /**
1736
+ * Fetch (or lazily create) the per-run state for an invocation id, back-filling
1737
+ * any run-identity fields not known when it was first created (a model/tool
1738
+ * callback can reach a run before beforeRun in some ADK flows).
1739
+ */
1740
+ private getOrCreateState;
1741
+ private stateFromInvocationContext;
1742
+ private stateFromCallbackContext;
1743
+ /**
1744
+ * Begin the Sentrial session for this run exactly once. Event/agent name
1745
+ * preference: explicit `defaultAgent` > root agent name > 'google-adk'. The ADK
1746
+ * session id becomes `convoId`; the ADK session `userId` (or the configured
1747
+ * override) becomes the Sentrial user.
1748
+ *
1749
+ * DELIBERATE (matches the openai-agents.ts TS twin): the interaction promise is
1750
+ * cached, so a run whose `begin()` REJECTED never retries (avoids a partial
1751
+ * session whose earlier events were lost). The Python google_adk leaves the
1752
+ * interaction unset and retries at the next callback — both are documented,
1753
+ * per-SDK choices, not drift.
1754
+ */
1755
+ private ensureInteraction;
1756
+ private handleRunStart;
1757
+ private handleRunEnd;
1758
+ private handleRunError;
1759
+ private handleModelStart;
1760
+ private handleModelEnd;
1761
+ private handleModelError;
1762
+ private emitLlmEvent;
1763
+ private handleToolStart;
1764
+ private popToolTiming;
1765
+ private handleToolEnd;
1766
+ private handleToolError;
1767
+ private finishState;
1768
+ }
1769
+ /**
1770
+ * Register a {@link SentrialAdkPlugin} on an already-constructed ADK `Runner`.
1771
+ *
1772
+ * Convenience for when you didn't pass `plugins: [new SentrialAdkPlugin()]` at
1773
+ * Runner construction. Pass the runner (its `pluginManager.registerPlugin` is the
1774
+ * only surface used) — the `@google/adk` peer is never imported here, mirroring
1775
+ * `instrumentOpenAIAgents(agents)`.
1776
+ *
1777
+ * Idempotent per runner: calling it again on the same runner returns the FIRST
1778
+ * registered plugin instead of registering a duplicate (which ADK would reject on
1779
+ * the duplicate name anyway, and which would mirror every run into two sessions).
1780
+ * The first duplicate call emits a console warning (silent thereafter).
1781
+ *
1782
+ * @example
1783
+ * ```ts
1784
+ * import { InMemoryRunner } from '@google/adk';
1785
+ * import { instrumentAdk } from '@sentrial/sdk';
1786
+ *
1787
+ * const runner = new InMemoryRunner({ agent });
1788
+ * instrumentAdk(runner, { userId: 'user_123' });
1789
+ * ```
1790
+ *
1791
+ * @param runner - An ADK `Runner` exposing `pluginManager.registerPlugin`.
1792
+ * @param options - Plugin options (client, name, defaultAgent, userId, extraMetadata).
1793
+ * @returns The registered plugin.
1794
+ */
1795
+ declare function instrumentAdk(runner: AdkRunnerLike, options?: AdkPluginOptions): SentrialAdkPlugin;
1796
+
1383
1797
  /**
1384
1798
  * Sentrial Decorators - Easy instrumentation for AI agents
1385
1799
  *
@@ -1948,4 +2362,4 @@ declare class Experiment {
1948
2362
  getResults(): Promise<ExperimentResults | null>;
1949
2363
  }
1950
2364
 
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 };
2365
+ 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 };