@runtypelabs/persona 3.23.0 → 3.25.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/src/index.ts CHANGED
@@ -1,409 +1,33 @@
1
- import {
2
- initAgentWidget as initAgentWidgetFn,
3
- type AgentWidgetInitHandle
4
- } from "./runtime/init";
5
-
6
- export type {
7
- AgentWidgetConfig,
8
- AgentWidgetFeatureFlags,
9
- AgentWidgetArtifactsFeature,
10
- AgentWidgetArtifactsLayoutConfig,
11
- PersonaArtifactKind,
12
- PersonaArtifactRecord,
13
- PersonaArtifactManualUpsert,
14
- ArtifactConfigPayload,
15
- AgentWidgetInitOptions,
16
- AgentWidgetMessage,
17
- AgentWidgetLauncherConfig,
18
- AgentWidgetDockConfig,
19
- AgentWidgetEvent,
20
- AgentWidgetStreamParser,
21
- AgentWidgetStreamParserResult,
22
- AgentWidgetRequestPayload,
23
- // Context provider types (e.g. for config.contextProviders)
24
- AgentWidgetContextProvider,
25
- AgentWidgetContextProviderContext,
26
- AgentWidgetCustomFetch,
27
- AgentWidgetSSEEventParser,
28
- AgentWidgetSSEEventResult,
29
- AgentWidgetHeadersFunction,
30
- // Multi-modal content types
31
- TextContentPart,
32
- ImageContentPart,
33
- ContentPart,
34
- MessageContent,
35
- // Attachment config type
36
- AgentWidgetAttachmentsConfig,
37
- AgentWidgetComposerConfig,
38
- // Layout types
39
- AgentWidgetLayoutConfig,
40
- AgentWidgetHeaderLayoutConfig,
41
- AgentWidgetMessageLayoutConfig,
42
- AgentWidgetAvatarConfig,
43
- AgentWidgetTimestampConfig,
44
- WidgetLayoutSlot,
45
- SlotRenderer,
46
- SlotRenderContext,
47
- HeaderRenderContext,
48
- MessageRenderContext,
49
- // Markdown types
50
- AgentWidgetMarkdownConfig,
51
- AgentWidgetMarkdownOptions,
52
- AgentWidgetMarkdownRendererOverrides,
53
- // Message actions types
54
- AgentWidgetMessageActionsConfig,
55
- AgentWidgetMessageFeedback,
56
- // Client token types
57
- ClientSession,
58
- ClientInitResponse,
59
- ClientChatRequest,
60
- ClientFeedbackRequest,
61
- ClientFeedbackType,
62
- // Message injection types
63
- InjectMessageOptions,
64
- InjectAssistantMessageOptions,
65
- InjectUserMessageOptions,
66
- InjectSystemMessageOptions,
67
- InjectComponentDirectiveOptions,
68
- // Loading indicator types
69
- LoadingIndicatorRenderContext,
70
- AgentWidgetLoadingIndicatorConfig,
71
- // Idle indicator types
72
- IdleIndicatorRenderContext,
73
- // Agent execution types
74
- AgentConfig,
75
- AgentLoopConfig,
76
- AgentToolsConfig,
77
- AgentRequestOptions,
78
- AgentExecutionState,
79
- AgentMessageMetadata,
80
- AgentWidgetAgentRequestPayload,
81
- // Approval types
82
- AgentWidgetApproval,
83
- AgentWidgetApprovalConfig,
84
- // WebMCP — page-discovered tool consumption
85
- AgentWidgetWebMcpConfig,
86
- ClientToolDefinition,
87
- WebMcpConfirmHandler,
88
- WebMcpConfirmInfo,
89
- WebMcpToolResult,
90
- // Event stream types
91
- SSEEventRecord,
92
- EventStreamConfig,
93
- EventStreamBadgeColor,
94
- EventStreamViewRenderContext,
95
- EventStreamRowRenderContext,
96
- EventStreamToolbarRenderContext,
97
- EventStreamPayloadRenderContext,
98
- // Controller event map
99
- AgentWidgetControllerEventMap,
100
- // Ask-user-question (built-in answer-pill sheet) types
101
- AskUserQuestionPayload,
102
- AskUserQuestionPrompt,
103
- AskUserQuestionOption,
104
- AgentWidgetAskUserQuestionFeature,
105
- AgentWidgetAskUserQuestionStyles
106
- } from "./types";
107
-
108
- export {
109
- ASK_USER_QUESTION_TOOL_NAME,
110
- createAskUserQuestionBubble,
111
- ensureAskUserQuestionSheet,
112
- removeAskUserQuestionSheet,
113
- isAskUserQuestionMessage,
114
- parseAskUserQuestionPayload
115
- } from "./components/ask-user-question-bubble";
116
-
117
- export { initAgentWidgetFn as initAgentWidget };
118
- export {
119
- createWidgetHostLayout,
120
- type WidgetHostLayout,
121
- type WidgetHostLayoutMode
122
- } from "./runtime/host-layout";
123
- export {
124
- createAgentExperience,
125
- type AgentWidgetController
126
- } from "./ui";
127
- export {
128
- AgentWidgetSession,
129
- type AgentWidgetSessionStatus
130
- } from "./session";
131
- export { AgentWidgetClient } from "./client";
132
- export type { SSEEventCallback } from "./client";
133
- export {
134
- WebMcpBridge,
135
- WEBMCP_TOOL_PREFIX,
136
- isWebMcpToolName,
137
- stripWebMcpPrefix
138
- } from "./webmcp-bridge";
139
- export { createLocalStorageAdapter } from "./utils/storage";
140
- export {
141
- createActionManager,
142
- defaultActionHandlers,
143
- defaultJsonActionParser
144
- } from "./utils/actions";
145
- export {
146
- markdownPostprocessor,
147
- escapeHtml,
148
- directivePostprocessor,
149
- createMarkdownProcessor,
150
- createMarkdownProcessorFromConfig,
151
- createDirectivePostprocessor
152
- } from "./postprocessors";
153
- export type { MarkdownProcessorOptions } from "./postprocessors";
154
- export {
155
- createDefaultSanitizer,
156
- resolveSanitizer
157
- } from "./utils/sanitize";
158
- export type { SanitizeFunction } from "./utils/sanitize";
159
- export {
160
- createPlainTextParser,
161
- createJsonStreamParser,
162
- createFlexibleJsonStreamParser,
163
- createRegexJsonParser,
164
- createXmlParser
165
- } from "./utils/formatting";
166
- export {
167
- // Multi-modal content utilities
168
- normalizeContent,
169
- getDisplayText,
170
- hasImages,
171
- getImageParts,
172
- createTextPart,
173
- createImagePart,
174
- fileToImagePart,
175
- validateImageFile
176
- } from "./utils/content";
177
- export {
178
- collectEnrichedPageContext,
179
- formatEnrichedContext,
180
- generateStableSelector,
181
- defaultParseRules
182
- } from "./utils/dom-context";
183
- export type {
184
- EnrichedPageElement,
185
- DomContextOptions,
186
- DomContextMode,
187
- ParseOptionsConfig,
188
- ParseRule,
189
- RuleScoringContext,
190
- FormatEnrichedContextOptions
191
- } from "./utils/dom-context";
192
- export {
193
- AttachmentManager,
194
- type PendingAttachment,
195
- type AttachmentManagerConfig
196
- } from "./utils/attachment-manager";
197
- export {
198
- generateMessageId,
199
- generateUserMessageId,
200
- generateAssistantMessageId
201
- } from "./utils/message-id";
202
- export { isDockedMountMode, resolveDockConfig } from "./utils/dock";
1
+ /**
2
+ * npm package entry (`@runtypelabs/persona`).
3
+ *
4
+ * This is a thin barrel over `index-core.ts` (the shared public API) that adds
5
+ * back the **dev/config-tool-only** helpers — `generateCodeSnippet` and
6
+ * `createDemoCarousel`. Those are kept out of `index-core.ts` so the IIFE/CDN
7
+ * build (`index-global.ts`, which re-exports from `index-core.ts`) doesn't ship
8
+ * them: a running widget never needs them, only build-time/demo tooling does.
9
+ *
10
+ * Net effect: npm consumers get the full API (unchanged), while the script-tag
11
+ * `window.AgentWidget` global no longer exposes `generateCodeSnippet` /
12
+ * `createDemoCarousel`.
13
+ */
14
+
15
+ // Full public API (everything except the two dev-only helpers below).
16
+ export * from "./index-core";
17
+ export { default } from "./index-core";
18
+
19
+ // Dev / config-tool helper: generate install snippets from a widget config.
203
20
  export { generateCodeSnippet } from "./utils/code-generators";
204
- export type { CodeFormat, CodeGeneratorHooks, CodeGeneratorOptions } from "./utils/code-generators";
205
- export { VERSION } from "./version";
206
- export type { AgentWidgetInitHandle };
207
-
208
- // Plugin system exports
209
- export type { AgentWidgetPlugin } from "./plugins/types";
210
- export { pluginRegistry } from "./plugins/registry";
211
-
212
- // Stream animation plugin API — lets consumers register custom animations
213
- // that match the built-in surface (typewriter, pop-bubble) and subpath
214
- // modules (letter-rise, word-fade, wipe, glyph-cycle).
215
- export {
216
- registerStreamAnimationPlugin,
217
- unregisterStreamAnimationPlugin,
218
- listRegisteredStreamAnimations,
219
- } from "./utils/stream-animation";
220
- export type {
221
- StreamAnimationPlugin,
222
- StreamAnimationContext,
223
- AgentWidgetStreamAnimationBuffer,
224
- AgentWidgetStreamAnimationBuiltinType,
225
- AgentWidgetStreamAnimationType,
226
- AgentWidgetStreamAnimationFeature,
227
- AgentWidgetStreamAnimationPlaceholder,
228
- } from "./types";
229
-
230
- // Action system types — needed to type the `actionHandlers` / `actionParsers`
231
- // config options and to author custom handlers/parsers.
232
- export type {
233
- AgentWidgetActionHandler,
234
- AgentWidgetActionHandlerResult,
235
- AgentWidgetActionParser,
236
- AgentWidgetParsedAction,
237
- AgentWidgetActionContext,
238
- AgentWidgetActionEventPayload,
239
- } from "./types";
240
-
241
- // Dropdown utility exports
242
- export { createDropdownMenu } from "./utils/dropdown";
243
- export type { DropdownMenuItem, CreateDropdownOptions, DropdownMenuHandle } from "./utils/dropdown";
244
-
245
- // Icon utility exports
246
- export { renderLucideIcon } from "./utils/icons";
247
- export type { IconName } from "./utils/icons";
248
-
249
- // Button utility exports
250
- export { createIconButton, createLabelButton, createToggleGroup, createComboButton } from "./utils/buttons";
251
21
  export type {
252
- CreateIconButtonOptions,
253
- CreateLabelButtonOptions,
254
- CreateToggleGroupOptions,
255
- ToggleGroupItem,
256
- ToggleGroupHandle,
257
- CreateComboButtonOptions,
258
- ComboButtonHandle
259
- } from "./utils/buttons";
22
+ CodeFormat,
23
+ CodeGeneratorHooks,
24
+ CodeGeneratorOptions
25
+ } from "./utils/code-generators";
260
26
 
261
- // Demo carousel exports
27
+ // Demo-only component: the examples' showcase carousel.
262
28
  export { createDemoCarousel } from "./components/demo-carousel";
263
29
  export type {
264
30
  DemoCarouselItem,
265
31
  DemoCarouselOptions,
266
32
  DemoCarouselHandle
267
33
  } from "./components/demo-carousel";
268
-
269
- // Theme system exports
270
- export {
271
- createTheme,
272
- resolveTokens,
273
- themeToCssVariables,
274
- applyThemeVariables,
275
- getActiveTheme,
276
- getColorScheme,
277
- detectColorScheme,
278
- createThemeObserver
279
- } from "./utils/theme";
280
- export {
281
- DEFAULT_PALETTE,
282
- DEFAULT_SEMANTIC,
283
- DEFAULT_COMPONENTS,
284
- validateTheme,
285
- THEME_ZONES
286
- } from "./utils/tokens";
287
- export type { ThemeZone } from "./utils/tokens";
288
- export {
289
- accessibilityPlugin,
290
- animationsPlugin,
291
- brandPlugin,
292
- reducedMotionPlugin,
293
- highContrastPlugin,
294
- createPlugin
295
- } from "./utils/plugins";
296
- export type {
297
- DeepPartial,
298
- PersonaTheme,
299
- PersonaThemePlugin,
300
- CreateThemeOptions,
301
- TokenReference,
302
- ColorShade,
303
- ColorPalette,
304
- SpacingScale,
305
- TypographyScale,
306
- ShadowScale,
307
- BorderScale,
308
- RadiusScale,
309
- SemanticColors,
310
- SemanticSpacing,
311
- SemanticTypography,
312
- ComponentTokens,
313
- ArtifactToolbarTokens,
314
- ArtifactTabTokens,
315
- ArtifactPaneTokens,
316
- IconButtonTokens,
317
- LabelButtonTokens,
318
- ToggleGroupTokens,
319
- ThemeValidationResult,
320
- ThemeValidationError
321
- } from "./types/theme";
322
-
323
- // Component system exports
324
- export { componentRegistry } from "./components/registry";
325
- export type { ComponentRenderer, ComponentContext } from "./components/registry";
326
- export {
327
- createComponentStreamParser,
328
- isComponentDirectiveType
329
- } from "./utils/component-parser";
330
- export type { ComponentDirective } from "./utils/component-parser";
331
- export {
332
- renderComponentDirective,
333
- createComponentMiddleware,
334
- hasComponentDirective,
335
- extractComponentDirectiveFromMessage
336
- } from "./utils/component-middleware";
337
-
338
- // Default configuration exports
339
- export {
340
- DEFAULT_WIDGET_CONFIG,
341
- DEFAULT_FLOATING_LAUNCHER_MAX_WIDTH,
342
- DEFAULT_FLOATING_LAUNCHER_WIDTH,
343
- mergeWithDefaults
344
- } from "./defaults";
345
- export {
346
- PRESETS,
347
- getPreset,
348
- PRESET_SHOP,
349
- PRESET_MINIMAL,
350
- PRESET_FULLSCREEN
351
- } from "./presets";
352
- export type { WidgetPreset } from "./presets";
353
-
354
- // Layout system exports
355
- export {
356
- buildHeader,
357
- buildComposer,
358
- attachHeaderToContainer
359
- } from "./components/panel";
360
- export type {
361
- HeaderElements,
362
- HeaderBuildContext,
363
- ComposerElements,
364
- ComposerBuildContext
365
- } from "./components/panel";
366
- export {
367
- headerLayouts,
368
- getHeaderLayout,
369
- buildHeaderWithLayout,
370
- buildDefaultHeader,
371
- buildMinimalHeader
372
- } from "./components/header-layouts";
373
- export type {
374
- HeaderLayoutContext,
375
- HeaderLayoutRenderer
376
- } from "./components/header-layouts";
377
- export {
378
- createStandardBubble,
379
- createBubbleWithLayout,
380
- createTypingIndicator,
381
- createMessageActions,
382
- renderLoadingIndicatorWithFallback
383
- } from "./components/message-bubble";
384
- export type {
385
- MessageTransform,
386
- MessageActionCallbacks,
387
- LoadingIndicatorRenderer,
388
- CreateStandardBubbleOptions
389
- } from "./components/message-bubble";
390
- export {
391
- createCSATFeedback,
392
- createNPSFeedback
393
- } from "./components/feedback";
394
- export type { CSATFeedbackOptions, NPSFeedbackOptions } from "./components/feedback";
395
-
396
- // Voice module exports
397
- export {
398
- createVoiceProvider,
399
- createBestAvailableVoiceProvider,
400
- isVoiceSupported
401
- } from "./voice";
402
- export type {
403
- VoiceProvider,
404
- VoiceResult,
405
- VoiceStatus,
406
- VoiceConfig
407
- } from "./types";
408
-
409
- export default initAgentWidgetFn;
package/src/session.ts CHANGED
@@ -2059,6 +2059,10 @@ export class AgentWidgetSession {
2059
2059
  // a tool with the same key resolved in the prior conversation.
2060
2060
  this.webMcpInflightKeys.clear();
2061
2061
  this.webMcpResolvedKeys.clear();
2062
+ // A fresh conversation must resend the full WebMCP tool list on its next
2063
+ // turn — drop the diff-only fingerprint cache (server keys by recordId, so
2064
+ // a new conversation has no stored set to match).
2065
+ this.client.resetClientToolsFingerprint();
2062
2066
  this.setStreaming(false);
2063
2067
  this.setStatus("idle");
2064
2068
  this.callbacks.onMessagesChanged([...this.messages]);
package/src/types.ts CHANGED
@@ -2224,6 +2224,23 @@ export type ClientChatRequest = {
2224
2224
  context?: Record<string, unknown>;
2225
2225
  /** WebMCP page-discovered tools — same shape as `dispatch.clientTools[]`. */
2226
2226
  clientTools?: ClientToolDefinition[];
2227
+ /**
2228
+ * Diff-only / send-once: order-independent fingerprint of the client tool set.
2229
+ * When the set is unchanged from the previous turn the widget sends this
2230
+ * WITHOUT `clientTools` and the server reuses its stored set. On a cache miss
2231
+ * the server replies `409 { error: 'client_tools_resend_required' }` and the
2232
+ * widget retries once with the full `clientTools[]`.
2233
+ */
2234
+ clientToolsFingerprint?: string;
2235
+ };
2236
+
2237
+ /**
2238
+ * Body the server returns (HTTP 409) when it holds no stored tool set matching
2239
+ * a fingerprint-only `/client/chat` turn. The widget retries once with the full
2240
+ * `clientTools[]` (and the fingerprint).
2241
+ */
2242
+ export type ClientToolsResendRequiredResponse = {
2243
+ error: 'client_tools_resend_required';
2227
2244
  };
2228
2245
 
2229
2246
  /**
@@ -29,7 +29,9 @@ import {
29
29
  WebMcpBridge,
30
30
  isWebMcpToolName,
31
31
  stripWebMcpPrefix,
32
+ computeClientToolsFingerprint,
32
33
  } from "./webmcp-bridge";
34
+ import type { ClientToolDefinition } from "./types";
33
35
 
34
36
  type MockClient = { requestUserInteraction: (cb: () => unknown) => Promise<unknown> };
35
37
 
@@ -427,3 +429,81 @@ describe("WebMcpBridge.executeToolCall", () => {
427
429
  expect(executeSpy).not.toHaveBeenCalled();
428
430
  });
429
431
  });
432
+
433
+ describe("computeClientToolsFingerprint — diff-only / send-once", () => {
434
+ const tool = (over: Partial<ClientToolDefinition> = {}): ClientToolDefinition => ({
435
+ name: "search",
436
+ description: "Search the catalog",
437
+ parametersSchema: { type: "object", properties: { q: { type: "string" } } },
438
+ origin: "webmcp",
439
+ ...over,
440
+ });
441
+
442
+ it("returns a stable sentinel for an empty set", () => {
443
+ expect(computeClientToolsFingerprint([])).toBe(computeClientToolsFingerprint([]));
444
+ expect(computeClientToolsFingerprint([])).toBe("0:empty");
445
+ });
446
+
447
+ it("is deterministic for the same set", () => {
448
+ const a = computeClientToolsFingerprint([tool({ name: "a" }), tool({ name: "b" })]);
449
+ const b = computeClientToolsFingerprint([tool({ name: "a" }), tool({ name: "b" })]);
450
+ expect(a).toBe(b);
451
+ });
452
+
453
+ it("is order-independent (tool order does not matter)", () => {
454
+ const ab = computeClientToolsFingerprint([tool({ name: "a" }), tool({ name: "b" })]);
455
+ const ba = computeClientToolsFingerprint([tool({ name: "b" }), tool({ name: "a" })]);
456
+ expect(ab).toBe(ba);
457
+ });
458
+
459
+ it("changes when a description changes", () => {
460
+ expect(computeClientToolsFingerprint([tool({ description: "x" })])).not.toBe(
461
+ computeClientToolsFingerprint([tool({ description: "y" })]),
462
+ );
463
+ });
464
+
465
+ it("changes when the schema changes", () => {
466
+ const base = computeClientToolsFingerprint([tool()]);
467
+ const changed = computeClientToolsFingerprint([
468
+ tool({ parametersSchema: { type: "object", properties: { q: { type: "number" } } } }),
469
+ ]);
470
+ expect(changed).not.toBe(base);
471
+ });
472
+
473
+ it("changes when a tool is added", () => {
474
+ const one = computeClientToolsFingerprint([tool({ name: "a" })]);
475
+ const two = computeClientToolsFingerprint([tool({ name: "a" }), tool({ name: "b" })]);
476
+ expect(two).not.toBe(one);
477
+ });
478
+
479
+ it("ignores pageOrigin (audit metadata, not part of the contract)", () => {
480
+ const withOrigin = computeClientToolsFingerprint([tool({ pageOrigin: "https://a.example" })]);
481
+ const without = computeClientToolsFingerprint([tool({ pageOrigin: undefined })]);
482
+ expect(withOrigin).toBe(without);
483
+ });
484
+
485
+ it("reflects annotations (they ride along to the server)", () => {
486
+ const plain = computeClientToolsFingerprint([tool()]);
487
+ const annotated = computeClientToolsFingerprint([
488
+ tool({ annotations: { readOnlyHint: true } }),
489
+ ]);
490
+ expect(annotated).not.toBe(plain);
491
+ });
492
+
493
+ it("stays within the server's 128-char wire bound for large tool sets", () => {
494
+ // The server validates `clientToolsFingerprint` as `z.string().max(128)`.
495
+ // A fingerprint that grew with the tool content would 400 the first turn.
496
+ const many = Array.from({ length: 50 }, (_, i) =>
497
+ tool({
498
+ name: `tool_${i}`,
499
+ description: `A fairly long description for tool number ${i} `.repeat(8),
500
+ parametersSchema: {
501
+ type: "object",
502
+ properties: { a: { type: "string" }, b: { type: "number" }, c: { type: "boolean" } },
503
+ },
504
+ }),
505
+ );
506
+ const fp = computeClientToolsFingerprint(many);
507
+ expect(fp.length).toBeLessThanOrEqual(128);
508
+ });
509
+ });
@@ -86,6 +86,74 @@ const log = {
86
86
  },
87
87
  };
88
88
 
89
+ /**
90
+ * Compute a stable, order-independent fingerprint of a `ClientToolDefinition[]`
91
+ * snapshot, for the diff-only / send-once dispatch path (client-token mode).
92
+ *
93
+ * The widget caches "the fingerprint of the tool set last sent in full" for the
94
+ * current session; an unchanged set on a follow-up turn lets it ship only the
95
+ * fingerprint instead of the whole array. Per-tool strings are sorted so tool
96
+ * ordering does not affect the result. `pageOrigin` is deliberately excluded —
97
+ * it is audit metadata, not part of the tool contract.
98
+ *
99
+ * This is a fast, non-cryptographic content key. The canonical per-tool content
100
+ * is hashed down to a short, fixed-length digest so the result fits the server's
101
+ * `clientToolsFingerprint` wire field (`z.string().max(128)`) regardless of how
102
+ * many tools the page registers — sending the raw concatenated content would
103
+ * overflow that bound and be rejected with a 400. The server stores and compares
104
+ * the widget's fingerprint verbatim, so cross-implementation byte-equality is NOT
105
+ * required — only self-consistency across this widget's turns.
106
+ */
107
+ export function computeClientToolsFingerprint(
108
+ tools: ClientToolDefinition[],
109
+ ): string {
110
+ if (tools.length === 0) return "0:empty";
111
+ const parts = tools
112
+ .map((t) =>
113
+ [
114
+ t.name,
115
+ t.description ?? "",
116
+ t.parametersSchema ? JSON.stringify(t.parametersSchema) : "",
117
+ t.origin ?? "",
118
+ t.annotations ? JSON.stringify(t.annotations) : "",
119
+ ].join("\x1f"),
120
+ )
121
+ .sort();
122
+ return `${tools.length}:${hashFingerprintContent(parts.join("\x1e"))}`;
123
+ }
124
+
125
+ /**
126
+ * cyrb53 — a fast, well-distributed non-cryptographic string hash. Returns a
127
+ * 53-bit value (safe-integer range). Two independent seeds are combined by the
128
+ * caller for a ~106-bit digest, which makes accidental collisions across a
129
+ * single conversation's handful of tool-set variants infeasible.
130
+ */
131
+ function cyrb53(str: string, seed: number): number {
132
+ let h1 = 0xdeadbeef ^ seed;
133
+ let h2 = 0x41c6ce57 ^ seed;
134
+ for (let i = 0; i < str.length; i++) {
135
+ const ch = str.charCodeAt(i);
136
+ h1 = Math.imul(h1 ^ ch, 2654435761);
137
+ h2 = Math.imul(h2 ^ ch, 1597334677);
138
+ }
139
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507);
140
+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909);
141
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507);
142
+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909);
143
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
144
+ }
145
+
146
+ /**
147
+ * Compress the canonical tool-set content string into a short, fixed-length
148
+ * fingerprint (≤ ~24 chars) that fits the server's 128-char wire bound. Uses two
149
+ * seeded cyrb53 passes, base-36 encoded.
150
+ */
151
+ function hashFingerprintContent(content: string): string {
152
+ const a = cyrb53(content, 0).toString(36);
153
+ const b = cyrb53(content, 0x9e3779b1).toString(36);
154
+ return `${a}.${b}`;
155
+ }
156
+
89
157
  export class WebMcpBridge {
90
158
  private confirmHandler: WebMcpConfirmHandler | null;
91
159
  private readonly timeoutMs: number;