@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/dist/index.cjs +35 -35
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +206 -190
- package/dist/index.d.ts +206 -190
- package/dist/index.global.js +78 -398
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +35 -35
- package/dist/index.js.map +1 -1
- package/dist/theme-editor.cjs +17 -17
- package/dist/theme-editor.js +21 -21
- package/package.json +1 -1
- package/src/client.test.ts +261 -0
- package/src/client.ts +103 -25
- package/src/index-core.ts +406 -0
- package/src/index-global.ts +4 -2
- package/src/index.ts +24 -400
- package/src/session.ts +4 -0
- package/src/types.ts +17 -0
- package/src/webmcp-bridge.test.ts +80 -0
- package/src/webmcp-bridge.ts +68 -0
package/src/index.ts
CHANGED
|
@@ -1,409 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
|
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
|
+
});
|
package/src/webmcp-bridge.ts
CHANGED
|
@@ -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;
|