@runtypelabs/persona 3.34.0 → 3.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -11
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-CthJFfNx.d.cts → types-DgYsuwXL.d.cts} +15 -14
- package/dist/animations/{types-CthJFfNx.d.ts → types-DgYsuwXL.d.ts} +15 -14
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/codegen.cjs +11 -11
- package/dist/codegen.js +10 -10
- package/dist/index.cjs +46 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +514 -158
- package/dist/index.d.ts +514 -158
- package/dist/index.global.js +41 -89
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +46 -44
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +2 -2
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +53 -0
- package/dist/plugin-kit.d.cts +4 -4
- package/dist/plugin-kit.d.ts +4 -4
- package/dist/runtype-tts.js +1 -0
- package/dist/smart-dom-reader.cjs +1 -1
- package/dist/smart-dom-reader.d.cts +352 -103
- package/dist/smart-dom-reader.d.ts +352 -103
- package/dist/smart-dom-reader.js +1 -1
- package/dist/theme-editor-preview.cjs +203 -0
- package/dist/theme-editor-preview.d.cts +5748 -0
- package/dist/theme-editor-preview.d.ts +5748 -0
- package/dist/theme-editor-preview.js +203 -0
- package/dist/theme-editor.cjs +14 -131
- package/dist/theme-editor.d.cts +362 -574
- package/dist/theme-editor.d.ts +362 -574
- package/dist/theme-editor.js +14 -131
- package/dist/theme-reference.cjs +1 -1
- package/dist/theme-reference.d.cts +1 -1
- package/dist/theme-reference.d.ts +1 -1
- package/dist/theme-reference.js +1 -1
- package/dist/voice-worklet-player.cjs +80 -0
- package/dist/voice-worklet-player.d.cts +215 -0
- package/dist/voice-worklet-player.d.ts +215 -0
- package/dist/voice-worklet-player.js +80 -0
- package/dist/widget.css +54 -36
- package/package.json +16 -2
- package/src/animations/glyph-cycle.ts +11 -14
- package/src/animations/wipe.ts +1 -1
- package/src/ask-user-question-tool.test.ts +1 -1
- package/src/ask-user-question-tool.ts +6 -8
- package/src/client.test.ts +67 -12
- package/src/client.ts +40 -32
- package/src/codegen.test.ts +1 -1
- package/src/codegen.ts +1 -1
- package/src/components/approval-bubble.test.ts +1 -1
- package/src/components/approval-bubble.ts +3 -3
- package/src/components/artifact-pane.ts +3 -3
- package/src/components/ask-user-question-bubble.test.ts +14 -14
- package/src/components/ask-user-question-bubble.ts +10 -10
- package/src/components/composer-builder.ts +17 -16
- package/src/components/composer-parts.test.ts +54 -0
- package/src/components/composer-parts.ts +95 -126
- package/src/components/demo-carousel.ts +1 -1
- package/src/components/event-stream-view.test.ts +7 -7
- package/src/components/event-stream-view.ts +5 -5
- package/src/components/header-builder.ts +33 -27
- package/src/components/header-layouts.ts +1 -1
- package/src/components/header-parts.test.ts +90 -0
- package/src/components/header-parts.ts +62 -93
- package/src/components/launcher.test.ts +11 -0
- package/src/components/launcher.ts +1 -1
- package/src/components/message-bubble.test.ts +74 -2
- package/src/components/message-bubble.ts +25 -11
- package/src/components/messages.ts +2 -2
- package/src/components/panel.test.ts +1 -1
- package/src/components/panel.ts +92 -97
- package/src/components/pill-composer-builder.ts +28 -24
- package/src/components/reasoning-bubble.ts +2 -2
- package/src/components/suggestions.ts +1 -1
- package/src/components/tool-bubble.ts +1 -1
- package/src/components/widget-view.test.ts +181 -0
- package/src/components/widget-view.ts +234 -0
- package/src/defaults.ts +54 -51
- package/src/generated/runtype-openapi-contract.ts +5 -0
- package/src/index-core.ts +30 -4
- package/src/index-global.ts +73 -6
- package/src/index.ts +7 -1
- package/src/install.test.ts +11 -11
- package/src/install.ts +28 -18
- package/src/launcher-global.ts +25 -14
- package/src/markdown-parsers-eager.ts +20 -0
- package/src/markdown-parsers-entry.ts +4 -0
- package/src/markdown-parsers-loader.ts +49 -0
- package/src/plugin-kit.test.ts +1 -1
- package/src/plugin-kit.ts +7 -7
- package/src/plugins/types.ts +5 -5
- package/src/postprocessors.ts +27 -14
- package/src/runtime/host-layout.test.ts +1 -1
- package/src/runtime/host-layout.ts +4 -5
- package/src/runtime/init.ts +2 -2
- package/src/session.test.ts +5 -6
- package/src/session.ts +333 -227
- package/src/session.voice.test.ts +146 -0
- package/src/session.webmcp.test.ts +24 -25
- package/src/smart-dom-reader.ts +3 -4
- package/src/styles/widget.css +54 -36
- package/src/suggest-replies-tool.test.ts +3 -3
- package/src/suggest-replies-tool.ts +5 -5
- package/src/testing/index.ts +1 -1
- package/src/theme-editor/color-utils.ts +2 -2
- package/src/theme-editor/index.ts +4 -13
- package/src/theme-editor/presets.ts +1 -1
- package/src/theme-editor/preview-utils.ts +22 -20
- package/src/theme-editor/preview.ts +4 -1
- package/src/theme-editor/role-mappings.ts +3 -3
- package/src/theme-editor/sections.ts +21 -20
- package/src/theme-editor/types.ts +1 -1
- package/src/theme-editor/webmcp/index.ts +1 -1
- package/src/theme-editor/webmcp/summary.ts +1 -2
- package/src/theme-editor/webmcp/tools.ts +30 -30
- package/src/theme-editor/webmcp/types.ts +2 -2
- package/src/theme-editor-preview.ts +10 -0
- package/src/theme-reference.ts +26 -26
- package/src/types.ts +368 -99
- package/src/ui.approval-plugin.test.ts +3 -3
- package/src/ui.ask-user-question-plugin.test.ts +9 -9
- package/src/ui.component-directive.test.ts +4 -4
- package/src/ui.composer-bar.test.ts +10 -11
- package/src/ui.composer-keyboard.test.ts +5 -5
- package/src/ui.header-icon-color.test.ts +59 -0
- package/src/ui.scroll.test.ts +5 -6
- package/src/ui.stop-button.test.ts +3 -4
- package/src/ui.suggest-replies.test.ts +2 -2
- package/src/ui.ts +292 -131
- package/src/utils/buttons.ts +1 -1
- package/src/utils/code-generators.ts +1 -1
- package/src/utils/composer-history.ts +3 -3
- package/src/utils/dom-context.test.ts +2 -2
- package/src/utils/dom-context.ts +7 -7
- package/src/utils/dom.test.ts +67 -0
- package/src/utils/dom.ts +75 -0
- package/src/utils/dropdown.ts +2 -2
- package/src/utils/event-stream-buffer.test.ts +2 -2
- package/src/utils/message-fingerprint.test.ts +9 -0
- package/src/utils/message-fingerprint.ts +5 -0
- package/src/utils/morph.ts +1 -1
- package/src/utils/sanitize.ts +32 -16
- package/src/utils/sequence-buffer.test.ts +8 -8
- package/src/utils/sequence-buffer.ts +4 -4
- package/src/utils/smart-dom-adapter.test.ts +1 -1
- package/src/utils/smart-dom-adapter.ts +4 -5
- package/src/utils/speech-text.test.ts +122 -0
- package/src/utils/speech-text.ts +101 -0
- package/src/utils/stream-animation.test.ts +3 -3
- package/src/utils/stream-animation.ts +6 -6
- package/src/utils/theme.test.ts +1 -1
- package/src/utils/throughput-tracker.test.ts +11 -11
- package/src/utils/throughput-tracker.ts +9 -9
- package/src/utils/tokens.ts +13 -13
- package/src/utils/virtual-scroller.ts +1 -1
- package/src/vendor/smart-dom-reader/README.md +4 -4
- package/src/vendor/smart-dom-reader/index.d.ts +1 -1
- package/src/vendor/smart-dom-reader/index.js +2 -2
- package/src/version.ts +1 -1
- package/src/voice/audio-playback-manager.test.ts +159 -0
- package/src/voice/audio-playback-manager.ts +145 -30
- package/src/voice/browser-speech-engine.ts +130 -0
- package/src/voice/fallback-speech-engine.ts +89 -0
- package/src/voice/index.ts +21 -0
- package/src/voice/read-aloud-controller.test.ts +143 -0
- package/src/voice/read-aloud-controller.ts +136 -0
- package/src/voice/runtype-speech-engine.test.ts +280 -0
- package/src/voice/runtype-speech-engine.ts +211 -0
- package/src/voice/runtype-tts-entry.ts +10 -0
- package/src/voice/runtype-tts-loader.test.ts +47 -0
- package/src/voice/runtype-tts-loader.ts +42 -0
- package/src/voice/runtype-voice-provider.ts +332 -537
- package/src/voice/voice-factory.ts +26 -4
- package/src/voice/voice.test.ts +321 -7
- package/src/voice/worklet-playback-engine.ts +272 -0
- package/src/voice-worklet-player.ts +39 -0
- package/src/webmcp-bridge.test.ts +6 -6
- package/src/webmcp-bridge.ts +24 -25
- package/src/webmcp-polyfill.ts +1 -1
- package/src/voice/voice-activity-detector.ts +0 -90
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optional PCM playback engine entry (`@runtypelabs/persona/voice-worklet-player`).
|
|
3
|
+
*
|
|
4
|
+
* A jitter-buffered AudioWorklet player for raw PCM16 / 24 kHz / mono streams,
|
|
5
|
+
* kept out of the main bundle. Two ways in:
|
|
6
|
+
* - `createPcmStreamPlayer({ prebufferMs })` — general primitive with
|
|
7
|
+
* pause/resume; reuse it inside a hosted `SpeechEngine` (server/ElevenLabs/
|
|
8
|
+
* OpenAI TTS) for gapless "Read aloud".
|
|
9
|
+
* - `createWorkletPlaybackEngine` — realtime-named alias; pass it to
|
|
10
|
+
* `voiceRecognition.provider.runtype.createPlaybackEngine`.
|
|
11
|
+
*/
|
|
12
|
+
export {
|
|
13
|
+
createPcmStreamPlayer,
|
|
14
|
+
createWorkletPlaybackEngine,
|
|
15
|
+
} from "./voice/worklet-playback-engine";
|
|
16
|
+
export type { PcmStreamPlayerOptions } from "./voice/worklet-playback-engine";
|
|
17
|
+
export type { VoicePlaybackEngine, PcmStreamPlayer } from "./types";
|
|
18
|
+
|
|
19
|
+
// Runtype-hosted "Read aloud" engine. It streams PCM from Runtype's
|
|
20
|
+
// `/v1/agents/:id/speak` into a PcmStreamPlayer. By default that's the in-bundle
|
|
21
|
+
// AudioPlaybackManager, so the engine no longer depends on the worklet — but it
|
|
22
|
+
// pairs naturally with `createPcmStreamPlayer` above (pass it via
|
|
23
|
+
// `createPlaybackEngine`) for jitter-buffered playback. Most integrations don't
|
|
24
|
+
// import it directly — `textToSpeech: { provider: 'runtype' }` wires it
|
|
25
|
+
// automatically; this export is for manual `textToSpeech.createEngine` control.
|
|
26
|
+
export {
|
|
27
|
+
RuntypeSpeechEngine,
|
|
28
|
+
type RuntypeSpeechEngineOptions,
|
|
29
|
+
} from "./voice/runtype-speech-engine";
|
|
30
|
+
|
|
31
|
+
// Browser-fallback wrapper for a hosted SpeechEngine: tries the primary, and on
|
|
32
|
+
// a pre-playback failure silently switches to a secondary (browser) voice.
|
|
33
|
+
// `textToSpeech: { provider: 'runtype' }` wires it automatically; exported here
|
|
34
|
+
// for manual `textToSpeech.createEngine` control. Kept off the main entry so the
|
|
35
|
+
// IIFE/CDN build can defer the whole read-aloud path to `runtype-tts.js`.
|
|
36
|
+
export {
|
|
37
|
+
FallbackSpeechEngine,
|
|
38
|
+
type FallbackSpeechEngineOptions,
|
|
39
|
+
} from "./voice/fallback-speech-engine";
|
|
@@ -154,7 +154,7 @@ describe("WebMcpBridge.snapshotForDispatch", () => {
|
|
|
154
154
|
});
|
|
155
155
|
|
|
156
156
|
it("returns empty when initializeWebMCPPolyfill() throws", async () => {
|
|
157
|
-
// No pre-existing modelContext
|
|
157
|
+
// No pre-existing modelContext: otherwise install() short-circuits
|
|
158
158
|
// before importing the polyfill and the throwing init never runs.
|
|
159
159
|
vi.stubGlobal("document", {});
|
|
160
160
|
polyfillMock.initThrows = true;
|
|
@@ -249,7 +249,7 @@ describe("setWebMcpPolyfillLoader", () => {
|
|
|
249
249
|
});
|
|
250
250
|
|
|
251
251
|
it("never invokes the loader when a compatible modelContext is already present", async () => {
|
|
252
|
-
// beforeEach stubs a compatible document.modelContext
|
|
252
|
+
// beforeEach stubs a compatible document.modelContext: the install
|
|
253
253
|
// short-circuit must win, so a failing loader is irrelevant.
|
|
254
254
|
registry.tools = [fakeTool({ name: "search" })];
|
|
255
255
|
const loader = vi.fn(() => Promise.reject(new Error("should not load")));
|
|
@@ -439,7 +439,7 @@ describe("WebMcpBridge.executeToolCall", () => {
|
|
|
439
439
|
];
|
|
440
440
|
const bridge = new WebMcpBridge({ enabled: true, onConfirm: confirmSpy });
|
|
441
441
|
const r = await bridge.executeToolCall("webmcp:sensitive", {});
|
|
442
|
-
// Only the single outer gate fires
|
|
442
|
+
// Only the single outer gate fires: the polyfill owns the in-tool callback.
|
|
443
443
|
expect(confirmSpy).toHaveBeenCalledTimes(1);
|
|
444
444
|
expect(r.isError).toBeUndefined();
|
|
445
445
|
expect(r.content[0]).toEqual({
|
|
@@ -532,13 +532,13 @@ describe("WebMcpBridge.executeToolCall", () => {
|
|
|
532
532
|
const r = await pending;
|
|
533
533
|
expect(r.isError).toBe(true);
|
|
534
534
|
expect((r.content[0] as { text: string }).text).toMatch(/abort/i);
|
|
535
|
-
// Late resolve from the page side
|
|
535
|
+
// Late resolve from the page side: must not poison anything.
|
|
536
536
|
resolveStuck?.("late");
|
|
537
537
|
});
|
|
538
538
|
|
|
539
539
|
it("rejects a webmcp call for a tool excluded by the client allowlist", async () => {
|
|
540
540
|
// snapshotForDispatch filters by allowlist for the wire surface, but
|
|
541
|
-
// executeToolCall must also re-check it
|
|
541
|
+
// executeToolCall must also re-check it: defense-in-depth alongside the
|
|
542
542
|
// server-side check.
|
|
543
543
|
const executeSpy = vi.fn(() => "should not run");
|
|
544
544
|
registry.tools = [
|
|
@@ -556,7 +556,7 @@ describe("WebMcpBridge.executeToolCall", () => {
|
|
|
556
556
|
});
|
|
557
557
|
});
|
|
558
558
|
|
|
559
|
-
describe("computeClientToolsFingerprint
|
|
559
|
+
describe("computeClientToolsFingerprint: diff-only / send-once", () => {
|
|
560
560
|
const tool = (over: Partial<ClientToolDefinition> = {}): ClientToolDefinition => ({
|
|
561
561
|
name: "search",
|
|
562
562
|
description: "Search the catalog",
|
package/src/webmcp-bridge.ts
CHANGED
|
@@ -19,15 +19,15 @@
|
|
|
19
19
|
* with no MCP-B-only extensions. The spec standardizes the *producer* side;
|
|
20
20
|
* Persona is an in-page *consumer*, so it reads the registry via the
|
|
21
21
|
* producer-facing preview API:
|
|
22
|
-
* - `getTools()
|
|
22
|
+
* - `getTools()`: async; returns `{ name, description, inputSchema }` where
|
|
23
23
|
* `inputSchema` is a JSON *string*. Annotations are not exposed here.
|
|
24
|
-
* - `executeTool(toolInfo, inputArgsJson, { signal })
|
|
24
|
+
* - `executeTool(toolInfo, inputArgsJson, { signal })`: async; validates args
|
|
25
25
|
* against the tool's schema, runs `execute()`, and returns the raw result as
|
|
26
26
|
* a JSON *string* (or `null` for `undefined`). Honors `signal` for abort.
|
|
27
27
|
*
|
|
28
28
|
* The polyfill auto-installs `document.modelContext` at module-evaluation time,
|
|
29
29
|
* so it is imported *dynamically* and only when `config.webmcp.enabled === true`
|
|
30
|
-
|
|
30
|
+
*: a static import would install the global for every widget consumer,
|
|
31
31
|
* including those that never opted into WebMCP.
|
|
32
32
|
*
|
|
33
33
|
* Confirm model: every `webmcp:*` call goes through one confirm gate before
|
|
@@ -49,7 +49,7 @@ import type {
|
|
|
49
49
|
* Persona waits before telling the agent the tool failed, keeping a misbehaving
|
|
50
50
|
* tool from pinning the agent indefinitely. The timeout aborts the polyfill's
|
|
51
51
|
* `executeTool` via an `AbortSignal`, so the page's work is asked to stop too
|
|
52
|
-
* (cooperatively
|
|
52
|
+
* (cooperatively: a tool that ignores the signal may still complete).
|
|
53
53
|
*/
|
|
54
54
|
const DEFAULT_TOOL_TIMEOUT_MS = 30_000;
|
|
55
55
|
|
|
@@ -70,7 +70,7 @@ interface ModelContextToolInfo {
|
|
|
70
70
|
* Display title declared on the tool (`ToolDescriptor.title` in the WebMCP
|
|
71
71
|
* spec). The polyfill returns `""` when the tool didn't declare one. Note:
|
|
72
72
|
* `annotations` (incl. the legacy `annotations.title`) are NOT exposed on
|
|
73
|
-
* this strict consumer surface
|
|
73
|
+
* this strict consumer surface: top-level `title` is the only display-name
|
|
74
74
|
* channel available to us.
|
|
75
75
|
*/
|
|
76
76
|
title?: string;
|
|
@@ -97,7 +97,7 @@ const webMcpToolDisplayTitles = new Map<string, string>();
|
|
|
97
97
|
|
|
98
98
|
/**
|
|
99
99
|
* Record declared display titles from a fresh `getTools()` read. The map is
|
|
100
|
-
* rebuilt from scratch
|
|
100
|
+
* rebuilt from scratch, callers always pass the FULL registry snapshot, so
|
|
101
101
|
* a tool that unregistered or dropped its title can't leave a stale label
|
|
102
102
|
* behind. Exported for tests; production callers are the bridge's registry
|
|
103
103
|
* reads.
|
|
@@ -160,16 +160,15 @@ export const setWebMcpPolyfillLoader = (
|
|
|
160
160
|
* The widget caches "the fingerprint of the tool set last sent in full" for the
|
|
161
161
|
* current session; an unchanged set on a follow-up turn lets it ship only the
|
|
162
162
|
* fingerprint instead of the whole array. Per-tool strings are sorted so tool
|
|
163
|
-
* ordering does not affect the result. `pageOrigin` is deliberately excluded
|
|
164
|
-
* it is audit metadata, not part of the tool contract.
|
|
163
|
+
* ordering does not affect the result. `pageOrigin` is deliberately excluded: * it is audit metadata, not part of the tool contract.
|
|
165
164
|
*
|
|
166
165
|
* This is a fast, non-cryptographic content key. The canonical per-tool content
|
|
167
166
|
* is hashed down to a short, fixed-length digest so the result fits the server's
|
|
168
167
|
* `clientToolsFingerprint` wire field (`z.string().max(128)`) regardless of how
|
|
169
|
-
* many tools the page registers
|
|
168
|
+
* many tools the page registers: sending the raw concatenated content would
|
|
170
169
|
* overflow that bound and be rejected with a 400. The server stores and compares
|
|
171
170
|
* the widget's fingerprint verbatim, so cross-implementation byte-equality is NOT
|
|
172
|
-
* required
|
|
171
|
+
* required: only self-consistency across this widget's turns.
|
|
173
172
|
*/
|
|
174
173
|
export function computeClientToolsFingerprint(
|
|
175
174
|
tools: ClientToolDefinition[],
|
|
@@ -190,7 +189,7 @@ export function computeClientToolsFingerprint(
|
|
|
190
189
|
}
|
|
191
190
|
|
|
192
191
|
/**
|
|
193
|
-
* cyrb53
|
|
192
|
+
* cyrb53: a fast, well-distributed non-cryptographic string hash. Returns a
|
|
194
193
|
* 53-bit value (safe-integer range). Two independent seeds are combined by the
|
|
195
194
|
* caller for a ~106-bit digest, which makes accidental collisions across a
|
|
196
195
|
* single conversation's handful of tool-set variants infeasible.
|
|
@@ -252,7 +251,7 @@ export class WebMcpBridge {
|
|
|
252
251
|
|
|
253
252
|
/**
|
|
254
253
|
* `true` when the bridge can both snapshot the registry AND execute returned
|
|
255
|
-
* tool calls
|
|
254
|
+
* tool calls: i.e. the polyfill is installed and `document.modelContext`
|
|
256
255
|
* exposes the consumer surface (`getTools` / `executeTool`). Native browsers
|
|
257
256
|
* that ship `document.modelContext` satisfy this too.
|
|
258
257
|
*
|
|
@@ -268,7 +267,7 @@ export class WebMcpBridge {
|
|
|
268
267
|
|
|
269
268
|
/**
|
|
270
269
|
* Per-turn snapshot for `dispatch.clientTools[]`. Returns the JSON-only
|
|
271
|
-
* surface
|
|
270
|
+
* surface: `execute` stays client-side, reached later via `executeToolCall`.
|
|
272
271
|
*
|
|
273
272
|
* Async because the strict polyfill's `getTools()` is async. Both payload
|
|
274
273
|
* builders in `client.ts` already `await`, so this adds no new ceremony.
|
|
@@ -284,7 +283,7 @@ export class WebMcpBridge {
|
|
|
284
283
|
try {
|
|
285
284
|
infos = await mc.getTools();
|
|
286
285
|
} catch (err) {
|
|
287
|
-
log.warn("getTools() threw
|
|
286
|
+
log.warn("getTools() threw: shipping an empty WebMCP snapshot.", err);
|
|
288
287
|
return [];
|
|
289
288
|
}
|
|
290
289
|
recordWebMcpToolDisplayTitles(infos);
|
|
@@ -310,7 +309,7 @@ export class WebMcpBridge {
|
|
|
310
309
|
* Execute a `webmcp:<name>` tool call returned by the agent and return the
|
|
311
310
|
* normalized MCP-shaped result for `/resume`.
|
|
312
311
|
*
|
|
313
|
-
* Failure modes
|
|
312
|
+
* Failure modes: all return `{ isError: true, content: [...] }` rather than
|
|
314
313
|
* throwing, so the dispatch can resume cleanly:
|
|
315
314
|
* - bridge not operational
|
|
316
315
|
* - tool not in registry (e.g. unmounted between snapshot and call)
|
|
@@ -374,7 +373,7 @@ export class WebMcpBridge {
|
|
|
374
373
|
|
|
375
374
|
// Re-apply the client-side allowlist at execute time. `snapshotForDispatch`
|
|
376
375
|
// already filters it for `clientTools[]`, but the agent could request a
|
|
377
|
-
// tool that the integrator excluded
|
|
376
|
+
// tool that the integrator excluded: e.g. a `webmcp:` call replayed from
|
|
378
377
|
// history, a server bug, or a page that re-registered a previously-hidden
|
|
379
378
|
// tool. The server is the trust boundary; this is a defense-in-depth
|
|
380
379
|
// convenience check to keep us symmetric with the snapshot.
|
|
@@ -384,7 +383,7 @@ export class WebMcpBridge {
|
|
|
384
383
|
);
|
|
385
384
|
}
|
|
386
385
|
|
|
387
|
-
// Bail before the confirm renders
|
|
386
|
+
// Bail before the confirm renders: a late approval after cancel() would
|
|
388
387
|
// otherwise fire a host-page side effect with no matching /resume.
|
|
389
388
|
if (signal?.aborted) {
|
|
390
389
|
return errorResult("Aborted by cancel()");
|
|
@@ -413,7 +412,7 @@ export class WebMcpBridge {
|
|
|
413
412
|
|
|
414
413
|
// Drive both the 30s timeout and the caller's `signal` through a single
|
|
415
414
|
// AbortController passed to `executeTool`. The polyfill races the page's
|
|
416
|
-
// `execute()` against this signal, so abort is cooperative
|
|
415
|
+
// `execute()` against this signal, so abort is cooperative: a tool that
|
|
417
416
|
// ignores the signal may still complete on the page after the agent gets
|
|
418
417
|
// an `isError` result. Side-effectful tools should bound their own work.
|
|
419
418
|
const controller = new AbortController();
|
|
@@ -457,7 +456,7 @@ export class WebMcpBridge {
|
|
|
457
456
|
* widget consumers that never enable WebMCP.
|
|
458
457
|
*
|
|
459
458
|
* Producer pages should still install the polyfill themselves (or import it)
|
|
460
|
-
* before registering tools
|
|
459
|
+
* before registering tools: Persona's install is a fallback, and a page that
|
|
461
460
|
* registers tools at load before Persona's first dispatch needs the global to
|
|
462
461
|
* already exist.
|
|
463
462
|
*/
|
|
@@ -472,7 +471,7 @@ export class WebMcpBridge {
|
|
|
472
471
|
private async install(): Promise<void> {
|
|
473
472
|
try {
|
|
474
473
|
// A compatible registry is already on the page (the host installed the
|
|
475
|
-
// polyfill, or a native impl)
|
|
474
|
+
// polyfill, or a native impl): initialize would no-op against it, so
|
|
476
475
|
// skip loading the module entirely. Pages that register tools before
|
|
477
476
|
// Persona's first dispatch always land here, because registering
|
|
478
477
|
// requires `document.modelContext` to exist.
|
|
@@ -489,7 +488,7 @@ export class WebMcpBridge {
|
|
|
489
488
|
this.installed = true;
|
|
490
489
|
} catch (err) {
|
|
491
490
|
log.warn(
|
|
492
|
-
"Failed to load @mcp-b/webmcp-polyfill
|
|
491
|
+
"Failed to load @mcp-b/webmcp-polyfill: WebMCP consumption disabled.",
|
|
493
492
|
err,
|
|
494
493
|
);
|
|
495
494
|
this.installed = false;
|
|
@@ -504,7 +503,7 @@ export class WebMcpBridge {
|
|
|
504
503
|
if (typeof document === "undefined") return null;
|
|
505
504
|
const mc = (document as Document & { modelContext?: unknown }).modelContext;
|
|
506
505
|
if (!mc || typeof mc !== "object") {
|
|
507
|
-
// Absent (not yet installed, or no WebMCP on this page)
|
|
506
|
+
// Absent (not yet installed, or no WebMCP on this page): not an error,
|
|
508
507
|
// and not worth warning about; the snapshot/execute paths fall back to a
|
|
509
508
|
// clean "not operational" result.
|
|
510
509
|
return null;
|
|
@@ -517,13 +516,13 @@ export class WebMcpBridge {
|
|
|
517
516
|
// A `document.modelContext` IS present but doesn't expose the strict-core
|
|
518
517
|
// surface we consume (`getTools` / `executeTool`). This usually means a
|
|
519
518
|
// different or older WebMCP polyfill (or a native impl on a divergent
|
|
520
|
-
// draft) installed the global first
|
|
519
|
+
// draft) installed the global first: which `@mcp-b/webmcp-polyfill`
|
|
521
520
|
// correctly declines to overwrite. Warn once so integrators understand
|
|
522
521
|
// why WebMCP is inert instead of seeing a silent no-op.
|
|
523
522
|
if (!this.incompatibleContextWarned) {
|
|
524
523
|
this.incompatibleContextWarned = true;
|
|
525
524
|
log.warn(
|
|
526
|
-
"document.modelContext is present but does not expose getTools()/executeTool()
|
|
525
|
+
"document.modelContext is present but does not expose getTools()/executeTool(): " +
|
|
527
526
|
"WebMCP consumption is disabled. Another (incompatible or older) WebMCP polyfill " +
|
|
528
527
|
"likely installed document.modelContext before Persona. Remove it, or use a polyfill " +
|
|
529
528
|
"implementing the strict standard surface (e.g. @mcp-b/webmcp-polyfill).",
|
|
@@ -618,7 +617,7 @@ const normalizeSerializedResult = (raw: string | null): WebMcpToolResult => {
|
|
|
618
617
|
try {
|
|
619
618
|
parsed = JSON.parse(raw);
|
|
620
619
|
} catch {
|
|
621
|
-
// Not valid JSON (shouldn't happen
|
|
620
|
+
// Not valid JSON (shouldn't happen, the polyfill stringifies), surface
|
|
622
621
|
// the raw string as text rather than dropping it.
|
|
623
622
|
return { content: [{ type: "text", text: raw }] };
|
|
624
623
|
}
|
package/src/webmcp-polyfill.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* imports this chunk from a URL derived from the widget script's own `src`
|
|
9
9
|
* (see the loader registered in `index-global.ts`).
|
|
10
10
|
*
|
|
11
|
-
* Built self-contained (`--no-external`)
|
|
11
|
+
* Built self-contained (`--no-external`): it must work standalone on a CDN
|
|
12
12
|
* with no module resolution. npm/bundler consumers never load this file;
|
|
13
13
|
* their bundlers resolve the bare `import("@mcp-b/webmcp-polyfill")` in
|
|
14
14
|
* `webmcp-bridge.ts` directly.
|
|
@@ -1,90 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Voice Activity Detector (VAD)
|
|
3
|
-
*
|
|
4
|
-
* Reusable RMS-based voice activity detection that monitors a mic stream
|
|
5
|
-
* and fires a callback when a condition is sustained for a given duration.
|
|
6
|
-
*
|
|
7
|
-
* - mode 'silence': fires when volume stays below threshold (user stopped talking)
|
|
8
|
-
* - mode 'speech': fires when volume stays above threshold (user started talking)
|
|
9
|
-
*
|
|
10
|
-
* Fires callback exactly once per start() call, then stops checking.
|
|
11
|
-
* Calling start() again implicitly calls stop() first.
|
|
12
|
-
*/
|
|
13
|
-
export class VoiceActivityDetector {
|
|
14
|
-
private sourceNode: MediaStreamAudioSourceNode | null = null;
|
|
15
|
-
private analyserNode: AnalyserNode | null = null;
|
|
16
|
-
private interval: ReturnType<typeof setInterval> | null = null;
|
|
17
|
-
private conditionStart: number | null = null;
|
|
18
|
-
private fired = false;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Begin monitoring the given stream for voice activity.
|
|
22
|
-
*
|
|
23
|
-
* @param audioContext Active AudioContext
|
|
24
|
-
* @param stream MediaStream from getUserMedia
|
|
25
|
-
* @param mode 'silence' fires when quiet for duration, 'speech' fires when loud for duration
|
|
26
|
-
* @param config threshold (RMS level) and duration (ms)
|
|
27
|
-
* @param callback Fires exactly once when the condition is met
|
|
28
|
-
*/
|
|
29
|
-
start(
|
|
30
|
-
audioContext: AudioContext,
|
|
31
|
-
stream: MediaStream,
|
|
32
|
-
mode: "silence" | "speech",
|
|
33
|
-
config: { threshold: number; duration: number },
|
|
34
|
-
callback: () => void,
|
|
35
|
-
): void {
|
|
36
|
-
this.stop();
|
|
37
|
-
|
|
38
|
-
this.fired = false;
|
|
39
|
-
this.conditionStart = null;
|
|
40
|
-
|
|
41
|
-
this.sourceNode = audioContext.createMediaStreamSource(stream);
|
|
42
|
-
this.analyserNode = audioContext.createAnalyser();
|
|
43
|
-
this.analyserNode.fftSize = 2048;
|
|
44
|
-
this.sourceNode.connect(this.analyserNode);
|
|
45
|
-
|
|
46
|
-
const dataArray = new Float32Array(this.analyserNode.fftSize);
|
|
47
|
-
|
|
48
|
-
this.interval = setInterval(() => {
|
|
49
|
-
if (!this.analyserNode || this.fired) return;
|
|
50
|
-
this.analyserNode.getFloatTimeDomainData(dataArray);
|
|
51
|
-
|
|
52
|
-
// Compute RMS volume
|
|
53
|
-
let sum = 0;
|
|
54
|
-
for (let i = 0; i < dataArray.length; i++) {
|
|
55
|
-
sum += dataArray[i] * dataArray[i];
|
|
56
|
-
}
|
|
57
|
-
const rms = Math.sqrt(sum / dataArray.length);
|
|
58
|
-
|
|
59
|
-
const conditionMet =
|
|
60
|
-
mode === "silence"
|
|
61
|
-
? rms < config.threshold
|
|
62
|
-
: rms >= config.threshold;
|
|
63
|
-
|
|
64
|
-
if (conditionMet) {
|
|
65
|
-
if (this.conditionStart === null) {
|
|
66
|
-
this.conditionStart = Date.now();
|
|
67
|
-
} else if (Date.now() - this.conditionStart >= config.duration) {
|
|
68
|
-
this.fired = true;
|
|
69
|
-
callback();
|
|
70
|
-
}
|
|
71
|
-
} else {
|
|
72
|
-
this.conditionStart = null;
|
|
73
|
-
}
|
|
74
|
-
}, 100);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
stop(): void {
|
|
78
|
-
if (this.interval) {
|
|
79
|
-
clearInterval(this.interval);
|
|
80
|
-
this.interval = null;
|
|
81
|
-
}
|
|
82
|
-
if (this.sourceNode) {
|
|
83
|
-
this.sourceNode.disconnect();
|
|
84
|
-
this.sourceNode = null;
|
|
85
|
-
}
|
|
86
|
-
this.analyserNode = null;
|
|
87
|
-
this.conditionStart = null;
|
|
88
|
-
this.fired = false;
|
|
89
|
-
}
|
|
90
|
-
}
|