@runtypelabs/persona 4.28.2 → 4.29.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/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/approval-ui.cjs +3 -3
- package/dist/approval-ui.d.cts +1 -1
- package/dist/approval-ui.d.ts +1 -1
- package/dist/approval-ui.js +3 -3
- package/dist/artifacts-ui.d.cts +2 -2
- package/dist/artifacts-ui.d.ts +2 -2
- package/dist/chunk-B1-_pp-Y.js +4 -0
- package/dist/{chunk-DrYAfpvk.js → chunk-BcLIgi1h.js} +1 -1
- package/dist/chunk-BkW8s3fj.js +2 -0
- package/dist/chunk-Bm2-bemv.js +1 -0
- package/dist/chunk-BvhTpG6k.js +1 -0
- package/dist/chunk-Csks3Qw1.js +1 -0
- package/dist/chunk-D5B629Mj.js +1 -0
- package/dist/chunk-DGlFKA0r.js +9 -0
- package/dist/chunk-DO9_WSpy.js +1 -0
- package/dist/chunk-DTDHeUsG.js +1 -0
- package/dist/chunk-DjFpGFGA.js +1 -0
- package/dist/{chunk-dRBlrLd7.js → chunk-DtmtfIkd.js} +1 -1
- package/dist/chunk-lA5CJCgK.js +1 -0
- package/dist/client-history.js +1 -1
- package/dist/client-stream.js +1 -1
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/{composer-input-C39npJaz.d.cts → composer-input-B0P4OtNi.d.cts} +1 -1
- package/dist/{composer-input-C39npJaz.d.ts → composer-input-B0P4OtNi.d.ts} +1 -1
- package/dist/{context-mention-manager-DLCB1l9E.d.cts → context-mention-manager-DmLI_RM6.d.cts} +1 -1
- package/dist/{context-mention-manager-DLCB1l9E.d.ts → context-mention-manager-DmLI_RM6.d.ts} +1 -1
- package/dist/context-mentions-inline.d.cts +2 -2
- package/dist/context-mentions-inline.d.ts +2 -2
- package/dist/context-mentions.d.cts +3 -3
- package/dist/context-mentions.d.ts +3 -3
- package/dist/{defaults-DiAgWHXy.d.cts → defaults-DVo1wbZs.d.cts} +1 -1
- package/dist/{defaults-DiAgWHXy.d.ts → defaults-DVo1wbZs.d.ts} +1 -1
- package/dist/event-stream-view.d.cts +1 -1
- package/dist/event-stream-view.d.ts +1 -1
- package/dist/{fallback-speech-engine-C5_PUZjH.d.cts → fallback-speech-engine-BMnlmKnY.d.cts} +1 -1
- package/dist/{fallback-speech-engine-C5_PUZjH.d.ts → fallback-speech-engine-BMnlmKnY.d.ts} +1 -1
- package/dist/forms-ui.d.cts +2 -2
- package/dist/forms-ui.d.ts +2 -2
- package/dist/history-view.d.cts +1 -1
- package/dist/history-view.d.ts +1 -1
- package/dist/index.cjs +50 -49
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -7
- package/dist/index.d.ts +7 -7
- package/dist/index.global.js +2 -3
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +38 -37
- package/dist/index.js.map +1 -1
- package/dist/{markdown-parsers-loader-BBdyIEU7.d.cts → markdown-parsers-loader-CeovcyYf.d.cts} +1 -1
- package/dist/{markdown-parsers-loader-BBdyIEU7.d.ts → markdown-parsers-loader-CeovcyYf.d.ts} +1 -1
- package/dist/{preview-Chjb0bmI.d.cts → preview-Bkb4UrQC.d.cts} +2 -2
- package/dist/{preview-Chjb0bmI.d.ts → preview-Bkb4UrQC.d.ts} +2 -2
- package/dist/session-actions.js +1 -1
- package/dist/{session-a_qIp4xp.d.ts → session-iLyKipoM.d.cts} +28 -10
- package/dist/{session-a_qIp4xp.d.cts → session-iLyKipoM.d.ts} +28 -10
- package/dist/session-reconnect.cjs +1 -1
- package/dist/session-reconnect.d.cts +8 -2
- package/dist/session-reconnect.d.ts +8 -2
- package/dist/session-reconnect.js +1 -1
- package/dist/smart-dom-reader.d.cts +1 -1
- package/dist/smart-dom-reader.d.ts +1 -1
- package/dist/theme-editor-preview.cjs +61 -60
- package/dist/theme-editor-preview.d.cts +1 -1
- package/dist/theme-editor-preview.d.ts +1 -1
- package/dist/theme-editor-preview.js +40 -40
- package/dist/theme-editor.d.cts +2 -2
- package/dist/theme-editor.d.ts +2 -2
- package/dist/theme-reference.d.cts +2 -2
- package/dist/theme-reference.d.ts +2 -2
- package/dist/{types-BdO1jNRm.d.cts → types-BuDirJkQ.d.cts} +200 -23
- package/dist/{types-BdO1jNRm.d.ts → types-BuDirJkQ.d.ts} +200 -23
- package/dist/voice-client-tools.cjs +1 -0
- package/dist/voice-client-tools.d.cts +13 -0
- package/dist/voice-client-tools.d.ts +13 -0
- package/dist/voice-client-tools.js +1 -0
- package/dist/voice-runtime.cjs +3 -3
- package/dist/voice-runtime.d.cts +15 -5
- package/dist/voice-runtime.d.ts +15 -5
- package/dist/voice-runtime.js +5 -5
- package/dist/voice-worklet-player.d.cts +2 -2
- package/dist/voice-worklet-player.d.ts +2 -2
- package/dist/{webmcp-runtime-entry-B6bpVWSa.d.ts → webmcp-runtime-entry-CXLjRt1k.d.cts} +26 -5
- package/dist/{webmcp-runtime-entry-B6bpVWSa.d.cts → webmcp-runtime-entry-CXLjRt1k.d.ts} +26 -5
- package/dist/webmcp-runtime.cjs +2 -1
- package/dist/webmcp-runtime.d.cts +1 -1
- package/dist/webmcp-runtime.d.ts +1 -1
- package/dist/webmcp-runtime.js +2 -1
- package/dist/widget.css +1 -1
- package/package.json +6 -1
- package/src/client-approve.test.ts +17 -1
- package/src/client-resume.ts +15 -3
- package/src/client.interrupt.test.ts +21 -0
- package/src/client.ts +61 -4
- package/src/client.voice-tools.test.ts +65 -0
- package/src/components/activity-row.ts +3 -2
- package/src/components/approval-actions.test.ts +8 -0
- package/src/components/approval-actions.ts +6 -2
- package/src/components/approval-bubble.test.ts +14 -0
- package/src/components/approval-bubble.ts +8 -1
- package/src/components/tool-bubble.ts +8 -5
- package/src/generated/runtype-openapi-contract.ts +39 -1
- package/src/index-core.ts +11 -0
- package/src/index-global.ts +6 -0
- package/src/resume-dedupe-local-only.test.ts +273 -0
- package/src/runtime/persist-state.test.ts +86 -0
- package/src/session-actions.ts +20 -6
- package/src/session-reconnect.ts +18 -1
- package/src/session.restored-browser-tools.test.ts +272 -0
- package/src/session.test.ts +26 -0
- package/src/session.ts +185 -31
- package/src/session.voice.test.ts +212 -0
- package/src/styles/widget.css +4 -2
- package/src/types.ts +178 -22
- package/src/ui.restored-browser-tools.test.ts +128 -0
- package/src/ui.ts +32 -18
- package/src/utils/formatting.test.ts +9 -1
- package/src/utils/formatting.ts +4 -0
- package/src/utils/local-only-message.ts +15 -0
- package/src/utils/restored-browser-state.ts +50 -0
- package/src/utils/storage.ts +12 -1
- package/src/utils/voice-prior-tool-results.ts +37 -0
- package/src/voice/runtype-voice-provider.ts +94 -7
- package/src/voice/session-voice-wiring.ts +27 -3
- package/src/voice/voice-client-tools-session.ts +116 -0
- package/src/voice/voice-delegation.ts +19 -3
- package/src/voice/voice.test.ts +396 -0
- package/src/voice-client-tools-bundle.test.ts +37 -0
- package/src/voice-client-tools-loader.ts +24 -0
- package/src/voice-client-tools.ts +9 -0
- package/src/webmcp-bridge.test.ts +152 -1
- package/src/webmcp-bridge.ts +5 -6
- package/src/webmcp-polyfill.integration.test.ts +5 -1
- package/src/webmcp-runtime-entry.ts +151 -26
- package/dist/chunk-B6zZsT6F.js +0 -1
- package/dist/chunk-CztSjKHK.js +0 -9
- package/dist/chunk-DbhxjyOB.js +0 -4
- package/dist/chunk-DsdqLStH.js +0 -1
- package/dist/chunk-GIqbL2Eg.js +0 -1
- package/dist/chunk-HUOUif-P.js +0 -1
- package/dist/chunk-UlcxdHef.js +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@runtypelabs/persona",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.29.1",
|
|
4
4
|
"description": "Themeable, pluggable streaming agent widget for websites, in plain JS with support for voice input and reasoning / tool output.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|
|
@@ -117,6 +117,11 @@
|
|
|
117
117
|
"import": "./dist/voice-runtime.js",
|
|
118
118
|
"require": "./dist/voice-runtime.cjs"
|
|
119
119
|
},
|
|
120
|
+
"./voice-client-tools": {
|
|
121
|
+
"types": "./dist/voice-client-tools.d.ts",
|
|
122
|
+
"import": "./dist/voice-client-tools.js",
|
|
123
|
+
"require": "./dist/voice-client-tools.cjs"
|
|
124
|
+
},
|
|
120
125
|
"./forms-ui": {
|
|
121
126
|
"types": "./dist/forms-ui.d.ts",
|
|
122
127
|
"import": "./dist/forms-ui.js",
|
|
@@ -176,6 +176,22 @@ describe("AgentWidgetSession.resolveApproval in client-token mode", () => {
|
|
|
176
176
|
expect(errors).toEqual([]);
|
|
177
177
|
});
|
|
178
178
|
|
|
179
|
+
it("sends the continuation bubble's id so the stored reply dedupes by id (core#9961)", async () => {
|
|
180
|
+
const calls = mockFetch(
|
|
181
|
+
sse([
|
|
182
|
+
{ type: "text_start", id: "t1" },
|
|
183
|
+
{ type: "text_delta", id: "t1", delta: "Order placed." },
|
|
184
|
+
{ type: "text_complete", id: "t1" },
|
|
185
|
+
{ type: "execution_complete", kind: "agent", success: true },
|
|
186
|
+
])
|
|
187
|
+
);
|
|
188
|
+
const { approve, last } = setup();
|
|
189
|
+
await approve();
|
|
190
|
+
|
|
191
|
+
expect(calls[0].body.assistantMessageId).toMatch(/^ast_/);
|
|
192
|
+
expect(last()?.id).toBe(calls[0].body.assistantMessageId);
|
|
193
|
+
});
|
|
194
|
+
|
|
179
195
|
it.each([
|
|
180
196
|
[
|
|
181
197
|
403,
|
|
@@ -210,7 +226,7 @@ describe("AgentWidgetSession.resolveApproval in client-token mode", () => {
|
|
|
210
226
|
await approve();
|
|
211
227
|
|
|
212
228
|
expect(bubble()?.approval?.status).toBe(badge);
|
|
213
|
-
expect(last()).toMatchObject({ id: "approval-error-appr_1", content: text });
|
|
229
|
+
expect(last()).toMatchObject({ id: "approval-error-appr_1", content: text, localOnly: true });
|
|
214
230
|
expect(session.isStreaming()).toBe(false);
|
|
215
231
|
expect(errors.map((e) => e.message)).toEqual([text]);
|
|
216
232
|
// A refused request settles too: its token must not outlive it.
|
package/src/client-resume.ts
CHANGED
|
@@ -41,7 +41,8 @@ export type ClientResumeHost = {
|
|
|
41
41
|
export async function resolveApproval(
|
|
42
42
|
h: ClientResumeHost,
|
|
43
43
|
approval: { agentId: string; executionId: string; approvalId: string },
|
|
44
|
-
decision: "approved" | "denied"
|
|
44
|
+
decision: "approved" | "denied",
|
|
45
|
+
options?: { assistantMessageId?: string }
|
|
45
46
|
): Promise<Response> {
|
|
46
47
|
let headers: Record<string, string> = {
|
|
47
48
|
'Content-Type': 'application/json',
|
|
@@ -68,7 +69,10 @@ export async function resolveApproval(
|
|
|
68
69
|
|
|
69
70
|
// Same re-validation as `resumeFlow`: an approval can sit for a long time.
|
|
70
71
|
const { sessionId } = await h.initSession();
|
|
71
|
-
const response = await post('/v1/client/approve', {
|
|
72
|
+
const response = await post('/v1/client/approve', {
|
|
73
|
+
sessionId,
|
|
74
|
+
...(options?.assistantMessageId ? { assistantMessageId: options.assistantMessageId } : {}),
|
|
75
|
+
});
|
|
72
76
|
// A core without the route answers with its generic `Not Found`; an unknown
|
|
73
77
|
// or expired pause 404s with its own error, which the caller surfaces.
|
|
74
78
|
if (response.status === 404) {
|
|
@@ -83,7 +87,13 @@ export async function resumeFlow(
|
|
|
83
87
|
h: ClientResumeHost,
|
|
84
88
|
executionId: string,
|
|
85
89
|
toolOutputs: Record<string, unknown>,
|
|
86
|
-
options?: {
|
|
90
|
+
options?: {
|
|
91
|
+
streamResponse?: boolean;
|
|
92
|
+
signal?: AbortSignal;
|
|
93
|
+
after?: string;
|
|
94
|
+
/** Id of the local bubble the resumed stream renders into (client-token only). */
|
|
95
|
+
assistantMessageId?: string;
|
|
96
|
+
}
|
|
87
97
|
): Promise<Response> {
|
|
88
98
|
const isClientToken = h.isClientTokenMode();
|
|
89
99
|
const url = isClientToken
|
|
@@ -125,6 +135,8 @@ export async function resumeFlow(
|
|
|
125
135
|
// Thread the (refreshed) sessionId through like `/v1/client/chat` does.
|
|
126
136
|
if (resumeSessionId) {
|
|
127
137
|
body.sessionId = resumeSessionId;
|
|
138
|
+
// WHY(runtypelabs/core#9961): the server stores the reply under this id, so a later re-send dedupes by id.
|
|
139
|
+
if (options?.assistantMessageId) body.assistantMessageId = options.assistantMessageId;
|
|
128
140
|
}
|
|
129
141
|
|
|
130
142
|
if (isClientToken && resumeSessionId) {
|
|
@@ -108,6 +108,27 @@ describe("client-token turn identity", () => {
|
|
|
108
108
|
expect(bodies[1]).not.toHaveProperty("voice");
|
|
109
109
|
});
|
|
110
110
|
|
|
111
|
+
it("a voice hand-back with prior tool results sends them under voice; an empty list sends none", async () => {
|
|
112
|
+
const bodies: Record<string, unknown>[] = [];
|
|
113
|
+
global.fetch = vi.fn(async (url: unknown, init: any) => {
|
|
114
|
+
if (String(url).endsWith("/v1/client/init")) return initResponse();
|
|
115
|
+
bodies.push(JSON.parse(init.body));
|
|
116
|
+
const stream = controlledStream();
|
|
117
|
+
stream.close();
|
|
118
|
+
return stream.response;
|
|
119
|
+
}) as unknown as typeof fetch;
|
|
120
|
+
const prior = [
|
|
121
|
+
{ toolCallId: "vtc_1", name: "webmcp:jump_to_slide", arguments: { position: 5 }, result: { slideTitle: "Pricing" } },
|
|
122
|
+
];
|
|
123
|
+
|
|
124
|
+
const client = clientTokenClient();
|
|
125
|
+
await client.dispatch({ messages, voiceSpoken: true, voicePriorToolResults: prior }, () => {});
|
|
126
|
+
await client.dispatch({ messages, voiceSpoken: true, voicePriorToolResults: [] }, () => {});
|
|
127
|
+
|
|
128
|
+
expect(bodies[0].voice).toEqual({ spoken: true, priorToolResults: prior });
|
|
129
|
+
expect(bodies[1].voice).toEqual({ spoken: true });
|
|
130
|
+
});
|
|
131
|
+
|
|
111
132
|
it("drops SSE events belonging to a superseded turn", async () => {
|
|
112
133
|
const first = controlledStream();
|
|
113
134
|
const second = controlledStream();
|
package/src/client.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
AgentWidgetSSEEventResult as _AgentWidgetSSEEventResult,
|
|
15
15
|
ClientSession,
|
|
16
16
|
ClientChatRequest,
|
|
17
|
+
VoicePriorToolResult,
|
|
17
18
|
ClientToolDefinition,
|
|
18
19
|
ClientFeedbackRequest,
|
|
19
20
|
ClientFeedbackType,
|
|
@@ -34,6 +35,7 @@ import {
|
|
|
34
35
|
recordWebMcpToolDisplayTitles,
|
|
35
36
|
} from "./webmcp-bridge";
|
|
36
37
|
import { loadWebMcpRuntime } from "./webmcp-runtime-loader";
|
|
38
|
+
import { loadVoiceClientTools } from "./voice-client-tools-loader";
|
|
37
39
|
import type { WebMcpBridge } from "./webmcp-runtime-entry";
|
|
38
40
|
import { resolveTarget } from "./utils/target";
|
|
39
41
|
import { generateTurnId } from "./utils/message-id";
|
|
@@ -54,6 +56,7 @@ import type { ClientResumeHost } from "./client-resume";
|
|
|
54
56
|
/** History transcripts stay on the wire shape; `utils/history-messages.ts` maps them. */
|
|
55
57
|
export type { HistoryWireMessage } from "./utils/history-messages";
|
|
56
58
|
import { divergentDisplayProjection } from "./utils/history-messages";
|
|
59
|
+
import { isLocalOnlyMessage } from "./utils/local-only-message";
|
|
57
60
|
// artifactsSidebarEnabled is used in ui.ts to gate the sidebar pane rendering;
|
|
58
61
|
// artifact events are always processed here regardless of config.
|
|
59
62
|
|
|
@@ -80,6 +83,12 @@ type DispatchOptions = {
|
|
|
80
83
|
* answer.
|
|
81
84
|
*/
|
|
82
85
|
voiceSpoken?: boolean;
|
|
86
|
+
/**
|
|
87
|
+
* Client-token only: page tools the voice server already ran for this
|
|
88
|
+
* delegated turn, sent as `voice.priorToolResults` with `voiceSpoken`.
|
|
89
|
+
* Proxy/agent payloads carry them as replayed tool rows instead.
|
|
90
|
+
*/
|
|
91
|
+
voicePriorToolResults?: VoicePriorToolResult[];
|
|
83
92
|
};
|
|
84
93
|
|
|
85
94
|
export type SSEHandler = (event: AgentWidgetEvent) => void;
|
|
@@ -152,6 +161,7 @@ const isHistoryClientError = (
|
|
|
152
161
|
const hasValidContent = (message: AgentWidgetMessage): boolean => {
|
|
153
162
|
// Display-only voice captions are never conversation.
|
|
154
163
|
if (message.voiceCaption) return false;
|
|
164
|
+
if (isLocalOnlyMessage(message)) return false;
|
|
155
165
|
// Check contentParts (multi-modal content)
|
|
156
166
|
if (message.contentParts && message.contentParts.length > 0) {
|
|
157
167
|
return true;
|
|
@@ -406,6 +416,42 @@ export class AgentWidgetClient {
|
|
|
406
416
|
* session that it should fall back to the legacy local-tool resume path,
|
|
407
417
|
* if any).
|
|
408
418
|
*/
|
|
419
|
+
/**
|
|
420
|
+
* WebMCP access for a Runtype voice call's fast tier: the same per-turn
|
|
421
|
+
* snapshot `clientTools[]` carries, registry change notifications, and the
|
|
422
|
+
* voice execute path. `null` when WebMCP is not enabled.
|
|
423
|
+
*/
|
|
424
|
+
public getVoiceClientTools():import("./types").VoiceClientTools | null {
|
|
425
|
+
if (this.config.webmcp?.enabled !== true) return null;
|
|
426
|
+
const bridge = () => this.#getWebMcpBridge();
|
|
427
|
+
const surface: import("./types").VoiceClientTools = {
|
|
428
|
+
open: async (channel) => (await loadVoiceClientTools()).createVoiceClientToolsSession(surface, channel),
|
|
429
|
+
snapshot: async () => {
|
|
430
|
+
const tools = (await (await bridge())?.snapshotForDispatch()) ?? [];
|
|
431
|
+
return { tools, fingerprint: computeClientToolsFingerprint(tools) };
|
|
432
|
+
},
|
|
433
|
+
onChange: (listener) => {
|
|
434
|
+
let stop: (() => void) | null = null;
|
|
435
|
+
let stopped = false;
|
|
436
|
+
void bridge().then((b) => {
|
|
437
|
+
if (b && !stopped) stop = b.watchToolChanges(listener);
|
|
438
|
+
});
|
|
439
|
+
return () => {
|
|
440
|
+
stopped = true;
|
|
441
|
+
stop?.();
|
|
442
|
+
};
|
|
443
|
+
},
|
|
444
|
+
execute: async (name, args, options) => {
|
|
445
|
+
const b = await bridge();
|
|
446
|
+
if (!b) {
|
|
447
|
+
return { status: "failed", error: { message: "WebMCP runtime failed to load." } };
|
|
448
|
+
}
|
|
449
|
+
return b.executeVoiceToolCall(name, args, options);
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
return surface;
|
|
453
|
+
}
|
|
454
|
+
|
|
409
455
|
public executeWebMcpToolCall(
|
|
410
456
|
wireToolName: string,
|
|
411
457
|
args: unknown,
|
|
@@ -1516,7 +1562,12 @@ export class AgentWidgetClient {
|
|
|
1516
1562
|
// superseded run and this client can drop its stale events below.
|
|
1517
1563
|
turnId,
|
|
1518
1564
|
...(options.interrupt && { submitMode: 'interrupt' as const }),
|
|
1519
|
-
...(options.voiceSpoken && {
|
|
1565
|
+
...(options.voiceSpoken && {
|
|
1566
|
+
voice: {
|
|
1567
|
+
spoken: true as const,
|
|
1568
|
+
...(options.voicePriorToolResults?.length && { priorToolResults: options.voicePriorToolResults }),
|
|
1569
|
+
},
|
|
1570
|
+
}),
|
|
1520
1571
|
};
|
|
1521
1572
|
|
|
1522
1573
|
// Diff-only / send-once WebMCP tool dispatch. `buildPayload()` already
|
|
@@ -1830,9 +1881,10 @@ export class AgentWidgetClient {
|
|
|
1830
1881
|
*/
|
|
1831
1882
|
public async resolveApproval(
|
|
1832
1883
|
approval: { agentId: string; executionId: string; approvalId: string },
|
|
1833
|
-
decision: 'approved' | 'denied'
|
|
1884
|
+
decision: 'approved' | 'denied',
|
|
1885
|
+
options?: { assistantMessageId?: string }
|
|
1834
1886
|
): Promise<Response> {
|
|
1835
|
-
return (await loadClientStream()).resolveApproval(this.#resumeHost(), approval, decision);
|
|
1887
|
+
return (await loadClientStream()).resolveApproval(this.#resumeHost(), approval, decision, options);
|
|
1836
1888
|
}
|
|
1837
1889
|
|
|
1838
1890
|
/**
|
|
@@ -1972,7 +2024,12 @@ export class AgentWidgetClient {
|
|
|
1972
2024
|
public async resumeFlow(
|
|
1973
2025
|
executionId: string,
|
|
1974
2026
|
toolOutputs: Record<string, unknown>,
|
|
1975
|
-
options?: {
|
|
2027
|
+
options?: {
|
|
2028
|
+
streamResponse?: boolean;
|
|
2029
|
+
signal?: AbortSignal;
|
|
2030
|
+
after?: string;
|
|
2031
|
+
assistantMessageId?: string;
|
|
2032
|
+
}
|
|
1976
2033
|
): Promise<Response> {
|
|
1977
2034
|
return (await loadClientStream()).resumeFlow(this.#resumeHost(), executionId, toolOutputs, options);
|
|
1978
2035
|
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { AgentWidgetClient } from "./client";
|
|
4
|
+
import type { ClientToolDefinition } from "./types";
|
|
5
|
+
import { computeClientToolsFingerprint } from "./webmcp-bridge";
|
|
6
|
+
|
|
7
|
+
const tool: ClientToolDefinition = {
|
|
8
|
+
name: "next_slide",
|
|
9
|
+
description: "Advance the presentation to the next slide.",
|
|
10
|
+
parametersSchema: { type: "object", properties: {} },
|
|
11
|
+
origin: "webmcp",
|
|
12
|
+
annotations: { readOnlyHint: true },
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const withBridge = (bridge: Record<string, unknown>) => {
|
|
16
|
+
const client = new AgentWidgetClient({ apiUrl: "http://test", webmcp: { enabled: true } });
|
|
17
|
+
(client as unknown as { webMcpBridge: unknown }).webMcpBridge = bridge;
|
|
18
|
+
return client;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
describe("AgentWidgetClient.getVoiceClientTools", () => {
|
|
22
|
+
it("is null when WebMCP is off", () => {
|
|
23
|
+
expect(new AgentWidgetClient({ apiUrl: "http://test" }).getVoiceClientTools()).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("snapshots exactly the dispatch clientTools[] entries, with their fingerprint", async () => {
|
|
27
|
+
const client = withBridge({ snapshotForDispatch: async () => [tool] });
|
|
28
|
+
const snapshot = await client.getVoiceClientTools()!.snapshot();
|
|
29
|
+
expect(snapshot).toEqual({ tools: [tool], fingerprint: computeClientToolsFingerprint([tool]) });
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("routes execute and change subscriptions to the bridge's voice methods", async () => {
|
|
33
|
+
const stop = vi.fn();
|
|
34
|
+
const watchToolChanges = vi.fn(() => stop);
|
|
35
|
+
const executeVoiceToolCall = vi.fn(async () => ({ status: "completed", result: { slidePosition: 2 } }));
|
|
36
|
+
const surface = withBridge({ watchToolChanges, executeVoiceToolCall }).getVoiceClientTools()!;
|
|
37
|
+
|
|
38
|
+
const signal = new AbortController().signal;
|
|
39
|
+
const outcome = await surface.execute("webmcp:next_slide", {}, { signal, autoConfirmReadOnly: true });
|
|
40
|
+
expect(executeVoiceToolCall).toHaveBeenCalledWith("webmcp:next_slide", {}, { signal, autoConfirmReadOnly: true });
|
|
41
|
+
expect(outcome).toEqual({ status: "completed", result: { slidePosition: 2 } });
|
|
42
|
+
|
|
43
|
+
const listener = vi.fn();
|
|
44
|
+
const unsubscribe = surface.onChange(listener);
|
|
45
|
+
await Promise.resolve();
|
|
46
|
+
await Promise.resolve();
|
|
47
|
+
expect(watchToolChanges).toHaveBeenCalledWith(listener);
|
|
48
|
+
unsubscribe();
|
|
49
|
+
expect(stop).toHaveBeenCalledTimes(1);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("opens a fast-tier session from the lazy chunk that sends the snapshot through the channel", async () => {
|
|
53
|
+
const client = new AgentWidgetClient({ apiUrl: "http://test", webmcp: { enabled: true } });
|
|
54
|
+
const slot = client as unknown as { webMcpBridge: unknown };
|
|
55
|
+
// Let the real lazy bridge adopt first, so it cannot replace the fake mid-test.
|
|
56
|
+
await vi.waitFor(() => expect(slot.webMcpBridge).not.toBeNull());
|
|
57
|
+
slot.webMcpBridge = { snapshotForDispatch: async () => [tool], watchToolChanges: () => () => undefined };
|
|
58
|
+
const surface = client.getVoiceClientTools()!;
|
|
59
|
+
const sent: Array<Record<string, unknown>> = [];
|
|
60
|
+
const session = await surface.open({ send: (frame) => (sent.push(frame), true), cancelled: () => false });
|
|
61
|
+
await vi.waitFor(() => expect(sent).toHaveLength(1));
|
|
62
|
+
expect(sent[0]).toMatchObject({ type: "client_tools", tools: [{ ...tool, name: "webmcp:next_slide" }] });
|
|
63
|
+
session.close();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -41,13 +41,14 @@ export function applyActivityRow(bubble: HTMLElement, message: AgentWidgetMessag
|
|
|
41
41
|
const data = kind === "tool" ? message.toolCall : message.reasoning;
|
|
42
42
|
const active = data?.status !== "complete";
|
|
43
43
|
const approval = message.approval?.status ?? message.toolCall?.approvalStatus;
|
|
44
|
-
const state =
|
|
44
|
+
const state = message.toolCall?.interrupted ? "interrupted"
|
|
45
|
+
: approval === "denied" ? "denied" : approval === "pending" ? "awaiting-approval"
|
|
45
46
|
: message.toolCall?.success === false ? "error" : !active ? "done"
|
|
46
47
|
: data?.status === "pending" ? "pending" : "running";
|
|
47
48
|
bubble.dataset.activityState = state;
|
|
48
49
|
const icon = createElement("span", "persona-activity-icon");
|
|
49
50
|
icon.dataset.activityIcon = state;
|
|
50
|
-
const names = { pending: "circle", running: "loader-circle", done: "check", error: "x", denied: "x", "awaiting-approval": "clock" };
|
|
51
|
+
const names = { pending: "circle", running: "loader-circle", done: "check", error: "x", denied: "x", interrupted: "x", "awaiting-approval": "clock" };
|
|
51
52
|
const customIcon = kind === "reasoning" ? config.features?.reasoningDisplay?.iconName : undefined;
|
|
52
53
|
const glyph = customIcon ? renderLucideIcon(customIcon, 16, "currentColor", 2)
|
|
53
54
|
: state === "pending" ? renderIconNode(Circle, 16, "currentColor", 2)
|
|
@@ -346,3 +346,11 @@ describe("built-in approval — resolved states", () => {
|
|
|
346
346
|
expect(el?.textContent).toContain("timed out");
|
|
347
347
|
});
|
|
348
348
|
});
|
|
349
|
+
|
|
350
|
+
it("interrupted approvals explain expiry without action controls", () => {
|
|
351
|
+
const { el } = render({}, makeMessage({ status: "timeout", interrupted: true }));
|
|
352
|
+
expect(el?.textContent).toContain("approval expired");
|
|
353
|
+
expect(el?.textContent).toContain("chat restarted");
|
|
354
|
+
expect(el?.textContent).toContain("fresh proposal");
|
|
355
|
+
expect(el?.querySelector("button, [data-action]")).toBeNull();
|
|
356
|
+
});
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* tool call takes over the transcript); denied/timeout → a subtle one-line
|
|
19
19
|
* trace.
|
|
20
20
|
*/
|
|
21
|
-
import { createElement } from "../utils/dom";
|
|
21
|
+
import { createElement, createNode } from "../utils/dom";
|
|
22
|
+
import { BROWSER_TOOL_INTERRUPTION } from "../utils/restored-browser-state";
|
|
22
23
|
// The webmcp-title lookup is injected by ui.ts at chunk adoption (see
|
|
23
24
|
// approval-deps): this module ships in the lazy approval-ui chunk. Icons are
|
|
24
25
|
// direct lucide data imports.
|
|
@@ -168,7 +169,10 @@ const buildResolvedTrace = (approval: Approval): HTMLElement => {
|
|
|
168
169
|
if (icon) row.appendChild(icon);
|
|
169
170
|
const name = createElement("span", "persona-approval-resolved-name");
|
|
170
171
|
name.textContent = approval.toolName ? humanizeToolName(approval.toolName) : "Tool";
|
|
171
|
-
|
|
172
|
+
const explanation = approval.status === "timeout" && approval.interrupted
|
|
173
|
+
? ` approval expired. ${BROWSER_TOOL_INTERRUPTION}`
|
|
174
|
+
: approval.status === "timeout" ? " timed out" : " denied";
|
|
175
|
+
row.appendChild(createNode("span", { className: "persona-approval-resolved-text" }, name, explanation));
|
|
172
176
|
return row;
|
|
173
177
|
};
|
|
174
178
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createApprovalBubble,
|
|
6
6
|
humanizeToolName,
|
|
7
7
|
updateApprovalDetailsUI,
|
|
8
|
+
updateApprovalBubbleUI,
|
|
8
9
|
} from "./approval-bubble";
|
|
9
10
|
import {
|
|
10
11
|
getWebMcpToolDisplayTitle,
|
|
@@ -45,6 +46,19 @@ const makeMessage = (
|
|
|
45
46
|
},
|
|
46
47
|
});
|
|
47
48
|
|
|
49
|
+
it("shows interruption metadata while keeping the existing timeout status", () => {
|
|
50
|
+
const bubble = createApprovalBubble(makeMessage({ status: "timeout", interrupted: true }));
|
|
51
|
+
const badge = bubble.querySelector("[data-approval-status]");
|
|
52
|
+
expect(badge?.getAttribute("data-approval-status")).toBe("timeout");
|
|
53
|
+
expect(badge?.textContent).toBe("Interrupted");
|
|
54
|
+
expect(bubble.textContent).toContain("chat restarted");
|
|
55
|
+
expect(bubble.textContent).toContain("fresh proposal");
|
|
56
|
+
expect(bubble.querySelector('[data-action="allow"], [data-action="deny"]')).toBeNull();
|
|
57
|
+
updateApprovalBubbleUI("msg-1", bubble, {}, makeMessage({ status: "timeout", interrupted: true }).approval);
|
|
58
|
+
expect(badge?.textContent).toBe("Interrupted");
|
|
59
|
+
expect(badge?.getAttribute("data-approval-status")).toBe("timeout");
|
|
60
|
+
});
|
|
61
|
+
|
|
48
62
|
describe("createApprovalBubble shadow", () => {
|
|
49
63
|
it("falls back to the themeable --persona-approval-shadow variable when no config.shadow is set", () => {
|
|
50
64
|
const bubble = createApprovalBubble(makeMessage());
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BROWSER_TOOL_INTERRUPTION } from "../utils/restored-browser-state";
|
|
1
2
|
import { createElement } from "../utils/dom";
|
|
2
3
|
import { AgentWidgetMessage, AgentWidgetConfig } from "../types";
|
|
3
4
|
import { formatUnknownValue } from "../utils/formatting";
|
|
@@ -100,6 +101,7 @@ export const updateApprovalBubbleUI = (
|
|
|
100
101
|
if (statusBadge) {
|
|
101
102
|
statusBadge.textContent = approval.status === "approved" ? "Approved"
|
|
102
103
|
: approval.status === "denied" ? "Denied"
|
|
104
|
+
: (approval.status === "timeout" && approval.interrupted) ? "Interrupted"
|
|
103
105
|
: approval.status === "timeout" ? "Timeout"
|
|
104
106
|
: "Pending";
|
|
105
107
|
|
|
@@ -232,12 +234,17 @@ export const createApprovalBubble = (
|
|
|
232
234
|
} else if (approval.status === "timeout") {
|
|
233
235
|
badge.style.backgroundColor = "var(--persona-palette-colors-warning-100)";
|
|
234
236
|
badge.style.color = "var(--persona-palette-colors-warning-700)";
|
|
235
|
-
badge.textContent = "Timeout";
|
|
237
|
+
badge.textContent = (approval.status === "timeout" && approval.interrupted) ? "Interrupted" : "Timeout";
|
|
236
238
|
}
|
|
237
239
|
titleRow.appendChild(badge);
|
|
238
240
|
}
|
|
239
241
|
|
|
240
242
|
content.appendChild(titleRow);
|
|
243
|
+
if (approval.status === "timeout" && approval.interrupted) {
|
|
244
|
+
const notice = createElement("p", "persona-text-sm");
|
|
245
|
+
notice.textContent = BROWSER_TOOL_INTERRUPTION;
|
|
246
|
+
content.appendChild(notice);
|
|
247
|
+
}
|
|
241
248
|
|
|
242
249
|
// User-facing summary line. The wire `description` is the tool's
|
|
243
250
|
// agent-facing description (prompt prose, usage rules), so it is not shown
|
|
@@ -78,7 +78,8 @@ const getToolSummaryText = (
|
|
|
78
78
|
message,
|
|
79
79
|
toolDisplayConfig?.previewMaxLines ?? DEFAULT_TOOL_CALL_DISPLAY.previewMaxLines
|
|
80
80
|
);
|
|
81
|
-
const defaultSummary = tool ?
|
|
81
|
+
const defaultSummary = tool?.interrupted ? `${tool.name?.trim() || "Tool"} · Interrupted`
|
|
82
|
+
: tool ? (activityVariant(config, "tool") === "row" ? `${tool.status === "complete" ? "Used" : "Using"} ${tool.name?.trim() || "tool"}` : describeToolTitle(tool)) : "";
|
|
82
83
|
|
|
83
84
|
if (!tool) {
|
|
84
85
|
return { summary: defaultSummary, previewText, isActive: false };
|
|
@@ -96,11 +97,13 @@ const getToolSummaryText = (
|
|
|
96
97
|
// Apply text templates if configured
|
|
97
98
|
if (isActive && toolCallConfig.activeTextTemplate) {
|
|
98
99
|
summary = resolveToolHeaderText(tool, toolCallConfig.activeTextTemplate, summary);
|
|
99
|
-
} else if (!isActive && toolCallConfig.completeTextTemplate) {
|
|
100
|
+
} else if (!isActive && !tool.interrupted && toolCallConfig.completeTextTemplate) {
|
|
100
101
|
summary = resolveToolHeaderText(tool, toolCallConfig.completeTextTemplate, summary);
|
|
101
102
|
}
|
|
102
103
|
|
|
103
|
-
if (tool.
|
|
104
|
+
if (tool.interrupted) {
|
|
105
|
+
if (!summary.includes(" · Interrupted")) summary += " · Interrupted";
|
|
106
|
+
} else if (tool.success === false) summary += " · Failed";
|
|
104
107
|
else if (tool.superseded) summary += " · Approved, ran again below";
|
|
105
108
|
return { summary, previewText, isActive };
|
|
106
109
|
};
|
|
@@ -184,7 +187,7 @@ export const createToolBubble = (
|
|
|
184
187
|
// Helper: build a <span data-tool-elapsed> that the global timer in ui.ts updates
|
|
185
188
|
const createElapsedSpan = (): HTMLElement => {
|
|
186
189
|
const span = createElement("span", "");
|
|
187
|
-
span.setAttribute("data-tool-elapsed", startedAt);
|
|
190
|
+
if (isActive) span.setAttribute("data-tool-elapsed", startedAt);
|
|
188
191
|
span.textContent = computeToolElapsed(tool);
|
|
189
192
|
return span;
|
|
190
193
|
};
|
|
@@ -213,7 +216,7 @@ export const createToolBubble = (
|
|
|
213
216
|
// Apply loading animation when tool is active and no custom HTMLElement was provided
|
|
214
217
|
const loadingAnimation = toolDisplayConfig.loadingAnimation ?? DEFAULT_TOOL_CALL_DISPLAY.loadingAnimation;
|
|
215
218
|
const activeTemplate = toolCallConfig.activeTextTemplate;
|
|
216
|
-
const completeTemplate = toolCallConfig.completeTextTemplate;
|
|
219
|
+
const completeTemplate = tool.interrupted ? undefined : toolCallConfig.completeTextTemplate;
|
|
217
220
|
const currentTemplate = isActive ? activeTemplate : completeTemplate;
|
|
218
221
|
const skipCustomElement = customSummary instanceof HTMLElement;
|
|
219
222
|
|
|
@@ -35,6 +35,7 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
35
35
|
finalOutput?: string;
|
|
36
36
|
iterations?: number;
|
|
37
37
|
kind: "agent" | "flow";
|
|
38
|
+
outcome?: "completed" | "error" | "aborted";
|
|
38
39
|
seq: number;
|
|
39
40
|
stepErrorCount?: number;
|
|
40
41
|
stopReason?: string;
|
|
@@ -60,6 +61,7 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
60
61
|
executionId: string;
|
|
61
62
|
finalOutput?: string;
|
|
62
63
|
kind: "agent" | "flow";
|
|
64
|
+
outcome?: "completed" | "error" | "aborted";
|
|
63
65
|
seq: number;
|
|
64
66
|
stopReason?: string;
|
|
65
67
|
type: "execution_error";
|
|
@@ -379,6 +381,15 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
379
381
|
seq: number;
|
|
380
382
|
type: "state_delta";
|
|
381
383
|
}) | ({
|
|
384
|
+
annotations?: {
|
|
385
|
+
consequentialHint?: boolean;
|
|
386
|
+
debugging?: boolean;
|
|
387
|
+
destructiveHint?: boolean;
|
|
388
|
+
idempotentHint?: boolean;
|
|
389
|
+
openWorldHint?: boolean;
|
|
390
|
+
readOnlyHint?: boolean;
|
|
391
|
+
untrustedContentHint?: boolean;
|
|
392
|
+
};
|
|
382
393
|
executionId: string;
|
|
383
394
|
hiddenParameterNames?: Array<string>;
|
|
384
395
|
iteration?: number;
|
|
@@ -477,6 +488,15 @@ export type RuntypeExecutionStreamEvent = ({
|
|
|
477
488
|
seq: number;
|
|
478
489
|
type: "approval_complete";
|
|
479
490
|
}) | ({
|
|
491
|
+
annotations?: {
|
|
492
|
+
consequentialHint?: boolean;
|
|
493
|
+
debugging?: boolean;
|
|
494
|
+
destructiveHint?: boolean;
|
|
495
|
+
idempotentHint?: boolean;
|
|
496
|
+
openWorldHint?: boolean;
|
|
497
|
+
readOnlyHint?: boolean;
|
|
498
|
+
untrustedContentHint?: boolean;
|
|
499
|
+
};
|
|
480
500
|
awaitReason?: string;
|
|
481
501
|
awaitedAt?: string;
|
|
482
502
|
crawlId?: string;
|
|
@@ -641,6 +661,15 @@ export type RuntypeClientInitResponse = {
|
|
|
641
661
|
export type RuntypeClientChatRequest = {
|
|
642
662
|
assistantMessageId?: string;
|
|
643
663
|
clientTools?: Array<{
|
|
664
|
+
annotations?: {
|
|
665
|
+
consequentialHint?: boolean;
|
|
666
|
+
debugging?: boolean;
|
|
667
|
+
destructiveHint?: boolean;
|
|
668
|
+
idempotentHint?: boolean;
|
|
669
|
+
openWorldHint?: boolean;
|
|
670
|
+
readOnlyHint?: boolean;
|
|
671
|
+
untrustedContentHint?: boolean;
|
|
672
|
+
};
|
|
644
673
|
description: string;
|
|
645
674
|
name: string;
|
|
646
675
|
origin?: "webmcp" | "sdk";
|
|
@@ -681,7 +710,7 @@ export type RuntypeClientChatRequest = {
|
|
|
681
710
|
}>;
|
|
682
711
|
metadata?: Record<string, unknown>;
|
|
683
712
|
sessionId: string;
|
|
684
|
-
submitMode?: "normal" | "interrupt" | "steer";
|
|
713
|
+
submitMode?: "normal" | "interrupt" | "steer" | "auto";
|
|
685
714
|
turnId?: string;
|
|
686
715
|
voice?: {
|
|
687
716
|
spoken?: boolean;
|
|
@@ -694,6 +723,15 @@ export type RuntypeClientResumeRequest = {
|
|
|
694
723
|
after?: string;
|
|
695
724
|
assistantMessageId?: string;
|
|
696
725
|
clientTools?: Array<{
|
|
726
|
+
annotations?: {
|
|
727
|
+
consequentialHint?: boolean;
|
|
728
|
+
debugging?: boolean;
|
|
729
|
+
destructiveHint?: boolean;
|
|
730
|
+
idempotentHint?: boolean;
|
|
731
|
+
openWorldHint?: boolean;
|
|
732
|
+
readOnlyHint?: boolean;
|
|
733
|
+
untrustedContentHint?: boolean;
|
|
734
|
+
};
|
|
697
735
|
description: string;
|
|
698
736
|
name: string;
|
|
699
737
|
origin?: "webmcp" | "sdk";
|
package/src/index-core.ts
CHANGED
|
@@ -174,6 +174,7 @@ export type {
|
|
|
174
174
|
// WebMCP: page-discovered tool consumption
|
|
175
175
|
AgentWidgetWebMcpConfig,
|
|
176
176
|
ClientToolDefinition,
|
|
177
|
+
ToolAnnotations,
|
|
177
178
|
WebMcpConfirmHandler,
|
|
178
179
|
WebMcpConfirmInfo,
|
|
179
180
|
WebMcpToolResult,
|
|
@@ -659,6 +660,16 @@ export type {
|
|
|
659
660
|
VoiceDelegationStatus,
|
|
660
661
|
VoiceDelegationFollowUp,
|
|
661
662
|
VoiceSessionBridge,
|
|
663
|
+
VoiceClientTools,
|
|
664
|
+
VoiceClientToolsSnapshot,
|
|
665
|
+
VoiceClientToolsChannel,
|
|
666
|
+
VoiceClientToolsSession,
|
|
667
|
+
VoiceClientToolOutcome,
|
|
668
|
+
VoiceClientToolStatus,
|
|
669
|
+
VoiceDirectTurn,
|
|
670
|
+
ClientToolsFrame,
|
|
671
|
+
ClientToolCallFrame,
|
|
672
|
+
ClientToolResultFrame,
|
|
662
673
|
PcmStreamPlayer,
|
|
663
674
|
AgentWidgetVoiceStatusEvent,
|
|
664
675
|
ReadAloudState,
|
package/src/index-global.ts
CHANGED
|
@@ -221,6 +221,12 @@ import { setVoiceRuntimeLoader } from "./voice-runtime-loader";
|
|
|
221
221
|
|
|
222
222
|
setVoiceRuntimeLoader(siblingChunk("voice-runtime.js"));
|
|
223
223
|
|
|
224
|
+
// The voice fast tier (`client_tools`), fetched by client.ts only when a voice
|
|
225
|
+
// call negotiates it. Same sibling-URL scheme.
|
|
226
|
+
import { setVoiceClientToolsLoader } from "./voice-client-tools-loader";
|
|
227
|
+
|
|
228
|
+
setVoiceClientToolsLoader(siblingChunk("voice-client-tools.js"));
|
|
229
|
+
|
|
224
230
|
// ---------------------------------------------------------------------------
|
|
225
231
|
// Deferred artifacts-ui loading.
|
|
226
232
|
//
|