@runtypelabs/persona 4.27.0 → 4.28.1
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 +25 -0
- package/dist/{approval-ui-QAKWNFNM.js → approval-ui-OBBI62ES.js} +1 -1
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +20 -5
- package/dist/approval-ui.d.ts +20 -5
- package/dist/approval-ui.js +2 -2
- package/dist/artifacts-ui.d.cts +20 -5
- package/dist/artifacts-ui.d.ts +20 -5
- package/dist/{chunk-NQLDCJAP.js → chunk-D4TDKCT5.js} +2 -2
- package/dist/chunk-FNXK3VTC.js +1 -0
- package/dist/{client-history-HTRKHBAF.js → client-history-QGAFMCBE.js} +1 -1
- package/dist/client-history.js +1 -1
- package/dist/{client-stream-N22JHTLE.js → client-stream-WXD2FL23.js} +1 -1
- package/dist/client-stream.js +3 -3
- package/dist/codegen.cjs +4 -4
- package/dist/codegen.js +5 -5
- package/dist/context-mentions.d.cts +20 -5
- package/dist/context-mentions.d.ts +20 -5
- package/dist/event-stream-view.d.cts +20 -5
- package/dist/event-stream-view.d.ts +20 -5
- package/dist/forms-ui.d.cts +26 -5
- package/dist/forms-ui.d.ts +26 -5
- package/dist/index.cjs +41 -41
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +59 -30
- package/dist/index.d.ts +59 -30
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +41 -41
- package/dist/index.js.map +1 -1
- package/dist/session-reconnect.d.cts +20 -5
- package/dist/session-reconnect.d.ts +20 -5
- package/dist/smart-dom-reader.d.cts +20 -5
- package/dist/smart-dom-reader.d.ts +20 -5
- package/dist/theme-editor-preview.cjs +63 -63
- package/dist/theme-editor-preview.d.cts +20 -5
- package/dist/theme-editor-preview.d.ts +20 -5
- package/dist/theme-editor-preview.js +6 -6
- package/dist/theme-editor.d.cts +21 -6
- package/dist/theme-editor.d.ts +21 -6
- package/dist/voice-runtime-TWFJ4UUS.js +17 -0
- package/dist/voice-runtime.cjs +8 -8
- package/dist/voice-runtime.d.cts +31 -5
- package/dist/voice-runtime.d.ts +31 -5
- package/dist/voice-runtime.js +8 -8
- package/dist/webmcp-runtime-5Q3IHAHP.js +6 -0
- package/dist/webmcp-runtime.cjs +2 -2
- package/dist/webmcp-runtime.d.cts +22 -26
- package/dist/webmcp-runtime.d.ts +22 -26
- package/dist/webmcp-runtime.js +2 -2
- package/package.json +5 -2
- package/src/client.interrupt.test.ts +18 -0
- package/src/client.ts +7 -0
- package/src/generated/runtype-openapi-contract.ts +3 -0
- package/src/session.ts +11 -3
- package/src/session.voice.test.ts +140 -0
- package/src/theme-editor/webmcp/types.ts +1 -1
- package/src/types.ts +26 -5
- package/src/voice/runtype-voice-provider.ts +93 -11
- package/src/voice/voice-clause-buffer.test.ts +143 -0
- package/src/voice/voice-clause-buffer.ts +187 -0
- package/src/voice/voice-delegation.ts +85 -3
- package/src/voice/voice.test.ts +257 -2
- package/src/webmcp-bridge.test.ts +87 -21
- package/src/webmcp-bridge.ts +21 -26
- package/src/webmcp-polyfill.integration.test.ts +339 -0
- package/src/webmcp-polyfill.ts +7 -4
- package/src/webmcp-runtime-bundle.test.ts +1 -1
- package/src/webmcp-runtime-entry.ts +34 -19
- package/dist/chunk-3255TNY7.js +0 -1
- package/dist/voice-runtime-RHRX4F3T.js +0 -17
- package/dist/webmcp-runtime-V6ZWW3PI.js +0 -6
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import {
|
|
2
|
+
afterAll,
|
|
3
|
+
afterEach,
|
|
4
|
+
beforeAll,
|
|
5
|
+
describe,
|
|
6
|
+
expect,
|
|
7
|
+
it,
|
|
8
|
+
vi,
|
|
9
|
+
} from "vitest";
|
|
10
|
+
import { JSDOM } from "jsdom";
|
|
11
|
+
import { MessageChannel } from "node:worker_threads";
|
|
12
|
+
import { readFileSync } from "node:fs";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { WebMcpBridge } from "./webmcp-bridge-public";
|
|
15
|
+
import { setWebMcpPolyfillLoader } from "./webmcp-bridge";
|
|
16
|
+
import type { ModelContextCoreLike } from "./webmcp-bridge";
|
|
17
|
+
|
|
18
|
+
type ToolCallback = (
|
|
19
|
+
input: unknown,
|
|
20
|
+
options?: { signal: AbortSignal }
|
|
21
|
+
) => unknown;
|
|
22
|
+
type TestContext = ModelContextCoreLike & {
|
|
23
|
+
registerTool(
|
|
24
|
+
tool: {
|
|
25
|
+
name: string;
|
|
26
|
+
description: string;
|
|
27
|
+
title?: string;
|
|
28
|
+
inputSchema?: object;
|
|
29
|
+
annotations?: { readOnlyHint: boolean; untrustedContentHint: boolean };
|
|
30
|
+
execute: ToolCallback;
|
|
31
|
+
},
|
|
32
|
+
options?: { signal: AbortSignal }
|
|
33
|
+
): Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Run the actual published distributions, not version-shaped mocks. The v6
|
|
37
|
+
// alias is test-only: Persona's automatic fallback deliberately stays on v5.
|
|
38
|
+
describe.each([
|
|
39
|
+
{ version: "v5", module: "@mcp-b/webmcp-polyfill", legacy: true },
|
|
40
|
+
{ version: "v6", module: "webmcp-polyfill-v6", legacy: false },
|
|
41
|
+
])("WebMcpBridge with published $version", ({ module, legacy }) => {
|
|
42
|
+
const dom = new JSDOM("<!doctype html><body></body>", {
|
|
43
|
+
url: "https://example.test",
|
|
44
|
+
runScripts: "outside-only",
|
|
45
|
+
});
|
|
46
|
+
const browser = dom.window;
|
|
47
|
+
const channels: MessageChannel[] = [];
|
|
48
|
+
const registrations: AbortController[] = [];
|
|
49
|
+
let context: TestContext;
|
|
50
|
+
|
|
51
|
+
beforeAll(() => {
|
|
52
|
+
// v5 also runs without the newer browser APIs required by v6. These are
|
|
53
|
+
// intentionally absent to exercise the compatibility of our fallback.
|
|
54
|
+
Object.defineProperty(browser, "isSecureContext", { value: true });
|
|
55
|
+
if (legacy) {
|
|
56
|
+
Object.defineProperty(browser.Promise, "withResolvers", {
|
|
57
|
+
value: undefined,
|
|
58
|
+
});
|
|
59
|
+
Object.defineProperty(browser.AbortSignal, "any", { value: undefined });
|
|
60
|
+
Object.defineProperty(browser.URL, "parse", { value: undefined });
|
|
61
|
+
}
|
|
62
|
+
Object.defineProperty(browser, "MessageChannel", {
|
|
63
|
+
value: class extends MessageChannel {
|
|
64
|
+
constructor() {
|
|
65
|
+
super();
|
|
66
|
+
channels.push(this);
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
vi.stubGlobal("AbortController", browser.AbortController);
|
|
71
|
+
vi.stubGlobal("AbortSignal", browser.AbortSignal);
|
|
72
|
+
vi.stubGlobal("window", browser);
|
|
73
|
+
vi.stubGlobal("document", browser.document);
|
|
74
|
+
vi.stubGlobal("location", browser.location);
|
|
75
|
+
// A true Window realm is required by v6's native Window.window getter.
|
|
76
|
+
browser.eval(
|
|
77
|
+
readFileSync(
|
|
78
|
+
createRequire(import.meta.url).resolve(`${module}/iife`),
|
|
79
|
+
"utf8"
|
|
80
|
+
)
|
|
81
|
+
);
|
|
82
|
+
context = (browser.document as unknown as { modelContext: TestContext })
|
|
83
|
+
.modelContext;
|
|
84
|
+
});
|
|
85
|
+
afterEach(() => {
|
|
86
|
+
for (const registration of registrations.splice(0)) registration.abort();
|
|
87
|
+
setWebMcpPolyfillLoader(null);
|
|
88
|
+
});
|
|
89
|
+
afterAll(() => {
|
|
90
|
+
for (const channel of channels) {
|
|
91
|
+
channel.port1.close();
|
|
92
|
+
channel.port2.close();
|
|
93
|
+
}
|
|
94
|
+
browser.close();
|
|
95
|
+
vi.unstubAllGlobals();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const register = async (name: string, execute: ToolCallback) => {
|
|
99
|
+
const registration = new browser.AbortController();
|
|
100
|
+
registrations.push(registration);
|
|
101
|
+
await context.registerTool(
|
|
102
|
+
{
|
|
103
|
+
name,
|
|
104
|
+
description: `Run ${name}`,
|
|
105
|
+
title: "Local tool",
|
|
106
|
+
inputSchema: {
|
|
107
|
+
type: "object",
|
|
108
|
+
properties: { query: { type: "string" } },
|
|
109
|
+
},
|
|
110
|
+
annotations: { readOnlyHint: true, untrustedContentHint: true },
|
|
111
|
+
execute,
|
|
112
|
+
},
|
|
113
|
+
{ signal: registration.signal }
|
|
114
|
+
);
|
|
115
|
+
};
|
|
116
|
+
const bridge = (onConfirm = vi.fn(async () => true)) =>
|
|
117
|
+
new WebMcpBridge({ enabled: true, onConfirm });
|
|
118
|
+
|
|
119
|
+
it("discovers tools without replacing the host registry or loading a fallback", async () => {
|
|
120
|
+
const load = vi.fn(async () => {
|
|
121
|
+
throw new Error("must preserve the host registry");
|
|
122
|
+
});
|
|
123
|
+
setWebMcpPolyfillLoader(load);
|
|
124
|
+
await register("lookup", (input) => ({ received: input }));
|
|
125
|
+
expect(await bridge().snapshotForDispatch()).toEqual([
|
|
126
|
+
expect.objectContaining({
|
|
127
|
+
name: "lookup",
|
|
128
|
+
parametersSchema: {
|
|
129
|
+
type: "object",
|
|
130
|
+
properties: { query: { type: "string" } },
|
|
131
|
+
},
|
|
132
|
+
annotations: { readOnlyHint: true, untrustedContentHint: true },
|
|
133
|
+
untrustedContentHint: true,
|
|
134
|
+
}),
|
|
135
|
+
]);
|
|
136
|
+
expect(load).not.toHaveBeenCalled();
|
|
137
|
+
expect(
|
|
138
|
+
(browser.document as unknown as { modelContext: TestContext })
|
|
139
|
+
.modelContext
|
|
140
|
+
).toBe(context);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it.each(
|
|
144
|
+
[
|
|
145
|
+
{ query: "shoes" },
|
|
146
|
+
{ nested: { text: 'quotes " and emoji 😀' }, values: [true, null, 2] },
|
|
147
|
+
{ toString: "an ordinary JSON property", valueOf: 7, __proto__: null },
|
|
148
|
+
["array", { nested: true }],
|
|
149
|
+
].map((args) => ({ args }))
|
|
150
|
+
)(
|
|
151
|
+
"executes JSON arguments once without mutating them: $args",
|
|
152
|
+
async ({ args }) => {
|
|
153
|
+
const execute = vi.fn((input: unknown) => ({ received: input }));
|
|
154
|
+
const confirm = vi.fn(async () => true);
|
|
155
|
+
const before = JSON.stringify(args);
|
|
156
|
+
await register("lookup", execute);
|
|
157
|
+
const result = await bridge(confirm).executeToolCall(
|
|
158
|
+
"webmcp:lookup",
|
|
159
|
+
args
|
|
160
|
+
);
|
|
161
|
+
expect(result).toEqual({
|
|
162
|
+
content: [{ type: "text", text: JSON.stringify({ received: args }) }],
|
|
163
|
+
});
|
|
164
|
+
expect(execute).toHaveBeenCalledTimes(1);
|
|
165
|
+
expect(execute.mock.calls[0]![0]).toEqual(JSON.parse(before));
|
|
166
|
+
expect(confirm).toHaveBeenCalledTimes(1);
|
|
167
|
+
expect(JSON.stringify(args)).toBe(before);
|
|
168
|
+
expect(Object.getOwnPropertySymbols(args)).toEqual([]);
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
it.each([undefined, () => "not JSON"])(
|
|
173
|
+
"defaults absent/non-JSON arguments to an empty object",
|
|
174
|
+
async (args) => {
|
|
175
|
+
const execute = vi.fn((input: unknown) => input);
|
|
176
|
+
await register("empty", execute);
|
|
177
|
+
expect(await bridge().executeToolCall("webmcp:empty", args)).toEqual({
|
|
178
|
+
content: [{ type: "text", text: "{}" }],
|
|
179
|
+
});
|
|
180
|
+
expect(execute).toHaveBeenCalledTimes(1);
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
it("keeps the legacy empty-object fallback for circular arguments", async () => {
|
|
185
|
+
const args: { self?: unknown } = {};
|
|
186
|
+
args.self = args;
|
|
187
|
+
await register("empty", (input) => input);
|
|
188
|
+
expect(await bridge().executeToolCall("webmcp:empty", args)).toEqual({
|
|
189
|
+
content: [{ type: "text", text: "{}" }],
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it.each([
|
|
194
|
+
["string", "hello", "hello"],
|
|
195
|
+
["null", null, "null"],
|
|
196
|
+
[
|
|
197
|
+
"envelope",
|
|
198
|
+
{ isError: true, content: [{ type: "text", text: "invalid input" }] },
|
|
199
|
+
null,
|
|
200
|
+
],
|
|
201
|
+
])("preserves a %s result", async (_label, result, text) => {
|
|
202
|
+
await register("result", () => result);
|
|
203
|
+
expect(await bridge().executeToolCall("webmcp:result", {})).toEqual(
|
|
204
|
+
text === null ? result : { content: [{ type: "text", text }] }
|
|
205
|
+
);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const circularResult: { self?: unknown } = {};
|
|
209
|
+
circularResult.self = circularResult;
|
|
210
|
+
it.each([
|
|
211
|
+
{ label: "undefined", result: undefined },
|
|
212
|
+
{ label: "circular object", result: circularResult },
|
|
213
|
+
{ label: "function", result: () => "function result" },
|
|
214
|
+
{ label: "BigInt", result: 1n },
|
|
215
|
+
])(
|
|
216
|
+
"preserves the installed runtime's $label-result semantics without retries",
|
|
217
|
+
async ({ result }) => {
|
|
218
|
+
const execute = vi.fn(() => result);
|
|
219
|
+
await register("unserializable_result", execute);
|
|
220
|
+
const output = await bridge().executeToolCall(
|
|
221
|
+
"webmcp:unserializable_result",
|
|
222
|
+
{}
|
|
223
|
+
);
|
|
224
|
+
if (legacy) {
|
|
225
|
+
expect(output).toEqual({
|
|
226
|
+
content: [{ type: "text", text: String(result) }],
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
expect(output.isError).toBe(true);
|
|
230
|
+
}
|
|
231
|
+
expect(execute).toHaveBeenCalledTimes(1);
|
|
232
|
+
}
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
it("preserves the installed runtime's empty-string result", async () => {
|
|
236
|
+
await register("empty_result", () => "");
|
|
237
|
+
expect(await bridge().executeToolCall("webmcp:empty_result", {})).toEqual({
|
|
238
|
+
content: [{ type: "text", text: legacy ? "Operation succeeded" : "" }],
|
|
239
|
+
});
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
it("returns callback failures without retrying a side effect", async () => {
|
|
243
|
+
const execute = vi.fn(() => {
|
|
244
|
+
throw new Error("failure after side effect");
|
|
245
|
+
});
|
|
246
|
+
await register("failed", execute);
|
|
247
|
+
expect((await bridge().executeToolCall("webmcp:failed", {})).isError).toBe(
|
|
248
|
+
true
|
|
249
|
+
);
|
|
250
|
+
expect(execute).toHaveBeenCalledTimes(1);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it("does not execute when approval is denied", async () => {
|
|
254
|
+
const execute = vi.fn(() => "unexpected");
|
|
255
|
+
await register("denied", execute);
|
|
256
|
+
expect(
|
|
257
|
+
(
|
|
258
|
+
await bridge(vi.fn(async () => false)).executeToolCall(
|
|
259
|
+
"webmcp:denied",
|
|
260
|
+
{}
|
|
261
|
+
)
|
|
262
|
+
).isError
|
|
263
|
+
).toBe(true);
|
|
264
|
+
expect(execute).not.toHaveBeenCalled();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("cancels the invocation and forwards the signal when the runtime supports it", async () => {
|
|
268
|
+
let started!: () => void;
|
|
269
|
+
const ready = new Promise<void>((resolve) => {
|
|
270
|
+
started = resolve;
|
|
271
|
+
});
|
|
272
|
+
let callbackSignal: AbortSignal | undefined;
|
|
273
|
+
const execute = vi.fn(
|
|
274
|
+
(_input: unknown, options?: { signal: AbortSignal }) => {
|
|
275
|
+
callbackSignal = options?.signal;
|
|
276
|
+
started();
|
|
277
|
+
return new Promise(() => {});
|
|
278
|
+
}
|
|
279
|
+
);
|
|
280
|
+
await register("wait", execute);
|
|
281
|
+
const controller = new AbortController();
|
|
282
|
+
const pending = bridge().executeToolCall(
|
|
283
|
+
"webmcp:wait",
|
|
284
|
+
{},
|
|
285
|
+
controller.signal
|
|
286
|
+
);
|
|
287
|
+
await ready;
|
|
288
|
+
controller.abort();
|
|
289
|
+
expect((await pending).isError).toBe(true);
|
|
290
|
+
expect(execute).toHaveBeenCalledTimes(1);
|
|
291
|
+
if (!legacy)
|
|
292
|
+
await vi.waitFor(() => expect(callbackSignal?.aborted).toBe(true));
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
it("does not execute a removed registration", async () => {
|
|
296
|
+
const execute = vi.fn(() => "unexpected");
|
|
297
|
+
await register("removed", execute);
|
|
298
|
+
registrations.at(-1)!.abort();
|
|
299
|
+
expect((await bridge().executeToolCall("webmcp:removed", {})).isError).toBe(
|
|
300
|
+
true
|
|
301
|
+
);
|
|
302
|
+
expect(execute).not.toHaveBeenCalled();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it.runIf(legacy)(
|
|
306
|
+
"lazily installs the real v5 fallback without v6 browser APIs",
|
|
307
|
+
async () => {
|
|
308
|
+
const runtime = (
|
|
309
|
+
browser as unknown as {
|
|
310
|
+
WebMCPPolyfill: {
|
|
311
|
+
cleanupWebMCPPolyfill(): void;
|
|
312
|
+
initializeWebMCPPolyfill(): void;
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
).WebMCPPolyfill;
|
|
316
|
+
runtime.cleanupWebMCPPolyfill();
|
|
317
|
+
expect(
|
|
318
|
+
(browser.document as unknown as { modelContext?: unknown }).modelContext
|
|
319
|
+
).toBeUndefined();
|
|
320
|
+
expect(Reflect.get(browser.Promise, "withResolvers")).toBeUndefined();
|
|
321
|
+
expect(browser.AbortSignal.any).toBeUndefined();
|
|
322
|
+
expect(browser.URL.parse).toBeUndefined();
|
|
323
|
+
const load = vi.fn(async () => runtime);
|
|
324
|
+
setWebMcpPolyfillLoader(load);
|
|
325
|
+
const consumer = bridge();
|
|
326
|
+
expect(await consumer.snapshotForDispatch()).toEqual([]);
|
|
327
|
+
context = (browser.document as unknown as { modelContext: TestContext })
|
|
328
|
+
.modelContext;
|
|
329
|
+
await register("legacy_fallback", () => undefined);
|
|
330
|
+
expect(
|
|
331
|
+
await consumer.executeToolCall("webmcp:legacy_fallback", {})
|
|
332
|
+
).toEqual({
|
|
333
|
+
content: [{ type: "text", text: "undefined" }],
|
|
334
|
+
});
|
|
335
|
+
expect(load).toHaveBeenCalledTimes(1);
|
|
336
|
+
expect(consumer.isOperational()).toBe(true);
|
|
337
|
+
}
|
|
338
|
+
);
|
|
339
|
+
});
|
package/src/webmcp-polyfill.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Standalone WebMCP
|
|
2
|
+
* Standalone WebMCP v5 fallback chunk (`dist/webmcp-polyfill.js`).
|
|
3
|
+
* Keep this fallback on v5 so upgrades do not change existing consumers'
|
|
4
|
+
* browser requirements or callback-result semantics. Host-installed v6 is
|
|
5
|
+
* supported separately by the bridge's dual-contract argument adapter.
|
|
3
6
|
*
|
|
4
7
|
* The IIFE/CDN widget bundle marks `@mcp-b/webmcp-polyfill` external so the
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
+
* polyfill never ships to consumers who don't enable WebMCP. When
|
|
9
|
+
* `config.webmcp.enabled` is true and no `document.modelContext` exists yet,
|
|
10
|
+
* the bridge dynamically
|
|
8
11
|
* imports this chunk from a URL derived from the widget script's own `src`
|
|
9
12
|
* (see the loader registered in `index-global.ts`).
|
|
10
13
|
*
|
|
@@ -48,7 +48,7 @@ describe("webmcp-runtime bundle split", () => {
|
|
|
48
48
|
}
|
|
49
49
|
// No polyfill inlined: `registerTool` / `cfworker` only exist inside
|
|
50
50
|
// @mcp-b/webmcp-polyfill (the class merely CALLS the injected module's
|
|
51
|
-
//
|
|
51
|
+
// initializer, so both initializer names legitimately appear here).
|
|
52
52
|
expect(chunk.includes("registerTool")).toBe(false);
|
|
53
53
|
expect(chunk.includes("cfworker")).toBe(false);
|
|
54
54
|
// A CJS twin exists for require()-based consumers.
|
|
@@ -121,7 +121,7 @@ export class WebMcpBridge {
|
|
|
121
121
|
|
|
122
122
|
let infos: ModelContextToolInfo[];
|
|
123
123
|
try {
|
|
124
|
-
infos = await mc.getTools();
|
|
124
|
+
infos = (await mc.getTools()).filter(isLocalTool);
|
|
125
125
|
} catch (err) {
|
|
126
126
|
log.warn("getTools() threw: shipping an empty WebMCP snapshot.", err);
|
|
127
127
|
return [];
|
|
@@ -162,7 +162,7 @@ export class WebMcpBridge {
|
|
|
162
162
|
* - tool not in registry (e.g. unmounted between snapshot and call)
|
|
163
163
|
* - tool excluded by the client allowlist
|
|
164
164
|
* - user declined the confirm gate
|
|
165
|
-
* - `execute()` threw or
|
|
165
|
+
* - `execute()` threw or returned a non-serializable result
|
|
166
166
|
* - `execute()` exceeded the 30s timeout
|
|
167
167
|
* - `signal` fired (session-level `cancel()`)
|
|
168
168
|
*
|
|
@@ -204,7 +204,7 @@ export class WebMcpBridge {
|
|
|
204
204
|
|
|
205
205
|
let infos: ModelContextToolInfo[];
|
|
206
206
|
try {
|
|
207
|
-
infos = await mc.getTools();
|
|
207
|
+
infos = (await mc.getTools()).filter(isLocalTool);
|
|
208
208
|
} catch (err) {
|
|
209
209
|
const message = err instanceof Error ? err.message : String(err);
|
|
210
210
|
return errorResult(`Failed to read WebMCP registry: ${message}`);
|
|
@@ -275,7 +275,7 @@ export class WebMcpBridge {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
try {
|
|
278
|
-
const raw = await mc.executeTool(info,
|
|
278
|
+
const raw = await mc.executeTool(info, createCompatibleToolInput(args), {
|
|
279
279
|
signal: controller.signal,
|
|
280
280
|
});
|
|
281
281
|
return normalizeSerializedResult(raw);
|
|
@@ -329,7 +329,8 @@ export class WebMcpBridge {
|
|
|
329
329
|
const mod = await this.#deps.loadPolyfill();
|
|
330
330
|
// Idempotent: no-ops if `document.modelContext` already exists (native or
|
|
331
331
|
// a prior install by the host page).
|
|
332
|
-
mod.initializeWebMCPPolyfill();
|
|
332
|
+
if ("initializeWebMCPPolyfill" in mod) mod.initializeWebMCPPolyfill();
|
|
333
|
+
else mod.installWebMCP();
|
|
333
334
|
this.#installed = true;
|
|
334
335
|
} catch (err) {
|
|
335
336
|
log.warn(
|
|
@@ -516,6 +517,28 @@ const normalizeSerializedResult = (raw: string | null): WebMcpToolResult => {
|
|
|
516
517
|
return { content: [{ type: "text", text }] };
|
|
517
518
|
};
|
|
518
519
|
|
|
520
|
+
/**
|
|
521
|
+
* One invocation works with both contracts: v5/native previews coerce input to
|
|
522
|
+
* JSON text, whereas v6 serializes the object. Keep the coercion hook off the
|
|
523
|
+
* JSON payload and clone first so we never mutate caller-owned arguments.
|
|
524
|
+
* Do not probe by executing/retrying: page tools may have irreversible effects.
|
|
525
|
+
*/
|
|
526
|
+
const createCompatibleToolInput = (args: unknown): object => {
|
|
527
|
+
let json: string;
|
|
528
|
+
try {
|
|
529
|
+
json = JSON.stringify(args) ?? "{}";
|
|
530
|
+
} catch {
|
|
531
|
+
// Preserve the historical fallback for non-serializable arguments.
|
|
532
|
+
json = "{}";
|
|
533
|
+
}
|
|
534
|
+
const input: unknown = JSON.parse(json);
|
|
535
|
+
if (input === null || typeof input !== "object") {
|
|
536
|
+
throw new TypeError("WebMCP tool arguments must be an object or array.");
|
|
537
|
+
}
|
|
538
|
+
Object.defineProperty(input, Symbol.toPrimitive, { value: () => json });
|
|
539
|
+
return input;
|
|
540
|
+
};
|
|
541
|
+
|
|
519
542
|
const errorResult = (message: string): WebMcpToolResult => ({
|
|
520
543
|
isError: true,
|
|
521
544
|
content: [{ type: "text", text: message }],
|
|
@@ -549,20 +572,12 @@ const previewArgs = (args: unknown): string => {
|
|
|
549
572
|
}
|
|
550
573
|
};
|
|
551
574
|
|
|
552
|
-
/**
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
if (args === undefined) return "{}";
|
|
559
|
-
try {
|
|
560
|
-
const json = JSON.stringify(args);
|
|
561
|
-
return json === undefined ? "{}" : json;
|
|
562
|
-
} catch {
|
|
563
|
-
return "{}";
|
|
564
|
-
}
|
|
565
|
-
};
|
|
575
|
+
/** Keep name-only dispatch scoped to this document, not related frames. */
|
|
576
|
+
const isLocalTool = (info: ModelContextToolInfo): boolean =>
|
|
577
|
+
(info.window === undefined ||
|
|
578
|
+
(typeof window !== "undefined" && info.window === window)) &&
|
|
579
|
+
(info.origin === undefined ||
|
|
580
|
+
(typeof location !== "undefined" && info.origin === location.origin));
|
|
566
581
|
|
|
567
582
|
/**
|
|
568
583
|
* `JSON.stringify` that tolerates circular references and non-serializable
|
package/dist/chunk-3255TNY7.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
var t="4.27.0";var n=t;export{n as a};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import{a as J}from"./chunk-6V3MNEKB.js";import{a as Y}from"./chunk-3255TNY7.js";var $=16e3,he=24e3,pe=4096,fe=$*2*8,me=4,ve=1380533830,ye=3e4,be=1e3,Se=5e3,we=3e4,ke=3e4,Ae=6e5,Ce="full-duplex-v1",Me=300,Te=2e3,Ee=2e3,_e="runtype-browser-v1",Ie=54e4,Pe="You're talking to an AI assistant. Voice is processed by OpenAI.",Ve=new Map([["idle_timeout","Voice call ended after a quiet period."],["max_duration","This voice call reached its time limit."],["provider_ended","The voice session ended."],["quota","Voice is unavailable right now."],["auth_expired","Voice session expired. Tap the mic to reconnect."]]),Z=["provider_error","server_restart"],V="Voice connection lost.",Re=8e3,Le=4e3,Oe=12,Q=2e3;function Ne(i,e){let t="Conversation so far:",s=e.slice(0,Le).trim(),n=Re-(s?s.length+2:0)-t.length,r=[];for(let d of i.slice(-Oe).reverse()){let a=d.content.replace(/\s+/g," ").trim();if(!a)continue;a.length>Q&&(a=`${a.slice(0,Q-1)}\u2026`);let o=`${d.role==="user"?"User":"Assistant"}: ${a}`;if(o.length+1>n)break;n-=o.length+1,r.unshift(o)}let l=r.length>0?[`${t}
|
|
2
|
-
${r.join(`
|
|
3
|
-
`)}`]:[];return s&&l.push(s),l.join(`
|
|
4
|
-
|
|
5
|
-
`)}function De(i){return i.byteLength>=44&&new DataView(i).getUint32(0,!1)===ve?new Uint8Array(i,44):new Uint8Array(i)}function ee(i){let e=i.replace(/\/+$/,"");return/^wss?:\/\//i.test(e)?e:/^https?:\/\//i.test(e)?e.replace(/^http/i,"ws"):`${typeof window<"u"&&window.location?.protocol==="https:"?"wss:":"ws:"}//${e}`}function Ue(i){return ee(i).replace(/^ws/i,"http")}function xe(i){return typeof i!="number"||!Number.isFinite(i)?we:Math.min(Ae,Math.max(ke,Math.round(i)))}var I=class{constructor(e){this.config=e;this.type="runtype";this.#e=null;this.#u=null;this.#g=[];this.#i=null;this.#r=null;this.#a=null;this.#v=[];this.#y=0;this.#n=null;this.#c=!1;this.#l=!1;this.#s=0;this.#A=!1;this.#K=Number.NEGATIVE_INFINITY;this.#o=null;this.#d=!1;this.#C=!1;this.#M=0;this.#f=0;this.#b=!1;this.bridge=null;this.#E=!1;this.#m=new Set;this.#S=new Map;this.#_=new Set;this.#I=null;this.#h=null;this.#D=!1;this.#U=!1;this.#x=!1;this.#P=null;this.#B=null;this.#W=Promise.resolve();this.#V=new Set;this.#w=!1;this.#R=new Set;this.#L=new Set;this.#J=[];this.#$=[];this.#q=[];this.#j=[];this.#H=[]}#e;#u;#g;#i;#r;#a;#v;#y;#n;#c;#l;#s;#A;#K;#o;#d;#C;#M;#f;#T;#b;#E;#m;#S;#_;#N;#I;#h;#Y;#D;#U;#x;#P;#B;#W;#V;#w;#R;#L;#J;#$;#q;#j;#H;async connect(){}setSessionBridge(e){this.bridge=e}prewarm(){if(this.#c||this.#o)return;let e=this.config?.agentId,t=this.config?.clientToken,s=this.config?.host;if(!e||!t||!s)return;let n=Date.now();if(!(n-this.#K<ye)){this.#K=n;try{this.config?.prewarmMode==="attach"?this.#re(s,e,t):this.#oe(s,e,t)}catch{}}}#oe(e,t,s){if(typeof fetch!="function")return;let n=`${Ue(e)}/v1/client/agents/${encodeURIComponent(t)}/voice/prewarm`;fetch(n,{method:"POST",headers:{Authorization:`Bearer ${s}`},keepalive:!0}).catch(()=>{})}#re(e,t,s){let n=new URLSearchParams,r=this.config?.attachIdleMs,l=xe(r);r!==void 0&&n.set("attachIdleMs",String(l));let d=new WebSocket(this.#Q(e,t,n),["runtype.bearer","runtype.attach",s]);d.binaryType="arraybuffer";let a=()=>{},o=new Promise(u=>{a=u}),c={ws:d,state:"connecting",ready:o,settle:a,openedAt:0,idleTimer:void 0};this.#o=c,d.onopen=()=>{this.#o===c&&(c.state=d.protocol==="runtype.attach"?"attached":"live",c.openedAt=Date.now(),this.#Z(c,l),c.state==="attached"&&d.send('{"type":"ping"}'),c.settle())},d.onmessage=u=>{if(this.#o!==c||typeof u.data!="string")return;let g;try{g=JSON.parse(u.data)}catch{return}g.type==="attached"&&typeof g.idleMs=="number"&&this.#Z(c,g.idleMs-(Date.now()-c.openedAt))},d.onerror=()=>{},d.onclose=()=>{this.#o===c&&this.#O()}}#Z(e,t){clearTimeout(e.idleTimer),e.idleTimer=setTimeout(()=>{this.#o===e&&this.#O()},Math.max(0,t))}#O(){let e=this.#o;if(!e)return;this.#o=null,clearTimeout(e.idleTimer);let{ws:t}=e;if(t.onopen=null,t.onmessage=null,t.onerror=null,t.onclose=null,e.settle(),t.readyState===WebSocket.CONNECTING||t.readyState===WebSocket.OPEN)try{t.close(1e3,"prewarm released")}catch{}}async#ae(){let e=this.#o;if(!e)return null;if(e.state==="connecting"){let t;await Promise.race([e.ready,new Promise(s=>{t=setTimeout(s,Se)})]),clearTimeout(t)}return this.#o!==e?null:e.state==="connecting"||e.ws.readyState!==WebSocket.OPEN?(this.#O(),null):(this.#o=null,clearTimeout(e.idleTimer),{ws:e.ws,live:e.state==="live"})}#Q(e,t,s){let n=["partial_transcript","context"];this.bridge&&this.config?.clientDelegation!==!1&&n.push("client_delegation","delegation_update","delegation_read_back"),s&&n.unshift("attach");let r=new URLSearchParams({voiceProtocol:_e,clientVersion:`persona/${Y}`,voiceCapabilities:Ce,clientCapabilities:n.join(",")});return s?.forEach((l,d)=>r.set(d,l)),`${ee(e)}/ws/agents/${encodeURIComponent(t)}/voice?${r}`}async startListening(){if(this.#c)return;let e=this.#U;e||(this.#D=!1,this.#h=null),this.#U=!1;let t=this.config?.agentId,s=this.config?.clientToken,n=this.config?.host;if(!t)throw new Error("Runtype voice requires an agentId");if(!s)throw new Error("Runtype voice requires a clientToken");if(!n)throw new Error("Runtype voice requires a host (or widget apiUrl)");let r=++this.#s;this.#A=!1,this.#c=!0;try{let l=await navigator.mediaDevices.getUserMedia({audio:{sampleRate:$,channelCount:1,echoCancellation:!0}});if(r!==this.#s){l.getTracks().forEach(g=>g.stop());return}this.#i=l;let d=window.AudioContext||window.webkitAudioContext,a=new d({sampleRate:$});if(this.#u=a,a.state==="suspended"){if(await Promise.race([a.resume().catch(()=>{}),new Promise(g=>setTimeout(g,e?500:be))]),r!==this.#s)return;if(a.state!=="running")throw e?new Error(this.#h=`${V} Tap the mic to reconnect.`):new Error("Audio blocked until a tap")}let o=this.config?.createPlaybackEngine?await this.config.createPlaybackEngine():new J(he);if(r!==this.#s){o.destroy(),l.getTracks().forEach(g=>g.stop()),a.close().catch(()=>{});return}this.#n=o,this.#X(),this.#le(a,l,r);let c=await this.#ae();if(r!==this.#s){c?.ws.close(1e3,"client ended call");return}if(c){let{ws:g}=c;this.#e=g,this.#ee(g,r),c.live||g.send('{"type":"start"}'),this.#F(g),this.#t("listening");return}let u=new WebSocket(this.#Q(n,t),["runtype.bearer",s]);u.binaryType="arraybuffer",this.#e=u,u.onopen=()=>{r===this.#s&&(this.#F(u),this.#t("listening"))},this.#ee(u,r)}catch(l){throw this.#p(),this.#k(l),this.#t("error"),l}}#ee(e,t){e.onmessage=s=>this.#de(s,t),e.onerror=()=>{t!==this.#s||this.#I||(this.#k(new Error("Voice connection failed")),this.#t("error"),this.#p())},e.onclose=s=>{if(this.#A){this.#A=!1;return}if(t!==this.#s)return;let n=this.#I??(s.code===4408?{reason:"idle_timeout"}:null);if(!n){if(s.code!==1e3){let r=s.code?` (code ${s.code})`:"";this.#k(new Error(`Voice connection closed${r}`)),this.#t("error")}else this.#t("idle");this.#p();return}this.#ce(n.reason,n.message)}}#ce(e,t){let s=Z.includes(e)&&!this.#D;if(this.#p(),s){this.#D=!0,this.#c=!0,this.#h=e==="provider_error"?`${V} Reconnecting\u2026`:"Reconnecting\u2026",this.#Y=setTimeout(()=>{this.#c=!1,this.#U=!0,this.startListening().catch(()=>{})},1e3+Math.random()*1e3),this.#t("idle");return}if(Z.includes(e))return this.#h=V,this.#k(new Error(V)),this.#t("error");if(e==="error")return this.#t("error");this.#h=e==="quota"&&t||Ve.get(e)||null,this.#t("idle")}takeNotice(){let e=this.#h;return this.#h=null,e}getDisclosure(){let e=this.config?.disclosureText;return this.#c&&this.#d&&e!==!1?e||Pe:null}async stopListening(){this.#p(),this.#t("idle")}async disconnect(){this.#p(),this.#t("disconnected"),this.#J=[],this.#$=[],this.#q=[],this.#j=[],this.#H=[]}stopPlayback(){this.#ne();let e=this.#e;e&&e.readyState===WebSocket.OPEN&&(this.#d&&(this.#M=Date.now()+Te,e.send('{"type":"cancel"}')),this.#t("listening"))}getInterruptionMode(){return"barge-in"}isBargeInActive(){return this.#c}onLevel(e){this.#g.push(e)}async deactivateBargeIn(){this.#p(),this.#t("idle")}#le(e,t,s){let n=e.createMediaStreamSource(t);this.#r=n;let r=e.createScriptProcessor(pe,1,1);this.#a=r,r.onaudioprocess=l=>{if(s!==this.#s)return;let d=this.#e,a=d?.readyState===WebSocket.OPEN;if(d&&!a&&d.readyState!==WebSocket.CONNECTING)return;let o=l.inputBuffer.getChannelData(0),c=new Int16Array(o.length),u=0;for(let g=0;g<o.length;g++){let m=Math.max(-1,Math.min(1,o[g]));u+=m*m,c[g]=m<0?m*32768:m*32767}if(d&&a)this.#F(d),d.send(c.buffer);else for(this.#v.push(c.buffer),this.#y+=c.byteLength;this.#y>fe;)this.#y-=this.#v.shift().byteLength;if(this.#g.length>0){let g=Math.sqrt(u/o.length),m=Math.max(0,Math.min(1,g*me));for(let h of this.#g)h(m)}},n.connect(r),r.connect(e.destination)}#F(e){for(let t of this.#v)e.send(t);this.#v=[],this.#y=0}#de(e,t){if(t!==this.#s)return;if(e.data instanceof ArrayBuffer){this.#pe(e.data,t);return}let s;try{s=JSON.parse(e.data)}catch{return}switch(s.type){case"session_config":s.speechMode&&(this.#d=s.speechMode==="speech_to_speech",this.#n?.setContinuousMode?.(this.#d)),Array.isArray(s.capabilities)&&(this.#m=new Set(s.capabilities)),typeof s.callId=="string"&&(this.#N=s.callId),this.#E=this.#d&&this.#m.has("client_delegation")&&!!this.bridge,this.#d&&!this.#l&&this.#t("listening"),this.#m.has("context")&&!this.#x&&(this.#x=!0,this.#P=this.#ue());break;case"transcript_update":{let n=s.role==="assistant"?"assistant":"user",r=s.utteranceId??s.turnId;if(!r||n==="assistant"&&this.#z())break;let l=String(r);if(n==="user")s.final===!0&&this.#te(t),this.#w=!1;else{if(this.#m.has("delegation_read_back")){let d=s.delegationId;typeof d=="string"&&this.#V.has(d)&&!this.#R.has(l)&&this.#L.add(l)}else this.#w&&!this.#R.has(l)&&(this.#L.add(l),this.#w=!1);if(this.#R.add(l),this.#L.has(l))break}this.#G(n,s.text??"",s.final===!0,{turnId:l,...typeof s.startMs=="number"&&{startMs:s.startMs},...typeof s.endMs=="number"&&{endMs:s.endMs},...this.#E&&{caption:!0}});break}case"delegation_started":case"delegation_completed":{let n=String(s.delegationId??s.turnId);if(s.type==="delegation_started"){this.#te(t);let r=s.input;this.#E&&r&&this.#he(n,{delegationId:n,userText:String(r.text??""),userUtteranceIds:Array.isArray(r.userUtteranceIds)?r.userUtteranceIds.filter(l=>typeof l=="string"):[],messages:Array.isArray(r.messages)?r.messages:[]},t)}else s.speak!==!1&&this.#V.has(n)&&(this.#w=!0);this.#C=s.type==="delegation_started",!this.#l&&!this.#z()&&this.#t(this.#C?"processing":"listening");break}case"delegation_cancelled":this.#se(String(s.delegationId),s.reason!=="session_ending"&&s.reason!=="provider_cancelled");break;case"warning":console.warn(`[Persona voice] ${s.code}: ${s.message??""}`,this.#N??""),s.delegationId&&(s.code==="UNKNOWN_DELEGATION"||s.code==="LATE_RESULT_LIMIT")&&this.#se(String(s.delegationId));break;case"audio_clear":this.#ne(),this.#M=0,this.#t("listening");break;case"transcript_interim":this.#t("listening"),this.#G("user",s.text??"",!1);break;case"transcript_final":{let n=s.role==="assistant"?"assistant":"user";this.#t(n==="user"?"processing":"speaking"),this.#G(n,s.text??"",!0);break}case"audio_end":this.#n?this.#n.markStreamEnd():(this.#l=!1,this.#t("listening"));break;case"metrics":this.#me({llmMs:s.llm_ms,ttsMs:s.tts_ms,firstAudioMs:s.first_audio_ms,totalMs:s.total_ms});break;case"error":this.#k(new Error(s.error||"Voice error")),this.#t("error");break;case"session_end":this.#I={reason:String(s.reason),...typeof s.message=="string"&&{message:s.message}};break}}async#ue(){let e=this.bridge?.getHistory()??[],t="";try{let s=this.config?.callContext;t=String((typeof s=="function"?await s():s)??"")}catch{}return Ne(e,t)}#te(e){let t=this.#P;if(!t)return;this.#P=null;let s=!1,n=t.then(r=>{let l=this.#e;s||!r||e!==this.#s||l?.readyState!==WebSocket.OPEN||l.send(JSON.stringify({type:"context",text:r}))});this.#B={done:n,drop:()=>s=!0}}async#ge(){let e=this.#B;if(!e)return;let t,s=await Promise.race([e.done.then(()=>!1),new Promise(n=>{t=setTimeout(()=>n(!0),Ee)})]);clearTimeout(t),s&&e.drop()}#se(e,t=!1){this.#_.add(e),this.bridge?.dropDelegation?.(e,t),this.#S.get(e)?.abort()}#he(e,t,s){let n=this.bridge,r=(l,d,a)=>{let o=this.#e;return s!==this.#s||o?.readyState!==WebSocket.OPEN||this.#_.has(e)?!1:(o.send(JSON.stringify({type:l,delegationId:e,status:d,text:a})),!0)};this.#W=this.#W.then(async()=>{if(s!==this.#s||this.#_.has(e))return;let l=await n.runDelegatedTurn(t).catch(()=>({status:"failed",text:""}));await this.#ge();let d=!!l.followUp,a=d&&this.#m.has("delegation_update");if(!r(a?"delegation_update":"delegation_result",d&&!a?"completed":l.status,l.text)||(l.status!=="failed"&&l.inChat!==!1&&this.#V.add(e),!l.followUp))return;let c=new AbortController;this.#S.set(e,c),l.followUp({signal:c.signal,approvalTimeoutMs:this.config?.approvalTimeoutMs&&Math.min(this.config.approvalTimeoutMs,Ie),readBack:a,onUpdate:u=>r("delegation_update","pending_approval",u)}).then(u=>{this.#S.delete(e),a&&u&&!c.signal.aborted&&r("delegation_result",u.status,u.text)})})}#pe(e,t){if(t!==this.#s||!this.#n||this.#z())return;let s=De(e);s.length!==0&&(this.#l||(this.#l=!0,this.#t("speaking")),this.#X(),this.#n.enqueue(s),this.#d&&this.#fe(s.length))}#z(){return Date.now()<this.#M}#X(){if(this.#b||!this.#n)return;this.#b=!0;let e=this.#s;this.#n.onFinished(()=>{e===this.#s&&(this.#b=!1,this.#ie())})}#fe(e){let t=Date.now();this.#f=Math.max(t,this.#f)+e/48,clearTimeout(this.#T),this.#T=setTimeout(()=>this.#ie(),this.#f-t+Me)}#ie(){this.#l=!1,this.#f=0,this.#e&&this.#e.readyState===WebSocket.OPEN&&this.#t(this.#C?"processing":"listening")}#ne(){this.#n?.flush(),this.#b=!1,this.#X(),this.#l=!1,this.#f=0,clearTimeout(this.#T)}#p(){this.#s+=1,this.#c=!1,this.#l=!1,this.#O(),this.#d=!1,this.#C=!1,this.#E=!1,this.#m=new Set;for(let e of this.#S.values())e.abort();if(this.#S.clear(),this.#_.clear(),this.#N=void 0,this.#I=null,clearTimeout(this.#Y),this.#x=!1,this.#P=null,this.#B=null,this.#W=Promise.resolve(),this.#V.clear(),this.#w=!1,this.#R.clear(),this.#L.clear(),this.#b=!1,this.#M=0,this.#f=0,clearTimeout(this.#T),this.#v=[],this.#y=0,this.#a&&(this.#a.onaudioprocess=null,this.#a.disconnect(),this.#a=null),this.#r&&(this.#r.disconnect(),this.#r=null),this.#i&&(this.#i.getTracks().forEach(e=>e.stop()),this.#i=null),this.#u&&(this.#u.close().catch(()=>{}),this.#u=null),this.#n&&(this.#n.destroy(),this.#n=null),this.#e){this.#A=!0;try{this.#e.close(1e3,"client ended call")}catch{}this.#e=null}}onResult(e){this.#J.push(e)}onError(e){this.#$.push(e)}onStatusChange(e){this.#q.push(e)}onTranscript(e){this.#j.push(e)}onMetrics(e){this.#H.push(e)}#t(e){this.#q.forEach(t=>t(e))}#k(e){this.#$.forEach(t=>t(e))}#G(e,t,s,n){this.#j.forEach(r=>r(e,t,s,n))}#me(e){this.#H.forEach(t=>t(e))}};var T=class{constructor(e={}){this.config=e;this.type="browser";this.#e=null;this.#u=[];this.#g=[];this.#i=[];this.#r=!1;this.#a=typeof window<"u"?window:void 0}#e;#u;#g;#i;#r;#a;async connect(){this.#i.forEach(e=>e("connected"))}async startListening(){try{if(this.#r)throw new Error("Already listening");if(!this.#a)throw new Error("Window object not available");let e=this.#a.SpeechRecognition||this.#a.webkitSpeechRecognition;if(!e)throw new Error("Browser speech recognition not supported");this.#e=new e,this.#e.lang=this.config?.language||"en-US",this.#e.continuous=this.config?.continuous||!1,this.#e.interimResults=!0,this.#e.onresult=t=>{let s=Array.from(t.results).map(r=>r[0]).map(r=>r.transcript).join(""),n=t.results[t.results.length-1].isFinal;this.#u.forEach(r=>r({text:s,confidence:n?.8:.5,provider:"browser"})),n&&!this.config?.continuous&&this.stopListening()},this.#e.onerror=t=>{this.#g.forEach(s=>s(new Error(t.error))),this.#i.forEach(s=>s("error"))},this.#e.onstart=()=>{this.#r=!0,this.#i.forEach(t=>t("listening"))},this.#e.onend=()=>{this.#r=!1,this.#i.forEach(t=>t("idle"))},this.#e.start()}catch(e){throw this.#g.forEach(t=>t(e)),this.#i.forEach(t=>t("error")),e}}async stopListening(){this.#e&&(this.#e.stop(),this.#e=null),this.#r=!1,this.#i.forEach(e=>e("idle"))}onResult(e){this.#u.push(e)}onError(e){this.#g.push(e)}onStatusChange(e){this.#i.push(e)}async disconnect(){await this.stopListening(),this.#i.forEach(e=>e("disconnected"))}static isSupported(){return"SpeechRecognition"in window||"webkitSpeechRecognition"in window}};function R(i){switch(i.type){case"runtype":if(!i.runtype)throw new Error("Runtype voice provider requires configuration");return new I(i.runtype);case"browser":if(!T.isSupported())throw new Error("Browser speech recognition not supported");return new T(i.browser||{});case"custom":{let e=i.custom;if(!e)throw new Error("Custom voice provider requires a `custom` provider instance or factory");let t=typeof e=="function"?e():e;if(!t||typeof t.startListening!="function")throw new Error("Custom voice provider `custom` must be a VoiceProvider (or a factory returning one)");return t}default:throw new Error(`Unknown voice provider type: ${i.type}`)}}function te(i){if(i?.type==="custom"&&i.custom)return R({type:"custom",custom:i.custom});if(i?.type==="runtype"&&i.runtype)return R({type:"runtype",runtype:i.runtype});if(T.isSupported())return R({type:"browser",browser:i?.browser||{language:"en-US"}});throw new Error("No supported voice providers available")}function Be(i){try{return te(i),!0}catch{return!1}}var se=i=>i.toLowerCase().replace(/[^\p{L}\p{N}]+/gu,""),q=class{constructor(e){this.host=e;this.turns=new Map;this.latestId=null;this.cancelled=new Set}apply(e,t,s,n,r,l){let d=this.host;t=t.trim();let a=this.turns.get(n);if(a||(a={},this.turns.set(n,a),this.latestId=n),e==="user"){a.userFinal=s;let o=a.userId?d.find(a.userId):void 0;if(o){let c=a.userStartMs===void 0&&r!==void 0;c&&(a.userStartMs=r),d.upsert({...o,content:t,voiceProcessing:!s,...c&&this.placeBefore(this.startedAfter(r))})}else{a.userStartMs=r;let c=a.assistantId?d.find(a.assistantId):void 0;a.userId=d.inject({role:"user",content:t,streaming:!1,voiceProcessing:!s,voiceCaption:l,...this.placeBefore(this.startedAfter(r)??c)}).id}}else if(!this.cancelled.has(n)){a.assistantFinal=s;let o=a.assistantId?d.find(a.assistantId):void 0;if(o){let c=a.assistantStartMs===void 0&&r!==void 0;c&&(a.assistantStartMs=r),d.upsert({...o,content:t,streaming:!s,voiceProcessing:!s,...c&&this.placeBefore(this.startedAfter(r))})}else t&&(a.assistantStartMs=r,a.assistantId=d.inject({role:"assistant",content:t,streaming:!s,voiceProcessing:!s,voiceCaption:l,...this.placeBefore(this.startedAfter(r))}).id);s&&a.assistantId&&d.markSpoken(a.assistantId)}this.sync()}claimUserTurn(e,t=[]){let s=t[t.length-1];for(let r of t)r!==s&&(this.turns.has(r)||this.turns.set(r,{}),this.turns.get(r).claimed=!0);let n;if(s){if(n=this.turns.get(s),n||(n={},this.turns.set(s,n)),n.claimed)return null;(!n.userId||!this.host.find(n.userId))&&(n.userFinal=!0,n.userId=this.host.inject({role:"user",content:e.trim()}).id)}else{let r=se(e),l=[...this.turns.values()].reverse().flatMap(d=>{let a=d.userId&&!d.claimed?this.host.find(d.userId):void 0;return a?[{turn:d,text:se(a.content)}]:[]}).filter(({text:d})=>d&&r);n=(l.find(({text:d})=>d===r)??l.find(({text:d})=>d.startsWith(r)||r.startsWith(d)))?.turn}return n?(n.claimed=!0,n.userId):null}cancel(){let e=this.inFlight();if(e.length===0){let t=this.awaiting();if(!t)return;e=[t]}for(let t of e)this.cancelled.add(t);this.closeReplies(e),this.sync()}fail(e){let t=this.awaiting(),s=this.inFlight();if(!t&&s.length===0)return;for(let l of s)this.turns.get(l).assistantFinal=!0;this.closeReplies(s);let n=this.host.inject({role:"assistant",content:e,streaming:!1,voiceProcessing:!1}),r=t?this.turns.get(t):void 0;r&&(r.assistantId=n.id,r.assistantFinal=!0),this.sync()}settle(){if(this.turns.size===0)return;let e=this.pending(),t=new Set;for(let s of this.turns.values())s.userId&&t.add(s.userId),s.assistantId&&(t.add(s.assistantId),this.host.markSpoken(s.assistantId));this.turns.clear(),this.latestId=null,this.host.settle(t),e&&this.host.setStreaming(!1)}startedAfter(e){if(e===void 0)return;let t;for(let s of this.turns.values())for(let[n,r]of[[s.userId,s.userStartMs],[s.assistantId,s.assistantStartMs]])n&&r!==void 0&&r>e&&(!t||r<t.start)&&(t={start:r,id:n});return t&&this.host.find(t.id)}placeBefore(e){if(!e)return;let t=e.sequence??0,s=t-1;for(let n of this.turns.values())for(let r of[n.userId,n.assistantId]){let l=r?this.host.find(r):void 0,d=l?.sequence;l?.createdAt===e.createdAt&&d!==void 0&&d>s&&d<t&&(s=d)}return{createdAt:e.createdAt,sequence:(s+t)/2}}closeReplies(e){let t=new Set;for(let s of e){let n=this.turns.get(s)?.assistantId;n&&(t.add(n),this.host.markSpoken(n))}t.size>0&&this.host.settle(t)}inFlight(){let e=[];for(let[t,s]of this.turns)s.assistantId&&!s.assistantFinal&&!this.cancelled.has(t)&&e.push(t);return e}awaiting(){let e=this.latestId;if(!e)return null;let t=this.turns.get(e);return t?.userFinal&&!t.assistantId&&!t.claimed&&!this.cancelled.has(e)?e:null}pending(){return this.awaiting()!==null||this.inFlight().length>0}sync(){this.host.setStreaming(this.pending())}};var We="I need your answer in the chat before I can continue.";var ie="no|nope|never ?mind|decline(?: it| that)?|cancel(?: it| that| this)?(?: (?:the|my|that|this) (?:[a-z]+ )?(?:order|request))?|don'?t (?:do|place) (?:it|that|the (?:[a-z]+ )?order)",$e=new RegExp(`^(?:${ie})(?: (?:${ie}))?(?: thanks| thank you)?$`),qe=i=>$e.test(i.toLowerCase().replace(/[^a-z' ]+/g," ").replace(/\s+/g," ").trim()),ne="Briefly tell the user what you're about to do and ask them to approve or decline it in the chat. Don't claim it's done.",oe=()=>new Promise(i=>setTimeout(i,50)),j=(i,e)=>i.length>e?`${i.slice(0,e-1).trimEnd()}\u2026`:i,_=i=>i.replace(/^webmcp:/,"").replace(/([a-z\d])([A-Z])/g,"$1 $2").replace(/[_\-.]+/g," ").trim().toLowerCase();function H(i,e=0){if(i==null||i==="")return"";if(typeof i!="object")return String(i);if(e>2)return"";if(Array.isArray(i))return i.map(r=>H(r,e+1)).filter(Boolean).join(", ");let t=i,s=t.quantity??t.qty??t.count,n=t.name??t.item??t.product??t.title;return s!=null&&n!=null&&typeof n!="object"?`${s} ${n}`:Object.entries(t).filter(([r])=>!r.startsWith("_")).map(([r,l])=>{let d=H(l,e+1);return d&&(e?`${_(r)} ${d}`:`${_(r)}: ${d}`)}).filter(Boolean).join(e?", ":"; ")}function re(i){let e=i.length>1?"These actions need the user's approval in the chat before they happen:":"This action needs the user's approval in the chat before it happens:",t=1e3-e.length-ne.length-2,s=i.map(n=>{let r=[_(n.toolName)];n.description&&r.push(`(${j(n.description.trim(),160)})`);let l=H(n.parameters);return l&&r.push(`with ${l}`),n.reason&&r.push(`because: ${j(n.reason.trim(),160)}`),j(`- ${r.join(" ")}`,Math.floor(t/i.length)-1)});return`${e}
|
|
6
|
-
${s.join(`
|
|
7
|
-
`)}
|
|
8
|
-
|
|
9
|
-
${ne}`}var L=i=>i.variant==="approval"&&i.approval?.status==="pending";function je(i){let e=null,t=null,s=()=>i.track(e??t),n=[],r=new Map,l=new Map,d=(h,f)=>{r.set(h,[...r.get(h)??[],f]),r.size>32&&r.delete(r.keys().next().value)},a=h=>i.messages().find(f=>f.id===h),o=h=>h.approvals.filter(f=>a(f)?.approval?.status==="pending"),c=h=>_(a(h)?.approval?.toolName??""),u=(h,f,S)=>{h.outcome={status:f,text:S};for(let v of o(h))i.decide(v)},g=h=>h.flatMap(f=>i.messages().find(S=>S.id===f)??[]).filter(f=>!f.variant&&f.content.trim()).map(f=>f.content.trim()).join(`
|
|
10
|
-
|
|
11
|
-
`),m=async h=>{do await oe();while(i.busy()&&!(h&&(i.parked()||h.failed)))};return{getHistory:()=>{let h=i.messages().filter(v=>(v.role==="user"||v.role==="assistant")&&!v.variant&&!v.voiceProcessing&&!v.streaming&&v.content),f=!1,S=[];for(let v=h.length-1;v>=0;v-=1){let k=h[v];if(k.role==="user")f=!1;else if(!k.voiceCaption)f=!0;else if(f)continue;S.unshift({role:k.role,content:k.content})}return S},dropDelegation(h,f){l.set(h,!!f),l.size>32&&l.delete(l.keys().next().value);let S=f&&n.find(v=>v.delegationId===h);S&&!S.outcome&&u(S,"timeout","");for(let v of r.get(h)??[])v.dropped=!0,v.ids.length&&i.unspoken(v.ids)},async runDelegatedTurn({delegationId:h,userText:f,userUtteranceIds:S}){let v=n.filter(p=>o(p).length),k=v.flatMap(o);if(k.length===1&&qe(f)){let p=c(k[0]);return u(v[0],"denied",""),{status:"denied",text:`Okay, I cancelled the ${p} request. Nothing was done.`,inChat:!1}}let F=i.parked()&&i.messages().every(p=>!L(p)||k.includes(p.id));i.busy()&&!F&&await m();let ae=new Set(i.messages().map(p=>p.id)),O=()=>i.messages().filter(p=>!ae.has(p.id)&&p.variant==="approval"),A={ids:[],failed:!1};d(h,A),e=A,s();try{await i.send(f,i.claim(f,S)??void 0),await m(A)}catch{A.failed=!0}finally{e=null,s()}let ce=A.ids.flatMap(p=>i.messages().find(b=>b.id===p)??[]),E=g(A.ids),P=A.failed?[]:O().filter(L),le=new Set(P.map(p=>_(p.approval.toolName)));for(let p of v){if(p.outcome)continue;for(let w of o(p).filter(N=>le.has(c(N))))p.replaced.add(w),i.decide(w);let b=c(p.approvals[0]);p.approvals.every(w=>p.replaced.has(w))?p.outcome={status:"cancelled",text:`The earlier ${b} request was replaced by the new one; it was not done.`}:F&&p.approvals.every(w=>a(w)?.approval?.status==="denied")&&(p.outcome={status:"cancelled",text:`The earlier ${b} request was replaced by the new one; it was not done.`})}if(P.length){let p=new Set(P.map(w=>w.id)),b={delegationId:h,approvals:[...p],at:Date.now(),replaced:new Set};return n.push(b),l.get(h)&&u(b,"timeout",""),E=`${E}
|
|
12
|
-
|
|
13
|
-
${re(P.map(w=>w.approval))}`.trim(),{status:"pending_approval",text:E,followUp:async({signal:w,approvalTimeoutMs:N=3e5,readBack:z=!0,onUpdate:de})=>{let C={ids:[],failed:!1};r.delete(h),d(h,C);let X=()=>O().filter(y=>p.has(y.id)||C.ids.includes(y.id)),ue=()=>O().filter(y=>L(y)&&!p.has(y.id)&&!n.some(M=>M.approvals.includes(y.id))&&(C.ids.includes(y.id)||y.approval?.toolType==="webmcp"&&t===C)),G=()=>{t=C,s()};z&&G();try{for(;!w.aborted&&!b.outcome&&(e||i.busy()||X().some(L));){z&&t!==C&&!o(b).length&&G();let y=ue();if(y.length){for(let M of y)p.add(M.id),b.approvals.push(M.id);b.at=Date.now(),de?.(re(y.map(M=>M.approval)))}if(Date.now()-b.at>=N&&o(b).length&&(!i.busy()||i.parked())){u(b,"timeout",`That ${c(b.approvals[0])} request expired, so nothing was done.`);break}await oe()}}finally{t===C&&(t=null,s()),n.splice(n.indexOf(b),1)}if(w.aborted)return null;if(b.outcome)return b.outcome;if(C.failed)return{status:"failed",text:`The ${c(b.approvals[0])} request didn't complete.`};let K=X(),D=y=>K.filter(y).map(M=>_(M.approval.toolName)).join(", "),U=K.some(y=>y.approval?.status==="approved"),x=D(y=>y.approval?.status==="denied"&&!b.replaced.has(y.id)),B=D(y=>b.replaced.has(y.id)),W=D(y=>y.approval?.status==="timeout"),ge=[U?g(C.ids):"",x&&`The user declined the ${x} request in the chat, so ${U?"that part was not done":"nothing was done"}.`,B&&`The earlier ${B} request was replaced by the new one; it was not done.`,W&&`The ${W} request timed out, so it was not done.`].filter(Boolean).join(`
|
|
14
|
-
|
|
15
|
-
`);return{status:U?"completed":x?"denied":B?"cancelled":W?"timeout":"completed",text:ge}}}}return!A.failed&&i.parked()||ce.some(p=>p.agentMetadata?.awaitingLocalTool&&!p.agentMetadata.askUserQuestionAnswered&&!p.toolCall?.name?.startsWith("webmcp:"))?{status:A.failed?"failed":"completed",text:`${E}
|
|
16
|
-
|
|
17
|
-
${We}`.trim()}:{status:!A.failed&&E?"completed":"failed",text:E}}}}var He=(i,e,t,s)=>{let n=i.session;try{i.keyed=t?new t({find:o=>n.messages.find(c=>c.id===o),inject:o=>n.injectMessage(o),upsert:o=>n.upsertMessage(o),settle:o=>{n.messages=n.messages.map(c=>o.has(c.id)?{...c,streaming:!1,voiceProcessing:!1}:c),n.callbacks.onMessagesChanged([...n.messages])},setStreaming:o=>{(o||!i.busy())&&i.setStreaming(o)},markSpoken:o=>{n.ttsSpokenMessageIds.add(o)}}):null,s&&e.setSessionBridge&&e.setSessionBridge(s({messages:()=>n.messages,busy:()=>i.busy(),parked:()=>n.webMcpApprovalResolvers.size>0,claim:(o,c)=>i.keyed?.claimUserTurn(o,c)??null,send:(o,c)=>n.sendMessage(o,{viaVoice:!0,voiceTurn:{userMessageId:c}}),track:o=>{i.setDelegation(o)},unspoken:o=>{for(let c of o)n.ttsSpokenMessageIds.delete(c);n.streaming||n.speakLatestAssistantMessage(o)},decide:o=>{if(n.webMcpApprovalResolvers.has(o))return n.resolveWebMcpApproval(o,"denied");let c=n.messages.find(u=>u.id===o)?.approval;c?.status==="pending"&&n.resolveApproval(c,"denied")}}));let r=i.generation(),l=()=>i.provider()===e&&r===i.generation(),a=(n.config.voiceRecognition??{}).processingErrorText??"Voice processing failed. Please try again.";e.onResult(o=>{l()&&o.provider!=="runtype"&&o.text&&o.text.trim()&&n.sendMessage(o.text,{viaVoice:!0})}),e.onTranscript&&e.onTranscript((o,c,u,g)=>{if(l()){if(g?.turnId){i.keyed?.apply(o,c,u,g.turnId,g.startMs,g.caption);return}if(o==="user"){if(i.pendingUser)n.upsertMessage({id:i.pendingUser,role:"user",content:c,createdAt:new Date().toISOString(),streaming:!1,voiceProcessing:!u});else{let m=n.injectMessage({role:"user",content:c,streaming:!1,voiceProcessing:!u});i.pendingUser=m.id}if(u){i.turns().start(g?.turnId),i.pendingUser=null;let m=n.injectMessage({role:"assistant",content:"",streaming:!0,voiceProcessing:!0});i.pendingAssistant=m.id,i.setStreaming(!0)}}else{if(!i.turns().accepts(g?.turnId))return;if(i.pendingAssistant)n.upsertMessage({id:i.pendingAssistant,role:"assistant",content:c,createdAt:new Date().toISOString(),streaming:!u,voiceProcessing:!u});else{let m=n.injectMessage({role:"assistant",content:c,streaming:!u,voiceProcessing:!u});i.pendingAssistant=m.id}u&&(i.pendingAssistant&&n.ttsSpokenMessageIds.add(i.pendingAssistant),i.setStreaming(!1),i.pendingAssistant=null)}}}),e.onLevel&&e.onLevel(o=>{l()&&i.setLevel(Number.isFinite(o)?Math.max(0,Math.min(1,o)):0)}),e.onMetrics&&e.onMetrics(o=>{l()&&n.config.voiceRecognition?.onMetrics?.(o)}),e.onError(o=>{l()&&(console.error("Voice error:",o),i.pendingAssistant&&(n.upsertMessage({id:i.pendingAssistant,role:"assistant",content:a,createdAt:new Date().toISOString(),streaming:!1,voiceProcessing:!1}),i.setStreaming(!1),i.pendingUser=null,i.pendingAssistant=null),i.keyed?.fail(a))}),e.onStatusChange(o=>{l()&&(i.setStatus(o),(o==="listening"||o==="idle"||o==="disconnected")&&i.settlePending(o!=="listening"),(o==="idle"||o==="disconnected")&&i.keyed?.settle(),n.callbacks.onVoiceStatusChanged?.(o))}),e.connect()}catch(r){console.error("Failed to setup voice:",r)}};var Fe=i=>{let e=a=>{if(i.original)return;let o=i.config().voiceRecognition??{};i.original={backgroundColor:a.style.backgroundColor,color:a.style.color,borderColor:a.style.borderColor,iconName:o.iconName??"mic",iconSize:parseFloat(o.iconSize??"")||i.iconFallbackPx}},t=(a,o,c)=>{let u=a.querySelector("svg");u&&u.remove();let g=i.original?.iconSize??(parseFloat(i.config().voiceRecognition?.iconSize??"")||i.iconFallbackPx),m=i.renderIcon(o,g,c,1.5);m&&a.appendChild(m)},s=a=>{a.classList.remove("persona-voice-recording","persona-voice-processing","persona-voice-speaking")};return{recording:()=>{let a=i.mic();if(!a)return;e(a);let o=i.config().voiceRecognition??{},c=o.recordingBackgroundColor,u=o.recordingIconColor,g=o.recordingBorderColor;if(s(a),a.classList.add("persona-voice-recording"),i.setMicState("recording"),a.style.backgroundColor=c??"var(--persona-voice-recording-bg, #ef4444)",a.style.color=u??"var(--persona-voice-recording-indicator, #ffffff)",u){let m=a.querySelector("svg");m&&m.setAttribute("stroke",u)}g&&(a.style.borderColor=g),a.setAttribute("aria-label","Stop voice recognition")},processing:()=>{let a=i.mic();if(!a)return;e(a);let o=i.config().voiceRecognition??{},c=i.session().getVoiceInterruptionMode(),u=i.original,g=o.processingIconName??"loader",m=o.processingIconColor??u?.color??"",h=o.processingBackgroundColor??u?.backgroundColor??"",f=o.processingBorderColor??u?.borderColor??"";s(a),a.classList.add("persona-voice-processing"),i.setMicState("processing"),a.style.backgroundColor=h,a.style.borderColor=f;let S=m||"currentColor";a.style.color=S,t(a,g,S),a.setAttribute("aria-label","Processing voice input"),c==="none"&&(a.style.cursor="default")},speaking:()=>{let a=i.mic();if(!a)return;e(a);let o=i.config().voiceRecognition??{},c=i.session().getVoiceInterruptionMode(),u=i.original,g=c==="cancel"?"square":c==="barge-in"?"mic":"volume-2",m=o.speakingIconName??g,h=o.speakingIconColor??(c==="barge-in"?o.recordingIconColor??u?.color??"":u?.color??""),f=o.speakingBackgroundColor??(c==="barge-in"?o.recordingBackgroundColor??"var(--persona-voice-recording-bg, #ef4444)":u?.backgroundColor??""),S=o.speakingBorderColor??(c==="barge-in"?o.recordingBorderColor??"":u?.borderColor??"");s(a),a.classList.add("persona-voice-speaking"),i.setMicState("speaking"),a.style.backgroundColor=f,a.style.borderColor=S;let v=h||"currentColor";a.style.color=v,t(a,m,v);let k=c==="cancel"?"Stop playback and re-record":c==="barge-in"?"Speak to interrupt":"Agent is speaking";a.setAttribute("aria-label",k),c==="none"&&(a.style.cursor="default"),c==="barge-in"&&a.classList.add("persona-voice-recording")},reset:()=>{let a=i.mic();if(!a)return;s(a),i.setMicState("idle");let o=i.original;o&&(a.style.backgroundColor=o.backgroundColor??"",a.style.color=o.color??"",a.style.borderColor=o.borderColor??"",t(a,o.iconName,o.color||"currentColor"),i.original=null),a.style.cursor="",a.setAttribute("aria-label","Start voice recognition")}}};export{T as BrowserVoiceProvider,q as KeyedVoiceTranscript,I as RuntypeVoiceProvider,te as createBestAvailableVoiceProvider,Fe as createMicStateStyles,R as createVoiceProvider,je as createVoiceSessionBridge,Be as isVoiceSupported,He as wireSessionVoice};
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
var h="webmcp:",T=t=>t.startsWith(h)?t.slice(h.length):t,u={warn(t,...e){typeof console<"u"&&typeof console.warn=="function"&&console.warn(`[Persona/WebMCP] ${t}`,...e)}},b=class{constructor(e,n){this.config=e;this.#e=n,this.#o=e.onConfirm??null,this.#r=3e4}#o;#r;#t=!1;#i=null;#l=!1;#e;setConfirmHandler(e){this.#o=e}isOperational(){return this.config.enabled!==!0||!this.#t?!1:this.#n()!==null}async snapshotForDispatch(){if(await this.#s(),this.config.enabled!==!0)return[];let e=this.#n();if(!e)return[];let n;try{n=await e.getTools()}catch(i){return u.warn("getTools() threw: shipping an empty WebMCP snapshot.",i),[]}this.#e.recordToolDisplayTitles(n);let o=typeof location<"u"?location.origin:"";return n.filter(i=>this.#c(i.name)).map(i=>{let l=W(i.annotations);return{name:i.name,description:i.description,parametersSchema:P(i.inputSchema),origin:"webmcp",...o?{pageOrigin:o}:{},...l?{annotations:l}:{},...l?.untrustedContentHint===!0?{untrustedContentHint:!0}:{}}})}async executeToolCall(e,n,o){if(await this.#s(),this.config.enabled!==!0)return s("WebMCP is not enabled on this widget.");let i=this.#n();if(!i){let r=typeof document<"u"&&!!document.modelContext;return s(r?"WebMCP is not operational: document.modelContext is present but does not expose the strict getTools()/executeTool() surface (likely a different or older WebMCP polyfill).":"WebMCP bridge is not operational on this page (document.modelContext not available).")}let l=T(e),c;try{c=await i.getTools()}catch(r){let p=r instanceof Error?r.message:String(r);return s(`Failed to read WebMCP registry: ${p}`)}this.#e.recordToolDisplayTitles(c);let f=c.find(r=>r.name===l);if(!f)return s(`WebMCP tool not registered on this page: ${l}`);if(!this.#c(l))return s(`WebMCP tool not allowed by client allowlist: ${l}`);if(o?.aborted)return s("Aborted by cancel()");let m=this.#e.getToolDisplayTitle(l),C={toolName:l,args:n,description:f.description,...m?{title:m}:{},reason:"gate"};if(!await this.#u(C))return s("User declined the tool call.");if(o?.aborted)return s("Aborted by cancel()");let a=new AbortController,y=!1,M=setTimeout(()=>{y=!0,a.abort()},this.#r),g=()=>a.abort();o&&(o.aborted?a.abort():o.addEventListener("abort",g,{once:!0}));try{let r=await i.executeTool(f,k(n),{signal:a.signal});return w(r)}catch(r){if(y)return s(`WebMCP tool '${l}' timed out after ${this.#r}ms`);if(o?.aborted)return s("Aborted by cancel()");let p=r instanceof Error?r.message:String(r);return s(p)}finally{clearTimeout(M),o&&o.removeEventListener("abort",g)}}#s(){return this.config.enabled!==!0?Promise.resolve():(this.#i||(this.#i=this.#a()),this.#i)}async#a(){try{if(this.#n()){this.#t=!0;return}(await this.#e.loadPolyfill()).initializeWebMCPPolyfill(),this.#t=!0}catch(e){u.warn("Failed to load @mcp-b/webmcp-polyfill: WebMCP consumption disabled.",e),this.#t=!1}}#n(){if(typeof document>"u")return null;let e=document.modelContext;if(!e||typeof e!="object")return null;let n=e;return typeof n.getTools!="function"||typeof n.executeTool!="function"?(this.#l||(this.#l=!0,u.warn("document.modelContext is present but does not expose getTools()/executeTool(): WebMCP consumption is disabled. Another (incompatible or older) WebMCP polyfill likely installed document.modelContext before Persona. Remove it, or use a polyfill implementing the strict standard surface (e.g. @mcp-b/webmcp-polyfill).")),null):e}async#u(e){let n=this.#o??A;try{return await n(e)}catch(o){return u.warn(`Confirm handler threw for WebMCP tool '${e.toolName}'; declining.`,o),!1}}#c(e){let n=this.config.allowlist;return!n||n.length===0?!0:n.some(o=>x(e,o))}},x=(t,e)=>{if(e==="*")return!0;let n=e.replace(/[.+?^${}()|[\]\\]/g,"\\$&");return new RegExp("^"+n.replace(/\*/g,".*")+"$").test(t)},d={type:"object",properties:{}},W=t=>{if(!t||typeof t!="object")return;let e={...typeof t.readOnlyHint=="boolean"?{readOnlyHint:t.readOnlyHint}:{},...typeof t.untrustedContentHint=="boolean"?{untrustedContentHint:t.untrustedContentHint}:{}};return Object.keys(e).length>0?e:void 0},P=t=>{if(t===void 0||t==="")return{...d};if(typeof t=="object")return t!==null&&!Array.isArray(t)?t:{...d};try{let e=JSON.parse(t);return e!==null&&typeof e=="object"&&!Array.isArray(e)?e:{...d}}catch{return{...d}}},w=t=>{if(t==null)return{content:[{type:"text",text:""}]};let e;try{e=JSON.parse(t)}catch{return{content:[{type:"text",text:t}]}}return e!==null&&typeof e=="object"&&Array.isArray(e.content)?e:{content:[{type:"text",text:typeof e=="string"?e:D(e)}]}},s=t=>({isError:!0,content:[{type:"text",text:t}]}),A=async t=>{if(typeof window>"u"||typeof window.confirm!="function")return!1;let e=O(t.args),n=`Allow the AI to call ${t.toolName}`+(e?`
|
|
2
|
-
|
|
3
|
-
Arguments:
|
|
4
|
-
${e}`:"")+(t.description?`
|
|
5
|
-
|
|
6
|
-
${t.description}`:"");return window.confirm(n)},O=t=>{if(t==null)return"";try{let e=JSON.stringify(t,null,2);return e.length>500?e.slice(0,500)+"\u2026":e}catch{return String(t)}},k=t=>{if(t===void 0)return"{}";try{let e=JSON.stringify(t);return e===void 0?"{}":e}catch{return"{}"}},D=t=>{if(t===void 0)return"";try{return JSON.stringify(t)}catch{return String(t)}};export{b as WebMcpBridge};
|