@nekuda/webmcp-sdk 0.4.0-dev.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,80 @@
1
+ import type { AnyWebMCPTool } from "./define.js";
2
+ import { type ModelContextLike } from "./spec.js";
3
+ import { type TrackingOptions } from "./tracking.js";
4
+ /**
5
+ * Per-tool outcome of a `registerTools` call:
6
+ * - `registered` — live on the page's WebMCP surface
7
+ * - `unsupported` — this browser has no WebMCP surface; nothing was registered
8
+ * - `aborted` — the registration's lifetime ended before/while registering
9
+ * - `failed` — the native surface rejected (e.g. duplicate name)
10
+ */
11
+ export type ToolRegistrationState = "registered" | "unsupported" | "aborted" | "failed";
12
+ export interface ToolRegistrationResult {
13
+ stableKey: string;
14
+ name: string;
15
+ state: ToolRegistrationState;
16
+ error?: unknown;
17
+ }
18
+ export interface RegisterToolsOptions {
19
+ /**
20
+ * External lifetime for the registration (e.g. a component's unmount signal).
21
+ * Aborting it unregisters every tool in this batch, same as `unregister()`.
22
+ */
23
+ signal?: AbortSignal;
24
+ /** Injectable surface for tests; defaults to the page's `document.modelContext`. */
25
+ modelContext?: ModelContextLike;
26
+ /**
27
+ * Configures anonymous tool-call tracking for this batch. Default-silent: with
28
+ * neither `apiKey` nor `otel` set — including an empty `tracking: {}` — nothing
29
+ * is built at all, so omitting it means zero tracking and no behavior change.
30
+ * Once an output is enabled, each tool invocation emits a `tool_call_request`
31
+ * event before its handler runs and a `tool_call_response` event after.
32
+ * `disabled: true` is this channel's consent gate; it does not narrow the
33
+ * default-on telemetry channel below, which resolves no identity and touches no
34
+ * storage. An `apiKey` set here is also sent as `x-api-key` on that channel's
35
+ * beacons, which only attributes them to this tenant.
36
+ */
37
+ tracking?: TrackingOptions;
38
+ /**
39
+ * Opt out of the default-on usage telemetry (`src/telemetry.ts`): `telemetry: false`
40
+ * means no `tool_registration` and no `tool_call` for this batch, and it cancels the
41
+ * page's one `sdk_init` outright — that event describes the page load, so it is
42
+ * cancelled page-wide and irreversibly. Unlike `tracking` this channel needs no
43
+ * `apiKey`.
44
+ *
45
+ * The other two events are per-batch, so the option is read per call: a *later*
46
+ * `registerTools` that omits it reports its own batch and calls. Cancelling
47
+ * `sdk_init` also requires winning the race with its deferred flush — a batch
48
+ * registered at module scope does, one registered from a mount effect or a consent
49
+ * callback may not. A site that wants the whole page silent regardless of where its
50
+ * `registerTools` calls come from (generated code, a CDN snippet) should use a
51
+ * page-level lever instead: `globalThis.__WEBMCP_TELEMETRY__ = false` — strictly
52
+ * `false`, so a truthy `"0"` does not opt out — or Global Privacy Control
53
+ * (`navigator.globalPrivacyControl === true`), both of which are read at emit time
54
+ * and so cover every event. With no `document` in scope the channel is silent
55
+ * anyway, so SSR never beacons.
56
+ */
57
+ telemetry?: boolean;
58
+ }
59
+ export interface ToolRegistration {
60
+ /**
61
+ * Settles when every tool's native registration settles. Never rejects — per-tool
62
+ * outcomes (including failures) are reported in the results, so one bad tool
63
+ * cannot mask the rest.
64
+ */
65
+ ready: Promise<ToolRegistrationResult[]>;
66
+ /** Unregister every tool in this batch. Idempotent. */
67
+ unregister(): void;
68
+ /** The signal carrying this registration's lifetime (aborted once unregistered). */
69
+ signal: AbortSignal;
70
+ }
71
+ /**
72
+ * Register a batch of `defineTool` tools on the page's WebMCP surface.
73
+ *
74
+ * Lifecycle: registration starts immediately; the tools stay live until
75
+ * `unregister()` is called or the external `options.signal` aborts (both funnel
76
+ * into one internal `AbortController`, the spec's only unregistration mechanism).
77
+ * On browsers without a WebMCP surface this is a graceful no-op — generated code
78
+ * can run unconditionally on every page.
79
+ */
80
+ export declare function registerTools(tools: readonly AnyWebMCPTool[], options?: RegisterToolsOptions): ToolRegistration;
package/dist/spec.d.ts ADDED
@@ -0,0 +1,56 @@
1
+ /**
2
+ * The pinned WebMCP spec surface — the ONLY module that knows the raw browser API.
3
+ *
4
+ * Pinned against the WebMCP editor's draft as of 2026-07
5
+ * (https://webmachinelearning.github.io/webmcp/):
6
+ *
7
+ * - `document.modelContext` is the surface (Chrome 150+). `navigator.modelContext`
8
+ * is deprecated but is the only surface on Chrome 149, so resolution reads
9
+ * `document` first and falls back — `??` picks exactly one object, so a browser
10
+ * exposing both never double-registers.
11
+ * - `registerTool(tool, { signal })` returns a promise that settles when the
12
+ * registration completes; it rejects on a duplicate name, an invalid tool, an
13
+ * inactive document, or an abort.
14
+ * - Unregistration happens ONLY by aborting the `AbortSignal` passed at
15
+ * registration. `unregisterTool()` and `provideContext()` are dead APIs and are
16
+ * never referenced.
17
+ * - Tool names: 1–128 chars of [A-Za-z0-9_\-.].
18
+ * - `annotations.readOnlyHint` / `annotations.untrustedContentHint`.
19
+ *
20
+ * The draft breaks monthly; absorbing that churn is this module's job (R23). All
21
+ * other modules — and all plugin-generated code — target `ModelContextLike`, never
22
+ * the raw API.
23
+ */
24
+ export interface ToolAnnotations {
25
+ /** The tool does not modify page or user state. */
26
+ readOnlyHint?: boolean;
27
+ /** The tool's output can contain untrusted (user or third-party) data. */
28
+ untrustedContentHint?: boolean;
29
+ }
30
+ /** The tool shape `registerTool` accepts (`ModelContextTool` in the draft IDL). */
31
+ export interface SpecTool {
32
+ name: string;
33
+ title?: string;
34
+ description: string;
35
+ inputSchema?: Record<string, unknown>;
36
+ annotations?: ToolAnnotations;
37
+ execute(input: Record<string, unknown>): Promise<unknown>;
38
+ }
39
+ export interface RegisterToolOptions {
40
+ /** Aborting unregisters the tool — the spec's only unregistration mechanism. */
41
+ signal?: AbortSignal;
42
+ }
43
+ /**
44
+ * The slice of the native surface the SDK uses, typed to the draft contract:
45
+ * `registerTool` returns a promise. Pre-draft surfaces that return nothing are a
46
+ * runtime concern only — the SDK normalizes via `Promise.resolve` at the call
47
+ * site, never by widening this type.
48
+ */
49
+ export interface ModelContextLike {
50
+ registerTool(tool: SpecTool, options?: RegisterToolOptions): Promise<void>;
51
+ }
52
+ /**
53
+ * Resolve the page's WebMCP surface; `undefined` on non-supporting browsers.
54
+ * Injectable global scope for tests.
55
+ */
56
+ export declare function resolveModelContext(g?: object): ModelContextLike | undefined;
@@ -0,0 +1,164 @@
1
+ /**
2
+ * Derived page and client context for the telemetry events (`src/telemetry.ts`):
3
+ * a route *template* instead of a URL, a referrer *class* instead of a referrer,
4
+ * a form factor instead of a screen size, a language subtag instead of a locale.
5
+ * Every function here exists so the event can carry the signal without carrying
6
+ * the identifying string it was derived from — no `url`, `referrer`, `title`,
7
+ * `screen`, or full UA string ever leaves the page. It also holds what the events
8
+ * report about the page's WebMCP surface, the navigation clock, and the in-memory
9
+ * session ID that joins them.
10
+ *
11
+ * Two properties hold throughout. First, nothing throws: every browser global is
12
+ * optional in a non-DOM runtime and page-script- and extension-writable in a real
13
+ * one, so each read goes through a guard and an unreadable value simply means the
14
+ * field is **omitted** — never sent as `null`. Second, every input is bounded:
15
+ * a pathname, referrer, or UA string is caller-controlled, and the truncation
16
+ * ladder in `src/tracking.ts` does not reach a derived field, so the cap has to
17
+ * happen here.
18
+ */
19
+ /**
20
+ * Prefix of a page-controlled string any derived field will even look at: a
21
+ * pathname, a referrer, an error message. All come from values page script can set
22
+ * to a megabyte-long one, and a regex scan over that is main-thread cost on the
23
+ * tool-call path. Real URLs and messages live well under 2 KB.
24
+ *
25
+ * What slicing first costs: templating only ever shortens and never moves a
26
+ * character earlier, so the *first* bytes of a derived value are unaffected — this
27
+ * prefix is an order of magnitude above every output cap ({@link MAX_ROUTE_BYTES},
28
+ * `MAX_ERROR_SIGNATURE_BYTES`). What it does drop is the tail of an input whose
29
+ * middle would have collapsed to nothing: a message that is two megabytes of digits
30
+ * between `order ` and ` not found` clusters as `order *`, without the trailing
31
+ * words. That is the accepted trade — the alternative is scanning a page-controlled
32
+ * megabyte on the tool-call path.
33
+ */
34
+ export declare const MAX_INPUT_CHARS = 4096;
35
+ /**
36
+ * Whether a run reads as an opaque blob rather than as words. Shared with
37
+ * `errorSignature` in `src/telemetry.ts`, which needs the same distinction on a
38
+ * message's long runs; it lives here because this module is the leaf of the two.
39
+ *
40
+ * Counted by index rather than by code point so the share is measured against the
41
+ * same unit as `length` — a route segment can hold any Unicode, and only ASCII
42
+ * capitals are evidence of an encoding.
43
+ */
44
+ export declare function uppercaseHeavy(run: string): boolean;
45
+ /**
46
+ * A `location.pathname` reduced to a low-cardinality route template
47
+ * (`/products/12345` → `/products/:id`). Query and fragment are dropped whole —
48
+ * they carry search terms, session tokens, and ad parameters. A non-string
49
+ * pathname is omitted; any string yields a rooted template, so the root path is
50
+ * `"/"`.
51
+ */
52
+ export declare function routeTemplate(pathname?: unknown): string | undefined;
53
+ /**
54
+ * Non-empty segments in a pathname, emitted alongside {@link routeTemplate}. It
55
+ * is the check on the templating: a route reported as `/` with a segment count of
56
+ * 4, or a count above {@link MAX_ROUTE_SEGMENTS}, says the template lost
57
+ * something — which is invisible from the template alone.
58
+ */
59
+ export declare function segmentCount(pathname?: unknown): number | undefined;
60
+ /** How the visitor arrived, as a class rather than a referrer URL. */
61
+ export type ReferrerClass = "internal" | "ai_assistant" | "search" | "social" | "direct" | "other";
62
+ /**
63
+ * Classify `document.referrer` against the page's own origin. An empty referrer
64
+ * is `direct` — that is what a typed URL, a bookmark, and a stripped
65
+ * cross-origin referrer all look like. An unparseable one is `other`, since it
66
+ * was *something*; conflating it with `direct` would inflate the direct bucket.
67
+ */
68
+ export declare function referrerClass(referrer?: unknown, origin?: unknown): ReferrerClass;
69
+ /** Device class, at the only granularity that stays non-identifying. */
70
+ export type FormFactor = "mobile" | "desktop";
71
+ /**
72
+ * Mobile vs desktop, from the narrowest signal that answers it: UA-Client-Hints
73
+ * `mobile` is a boolean the browser computes and survives UA reduction, so it
74
+ * wins; a coarse pointer with no hover is the CSS-level answer for browsers
75
+ * without UA-CH; screen width is the last resort, and the only one a desktop
76
+ * window resize can fool. Omitted when none of the three is readable.
77
+ */
78
+ export declare function formFactor(scope?: object): FormFactor | undefined;
79
+ /**
80
+ * The primary language subtag only (`en`, not `en-US`). The region half is a
81
+ * coarse location signal and adds nothing to the "what language do these tools
82
+ * get called in" question. An unrecognized tag (`x-private`, `""`) is omitted.
83
+ */
84
+ export declare function languageSubtag(scope?: object): string | undefined;
85
+ /** What is driving the page, as far as the page can tell. */
86
+ export type AgentRuntime = "chatgpt" | "claude" | "perplexity" | "headless" | "browser" | "unknown";
87
+ /**
88
+ * Best-effort runtime identification. Deliberately weak, and known to be: real
89
+ * AI crawlers fetch server-side and never run this code, so the browser almost
90
+ * always sees `browser` or `headless`. The edge is authoritative — it reads the
91
+ * beacon's own `User-Agent` header, which no body field may carry. The one signal
92
+ * only the client has is `navigator.webdriver`, folded into `headless` here.
93
+ *
94
+ * `unknown` rather than omitted when there is no UA at all: "we looked and could
95
+ * not tell" is a different fact from "this field was not collected".
96
+ */
97
+ export declare function agentRuntime(scope?: object): AgentRuntime;
98
+ /** Whether the SDK is running in the top-level document or inside a frame. */
99
+ export type FrameContext = "top" | "iframe";
100
+ /**
101
+ * Top document or embedded frame. Reading `top` from a cross-origin frame can
102
+ * throw, and the throw *is* the answer: only an embedded document can be denied
103
+ * access to its own top. Omitted when there is no `self` to compare against,
104
+ * which means no browser at all.
105
+ */
106
+ export declare function frameContext(scope?: object): FrameContext | undefined;
107
+ /** `document.visibilityState`, restricted to the values the spec defines. */
108
+ export type PageVisibility = "visible" | "hidden" | "prerender";
109
+ /**
110
+ * Whether the page was in the foreground when the event was assembled — a
111
+ * prerendered or background page is where "the SDK loaded but nothing happened"
112
+ * comes from. An unrecognized state is omitted rather than coerced.
113
+ */
114
+ export declare function visibility(scope?: object): PageVisibility | undefined;
115
+ /** Where the page's WebMCP surface came from. */
116
+ export type SurfaceProvenance = "native" | "polyfill" | "extension" | "none";
117
+ /** Which global carries the surface — the spec's own, or its deprecated predecessor. */
118
+ export type SurfaceGlobal = "document.modelContext" | "navigator.modelContext";
119
+ /** What `sdk_init` reports about the WebMCP surface it found (or didn't). */
120
+ export interface SurfaceInfo {
121
+ available: boolean;
122
+ provenance: SurfaceProvenance;
123
+ /** Absent when no surface was found. */
124
+ global?: SurfaceGlobal;
125
+ /** Absent unless the surface declares one. */
126
+ specVersion?: string;
127
+ }
128
+ /**
129
+ * The WebMCP surface as `registerTools` sees it. Availability comes from
130
+ * {@link resolveModelContext} itself rather than a second lookup, so what this
131
+ * event reports can never disagree with what registration actually used — the
132
+ * `document`-first, `navigator`-fallback precedence lives in exactly one place
133
+ * (`src/spec.ts`), and a browser exposing both is reported as the one that wins.
134
+ *
135
+ * A surface that is present but not the native API is worth separating: a
136
+ * polyfill's failure modes are its own, and an extension-injected surface means
137
+ * tools registered on a browser that has no WebMCP of its own.
138
+ */
139
+ export declare function surfaceInfo(scope?: object): SurfaceInfo;
140
+ /**
141
+ * Milliseconds from navigation start to now, the basis of `timeToInitMs` and
142
+ * `timeSinceInitMs`. `performance.now()` is already relative to
143
+ * `performance.timeOrigin` and monotonic, so it is the primary reading;
144
+ * `Date.now() - timeOrigin` is the fallback for a surface that exposes the origin
145
+ * but not a usable clock. `undefined` when neither is usable — a shimmed `now()`
146
+ * returning `NaN` or a negative value must not become a nonsense duration on the
147
+ * event.
148
+ */
149
+ export declare function timeSinceNavigation(scope?: object): number | undefined;
150
+ /**
151
+ * The init mark itself, reported as `sdk_init.timeToInitMs`. Deliberately not
152
+ * scope-injectable: reading it fresh when the deferred flush assembles the event
153
+ * would measure the flush hop instead of module load, and the three events would
154
+ * no longer share one baseline.
155
+ */
156
+ export declare function initAt(): number | undefined;
157
+ /**
158
+ * Milliseconds from SDK init to now. Both readings are navigation-relative, so
159
+ * their difference is the page time elapsed between them. Clamped at `0`: a
160
+ * shimmed clock that runs backwards must not report a negative age.
161
+ */
162
+ export declare function timeSinceInit(scope?: object): number | undefined;
163
+ /** The in-memory session ID for this module instance; see {@link SESSION_ID}. */
164
+ export declare function sessionId(): string;
@@ -0,0 +1,224 @@
1
+ /**
2
+ * The three telemetry events as explicit types — the wire contract `src/telemetry.ts`
3
+ * assembles and `/v1/telemetry` ingests. They live in their own leaf module so the
4
+ * shape can be read and diffed without the builders around it, and so the builders,
5
+ * the allowlist (`src/telemetry-fields.ts`), and `docs/telemetry-schema.md` all
6
+ * answer to one declaration.
7
+ *
8
+ * Two rules hold across every interface here:
9
+ *
10
+ * - **A field that can be absent is `?`, never `| null`.** Every builder omits an
11
+ * underivable field rather than sending a null, so a `null` on the wire is a bug
12
+ * and not a value. The one exception is `tool_call.errorClass`: "we looked and
13
+ * there was no error" is a different fact from "this event does not carry that
14
+ * field", and only the success path can state the former.
15
+ * - **No index signatures.** The v1 event type was open (`[key: string]: unknown`),
16
+ * which let a field reach the wire with nothing declaring it. Exact interfaces are
17
+ * what let the pinning tests and the allowlist disagree with a builder.
18
+ *
19
+ * Type-only imports throughout: this module contributes one constant and no runtime
20
+ * behavior, so nothing here pulls another module into a consumer's bundle.
21
+ */
22
+ import type { ToolIntent, ToolSource } from "./define.js";
23
+ import type { ToolRegistrationState } from "./register.js";
24
+ import type { ToolAnnotations } from "./spec.js";
25
+ import type { AgentRuntime, FormFactor, FrameContext, PageVisibility, ReferrerClass, SurfaceInfo } from "./telemetry-context.js";
26
+ import type { ToolShapeMetrics } from "./tool-metrics.js";
27
+ /**
28
+ * Wire-format version carried by every event as `schema`. `2` is a clean break from
29
+ * v1 — different event names, different field set, no storage-backed identity — and
30
+ * nothing is deployed against v1, so no compatibility shim exists.
31
+ */
32
+ export declare const TELEMETRY_SCHEMA_VERSION = 2;
33
+ /** The discriminant: which of the three events this is. */
34
+ export type TelemetryEventName = "sdk_init" | "tool_registration" | "tool_call";
35
+ /**
36
+ * What every event carries. `sessionId` is in-memory only (`sessionId()` in
37
+ * `src/telemetry-context.ts`) and is the sole join key between the three: arrival
38
+ * order is not guaranteed, so nothing downstream may assume `sdk_init` lands first.
39
+ */
40
+ export interface TelemetryEnvelope {
41
+ schema: typeof TELEMETRY_SCHEMA_VERSION;
42
+ event: TelemetryEventName;
43
+ /** Event time as an ISO-8601 string. */
44
+ ts: string;
45
+ sessionId: string;
46
+ }
47
+ /** How the SDK reached the page — a build-time fact, not an observed one. */
48
+ export type InstallMode = "npm" | "cdn_snippet";
49
+ /** `sdk.*` — which build of which package emitted the event. */
50
+ export interface SdkInfo {
51
+ name: string;
52
+ version: string;
53
+ installMode: InstallMode;
54
+ }
55
+ /**
56
+ * `page.*` — the page as derived signals rather than strings. `referrerClass` is
57
+ * always present because an absent referrer is itself a class (`direct`); the rest
58
+ * are omitted when the global they come from is missing or unreadable.
59
+ */
60
+ export interface PageContext {
61
+ routeTemplate?: string;
62
+ segmentCount?: number;
63
+ referrerClass: ReferrerClass;
64
+ frameContext?: FrameContext;
65
+ visibility?: PageVisibility;
66
+ }
67
+ /**
68
+ * `client.*` — who is driving the page. `agentRuntime` is always present because
69
+ * "we looked and could not tell" is reported as `unknown` rather than by omission;
70
+ * see the accuracy caveat on `agentRuntime` in `src/telemetry-context.ts`.
71
+ */
72
+ export interface ClientContext {
73
+ agentRuntime: AgentRuntime;
74
+ browser?: string;
75
+ browserMajor?: number;
76
+ formFactor?: FormFactor;
77
+ language?: string;
78
+ }
79
+ /**
80
+ * `sdk_init` — once per page load, at import time via a deferred flush. It fires
81
+ * even when nobody ever calls `registerTools`: a site that loads the SDK and never
82
+ * registers is a broken integration, and this event is the only way to see it.
83
+ */
84
+ export interface SdkInitEvent extends TelemetryEnvelope {
85
+ event: "sdk_init";
86
+ sdk: SdkInfo;
87
+ surface: SurfaceInfo;
88
+ page: PageContext;
89
+ client: ClientContext;
90
+ /** Milliseconds from navigation start; absent when the clock is unusable. */
91
+ timeToInitMs?: number;
92
+ }
93
+ /** What caused this `registerTools` call, inferred from module state. */
94
+ export type RegistrationTrigger = "initial" | "spa_navigation" | "re_register";
95
+ /**
96
+ * How one tool's registration settled. The registration states plus `pending`,
97
+ * which only telemetry can report: the batch emits on a timeout rather than wait
98
+ * forever on a surface whose `registerTool` never settles. Derived from
99
+ * {@link ToolRegistrationState} so the two can never drift apart.
100
+ */
101
+ export type ToolRegistrationOutcome = ToolRegistrationState | "pending";
102
+ /**
103
+ * `config.*` — what the authenticated `tracking` channel is actually doing on this
104
+ * page, moved here from `sdk_init` because it is per-batch information and at
105
+ * `sdk_init` flush time no batch may have run.
106
+ */
107
+ export interface TrackingConfigInfo {
108
+ trackingEnabled: boolean;
109
+ otelEnabled: boolean;
110
+ customEndpoint: boolean;
111
+ }
112
+ /**
113
+ * One tool in a `tool_registration` batch. The shape metrics are inherited as
114
+ * optional because they and `schemaHash` are derived from `inputSchema` together:
115
+ * an entry carries both or neither, matching `shapeMetrics`/`schemaHash`.
116
+ */
117
+ export interface RegisteredToolEntry extends Partial<ToolShapeMetrics> {
118
+ name: string;
119
+ stableKey: string;
120
+ version?: string;
121
+ schemaHash?: string;
122
+ source?: ToolSource;
123
+ intent?: ToolIntent;
124
+ outcome: ToolRegistrationOutcome;
125
+ /** Templated failure message; absent unless `outcome` is `failed`. */
126
+ failureSignature?: string;
127
+ descriptionLength: number;
128
+ /** Absent when the tool declares none — an empty object would say the same thing
129
+ * at the cost of a key on every entry. */
130
+ annotations?: ToolAnnotations;
131
+ }
132
+ /**
133
+ * What an entry degrades to on the truncation ladder's first `tools` rung (schema
134
+ * §"Size bound and truncation order", step 1): identity and outcome, the two things
135
+ * the event exists for. Declared rather than left implicit because the ladder runs
136
+ * *after* assembly, so a consumer typing against {@link ToolRegistrationEvent} would
137
+ * otherwise read `descriptionLength` as always-present on a beacon that has none.
138
+ */
139
+ export type StrippedToolEntry = Pick<RegisteredToolEntry, "name" | "stableKey" | "outcome" | "schemaHash" | "source" | "intent">;
140
+ /**
141
+ * What `tools` degrades to on the ladder's second rung (step 2) when stripping the
142
+ * entries still leaves the batch over the 64 KB bound: the count, which answers "did
143
+ * this site register tools" where an event dropped whole answers nothing.
144
+ */
145
+ export interface TruncatedTools {
146
+ __truncated: true;
147
+ /** Serialized bytes the entries occupied; `0` when they could not be serialized. */
148
+ originalBytes: number;
149
+ /** The `tools` array's own length, so it survives unserializable entries. */
150
+ toolCount: number;
151
+ }
152
+ /**
153
+ * `tool_registration` — once per `registerTools` call, carrying the per-tool
154
+ * outcomes v1 computed and threw away. A site where 3 of 8 tools fail on a
155
+ * duplicate name is the failure mode this event exists to make visible.
156
+ */
157
+ export interface ToolRegistrationEvent extends TelemetryEnvelope {
158
+ event: "tool_registration";
159
+ /** 1-based position of this batch within the page load. */
160
+ registrationIndex: number;
161
+ trigger: RegistrationTrigger;
162
+ routeTemplate?: string;
163
+ timeSinceInitMs?: number;
164
+ /** Milliseconds from the call to the batch settling (or to the timeout). */
165
+ settleMs: number;
166
+ config: TrackingConfigInfo;
167
+ /**
168
+ * One entry per tool in the batch — unless the batch could not be sent whole, in
169
+ * which case the truncation ladder degrades this field in two ordered steps. Narrow
170
+ * with `Array.isArray` before reading entries.
171
+ */
172
+ tools: RegisteredToolEntry[] | StrippedToolEntry[] | TruncatedTools;
173
+ }
174
+ /** How a tool invocation settled. */
175
+ export type ToolCallOutcome = "success" | "error";
176
+ /** `tool.*` on `tool_call` — identity and the two fields worth joining calls on. */
177
+ export interface ToolCallToolInfo {
178
+ stableKey: string;
179
+ schemaHash?: string;
180
+ intent?: ToolIntent;
181
+ }
182
+ /**
183
+ * `response.*` — the handler's result as measurements, never as content. Absent
184
+ * when there is no result to measure (the error path, or a handler returning
185
+ * nothing).
186
+ */
187
+ export interface ToolCallResponseMetrics {
188
+ bytes: number;
189
+ contentBlocks: number;
190
+ /** The normalized result's own `isError` flag — a handler-reported failure that
191
+ * did not throw, so `outcome` alone cannot see it. */
192
+ isError: boolean;
193
+ }
194
+ /**
195
+ * `tool_call` — once per settled invocation, derived signals only. No raw `input`,
196
+ * `response`, or error message: the reliability question is answered by `outcome`,
197
+ * `durationMs`, and the error *class* and *signature*, none of which carry merchant
198
+ * or visitor content.
199
+ */
200
+ export interface ToolCallEvent extends TelemetryEnvelope {
201
+ event: "tool_call";
202
+ /** Per-call random ID — deduplicates a retried beacon, joins nothing else. */
203
+ callId: string;
204
+ /** 1-based position among all tool calls in this page load. */
205
+ callIndex: number;
206
+ /** 1-based position among calls to *this* tool. */
207
+ toolCallIndex: number;
208
+ /** `stableKey` of the previously called tool; absent on the first call. */
209
+ precededBy?: string;
210
+ routeTemplate?: string;
211
+ agentRuntime: AgentRuntime;
212
+ timeSinceInitMs?: number;
213
+ tool: ToolCallToolInfo;
214
+ outcome: ToolCallOutcome;
215
+ /** Client-measured elapsed milliseconds. */
216
+ durationMs: number;
217
+ response?: ToolCallResponseMetrics;
218
+ /** Explicitly `null` on the success path — see the module doc. */
219
+ errorClass: string | null;
220
+ /** Templated error message; absent unless `outcome` is `error`. */
221
+ errorSignature?: string;
222
+ }
223
+ /** Any event the channel emits, discriminated on `event`. */
224
+ export type TelemetryEvent = SdkInitEvent | ToolRegistrationEvent | ToolCallEvent;
@@ -0,0 +1,119 @@
1
+ /**
2
+ * Field allowlist for the default-on usage-telemetry channel
3
+ * (`src/telemetry.ts`). Every field an event may carry is listed here, and nothing
4
+ * is emitted unless its entry is `true`. `pruneByAllowlist` applies both maps on
5
+ * the way out, so flipping an entry to `false` and releasing is the single change
6
+ * needed to stop collecting a field — no builder can leak it.
7
+ *
8
+ * **Two maps, because one event field is an array.** `tool_registration.tools` is a
9
+ * list of per-tool entries: a dotted path cannot address the third entry's
10
+ * `schemaHash`, and per-index paths would mean a different allowlist per batch size.
11
+ * {@link TELEMETRY_TOOL_FIELDS} is keyed by the per-entry field name and gates it
12
+ * across every entry at once. The same map gates `tool_call.tool`, whose keys are a
13
+ * subset of an entry's — one decision covers both places a tool is described. The
14
+ * two namespaces are independent on purpose: `outcome` is the *call's* outcome at
15
+ * the event level and the *tool's registration* outcome inside an entry.
16
+ *
17
+ * `tools` and `tool` also appear in {@link TELEMETRY_FIELDS} as whole-key gates, so
18
+ * dropping either collection outright does not mean disabling every per-entry field.
19
+ *
20
+ * Event-level keys are the paths as they appear on the wire, at most two levels
21
+ * deep — the full extent of the event schemas. `routeTemplate` and `timeSinceInitMs`
22
+ * are listed once each and gate both events that carry them; `page.routeTemplate` and
23
+ * `client.agentRuntime` are separate keys because `sdk_init` nests them.
24
+ *
25
+ * **What is missing is missing by design.** The v1 fields that carried PII or
26
+ * unbounded cardinality (`fullUserAgent`, `screen`, `timezone`, `platform`, `url`,
27
+ * `path`, `referrer`, `title`, raw `input`/`response`/`error`, storage-backed
28
+ * `visitorId`/`sessionId`) are not listed as `false` — they are gone, replaced by
29
+ * derived signals, so no allowlist entry can bring them back.
30
+ *
31
+ * The envelope (`schema`, `event`, `ts`, `sessionId`) is listed like everything
32
+ * else, but it is what the backend routes, versions, and joins on: turning one of
33
+ * those off would emit unroutable payloads, so treat them as fixed in practice.
34
+ *
35
+ * This module is pure data — no imports, no side effects at import time.
36
+ */
37
+ /** Event-level fields, keyed by their dotted path on the wire. */
38
+ export declare const TELEMETRY_FIELDS: {
39
+ readonly schema: true;
40
+ readonly event: true;
41
+ readonly ts: true;
42
+ readonly sessionId: true;
43
+ readonly "sdk.name": true;
44
+ readonly "sdk.version": true;
45
+ readonly "sdk.installMode": true;
46
+ readonly "surface.available": true;
47
+ readonly "surface.provenance": true;
48
+ readonly "surface.global": true;
49
+ readonly "surface.specVersion": true;
50
+ readonly "page.routeTemplate": true;
51
+ readonly "page.segmentCount": true;
52
+ readonly "page.referrerClass": true;
53
+ readonly "page.frameContext": true;
54
+ readonly "page.visibility": true;
55
+ readonly "client.agentRuntime": true;
56
+ readonly "client.browser": true;
57
+ readonly "client.browserMajor": true;
58
+ readonly "client.formFactor": true;
59
+ readonly "client.language": true;
60
+ readonly timeToInitMs: true;
61
+ readonly registrationIndex: true;
62
+ readonly trigger: true;
63
+ readonly settleMs: true;
64
+ readonly "config.trackingEnabled": true;
65
+ readonly "config.otelEnabled": true;
66
+ readonly "config.customEndpoint": true;
67
+ readonly tools: true;
68
+ readonly callId: true;
69
+ readonly callIndex: true;
70
+ readonly toolCallIndex: true;
71
+ readonly precededBy: true;
72
+ readonly tool: true;
73
+ readonly outcome: true;
74
+ readonly durationMs: true;
75
+ readonly "response.bytes": true;
76
+ readonly "response.contentBlocks": true;
77
+ readonly "response.isError": true;
78
+ readonly errorClass: true;
79
+ readonly errorSignature: true;
80
+ readonly routeTemplate: true;
81
+ readonly timeSinceInitMs: true;
82
+ readonly agentRuntime: true;
83
+ };
84
+ /**
85
+ * Per-entry fields inside `tool_registration.tools[]` and on `tool_call.tool`.
86
+ * `annotations` gates the whole sub-object: its two booleans are one declaration by
87
+ * the tool author, and splitting them would be a third level of allowlist for no
88
+ * decision anyone needs to make separately.
89
+ */
90
+ export declare const TELEMETRY_TOOL_FIELDS: {
91
+ readonly name: true;
92
+ readonly stableKey: true;
93
+ readonly version: true;
94
+ readonly schemaHash: true;
95
+ readonly source: true;
96
+ readonly intent: true;
97
+ readonly outcome: true;
98
+ readonly failureSignature: true;
99
+ readonly descriptionLength: true;
100
+ readonly annotations: true;
101
+ readonly paramCount: true;
102
+ readonly requiredCount: true;
103
+ readonly freeTextParamCount: true;
104
+ readonly enumParamCount: true;
105
+ readonly maxDepth: true;
106
+ readonly describedParamCount: true;
107
+ };
108
+ /**
109
+ * Every event-level field name, as a dotted event path. Derived from the map above
110
+ * so the field list exists in exactly one place — adding a field is one line, and
111
+ * it cannot be half-added.
112
+ */
113
+ export type TelemetryField = keyof typeof TELEMETRY_FIELDS;
114
+ /** Field name → collected. A `false` entry is pruned from every event. */
115
+ export type TelemetryFieldMap = Readonly<Record<TelemetryField, boolean>>;
116
+ /** Every per-entry field name, derived from {@link TELEMETRY_TOOL_FIELDS}. */
117
+ export type TelemetryToolField = keyof typeof TELEMETRY_TOOL_FIELDS;
118
+ /** Per-entry field name → collected. A `false` entry is pruned from every entry. */
119
+ export type TelemetryToolFieldMap = Readonly<Record<TelemetryToolField, boolean>>;