@swfte/nexus-sdk 0.1.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/ai.d.ts ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * `@swfte/nexus-sdk/ai` — the Vercel AI SDK bridge.
3
+ *
4
+ * The only capture path in Node that survives bundling. Module hooks intercept an *import*, and a
5
+ * bundler inlines the provider so there is no import left to intercept; `registerTelemetry` is a
6
+ * runtime registration, so nothing about the module graph can take it away. Measured, not argued —
7
+ * `fixtures/vercel/bundle-check.mjs`.
8
+ *
9
+ * Two properties of this bridge are worth knowing before you reach for it, because both are
10
+ * permanent rather than "not yet":
11
+ *
12
+ * 1. **It cannot deny.** The AI SDK's callback type is `(event) => PromiseLike<void> | void`.
13
+ * There is no return channel by which an integration could refuse a tool call, and a throw from
14
+ * `onToolExecutionStart` neither stops the tool nor reaches your code — it is swallowed.
15
+ * Enforcement in Node comes from the explicit API (`run.action(...)`, where you own the call
16
+ * site) or not at all.
17
+ * 2. **It emits metadata only, at every privacy tier.** Token counts, model ids, durations, tool
18
+ * names. Never prompt text, completion text, or tool inputs and outputs — not even at
19
+ * `tier: 'full'`. Gating those needs a redaction pass this SDK does not have.
20
+ */
21
+
22
+ /** Deliberately structural. This package does not depend on `ai`, so it cannot name `ai`'s types. */
23
+ export interface TelemetryIntegration {
24
+ onStart?: (event: unknown) => void;
25
+ onLanguageModelCallEnd?: (event: unknown) => void;
26
+ onToolExecutionEnd?: (event: unknown) => void;
27
+ onEnd?: (event: unknown) => void;
28
+ }
29
+
30
+ export interface TelemetryOptions {
31
+ /**
32
+ * Open a nexus run per top-level AI SDK operation when the application is not already inside
33
+ * one. Defaults to `false`.
34
+ *
35
+ * Off by default because double-counting is worse than under-counting: an application that
36
+ * already wraps its handler in `withAgent` would otherwise report two runs for one unit of work,
37
+ * and nothing downstream can tell those apart afterwards. Turn it on when the AI SDK call *is*
38
+ * the unit of work.
39
+ */
40
+ runs?: boolean;
41
+ /** Emit a `tool_action` per tool execution. Defaults to `true`. */
42
+ toolActions?: boolean;
43
+ }
44
+
45
+ /**
46
+ * Build the integration object to hand to the AI SDK's `registerTelemetry`.
47
+ *
48
+ * import { registerTelemetry } from 'ai';
49
+ * import { telemetry } from '@swfte/nexus-sdk/ai';
50
+ * registerTelemetry(telemetry());
51
+ *
52
+ * Every callback is individually guarded, so a bug in the bridge cannot surface inside your
53
+ * `generateText` call.
54
+ */
55
+ export function telemetry(opts?: TelemetryOptions): TelemetryIntegration;
56
+
57
+ /**
58
+ * Convenience wrapper: hand it the AI SDK's own `registerTelemetry` and it registers for you.
59
+ *
60
+ * import { registerTelemetry } from 'ai';
61
+ * register(registerTelemetry);
62
+ *
63
+ * @returns whether registration happened. A non-function argument is a counted no-op rather than a
64
+ * throw — a telemetry SDK that crashes an application's boot over its own optional integration
65
+ * has failed at its one job.
66
+ */
67
+ export function register(
68
+ registerTelemetry: (integration: TelemetryIntegration) => unknown,
69
+ opts?: TelemetryOptions,
70
+ ): boolean;
71
+
72
+ /** The value {@link import('./index.js').instrumentation} reports once this bridge is built. */
73
+ export const MODE: 'ai-sdk';
74
+
75
+ /**
76
+ * Where each number is read off the AI SDK's events, as measured against `ai@7.0.58`.
77
+ *
78
+ * Exported because it is the part most likely to move, and because two of these are traps: the
79
+ * cache split is `usage.inputTokenDetails.cacheReadTokens` (not `usage.cachedInputTokens`) and the
80
+ * duration is `performance.responseTimeMs` (not `performance.durationMs`). Reading the wrong one
81
+ * yields `undefined`, which `JSON.stringify` drops, which looks exactly like a working integration
82
+ * reporting zero tokens.
83
+ */
84
+ export const FIELD_MAP: Readonly<Record<string, string>>;
package/index.d.ts ADDED
@@ -0,0 +1,433 @@
1
+ /**
2
+ * nexus — agent governance as an application dependency.
3
+ *
4
+ * These types describe the **explicit API**: the surface a customer calls by hand. It is declared
5
+ * separately from the implementation on purpose. `SCOPE.md` §3 shows that auto-instrumentation is
6
+ * permanently unavailable in bundled Node deployments, so in Node the explicit API is not the
7
+ * fallback — for a large share of the market it is the whole product, and it has to be pleasant
8
+ * enough to write by hand and typed well enough to be discoverable from an editor.
9
+ *
10
+ * Two conventions run through the whole surface:
11
+ *
12
+ * - **Nothing here throws.** Every function is fail-open; when the SDK is switched off or has
13
+ * broken internally, calls return inert objects that accept the same chain. Host code never
14
+ * needs `if (nexus)`, and a `try/catch` around a nexus call is always dead code.
15
+ * - **Absent is not false.** Fields like `verified` are optional and stay absent when nothing was
16
+ * verified, rather than being recorded as `false`. The ledger has to be able to distinguish
17
+ * "not captured" from "captured negative".
18
+ */
19
+
20
+ /**
21
+ * How much text may leave the process.
22
+ *
23
+ * - `metadata_only` — no prompt or completion text, ever. Counts, models, durations, outcomes.
24
+ * - `hashed` — text is replaced by a stable fingerprint, so repetition is visible but content is not.
25
+ * - `full` — text on the wire. Requires a deliberate decision by whoever owns the data.
26
+ */
27
+ export type PrivacyTier = 'metadata_only' | 'hashed' | 'full';
28
+
29
+ /**
30
+ * How a run finished.
31
+ *
32
+ * The listed values are the vocabulary worth converging on — an outcome set that grows without
33
+ * limit becomes a free-text field, and a free-text field cannot be aggregated across services.
34
+ * They are suggestions with autocomplete, not a constraint: `contract/events.v1.json` declares no
35
+ * enum for `turn_outcome.outcome`, and the Python SDK accepts any string (truncated to 64 chars).
36
+ *
37
+ * The `(string & {})` arm is what keeps those two facts consistent. Without it this type would be
38
+ * *stricter than the product* — `run.outcome('success')`, the example in the Python SDK's own
39
+ * README, would be a compile error in Node and identical code would not port between the two.
40
+ */
41
+ export type Outcome =
42
+ | 'resolved' | 'success' | 'partial' | 'abandoned' | 'error' | 'blocked'
43
+ | (string & {});
44
+
45
+ /**
46
+ * What is capturing in this process, if anything.
47
+ *
48
+ * - `ai-sdk` — the Vercel AI SDK bridge (`@swfte/nexus-sdk/ai`). Survives bundling.
49
+ * - `none` — nothing is auto-capturing. In a bundled deployment this is the expected value, not a
50
+ * fault: the explicit API still works and is where the events come from.
51
+ *
52
+ * `esm-hooks` and `cjs-require` describe module-hook auto-instrumentation, which this release does
53
+ * not ship (see the README). They remain in the union because the value travels on every
54
+ * `pipeline_health` record, so a collector reading events from a future build — or from the spike
55
+ * builds that predate this release — must be able to name what it is seeing.
56
+ */
57
+ export type Instrumentation = 'ai-sdk' | 'esm-hooks' | 'cjs-require' | 'none';
58
+
59
+ export interface InitOptions {
60
+ /** Logical service name. The primary grouping key in every report. */
61
+ service?: string;
62
+ /** Deployment environment, e.g. `prod`, `staging`. */
63
+ env?: string;
64
+ /** Build or release identifier of the service. */
65
+ version?: string;
66
+ /** Defaults to `metadata_only`. Widening it is a decision, not a default. */
67
+ tier?: PrivacyTier;
68
+ /** Where events go. A file path; may be set alongside `collectorUrl`. */
69
+ sink?: string;
70
+ /** `POST <url>` with an NDJSON body. */
71
+ collectorUrl?: string;
72
+ /** Bearer credential for `collectorUrl`. */
73
+ apiKey?: string;
74
+ /** Static labels attached to every event from this process. */
75
+ tags?: Record<string, string>;
76
+ /** Explicit off switch. `NEXUS_ENABLED=0` beats this in both directions. */
77
+ enabled?: boolean;
78
+
79
+ // ── provenance (ANCHOR-INTEGRATION §6.1) ─────────────────────────────────────────────────
80
+ //
81
+ // The join keys that connect this running process to the commit-anchored devtools ledger. All
82
+ // are auto-detected from the platform when unset, and each detected value records the variable
83
+ // it came from. Note the types: `string | undefined`, with no `'unknown'` fallback of the kind
84
+ // `service` and `env` have. That difference is the whole feature — a fabricated commit silences
85
+ // the shadow-deploy alarm permanently, so an absent value stays absent and the console draws
86
+ // the break.
87
+
88
+ /** `app_id` — the application this service belongs to. Declared, never inferred. */
89
+ application?: string;
90
+ /** `github.com/acme/portal`, or `acme/portal` where the platform publishes no host. */
91
+ repo?: string;
92
+ /** The commit this artifact was built from. */
93
+ commit?: string;
94
+ /** The branch, when the platform published one. */
95
+ branch?: string;
96
+ /** The platform's own deployment identifier, e.g. `VERCEL_DEPLOYMENT_ID`. */
97
+ deploymentId?: string;
98
+
99
+ // ── queue and transport ──────────────────────────────────────────────────────────────────
100
+
101
+ /** Bounded queue depth. At the limit the OLDEST events are dropped, and drops are counted. */
102
+ queueCapacity?: number;
103
+ batchSize?: number;
104
+ flushIntervalMs?: number;
105
+ flushDeadlineMs?: number;
106
+ httpTimeoutMs?: number;
107
+
108
+ /**
109
+ * The host's `waitUntil` (Vercel), so a flush can outlive the response without delaying it.
110
+ * Also settable later with {@link setWaitUntil}.
111
+ */
112
+ waitUntil?: (promise: Promise<unknown>) => void;
113
+ }
114
+
115
+ /** Self-report of a deployment. Everything omitted is taken from the resolved config. */
116
+ export interface DeploymentOptions {
117
+ version?: string;
118
+ commit?: string;
119
+ env?: string;
120
+ repo?: string;
121
+ appId?: string;
122
+ /** The platform's id. A deterministic `self:…` key is derived when there is none. */
123
+ deploymentId?: string;
124
+ /** Who or what triggered it. Fingerprinted below `full` tier. */
125
+ actor?: string;
126
+ startedTs?: string;
127
+ finishedTs?: string;
128
+ outcome?: 'succeeded' | 'failed' | 'cancelled' | 'in_progress';
129
+ /** The `deployment_id` this reverted to. Present only on rollbacks, and load-bearing. */
130
+ rollbackOf?: string;
131
+ }
132
+
133
+ export interface IntegrationOptions {
134
+ /** What it is, in the reader's words — `crm`, `payments`, `warehouse`. */
135
+ kind?: string;
136
+ appId?: string;
137
+ /** Defaults to the configured service. */
138
+ service?: string;
139
+ /** Whether credentials were accepted, when the caller can tell that apart from reachability. */
140
+ authOk?: boolean;
141
+ }
142
+
143
+ /**
144
+ * Freshness, recorded separately from liveness — and the separation is the point. Whether the
145
+ * call completed and how old the data is answer different questions, and merging them destroys
146
+ * the only fact this feature exists to surface: *the API is healthy and the data is 72 h stale.*
147
+ */
148
+ export interface IntegrationData {
149
+ /**
150
+ * How many rows came back. `0` means *we counted and there were none*; omitting it means *we
151
+ * did not count*. Those are different facts and nothing here will invent one from the other.
152
+ */
153
+ rows?: number;
154
+ /**
155
+ * The newest **business** timestamp observed — the last row's `updated_at`, not our own clock,
156
+ * which would make a probe that fetched nothing look fresh.
157
+ */
158
+ watermark?: string | number | Date;
159
+ /** Wire-name alias for {@link IntegrationData.watermark}. */
160
+ lastDataTs?: string | number | Date;
161
+ /** Changes when the response shape changes. A silent schema break, made visible. */
162
+ schemaFingerprint?: string;
163
+ }
164
+
165
+ /**
166
+ * One bracketed observation of an outbound dependency.
167
+ *
168
+ * A handle that is never ended emits nothing — we did not observe an outcome, so there is no
169
+ * observation to record.
170
+ */
171
+ export interface Integration {
172
+ readonly name: string;
173
+ /** Record freshness. Chainable. */
174
+ data(fields: IntegrationData): Integration;
175
+ /**
176
+ * Record that the call did not complete. `errorClass` is content-free (`auth`, `timeout`,
177
+ * `http_5xx`, `schema`, `transport`) and survives every privacy tier, which is what lets an
178
+ * alarm branch on it — the free-text `error` does not exist below `full`.
179
+ */
180
+ failed(error: unknown, opts?: { errorClass?: string }): Integration;
181
+ /** The Python SDK's spelling of {@link Integration.failed}. One meaning, two spellings. */
182
+ fail(error: unknown, opts?: { errorClass?: string }): Integration;
183
+ /**
184
+ * Whether the credential was accepted. Only the caller knows; the SDK never guesses.
185
+ *
186
+ * Also available as `authOk` on {@link IntegrationOptions}, but that requires knowing the answer
187
+ * before the call is made — and whether a credential was accepted is precisely what you learn
188
+ * afterwards. This wins when both are given.
189
+ */
190
+ auth(ok: boolean): Integration;
191
+ /**
192
+ * Fingerprint the response's **shape**, so a silent schema break is visible before the rows stop
193
+ * matching. Keys only for an object; never values, at any privacy tier.
194
+ *
195
+ * Byte-identical to the Python SDK's digest for an object sample, because the fingerprint is a
196
+ * wire value two producers write into one column. For a non-object it cannot be — see
197
+ * PARITY.md §3b.
198
+ */
199
+ schema(sample: unknown): Integration;
200
+ /** Close the probe and emit exactly one observation. Idempotent. */
201
+ end(error?: unknown): Integration;
202
+ }
203
+
204
+ export interface ExpectsDataOptions {
205
+ /** `24h`, `30m`, `7d`, or an ISO-8601 duration. Unparseable spellings ship verbatim, unparsed. */
206
+ within: string;
207
+ appId?: string;
208
+ declaredBy?: string;
209
+ declaredTs?: string;
210
+ }
211
+
212
+ export interface AgentOptions {
213
+ /**
214
+ * What kind of work this is (`classify`, `summarise`, `refund`…). Coarse and stable by design:
215
+ * it is what makes runs comparable across services and over time.
216
+ */
217
+ goalClass?: string;
218
+ }
219
+
220
+ export interface OutcomeOptions {
221
+ /**
222
+ * Whether the outcome was checked by something other than the model's own claim. Omit it when
223
+ * nothing checked — do not pass `false` to mean "unknown".
224
+ */
225
+ verified?: boolean;
226
+ /** What did the checking: `test`, `human`, `schema`, `assertion`. Required for `verified` to count. */
227
+ verifiedBy?: string;
228
+ }
229
+
230
+ /**
231
+ * One **logical** model call — not one HTTP request. Vendor clients retry internally; `attempts`
232
+ * records that, and the call is still billed once.
233
+ */
234
+ export interface UsageRecord {
235
+ model: string;
236
+ provider?: string;
237
+ inputTokens?: number;
238
+ outputTokens?: number;
239
+ cacheReadTokens?: number;
240
+ cacheWriteTokens?: number;
241
+ costUsd?: number;
242
+ /** How the cost was derived, e.g. `catalog` or `vendor`. Cost without provenance is a rumour. */
243
+ costSource?: string;
244
+ /** HTTP attempts behind this one logical call. */
245
+ attempts?: number;
246
+ /** `true` when a stream was abandoned before completion, so the numbers are a lower bound. */
247
+ incomplete?: boolean;
248
+ /** Set by auto-instrumentation; leave unset when calling by hand. */
249
+ instrumentation?: Instrumentation;
250
+ }
251
+
252
+ /**
253
+ * One effect on the world, bracketed. This is the object the enforcement seam (WP-6) attaches to:
254
+ * `block()` exists so that a denied action is a first-class record rather than an absence.
255
+ */
256
+ export interface Action {
257
+ readonly name: string;
258
+ readonly target?: string;
259
+ /** Describe what the action actually did — `{ rows: 3 }`, `{ bytes: 812 }`. Chainable. */
260
+ effect(fields: Record<string, unknown>): Action;
261
+ /** Mark the action as refused, with a reason. Chainable. */
262
+ block(reason: string): Action;
263
+ /** Close the action, optionally with the error that ended it. Idempotent. */
264
+ end(error?: unknown): Action;
265
+ }
266
+
267
+ export interface Run {
268
+ readonly name: string;
269
+ readonly runId: string;
270
+ readonly sessionId: string;
271
+ readonly goalClass: string | null;
272
+ /** Open an action within this run. */
273
+ action(name: string, target?: string): Action;
274
+ /** Record one logical model call. */
275
+ usage(record: UsageRecord): Run;
276
+ /** Record what the run achieved. */
277
+ outcome(outcome: Outcome, opts?: OutcomeOptions): Run;
278
+ /** Close the run. Idempotent; safe from a `finally`. */
279
+ end(error?: unknown): Run;
280
+ }
281
+
282
+ /** The SDK's account of itself: events emitted and dropped, sink failures, contained errors. */
283
+ export interface Counters {
284
+ emitted?: number;
285
+ dropped?: number;
286
+ [key: string]: number | undefined;
287
+ }
288
+
289
+ /**
290
+ * Declare service identity and arm the SDK. Idempotent — safe to call from both `main()` and a
291
+ * framework startup hook. Optional: the first `agent()` call arms the SDK with defaults.
292
+ */
293
+ export function init(opts?: InitOptions): void;
294
+
295
+ /**
296
+ * Open a run: one unit of agent work, with an outcome.
297
+ *
298
+ * The returned object must be closed with `end()`. Prefer {@link withAgent} in async code — see
299
+ * the note there.
300
+ */
301
+ export function agent(name: string, opts?: AgentOptions): Run;
302
+
303
+ /**
304
+ * Scoped run. **Prefer this in async code.** Only this form propagates the current run across
305
+ * `await` boundaries (via `AsyncLocalStorage`); the object returned by {@link agent} cannot,
306
+ * because concurrent runs in one process would interleave on a shared stack.
307
+ *
308
+ * The run is closed automatically, including on throw, and the application's exception is
309
+ * re-thrown unchanged.
310
+ */
311
+ export function withAgent<T>(name: string, fn: (run: Run) => Promise<T> | T): Promise<T>;
312
+ export function withAgent<T>(
313
+ name: string,
314
+ opts: AgentOptions,
315
+ fn: (run: Run) => Promise<T> | T,
316
+ ): Promise<T>;
317
+
318
+ /**
319
+ * Open an action against the run currently in context — for code several frames below the call
320
+ * that opened the run, where threading a `Run` argument through would be intrusive.
321
+ */
322
+ export function action(name: string, target?: string): Action;
323
+
324
+ /** The run currently in context, or `null`. */
325
+ export function currentRun(): Run | null;
326
+
327
+ /**
328
+ * Drain the queue within a deadline. Call before a process that is about to stop.
329
+ *
330
+ * Asynchronous, and deliberately: the calling thread never performs I/O, so there is no
331
+ * synchronous form to offer. The deadline is bounded on purpose — the collector is exactly the
332
+ * thing most likely to be unhealthy while a fleet is restarting, and a telemetry SDK that will
333
+ * not let a pod terminate has become the outage.
334
+ */
335
+ export function flush(deadlineMs?: number): Promise<boolean>;
336
+
337
+ /** Final flush, then stop the background drain. */
338
+ export function shutdown(deadlineMs?: number): Promise<boolean>;
339
+
340
+ // ── operate plane (ANCHOR-INTEGRATION §6.1–6.4) ────────────────────────────────────────────
341
+
342
+ /**
343
+ * §6.4 — self-report a deployment, for an estate with no CI or cloud connector wired.
344
+ *
345
+ * nexus.deployment({ version: '2026.8.1', commit: SHA, env: 'prod' });
346
+ *
347
+ * Always stamped `detected_by: 'self'`, which is not a parameter: a process asserting its own
348
+ * deployment is the weakest evidence on the plane — precisely what a shadow deploy would also
349
+ * produce — and a caller able to claim `'ci'` could erase that distinction with one argument.
350
+ *
351
+ * @returns whether a record was emitted. `false` when neither a version nor a commit is known,
352
+ * because a deployment record that identifies no software makes the ledger longer, not truer.
353
+ */
354
+ export function deployment(opts?: DeploymentOptions): boolean;
355
+
356
+ /**
357
+ * §6.2 — the silent-failure primitive.
358
+ *
359
+ * const io = nexus.integration('salesforce', { kind: 'crm' });
360
+ * try {
361
+ * const rows = await client.fetchAccounts();
362
+ * io.data({ rows: rows.length, watermark: rows[rows.length - 1].updatedAt });
363
+ * } catch (err) { io.failed(err, { errorClass: 'timeout' }); }
364
+ * finally { io.end(); }
365
+ */
366
+ export function integration(name: string, opts?: IntegrationOptions): Integration;
367
+
368
+ /** The bracketed form of {@link integration}. Re-throws the application's exception unchanged. */
369
+ export function withIntegration<T>(
370
+ name: string, fn: (io: Integration) => Promise<T> | T,
371
+ ): Promise<T>;
372
+ export function withIntegration<T>(
373
+ name: string, opts: IntegrationOptions, fn: (io: Integration) => Promise<T> | T,
374
+ ): Promise<T>;
375
+
376
+ /**
377
+ * §6.2 — declare that data is expected within a window.
378
+ *
379
+ * An expectation with no matching probe inside its window is the silent-failure alarm. The alarm
380
+ * fires on **absence of evidence against a declared expectation**, never on absence alone:
381
+ * absence alone is indistinguishable from "nobody ever declared this".
382
+ *
383
+ * Deduplicated per process, so a declaration inside a scheduled function does not re-emit on
384
+ * every tick.
385
+ *
386
+ * @returns whether a declaration was emitted (`false` on a repeat, or with no window).
387
+ */
388
+ export function expectsData(name: string, opts: ExpectsDataOptions): boolean;
389
+
390
+ /**
391
+ * Close the `service_health` window now and emit it, even when nothing was measured.
392
+ *
393
+ * The honest form of "this process is alive": a window with a timestamp and **no quantities**. A
394
+ * `requests: 0` would be a fabricated zero — zero observed spans far more often means "this service
395
+ * never calls `run.action`" than it means an outage, and on a console the two look identical.
396
+ *
397
+ * Call it from a cron, a liveness probe, or a worker that drains a queue slowly. Windows close on
398
+ * their own every `NEXUS_HEALTH_INTERVAL_MS` when there is traffic; this is for when there is not.
399
+ *
400
+ * @returns whether an event was emitted.
401
+ */
402
+ export function heartbeat(): boolean;
403
+
404
+ /** Give the SDK the host's `waitUntil` (Vercel), so a flush can outlive the response. */
405
+ export function setWaitUntil(fn: (promise: Promise<unknown>) => void): void;
406
+
407
+ /**
408
+ * Wrap a serverless handler so the queue drains before the sandbox freezes.
409
+ *
410
+ * export const handler = nexus.instrumentHandler(async (event) => { … });
411
+ *
412
+ * The Node equivalent of Python's `instrument_lambda_handler`. **Without this, or without
413
+ * `waitUntil`, the last buffered events of every serverless invocation are lost — not delayed,
414
+ * lost.** A platform freezes the sandbox the instant the handler returns, and no library can read
415
+ * a signal that says it is about to happen.
416
+ */
417
+ export function instrumentHandler<F extends (...args: never[]) => unknown>(
418
+ handler: F, opts?: { budgetMs?: number },
419
+ ): (...args: Parameters<F>) => Promise<Awaited<ReturnType<F>>>;
420
+
421
+ /** Counters, for asserting in tests and for answering "is it actually capturing?" in production. */
422
+ export function counters(): Counters;
423
+
424
+ /** `true` unless `NEXUS_ENABLED` (or `init({ enabled: false })`) says otherwise. */
425
+ export function enabled(): boolean;
426
+
427
+ /**
428
+ * Which loader armed auto-instrumentation. `none` in a bundled deployment is expected, not a
429
+ * fault — see `SCOPE.md` §3.
430
+ */
431
+ export function instrumentation(): Instrumentation;
432
+
433
+ export const version: string;
package/otel.d.ts ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * `@swfte/nexus-sdk/otel` — the OpenTelemetry span bridge.
3
+ *
4
+ * Reads spans from whichever GenAI instrumentation you already run — OTel semantic conventions,
5
+ * OpenInference, OpenLLMetry/OpenLIT — and turns them into nexus events. **This package has no
6
+ * dependency on any `@opentelemetry/*` package**: `spanProcessor()` returns a plain object with the
7
+ * four methods the `SpanProcessor` interface requires, and your provider calls it.
8
+ *
9
+ * import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
10
+ * import * as nexus from '@swfte/nexus-sdk';
11
+ * import { spanProcessor } from '@swfte/nexus-sdk/otel';
12
+ *
13
+ * nexus.init({ service: 'checkout', env: 'prod' });
14
+ * new NodeTracerProvider({ spanProcessors: [spanProcessor()] }).register();
15
+ *
16
+ * Three properties worth knowing before you rely on it:
17
+ *
18
+ * 1. **An unclassified span is dropped, not guessed at.** A GenAI span whose kind this bridge does
19
+ * not model is counted under `bridge_unclassified` and emitted nowhere — never defaulted into
20
+ * `behavior_trace`, which is the class the ledger treats as evidence.
21
+ * 2. **Model text is re-gated by your privacy tier**, because upstream redaction is not trusted:
22
+ * every instrumentation in this space has its own content switch and its own idea of what a
23
+ * secret looks like, and some have none. Even `full` is redacted.
24
+ * 3. **One span, one observation.** The multi-span logical-call join that deduplicates retries is
25
+ * not ported. Against an instrumentation that emits a span per HTTP attempt, token counts sum
26
+ * across attempts rather than being deduplicated. See PARITY.md §6.
27
+ */
28
+
29
+ /**
30
+ * The `SpanProcessor` interface, structurally. Deliberately not typed against
31
+ * `@opentelemetry/sdk-trace-base` — naming that type would require depending on it.
32
+ */
33
+ export interface NexusSpanProcessor {
34
+ onStart(span: unknown, parentContext?: unknown): void;
35
+ onEnd(span: unknown): void;
36
+ forceFlush(): Promise<void>;
37
+ shutdown(): Promise<void>;
38
+ }
39
+
40
+ export interface BridgeOptions {
41
+ /**
42
+ * Throw on a GenAI span whose kind has no epistemic class, instead of dropping and counting it.
43
+ *
44
+ * Defaults to on under `node --test` and `NODE_ENV=test`, off otherwise — the two environments
45
+ * want opposite failure modes. A new span kind should fail a build; in production it should
46
+ * degrade to a counted gap rather than an exception on a request path.
47
+ */
48
+ strict?: boolean;
49
+ /** Build a new bridge rather than reusing the process-wide one. Mostly for tests. */
50
+ fresh?: boolean;
51
+ }
52
+
53
+ /** What the bridge has seen since it was created or reset. */
54
+ export interface BridgeStats {
55
+ /** Spans handed to the bridge, of any kind. */
56
+ seen: number;
57
+ /** Spans that produced at least one event. */
58
+ emitted: number;
59
+ /** Spans that were not GenAI at all — an HTTP call, a DB query. Not a coverage gap. */
60
+ ignored: number;
61
+ /** GenAI spans whose kind is not modelled. **This is the number worth alerting on.** */
62
+ unclassified: number;
63
+ }
64
+
65
+ /**
66
+ * A span processor to register with your `TracerProvider`.
67
+ *
68
+ * Only `onEnd` does work: a span's attributes are not complete until it closes, and reading them at
69
+ * `onStart` would report a call that has not happened yet.
70
+ */
71
+ export function spanProcessor(opts?: BridgeOptions): NexusSpanProcessor;
72
+
73
+ /** Read one finished span and emit its events directly, without a provider. */
74
+ export function ingest(span: unknown): boolean;
75
+
76
+ /** Bridge counters. `unclassified` rising is a coverage gap, not a fault in your application. */
77
+ export function stats(): BridgeStats;
78
+
79
+ /** The semconv release the reader targets. The spec is pre-stable and will move. */
80
+ export const SEMCONV_TAG: Readonly<{ genai: string; stability: string }>;