@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/README.md CHANGED
@@ -19,35 +19,66 @@ pnpm add @sentrial/sdk
19
19
  yarn add @sentrial/sdk
20
20
  ```
21
21
 
22
+ ## Breaking changes
23
+
24
+ **0.5.0**
25
+
26
+ - **Transport is now OpenTelemetry (OTLP).** Telemetry ships over `/api/otel/v1/traces`; this SDK no
27
+ longer POSTs sessions/events to the legacy `/api/sdk/*` endpoints (those endpoints remain available
28
+ server-side as thin compatibility adapters that translate onto the OTLP ingest pipeline, so older SDK
29
+ releases keep working — new integrations use OTLP; the Python sync and async clients both emit OTLP
30
+ too). The OTLP ingest route requires the API
31
+ key to carry **both `sessions:write` and `events:write`** scopes — dashboard-generated keys include
32
+ both, but a manually-scoped `sessions:write`-only key will silently drop data. Add `events:write`
33
+ to the key (see [Transport](#transport)).
34
+ - **Timestamp fields are now `string` (ISO 8601), not `Date`.** The `Session` fields (`createdAt` /
35
+ `updatedAt` / `completedAt`) and `Event.timestamp` were retyped because the API returns JSON and
36
+ `response.json()` never revived them into `Date` objects, so the old `Date` typing was incorrect —
37
+ but this is a compile-breaking change for code like `session.createdAt.getTime()`. Migrate by
38
+ parsing where needed: `new Date(session.createdAt).getTime()`.
39
+ - **`PiiConfig.enhancedDetection` was removed.** The field was a no-op (never read by redaction
40
+ logic) and is gone from `PiiConfig`, mirroring the Python SDK. Drop it from any `PiiConfig`
41
+ literal — it had no effect.
42
+
43
+ Shipped under a minor bump (not a patch), per the SDK compatibility policy.
44
+
22
45
  ## CLI
23
46
 
24
47
  The SDK package also ships a `sentrial` binary for scriptable workflows. This is the
25
48
  first packaging step toward a dedicated `npm install sentrial` style CLI package.
26
49
 
27
50
  ```bash
28
- # Run the CLI without a global install and set up the current project
29
- npx --yes --package=@sentrial/sdk@latest sentrial install
51
+ # Install globally
52
+ npm install -g @sentrial/sdk
53
+
54
+ # Or run without installing
55
+ npx -p @sentrial/sdk sentrial sessions list
30
56
 
31
57
  # Save credentials locally
32
- npx sentrial login --api-key sentrial_live_xxx
58
+ sentrial login --api-key sentrial_live_xxx
33
59
 
34
60
  # Install the SDK package and scaffold a safe example integration
35
- npx sentrial install
61
+ sentrial install
36
62
 
37
63
  # Or scaffold only, without installing packages
38
- npx sentrial init
64
+ sentrial init
39
65
 
40
66
  # Inspect production runs
41
- npx sentrial sessions list --last 7d --status failed --agent support
42
- npx sentrial sessions get <session-id>
43
- npx sentrial events list <session-id> --type tool_call
67
+ sentrial sessions list --last 7d --status failed --agent support
68
+ sentrial sessions get <session-id>
69
+ sentrial events list <session-id> --type tool_call
44
70
 
45
71
  # Review signals and run CI gates
46
- npx sentrial signals list --type negative --last 30d
47
- npx sentrial diagnose <session-id>
48
- npx sentrial evaluate <experiment-id> --min-score 80
72
+ sentrial signals list --type negative --last 30d
73
+ sentrial diagnose <session-id>
74
+ sentrial evaluate <experiment-id> --min-score 80
49
75
  ```
50
76
 
77
+ > Note: `sentrial evaluate` calls `/api/v1/evaluate`, which requires an API key
78
+ > scoped with `metrics:read`. Make sure the key you use (via `--api-key`,
79
+ > `SENTRIAL_API_KEY`, or `sentrial login`) carries that scope, or the CI gate
80
+ > will fail with a 403.
81
+
51
82
  ## Quick Start (30 seconds)
52
83
 
53
84
  ### Option 1: Auto-Wrap LLM Clients (Recommended)
@@ -95,11 +126,11 @@ const supportAgent = withSession('support-agent', async (userId: string, message
95
126
  ### Option 3: Vercel AI SDK
96
127
 
97
128
  ```typescript
98
- import { configure, wrapAISDK } from '@sentrial/sdk';
129
+ import { configureVercel, wrapAISDK } from '@sentrial/sdk';
99
130
  import * as ai from 'ai';
100
131
  import { openai } from '@ai-sdk/openai';
101
132
 
102
- configure({ apiKey: process.env.SENTRIAL_API_KEY, defaultAgent: 'my-agent' });
133
+ configureVercel({ apiKey: process.env.SENTRIAL_API_KEY });
103
134
 
104
135
  const { generateText, streamText } = wrapAISDK(ai);
105
136
 
@@ -110,6 +141,59 @@ const { text } = await generateText({
110
141
  });
111
142
  ```
112
143
 
144
+ ### Option 4: OpenAI Agents SDK
145
+
146
+ One call registers a trace processor with the Agents SDK (`@openai/agents`, an optional peer
147
+ dependency, supported range `>=0.1.0 <1.0.0`) — every run becomes a Sentrial session with LLM
148
+ events, tool calls, handoffs, and guardrails. Don't combine with `wrapOpenAI()` on the same run
149
+ (LLM calls would be double-counted).
150
+
151
+ ```typescript
152
+ import { Agent, run, addTraceProcessor } from '@openai/agents';
153
+ import { sentrial, instrumentOpenAIAgents } from '@sentrial/sdk';
154
+
155
+ sentrial.configure({ apiKey: process.env.SENTRIAL_API_KEY });
156
+ instrumentOpenAIAgents({ addTraceProcessor }, { userId: 'user_123' });
157
+
158
+ const agent = new Agent({ name: 'Assistant', instructions: 'Be helpful.' });
159
+ const result = await run(agent, 'Write a haiku about recursion.');
160
+ // The run is now a Sentrial session with LLM events, tool calls, handoffs.
161
+ ```
162
+
163
+ See the [OpenAI Agents SDK integration docs](https://www.sentrial.com/docs/integrations/openai-agents)
164
+ for configuration options, the run→session mapping, and batching/flush guidance.
165
+
166
+ ### Option 5: Google ADK
167
+
168
+ Register one Plugin on your Google ADK `Runner` (`@google/adk`, an optional peer dependency,
169
+ supported range `>=0.1.0 <2.0.0`) — every run becomes a Sentrial session with LLM events, tool
170
+ calls, tokens, cost, and errors. The Plugin fires globally across every agent, tool, and model
171
+ call in the invocation. Don't combine with `wrapGoogle()` on the same run (LLM calls would be
172
+ double-counted).
173
+
174
+ ```typescript
175
+ import { InMemoryRunner, LlmAgent } from '@google/adk';
176
+ import { sentrial, SentrialAdkPlugin } from '@sentrial/sdk';
177
+
178
+ sentrial.configure({ apiKey: process.env.SENTRIAL_API_KEY });
179
+
180
+ const agent = new LlmAgent({ name: 'assistant', model: 'gemini-2.0-flash' });
181
+ const runner = new InMemoryRunner({ agent, plugins: [new SentrialAdkPlugin()] });
182
+ // Every runner.runAsync(...) is now a Sentrial session with LLM events, tool calls, and cost.
183
+ ```
184
+
185
+ Already constructed the `Runner`? Attach after the fact — `instrumentAdk()` is idempotent per
186
+ runner:
187
+
188
+ ```typescript
189
+ import { instrumentAdk } from '@sentrial/sdk';
190
+
191
+ instrumentAdk(runner, { userId: 'user_123' });
192
+ ```
193
+
194
+ See the [Google ADK integration docs](https://www.sentrial.com/docs/integrations/google-adk)
195
+ for configuration options, the run→session mapping, and batching/flush guidance.
196
+
113
197
  ## Features
114
198
 
115
199
  ### LLM Auto-Wrappers
@@ -170,7 +254,7 @@ await experiment.run(async (testCase, variant, tracker) => {
170
254
 
171
255
  ### Vercel AI SDK Integration
172
256
 
173
- Full support for Vercel AI SDK v3-v6:
257
+ Full support for Vercel AI SDK v3-v7:
174
258
 
175
259
  ```typescript
176
260
  import { wrapAISDK } from '@sentrial/sdk';
@@ -193,6 +277,8 @@ const { text } = await generateText({
193
277
  });
194
278
  ```
195
279
 
280
+ > **Note:** `zod` is not a direct dependency of `@sentrial/sdk` — it comes transitively from the Vercel AI SDK (`ai`), which you install to use `wrapAISDK`. The `import { z } from 'zod'` above resolves because `ai` pulls `zod` in as a peer.
281
+
196
282
  ## Configuration
197
283
 
198
284
  ### Environment Variables
@@ -202,6 +288,45 @@ SENTRIAL_API_URL=https://api.sentrial.com
202
288
  SENTRIAL_API_KEY=sentrial_live_xxx
203
289
  ```
204
290
 
291
+ ### Optional agent reports
292
+
293
+ An agent can report missing context, a tool problem, a missing capability, or uncertain completion without marking the session failed:
294
+
295
+ ```ts
296
+ await interaction.reportDiagnostic({
297
+ reportId: 'claim-context-1',
298
+ category: 'missing_context',
299
+ state: 'reported',
300
+ description: 'The policy number needed for this claim is missing.',
301
+ });
302
+ // Later, reuse the report ID and set state: 'recovered' with a new description.
303
+ ```
304
+
305
+ The equivalent client method is `client.reportDiagnostic({ sessionId, report })`. Both a problem and its claimed recovery are agent assertions. They do not create a confirmed signal, training label, business outcome, or automatic action. Each call records a separate event; no prior report is overwritten. There are no model calls.
306
+
307
+ Use an opaque report ID (1–128 letters, digits, `.`, `_`, `:`, or `-`, starting with a letter/digit). Descriptions are limited to 4,096 UTF-8 bytes. Optional `relatedSpanIds` contains at most eight distinct, nonzero, lowercase 16-digit OTel span IDs from the current trace. These references are agent-supplied and are not verified or followed automatically. Normal metadata redaction, batching, transport retry and fail-silent behavior apply. A resolved call is not a durable-delivery receipt; use delivery diagnostics and inspect the retained event. Unknown/degraded/finished sessions cannot accept new reports.
308
+
309
+ The updated collector and trace UI display these as **Agent report**, including **Reported recovered**. Older collectors preserve generic event metadata but do not supply the validated report view. Existing signal evidence selectors do not consume reports as authoritative evidence; measuring incremental detection and review noise requires a separately reviewed evaluation.
310
+
311
+ ### Transport
312
+
313
+ The SDK sends telemetry over **OpenTelemetry (OTLP)** to `/api/otel/v1/traces` — this is the sole
314
+ ingestion path for this SDK (it no longer POSTs sessions/events to the legacy `/api/sdk/*` endpoints,
315
+ which remain available server-side as thin compatibility adapters translated onto the OTLP ingest
316
+ pipeline so older SDK releases keep working; the Python sync and async clients both emit OTLP too).
317
+ The OTLP ingest route
318
+ requires the API key to carry **both `sessions:write` and `events:write`** scopes — keys
319
+ generated in the dashboard include both automatically. If you use a manually-scoped key that
320
+ only has `sessions:write`, session creation will 403 and tracking is silently dropped; add
321
+ `events:write` to the key.
322
+
323
+ Events and completion attach to an in-memory span the client holds from
324
+ `createSession()` / `begin()`, so they only apply to sessions **created on the same client
325
+ instance**. If you obtain a session id elsewhere — a session created out-of-band, shared across
326
+ processes, or tracked on a different `SentrialClient` — its events and finalization are skipped
327
+ (the SDK logs a one-time warning). Use the same client instance for the full begin→track→finish
328
+ flow.
329
+
205
330
  ### Programmatic
206
331
 
207
332
  ```typescript
@@ -284,23 +409,23 @@ const cost = calculateOpenAICost({
284
409
 
285
410
  ## Framework Compatibility
286
411
 
287
- | Framework | Integration | Status |
288
- | -------------------- | ----------------- | ------ |
289
- | **Direct OpenAI** | `wrapOpenAI()` | ✔️ |
290
- | **Direct Anthropic** | `wrapAnthropic()` | ✔️ |
291
- | **Direct Gemini** | `wrapGoogle()` | ✔️ |
292
- | **Vercel AI SDK** | `wrapAISDK()` | ✔️ |
293
- | **Express/Fastify** | Decorators | ✔️ |
294
- | **Next.js** | Decorators | ✔️ |
295
- | **Custom agents** | Full API | ✔️ |
412
+ | Framework | Integration | Status |
413
+ | --------------------- | -------------------------- | ------ |
414
+ | **Direct OpenAI** | `wrapOpenAI()` | ✔️ |
415
+ | **Direct Anthropic** | `wrapAnthropic()` | ✔️ |
416
+ | **Direct Gemini** | `wrapGoogle()` | ✔️ |
417
+ | **Vercel AI SDK** | `wrapAISDK()` | ✔️ |
418
+ | **OpenAI Agents SDK** | `instrumentOpenAIAgents()` | ✔️ |
419
+ | **Google ADK** | `instrumentAdk()` | ✔️ |
420
+ | **Express/Fastify** | Decorators | ✔️ |
421
+ | **Next.js** | Decorators | ✔️ |
422
+ | **Custom agents** | Full API | ✔️ |
296
423
 
297
424
  ## Documentation
298
425
 
299
- See the `docs/` folder in the repository:
300
-
301
- - `docs/sdks/typescript-sdk.md` - Full SDK reference
302
- - `docs/integrations/vercel-ai-sdk.md` - Vercel AI SDK integration
303
- - `docs/features/experiments.md` - Experiments guide
426
+ - Full reference: [https://sentrial.com/docs](https://sentrial.com/docs)
427
+ - Repository README: the root `README.md` maps every SDK entry point and the ingest flow
428
+ - Framework wrappers live in `src/vercel.ts` (Vercel AI SDK), `src/openai-agents.ts` (OpenAI Agents SDK), `src/google-adk.ts` (Google ADK), `src/wrappers.ts` (OpenAI/Anthropic), and `src/decorators.ts`
304
429
 
305
430
  ## Support
306
431
 
@@ -313,20 +438,185 @@ See the `docs/` folder in the repository:
313
438
  Both SDKs share the same core API (`begin`/`finish`, tool & LLM tracking, production
314
439
  experiments, provider wrappers). Framework integrations currently differ:
315
440
 
316
- | Capability | TypeScript | Python |
317
- | ----------------------------------------------------- | --------------------- | ------------------- |
318
- | Core tracking (begin/finish, tools, LLM, experiments) | ✓ | ✓ |
319
- | OpenAI / Anthropic / Google wrappers | ✓ | ✓ |
320
- | Vercel AI SDK helper | ✓ | — (JS-only) |
321
- | Claude Agent SDK | partial (single-turn) | ✓ (full multi-turn) |
322
- | OpenTelemetry export | — | ✓ |
323
- | Langfuse dual-write | — | ✓ |
324
- | AutoGen / CrewAI / LangChain handlers | — | ✓ |
325
-
326
- Porting the missing TypeScript integrations (OpenTelemetry, Langfuse, Claude
327
- multi-turn) is planned; new TS integrations must keep V1 API response shapes
328
- backward-compatible.
441
+ | Capability | TypeScript | Python |
442
+ | ----------------------------------------------------- | -------------------------- | ------------------- |
443
+ | Core tracking (begin/finish, tools, LLM, experiments) | ✓ | ✓ |
444
+ | OpenAI / Anthropic / Google wrappers | ✓ | ✓ |
445
+ | Vercel AI SDK helper | ✓ | — (JS-only) |
446
+ | Claude Agent SDK | partial (single-turn) | ✓ (full multi-turn) |
447
+ | OpenTelemetry export | ✓ (default OTLP transport) | ✓ |
448
+ | Langfuse dual-write | — | ✓ |
449
+ | AutoGen / CrewAI / LangChain handlers | — | ✓ |
450
+ | OpenAI Agents SDK trace processor | ✓ | ✓ |
451
+ | Google ADK Runner plugin | ✓ | ✓ |
452
+
453
+ Porting the missing TypeScript integrations (Langfuse, Claude multi-turn) is
454
+ planned; new TS integrations must keep V1 API response shapes backward-compatible.
329
455
 
330
456
  ## License
331
457
 
332
458
  MIT
459
+
460
+ ## Delivery diagnostics and worker handoffs
461
+
462
+ ### Large tool content (opt-in preview)
463
+
464
+ For enabled workspaces, the native SDK can upload serialized tool inputs/outputs larger than 64 KiB directly to private storage and attach a small reference to the tool span. The configured PII policy runs before byte counting, hashing and upload. Enable the policy fields you need; `redactionRevision` labels that policy and does not itself redact data.
465
+
466
+ ```typescript
467
+ const client = new SentrialClient({
468
+ apiKey: process.env.SENTRIAL_API_KEY,
469
+ pii: { enabled: true },
470
+ largeToolContent: {
471
+ enabled: true,
472
+ redactionRevision: 'claims-redaction-v1',
473
+ timeoutMs: 20_000,
474
+ },
475
+ });
476
+ ```
477
+
478
+ This remains disabled by default and requires separately enabled server storage and workspace quotas. It applies to named `tool_call` input/output fields, including calls recorded through the native SDK integrations; arbitrary external OpenTelemetry spans, LLM messages, metadata and state snapshots are not uploaded. The [Python clients](../python-sdk/README.md#large-tool-content-opt-in-preview) implement the same opt-in; hosted acceptance remains pending.
479
+
480
+ Each field retains a prefix of at most 4 KiB of UTF-8 source bytes inline, with a 16 MiB object limit and two simultaneous uploads per client. Excess concurrent fields, quota failures, unavailable storage, cancellation and oversized objects keep the excerpt plus an explicit missing-content marker. Small fields and clients without the opt-in keep their existing inline behavior. Uploads use at most 24 requests and a configurable 1–60 second per-field deadline (20 seconds by default); this does not bound the subsequent normal trace delivery. A lost PUT acknowledgement is reconciled against retained parts before any retry. Recovery is in memory; restarting a process does not resume its SDK upload.
481
+
482
+ `completeSession()` and `flush()` wait for uploads already started before draining their telemetry. `shutdown()` cancels outstanding uploads and drains their fallback events; call `flush()` first when you want to wait for uploads. Do not emit more events after finishing a session. Abrupt process exit cannot guarantee delivery.
483
+
484
+ `getContentUploadDiagnostics()` returns payload-free attempted/submitted/missing/active/pending-event counts, or `null` when disabled. **Submitted is not verified or inspected:** the server must verify stored bytes, and a detector must separately load the relevant content within its evidence budget. A UI preview also does not establish detector coverage. See the [implementation and rollout limits](../../docs/plans/2026-10-17-telemetry-large-content.md).
485
+
486
+ ### Request and delivery diagnostics
487
+
488
+ For request latency and response reports, read `client.getTransportDiagnostics()` (also available without batching). It separates logical requests, retries, in-flight attempts, response outcomes, reported span acceptance and fixed server timing stages. It retains lifetime aggregates plus 32 recent completed attempts, with no payloads or identifiers. Missing values stay unknown; a successful HTTP response is not proof of unique durable writes. [Usage and measurement limits](../../docs/testing/sdk-transport-timing.md).
489
+
490
+ Call finish after the work belonging to a session has completed, then explicitly flush/close before process exit. Native SDK spans carry source sequence numbers and a final count, so missing events can be detected independently of server arrival order. This proves delivery of emitted spans, not that every application action was instrumented. Queues remain in memory and can drop events at capacity; abrupt termination cannot guarantee a flush.
491
+
492
+ Read `client.getDeliveryDiagnostics()` for payload-free batcher counters (`null` when batching is disabled). A `failed` request was not confirmed delivered; it may have reached the server.
493
+
494
+ ```typescript
495
+ import { actionMetadata, resumeHandoffContext } from '@sentrial/sdk';
496
+
497
+ const parent = await client.begin({
498
+ userId: 'example', event: 'workflow',
499
+ metadata: actionMetadata({ actionId: 'order:42' }),
500
+ });
501
+ const carrier = parent.sessionId
502
+ ? await client.handoff({ sessionId: parent.sessionId, actionId: 'order:42' })
503
+ : null;
504
+ // Pass carrier through your job queue, then in the worker:
505
+ const child = await workerClient.begin({
506
+ userId: 'example', event: 'worker', ...(carrier ? resumeHandoffContext(carrier) : {}),
507
+ });
508
+ const metadata = actionMetadata({ actionId: 'order:42', attemptId: 'try-2', retryOfAttemptId: 'try-1' });
509
+ ```
510
+
511
+ Handoff records delegation before returning the carrier and the worker starts a separate trace linked by parent session. The carrier is correlation metadata, not a credential or verified business outcome. Use opaque identifiers, never customer content or secrets. Low-level `createHandoffContext` / `create_handoff_context` only serializes a carrier; it does not record delegation. V2 signal evidence treats detached outcomes as unresolved until their context is supported.
512
+
513
+ ### Account for worker completion
514
+
515
+ The v2 lifecycle adds a dispatch ID and four explicit records. Existing `handoff()` carriers remain supported. Use a fresh UUID per worker execution; preserve it only when retrying delivery of the same dispatch. A retry that executes work again gets a new dispatch ID (and a new `attemptId`, optionally linked with `retryOfAttemptId`). Use opaque identifiers.
516
+
517
+ ```typescript
518
+ // Parent: record intent before sending the carrier through your queue/RPC.
519
+ import { randomUUID } from 'node:crypto';
520
+
521
+ const carrier = await parentClient.dispatchHandoff({
522
+ sessionId: parentSessionId,
523
+ actionId: 'order:42',
524
+ dispatchId: randomUUID(),
525
+ });
526
+
527
+ // Worker: use its own session, initialized from the received carrier.
528
+ const worker = await workerClient.begin({
529
+ userId: 'example', event: 'order-worker', ...resumeHandoffContext(carrier),
530
+ });
531
+ const workerSessionId = worker.getSessionId();
532
+ if (workerSessionId) {
533
+ await workerClient.acceptHandoff(workerSessionId, carrier);
534
+ // Execute the work here. Report the actual execution outcome.
535
+ await workerClient.finishHandoff(workerSessionId, carrier, 'completed');
536
+ }
537
+ await worker.finish({ output: 'Worker finished' });
538
+
539
+ // Parent: only after your application receives the worker's terminal result.
540
+ // Send the worker session ID and actual outcome with that result.
541
+ if (workerSessionId) {
542
+ await parentClient.receiveHandoff(parentSessionId, carrier, workerSessionId, 'completed');
543
+ }
544
+ ```
545
+
546
+ `completed`, `failed` and `cancelled` describe worker execution, not business success. Record `finishHandoff` before closing the worker session and `receiveHandoff` before closing the parent. Do not emit a completion in an unconditional `finally`; crashes, timeouts or lost results must remain unresolved. A telemetry method returning does not prove the queue or collector received anything. Continue using delivery diagnostics and flush/shutdown normally. If telemetry session creation fails, application work should still continue without these optional records.
547
+
548
+ The internal related-evidence reader detects missing phases, duplicate worker claims, conflicting outcomes and invalid local order. It uses SDK producer sequences rather than arrival order or clocks across machines. Captures remain incomplete for definitive signal judgments. See the [handoff verification contract](../../docs/plans/2026-10-09-handoff-lifecycle-delivery.md).
549
+
550
+ ### Report experiment exposure explicitly
551
+
552
+ `getVariant()` and `createSession()` associate an assignment with a session, including
553
+ holdouts. An association does **not** assert that your application used the configuration.
554
+ After using the returned settings, report that usage explicitly:
555
+
556
+ ```typescript
557
+ const assignment = await client.getVariant({
558
+ experimentName: 'support-prompt',
559
+ agentName: 'Support agent',
560
+ userId: 'stable-user-id',
561
+ });
562
+ const sessionId = await client.createSession({
563
+ name: 'Support request',
564
+ agentName: 'Support agent',
565
+ userId: 'stable-user-id',
566
+ });
567
+ // Apply the returned settings in your application before recording exposure.
568
+ if (sessionId && assignment.assignmentId && assignment.configurationHash) {
569
+ const receipt = await client.recordExperimentExposure({
570
+ sessionId,
571
+ assignmentId: assignment.assignmentId,
572
+ variant: assignment.variant,
573
+ configurationHash: assignment.configurationHash,
574
+ });
575
+ // null means delivery failed under failSilently:true; retry these same fields.
576
+ }
577
+ ```
578
+
579
+ Only report when your application used that configuration. Do not gate holdout reporting
580
+ on `assignment.active`: a holdout has `active:false` and `reason:'holdout'`. Historical
581
+ assignments may lack `configurationHash` and cannot produce a verified matching receipt.
582
+ The digest identifies the frozen returned configuration, not a model provider's behavior.
583
+
584
+ Receipts are idempotent per assignment/session; `recordedAt` is server arrival time.
585
+ Conflicting configuration reports return 409. Deleted source sessions return 410, with
586
+ history retained as tombstones. Delayed reports for previously issued assignments may
587
+ arrive after the experiment stops. The method flushes queued telemetry before sending;
588
+ it does not run inference. `failSilently:false` propagates delivery errors.
589
+
590
+ For overlapping sessions on one client, pass `sessionId` to `getVariant()` or pass
591
+ `experimentAssignmentIds` explicitly when creating a session. Automatic linking targets
592
+ the next session and should only be used when that matches your application's ordering.
593
+ Each experiment/session may belong to only one assignment identity.
594
+
595
+ ### Assignment identity and telemetry acknowledgment
596
+
597
+ An experiment's declared assignment unit is required: supply `userId`/`user_id` for
598
+ user assignment, `convoId`/`convo_id` for conversation assignment, or
599
+ `anonymousId`/`anonymous_id` for anonymous assignment. Another identifier or a session
600
+ ID will not substitute for it. Missing or whitespace-only identity returns
601
+ `active:false`, `reason:'missing_identity'`, the required `assignmentUnit`, and your
602
+ default variant. No assignment is created. Preserve a stable identifier across requests.
603
+ Pass that same user or conversation identifier when creating the session. A conflicting
604
+ session identifier rejects the link; once linked, it cannot be changed or cleared.
605
+ Assignments from different experiments must also agree on each shared assignment unit.
606
+ An anonymous assignment has no corresponding session identity field, so its consistency
607
+ is checked against the session's other anonymous assignments. This does not verify that
608
+ the caller's identifier represents a real person or remains stable outside this session.
609
+
610
+ New assignments report `identityProvenance:'declared'` and their immutable
611
+ `assignmentUnit`. Existing assignments report `identityProvenance:'legacy_unknown'`
612
+ when their historical fallback identity cannot be established. A lookup does not upgrade
613
+ that provenance. Start a new experiment for a prospectively defined identity/analysis plan.
614
+
615
+ Native telemetry commits a session's assignment links in the same transaction as its
616
+ session/events. A failed link fails the request, and a retry preserves the original trace
617
+ identity. Unknown or foreign assignments cannot be silently skipped. Explicit assignment
618
+ lists must contain at most 64 UUIDs; malformed or oversized lists receive HTTP 400 before
619
+ any trace in that batch writes. UUIDs are deduplicated without dropping distinct entries.
620
+ A valid list whose assignment was deleted or is unavailable fails instead of returning a
621
+ false success; correct the references before retrying a permanent error. The existing SDK
622
+ fail-silent/strict error policy still applies.
package/dist/cli.cjs CHANGED
@@ -45,8 +45,14 @@ var import_node_os = __toESM(require("os"), 1);
45
45
  var import_node_path = __toESM(require("path"), 1);
46
46
  var import_node_process = __toESM(require("process"), 1);
47
47
  var import_promises = __toESM(require("readline/promises"), 1);
48
+
49
+ // src/constants.ts
48
50
  var DEFAULT_API_URL = "https://api.sentrial.com";
49
- var CLI_VERSION = "0.4.5";
51
+
52
+ // package.json
53
+ var version = "0.5.0";
54
+
55
+ // src/cli.ts
50
56
  var CONFIG_DIR = import_node_path.default.join(import_node_os.default.homedir(), ".sentrial");
51
57
  var CONFIG_FILE = import_node_path.default.join(CONFIG_DIR, "config.json");
52
58
  var TIME_RANGES = /* @__PURE__ */ new Set(["24h", "3d", "7d", "30d"]);
@@ -75,9 +81,6 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
75
81
  "include-api-key",
76
82
  "json",
77
83
  "dry-run",
78
- "no-color",
79
- "no-events",
80
- "no-fail-on-regression",
81
84
  "version",
82
85
  "yes"
83
86
  ]);
@@ -95,8 +98,7 @@ Commands:
95
98
  events list <session-id> List events for a session
96
99
  signals list List detected signals
97
100
  diagnose <session-id> Print a terminal diagnosis from session events
98
- evaluate <experiment-id> Run the /api/v1/evaluate CI gate
99
- clusters Reserved for the upcoming clusters API
101
+ evaluate <experiment-id> Run the /api/v1/evaluate CI gate (API key needs the metrics:read scope)
100
102
  fix <session-id> Reserved for the upcoming Fixer API
101
103
  replay <session-id> Reserved for the upcoming replay API
102
104
 
@@ -332,7 +334,7 @@ async function commandInit(ctx, flags) {
332
334
  const requestedLanguage = getStringFlag(flags, "language");
333
335
  const language = requestedLanguage ?? detected.language ?? "typescript";
334
336
  const force = getBooleanFlag(flags, "force");
335
- const apiKey = getBooleanFlag(flags, "include-api-key") ? ctx.apiKey ?? "sentrial_live_xxx" : "sentrial_live_xxx";
337
+ const includeApiKey = getBooleanFlag(flags, "include-api-key");
336
338
  if (language !== "typescript" && language !== "python") {
337
339
  throw new CliError('Supported init languages are "typescript" and "python".');
338
340
  }
@@ -349,10 +351,18 @@ async function commandInit(ctx, flags) {
349
351
  ];
350
352
  files.push({
351
353
  path: import_node_path.default.join(projectRoot, ".env.sentrial.example"),
352
- content: `SENTRIAL_API_KEY=${apiKey}
354
+ content: `SENTRIAL_API_KEY=sentrial_live_xxx
353
355
  SENTRIAL_API_URL=${ctx.apiUrl}
354
356
  `
355
357
  });
358
+ if (includeApiKey && ctx.apiKey) {
359
+ files.push({
360
+ path: import_node_path.default.join(projectRoot, ".env.sentrial"),
361
+ content: `SENTRIAL_API_KEY=${ctx.apiKey}
362
+ SENTRIAL_API_URL=${ctx.apiUrl}
363
+ `
364
+ });
365
+ }
356
366
  const written = [];
357
367
  const skipped = [];
358
368
  for (const file of files) {
@@ -623,7 +633,6 @@ ${response.failures.map((f) => ` - ${String(f)}`).join("\n")}
623
633
  }
624
634
  function commandReserved(command) {
625
635
  const messages = {
626
- clusters: "The clusters CLI is reserved for the upcoming /api/v1/clusters endpoint.",
627
636
  fix: "The Fixer CLI is reserved for the upcoming scriptable Fixer API.",
628
637
  replay: "The replay CLI is reserved for the upcoming replay API."
629
638
  };
@@ -1065,7 +1074,7 @@ async function run(argv) {
1065
1074
  const ctx = createContext(parsed.flags);
1066
1075
  const [command, ...rest] = parsed.positionals;
1067
1076
  if (getBooleanFlag(parsed.flags, "version")) {
1068
- import_node_process.default.stdout.write(`${CLI_VERSION}
1077
+ import_node_process.default.stdout.write(`${version}
1069
1078
  `);
1070
1079
  return;
1071
1080
  }
@@ -1098,7 +1107,6 @@ async function run(argv) {
1098
1107
  case "evaluate":
1099
1108
  await commandEvaluate(ctx, parsed.flags, rest[0]);
1100
1109
  break;
1101
- case "clusters":
1102
1110
  case "fix":
1103
1111
  case "replay":
1104
1112
  commandReserved(command);