@runtypelabs/persona 4.25.0 → 4.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/animations/glyph-cycle.d.cts +1 -1
- package/dist/animations/glyph-cycle.d.ts +1 -1
- package/dist/animations/{types-Db61aI9J.d.cts → types-BwKTZhg9.d.cts} +14 -0
- package/dist/animations/{types-Db61aI9J.d.ts → types-BwKTZhg9.d.ts} +14 -0
- package/dist/animations/wipe.d.cts +1 -1
- package/dist/animations/wipe.d.ts +1 -1
- package/dist/animations-extra.d.cts +14 -0
- package/dist/animations-extra.d.ts +14 -0
- package/dist/approval-ui-QAKWNFNM.js +1 -0
- package/dist/approval-ui.cjs +2 -2
- package/dist/approval-ui.d.cts +180 -8
- package/dist/approval-ui.d.ts +180 -8
- package/dist/approval-ui.js +2 -2
- package/dist/{artifacts-ui-IGB5YKLO.js → artifacts-ui-NJGMM27I.js} +1 -1
- package/dist/artifacts-ui.cjs +4 -4
- package/dist/artifacts-ui.d.cts +180 -8
- package/dist/artifacts-ui.d.ts +180 -8
- package/dist/artifacts-ui.js +5 -5
- package/dist/chunk-2ANTRKTV.js +1 -0
- package/dist/chunk-4HPTPUC6.js +1 -0
- package/dist/chunk-6V3MNEKB.js +1 -0
- package/dist/chunk-HGUCGM3K.js +9 -0
- package/dist/chunk-IO5VVUKP.js +3 -0
- package/dist/chunk-J4J3Y2P2.js +1 -0
- package/dist/chunk-K6HKIP5K.js +1 -0
- package/dist/chunk-K7NA7G7J.js +1 -0
- package/dist/chunk-MXUTS3LB.js +1 -0
- package/dist/chunk-PDPVORUF.js +1 -0
- package/dist/chunk-QPPJUSZH.js +1 -0
- package/dist/chunk-QRKGQPU7.js +1 -0
- package/dist/chunk-S6PBWE75.js +1 -0
- package/dist/chunk-T7QM7YMF.js +1 -0
- package/dist/chunk-TR2H475E.js +1 -0
- package/dist/chunk-UPO4GUFC.js +1 -0
- package/dist/chunk-ZC66NAIR.js +1 -0
- package/dist/client-history-YPEG45Q4.js +1 -0
- package/dist/client-history.js +1 -0
- package/dist/client-stream-42FOI3EC.js +4 -0
- package/dist/client-stream.js +5 -0
- package/dist/codegen.cjs +1 -1
- package/dist/codegen.js +1 -1
- package/dist/context-mentions-E7HJYWO6.js +161 -0
- package/dist/{context-mentions-inline-I2RDEBAK.js → context-mentions-inline-QVESLW55.js} +1 -1
- package/dist/context-mentions.cjs +7 -7
- package/dist/context-mentions.d.cts +180 -8
- package/dist/context-mentions.d.ts +180 -8
- package/dist/context-mentions.js +8 -8
- package/dist/event-stream-view-FOIJHFQO.js +1 -0
- package/dist/event-stream-view.cjs +1 -1
- package/dist/event-stream-view.d.cts +183 -31
- package/dist/event-stream-view.d.ts +183 -31
- package/dist/event-stream-view.js +1 -1
- package/dist/forms-ui.d.cts +221 -667
- package/dist/forms-ui.d.ts +221 -667
- package/dist/history-shell-QDQ44PSE.js +3 -0
- package/dist/history-shell.js +3 -0
- package/dist/{history-view-BD6OXLNA.js → history-view-NCFBI5GR.js} +2 -2
- package/dist/history-view.cjs +2 -2
- package/dist/history-view.d.cts +35 -1
- package/dist/history-view.d.ts +35 -1
- package/dist/history-view.js +2 -2
- package/dist/index.cjs +80 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +227 -738
- package/dist/index.d.ts +227 -738
- package/dist/index.global.js +3 -215
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +81 -75
- package/dist/index.js.map +1 -1
- package/dist/install.global.js +1 -1
- package/dist/install.global.js.map +1 -1
- package/dist/launcher.global.js +3 -67
- package/dist/launcher.global.js.map +1 -1
- package/dist/markdown-parsers.js +22 -22
- package/dist/runtype-tts-entry-NFBVWR7N.js +1 -0
- package/dist/runtype-tts.js +1 -1
- package/dist/session-actions-ISSKSI66.js +1 -0
- package/dist/session-actions.js +1 -0
- package/dist/session-reconnect.d.cts +180 -8
- package/dist/session-reconnect.d.ts +180 -8
- package/dist/smart-dom-reader.cjs +13 -13
- package/dist/smart-dom-reader.d.cts +180 -8
- package/dist/smart-dom-reader.d.ts +180 -8
- package/dist/smart-dom-reader.js +11 -11
- package/dist/theme-editor-preview.cjs +116 -98
- package/dist/theme-editor-preview.d.cts +182 -8
- package/dist/theme-editor-preview.d.ts +182 -8
- package/dist/theme-editor-preview.js +49 -54
- package/dist/theme-editor.cjs +2 -2
- package/dist/theme-editor.d.cts +181 -23
- package/dist/theme-editor.d.ts +181 -23
- package/dist/theme-editor.js +2 -2
- package/dist/theme-reference.d.cts +14 -0
- package/dist/theme-reference.d.ts +14 -0
- package/dist/ui-extras-entry-WAYFKVNW.js +3 -0
- package/dist/ui-extras.js +72 -0
- package/dist/voice-runtime-UOQOFAFY.js +17 -0
- package/dist/voice-runtime.cjs +17 -1
- package/dist/voice-runtime.d.cts +11650 -232
- package/dist/voice-runtime.d.ts +11650 -232
- package/dist/voice-runtime.js +17 -1
- package/dist/voice-worklet-player.cjs +19 -4
- package/dist/voice-worklet-player.d.cts +9 -10
- package/dist/voice-worklet-player.d.ts +9 -10
- package/dist/voice-worklet-player.js +19 -4
- package/dist/webmcp-polyfill.js +1 -1
- package/dist/webmcp-runtime-V6ZWW3PI.js +6 -0
- package/dist/webmcp-runtime.cjs +2 -2
- package/dist/webmcp-runtime.d.cts +1 -33
- package/dist/webmcp-runtime.d.ts +1 -33
- package/dist/webmcp-runtime.js +2 -2
- package/dist/widget.css +1 -1
- package/package.json +11 -5
- package/src/approval-paused-tool-settle.test.ts +221 -0
- package/src/client-approve.test.ts +311 -0
- package/src/client-history-eager.ts +9 -0
- package/src/client-history-loader.ts +23 -0
- package/src/client-history.ts +579 -0
- package/src/client-resume.ts +177 -0
- package/src/client-stream-eager.ts +12 -0
- package/src/client-stream-loader.ts +25 -0
- package/src/client-stream.ts +1880 -0
- package/src/client.test.ts +31 -1
- package/src/client.ts +293 -2607
- package/src/components/approval-actions.test.ts +27 -0
- package/src/components/approval-actions.ts +27 -3
- package/src/components/message-bubble.test.ts +19 -0
- package/src/components/message-bubble.ts +2 -0
- package/src/components/registry.ts +6 -6
- package/src/components/tool-bubble.test.ts +9 -0
- package/src/components/tool-bubble.ts +1 -0
- package/src/history-shell-bundle.test.ts +54 -0
- package/src/history-shell-eager.ts +12 -0
- package/src/history-shell-loader.ts +27 -0
- package/src/history-shell.ts +2362 -0
- package/src/history-view-loader.ts +2 -0
- package/src/history-view.ts +3 -0
- package/src/index-core.ts +6 -0
- package/src/index-global.ts +90 -248
- package/src/index.ts +13 -0
- package/src/install.ts +6 -4
- package/src/internal/runtype-history-provider.test.ts +1 -0
- package/src/internal/runtype-history-provider.ts +28 -13
- package/src/plugins/registry.ts +8 -8
- package/src/session-actions-chunk.test.ts +55 -0
- package/src/session-actions-eager.ts +11 -0
- package/src/session-actions-loader.ts +37 -0
- package/src/session-actions.ts +909 -0
- package/src/session-history.test.ts +3 -1
- package/src/session.ts +852 -1536
- package/src/session.voice.test.ts +1170 -5
- package/src/styles/widget.css +58 -0
- package/src/theme-editor/state.ts +91 -91
- package/src/types.ts +198 -5
- package/src/ui-ask-user-sheet.ts +382 -0
- package/src/ui-extras-eager.ts +10 -0
- package/src/ui-extras-entry.ts +9 -0
- package/src/ui-extras-loader.ts +26 -0
- package/src/ui.artifact-rail-resize.test.ts +2 -1
- package/src/ui.ask-user-chunk.test.ts +93 -0
- package/src/ui.history-chunk.test.ts +2 -1
- package/src/ui.history-config-provider.test.ts +2 -1
- package/src/ui.history-confirm-fallback.test.ts +142 -0
- package/src/ui.history-controller.test.ts +2 -1
- package/src/ui.history-lifecycle.test.ts +2 -1
- package/src/ui.history-render-hooks.test.ts +2 -1
- package/src/ui.history-shell.test.ts +2 -1
- package/src/ui.rail-overlay.test.ts +2 -1
- package/src/ui.rail-resize.test.ts +2 -1
- package/src/ui.rail-trigger-pending.test.ts +2 -1
- package/src/ui.ts +340 -2770
- package/src/utils/attachment-manager.ts +67 -67
- package/src/utils/constants.ts +3 -0
- package/src/utils/context-mention-controller.ts +323 -323
- package/src/utils/context-mention-manager.ts +40 -40
- package/src/utils/context-mention-orchestrator.ts +28 -0
- package/src/utils/event-stream-buffer.ts +50 -50
- package/src/utils/event-stream-store.ts +43 -43
- package/src/utils/throughput-tracker.ts +34 -34
- package/src/utils/virtual-scroller.ts +67 -67
- package/src/utils/visitor-store.test.ts +50 -0
- package/src/utils/visitor-store.ts +23 -5
- package/src/voice/__fixtures__/voice-wire/README.md +1 -0
- package/src/voice/__fixtures__/voice-wire/client/cancel.json +3 -0
- package/src/voice/__fixtures__/voice-wire/client/context.json +4 -0
- package/src/voice/__fixtures__/voice-wire/client/delegation_result.json +6 -0
- package/src/voice/__fixtures__/voice-wire/client/delegation_update.json +6 -0
- package/src/voice/__fixtures__/voice-wire/client/ping.json +3 -0
- package/src/voice/__fixtures__/voice-wire/client/playback_progress.json +5 -0
- package/src/voice/__fixtures__/voice-wire/client/start.json +3 -0
- package/src/voice/__fixtures__/voice-wire/server/artifact.json +11 -0
- package/src/voice/__fixtures__/voice-wire/server/attached.json +4 -0
- package/src/voice/__fixtures__/voice-wire/server/audio_clear.json +4 -0
- package/src/voice/__fixtures__/voice-wire/server/audio_end.json +3 -0
- package/src/voice/__fixtures__/voice-wire/server/delegation_cancelled.json +5 -0
- package/src/voice/__fixtures__/voice-wire/server/delegation_completed.json +8 -0
- package/src/voice/__fixtures__/voice-wire/server/delegation_started.json +15 -0
- package/src/voice/__fixtures__/voice-wire/server/error.json +6 -0
- package/src/voice/__fixtures__/voice-wire/server/metrics.json +10 -0
- package/src/voice/__fixtures__/voice-wire/server/pong.json +3 -0
- package/src/voice/__fixtures__/voice-wire/server/session_config.json +16 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_final.json +7 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_interim.json +4 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_partial.json +6 -0
- package/src/voice/__fixtures__/voice-wire/server/transcript_update.json +10 -0
- package/src/voice/__fixtures__/voice-wire/server/warning.json +5 -0
- package/src/voice/audio-playback-manager.test.ts +57 -0
- package/src/voice/audio-playback-manager.ts +120 -98
- package/src/voice/browser-voice-provider.ts +37 -37
- package/src/voice/fallback-speech-engine.ts +22 -16
- package/src/voice/keyed-voice-transcript.ts +314 -0
- package/src/voice/mic-state-styles.ts +182 -0
- package/src/voice/read-aloud-controller.ts +46 -44
- package/src/voice/runtype-speech-engine.ts +35 -33
- package/src/voice/runtype-voice-provider.ts +670 -189
- package/src/voice/session-voice-wiring.ts +266 -0
- package/src/voice/voice-delegation.test.ts +71 -0
- package/src/voice/voice-delegation.ts +364 -0
- package/src/voice/voice-turn-tracker.ts +6 -6
- package/src/voice/voice.test.ts +981 -8
- package/src/voice/worklet-playback-engine.test.ts +81 -0
- package/src/voice/worklet-playback-engine.ts +21 -3
- package/src/voice-runtime-loader.ts +4 -1
- package/src/voice-runtime.ts +7 -0
- package/src/webmcp-runtime-entry.ts +41 -39
- package/dist/approval-ui-3NMYNXBD.js +0 -1
- package/dist/chunk-5EIIHQLQ.js +0 -1
- package/dist/chunk-7CO4T5KB.js +0 -1
- package/dist/chunk-EFJBUH5F.js +0 -1
- package/dist/chunk-R2TMIMDI.js +0 -3
- package/dist/chunk-UTVLOTWB.js +0 -9
- package/dist/context-mentions-45MODWOY.js +0 -161
- package/dist/event-stream-view-GL3ADRY2.js +0 -1
- package/dist/runtype-tts-entry-UJAEF7NZ.js +0 -1
- package/dist/voice-runtime-7DA7MJH4.js +0 -1
- package/dist/webmcp-runtime-JI7T4QYF.js +0 -6
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-side voice wiring: attaches a freshly constructed provider's
|
|
3
|
+
* callbacks (transcripts, status, level, errors) to an `AgentWidgetSession`.
|
|
4
|
+
*
|
|
5
|
+
* Lives in the lazy voice-runtime chunk (exported from `voice-runtime.ts`),
|
|
6
|
+
* not in `session.ts`: it only runs after that chunk resolves in
|
|
7
|
+
* `setupVoice`, so pages without voice never download it. The session hands
|
|
8
|
+
* over its private voice state through a `VoiceWiringHost`.
|
|
9
|
+
*/
|
|
10
|
+
import type { AgentWidgetSession } from "../session";
|
|
11
|
+
import type { AgentWidgetMessage, VoiceProvider, VoiceStatus } from "../types";
|
|
12
|
+
import type { KeyedVoiceTranscript } from "./keyed-voice-transcript";
|
|
13
|
+
import type { VoiceDelegationCapture, createVoiceSessionBridge } from "./voice-delegation";
|
|
14
|
+
import type { VoiceTurnTracker } from "./voice-turn-tracker";
|
|
15
|
+
|
|
16
|
+
/** The session members the wiring touches (some are TS-private on the class). */
|
|
17
|
+
export type VoiceWiringSession = Pick<
|
|
18
|
+
AgentWidgetSession,
|
|
19
|
+
"injectMessage" | "sendMessage" | "resolveWebMcpApproval" | "resolveApproval"
|
|
20
|
+
> & {
|
|
21
|
+
messages: AgentWidgetMessage[];
|
|
22
|
+
callbacks: AgentWidgetSession["callbacks"];
|
|
23
|
+
config: AgentWidgetSession["config"];
|
|
24
|
+
streaming: boolean;
|
|
25
|
+
ttsSpokenMessageIds: Set<string>;
|
|
26
|
+
webMcpApprovalResolvers: Map<string, unknown>;
|
|
27
|
+
upsertMessage: (message: AgentWidgetMessage) => void;
|
|
28
|
+
speakLatestAssistantMessage: (only?: string[]) => void;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/** Accessors for the session's `#private` voice state. */
|
|
32
|
+
export type VoiceWiringHost = {
|
|
33
|
+
session: VoiceWiringSession;
|
|
34
|
+
provider: () => VoiceProvider | null;
|
|
35
|
+
generation: () => number;
|
|
36
|
+
keyed: KeyedVoiceTranscript | null;
|
|
37
|
+
pendingUser: string | null;
|
|
38
|
+
pendingAssistant: string | null;
|
|
39
|
+
turns: () => VoiceTurnTracker;
|
|
40
|
+
busy: () => boolean;
|
|
41
|
+
setStreaming: (streaming: boolean) => void;
|
|
42
|
+
setDelegation: (capture: VoiceDelegationCapture | null) => void;
|
|
43
|
+
setLevel: (level: number) => void;
|
|
44
|
+
setStatus: (status: VoiceStatus) => void;
|
|
45
|
+
settlePending: (final: boolean) => void;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Wire callbacks onto a freshly constructed provider and connect it. */
|
|
49
|
+
export const wireSessionVoice = (
|
|
50
|
+
p: VoiceWiringHost,
|
|
51
|
+
provider: VoiceProvider,
|
|
52
|
+
Keyed?: typeof KeyedVoiceTranscript,
|
|
53
|
+
createBridge?: typeof createVoiceSessionBridge
|
|
54
|
+
): void => {
|
|
55
|
+
const s = p.session;
|
|
56
|
+
try {
|
|
57
|
+
p.keyed = Keyed
|
|
58
|
+
? new Keyed({
|
|
59
|
+
find: (id) => s.messages.find((m) => m.id === id),
|
|
60
|
+
inject: (options) => s.injectMessage(options),
|
|
61
|
+
upsert: (message) => s.upsertMessage(message),
|
|
62
|
+
settle: (ids) => {
|
|
63
|
+
s.messages = s.messages.map((m) =>
|
|
64
|
+
ids.has(m.id) ? { ...m, streaming: false, voiceProcessing: false } : m
|
|
65
|
+
);
|
|
66
|
+
s.callbacks.onMessagesChanged([...s.messages]);
|
|
67
|
+
},
|
|
68
|
+
// A chat turn (e.g. a delegated one) owns the flag while it runs.
|
|
69
|
+
setStreaming: (streaming) => {
|
|
70
|
+
if (streaming || !p.busy()) p.setStreaming(streaming);
|
|
71
|
+
},
|
|
72
|
+
markSpoken: (id) => {
|
|
73
|
+
s.ttsSpokenMessageIds.add(id);
|
|
74
|
+
}
|
|
75
|
+
})
|
|
76
|
+
: null;
|
|
77
|
+
if (createBridge && provider.setSessionBridge) {
|
|
78
|
+
provider.setSessionBridge(
|
|
79
|
+
createBridge({
|
|
80
|
+
messages: () => s.messages,
|
|
81
|
+
busy: () => p.busy(),
|
|
82
|
+
parked: () => s.webMcpApprovalResolvers.size > 0,
|
|
83
|
+
claim: (text, userUtteranceIds) => p.keyed?.claimUserTurn(text, userUtteranceIds) ?? null,
|
|
84
|
+
send: (text, userMessageId) =>
|
|
85
|
+
s.sendMessage(text, { viaVoice: true, voiceTurn: { userMessageId } }),
|
|
86
|
+
track: (capture) => {
|
|
87
|
+
p.setDelegation(capture);
|
|
88
|
+
},
|
|
89
|
+
unspoken: (ids) => {
|
|
90
|
+
for (const id of ids) s.ttsSpokenMessageIds.delete(id);
|
|
91
|
+
// Its stream already ended (browser TTS skipped it then): read it
|
|
92
|
+
// now. A stream still running reads it when it ends, once.
|
|
93
|
+
if (!s.streaming) s.speakLatestAssistantMessage(ids);
|
|
94
|
+
},
|
|
95
|
+
decide: (id) => {
|
|
96
|
+
if (s.webMcpApprovalResolvers.has(id)) return s.resolveWebMcpApproval(id, 'denied');
|
|
97
|
+
const approval = s.messages.find((m) => m.id === id)?.approval;
|
|
98
|
+
if (approval?.status === 'pending') void s.resolveApproval(approval, 'denied');
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
const generation = p.generation();
|
|
104
|
+
const isCurrent = () => p.provider() === provider && generation === p.generation();
|
|
105
|
+
|
|
106
|
+
// Read configurable text from widget config
|
|
107
|
+
const voiceRecognitionConfig = s.config.voiceRecognition ?? {};
|
|
108
|
+
const processingErrorText = voiceRecognitionConfig.processingErrorText ?? 'Voice processing failed. Please try again.';
|
|
109
|
+
|
|
110
|
+
// STT-style providers (browser + bring-your-own `custom`) deliver a final
|
|
111
|
+
// transcript that we send as a normal user message: the agent then runs
|
|
112
|
+
// via the standard SSE chat path. Only the realtime `runtype` provider is
|
|
113
|
+
// excluded here: it owns the whole turn and drives onTranscript below.
|
|
114
|
+
provider.onResult((result) => {
|
|
115
|
+
if (!isCurrent()) return;
|
|
116
|
+
if (result.provider !== 'runtype') {
|
|
117
|
+
if (result.text && result.text.trim()) {
|
|
118
|
+
s.sendMessage(result.text, { viaVoice: true });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Realtime (runtype) voice: drive the chat thread from streaming
|
|
124
|
+
// transcript frames. Live interim user text grows in place; the user
|
|
125
|
+
// message finalizes immediately on transcript_final{user}; the assistant
|
|
126
|
+
// reply lands (a single block, synced with audio) on its final frame.
|
|
127
|
+
// In-flight bubbles carry voiceProcessing=true so consumers can style
|
|
128
|
+
// them via messageTransform; it clears once the text is final.
|
|
129
|
+
if (provider.onTranscript) {
|
|
130
|
+
provider.onTranscript((role, text, isFinal, metadata) => {
|
|
131
|
+
if (!isCurrent()) return;
|
|
132
|
+
if (metadata?.turnId) {
|
|
133
|
+
p.keyed?.apply(role, text, isFinal, metadata.turnId, metadata.startMs, metadata.caption);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (role === 'user') {
|
|
137
|
+
if (!p.pendingUser) {
|
|
138
|
+
const msg = s.injectMessage({
|
|
139
|
+
role: 'user',
|
|
140
|
+
content: text,
|
|
141
|
+
streaming: false,
|
|
142
|
+
voiceProcessing: !isFinal
|
|
143
|
+
});
|
|
144
|
+
p.pendingUser = msg.id;
|
|
145
|
+
} else {
|
|
146
|
+
s.upsertMessage({
|
|
147
|
+
id: p.pendingUser,
|
|
148
|
+
role: 'user',
|
|
149
|
+
content: text,
|
|
150
|
+
createdAt: new Date().toISOString(),
|
|
151
|
+
streaming: false,
|
|
152
|
+
voiceProcessing: !isFinal
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (isFinal) {
|
|
157
|
+
p.turns().start(metadata?.turnId);
|
|
158
|
+
// User finished: the agent is now thinking. Release the user
|
|
159
|
+
// bubble (a new interim starts a fresh turn) and show a typing
|
|
160
|
+
// indicator in a fresh assistant placeholder.
|
|
161
|
+
p.pendingUser = null;
|
|
162
|
+
const assistantMsg = s.injectMessage({
|
|
163
|
+
role: 'assistant',
|
|
164
|
+
content: '',
|
|
165
|
+
streaming: true,
|
|
166
|
+
voiceProcessing: true
|
|
167
|
+
});
|
|
168
|
+
p.pendingAssistant = assistantMsg.id;
|
|
169
|
+
p.setStreaming(true);
|
|
170
|
+
}
|
|
171
|
+
} else {
|
|
172
|
+
if (!p.turns().accepts(metadata?.turnId)) return;
|
|
173
|
+
// assistant: runtype sends a single final; the isFinal=false path
|
|
174
|
+
// is reserved for delta-streaming providers (future BYO).
|
|
175
|
+
if (p.pendingAssistant) {
|
|
176
|
+
s.upsertMessage({
|
|
177
|
+
id: p.pendingAssistant,
|
|
178
|
+
role: 'assistant',
|
|
179
|
+
content: text,
|
|
180
|
+
createdAt: new Date().toISOString(),
|
|
181
|
+
streaming: !isFinal,
|
|
182
|
+
voiceProcessing: !isFinal
|
|
183
|
+
});
|
|
184
|
+
} else {
|
|
185
|
+
const msg = s.injectMessage({
|
|
186
|
+
role: 'assistant',
|
|
187
|
+
content: text,
|
|
188
|
+
streaming: !isFinal,
|
|
189
|
+
voiceProcessing: !isFinal
|
|
190
|
+
});
|
|
191
|
+
p.pendingAssistant = msg.id;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (isFinal) {
|
|
195
|
+
// The provider plays this reply's audio: mark it spoken so
|
|
196
|
+
// browser TTS doesn't double-speak when streaming ends. Must run
|
|
197
|
+
// BEFORE setStreaming(false), which triggers the TTS check.
|
|
198
|
+
if (p.pendingAssistant) {
|
|
199
|
+
s.ttsSpokenMessageIds.add(p.pendingAssistant);
|
|
200
|
+
}
|
|
201
|
+
p.setStreaming(false);
|
|
202
|
+
p.pendingAssistant = null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Live capture amplitude, when the provider owns an audio graph. Stored,
|
|
209
|
+
// not forwarded per callback: the UI samples it on its own frame loop.
|
|
210
|
+
if (provider.onLevel) {
|
|
211
|
+
provider.onLevel((level) => {
|
|
212
|
+
if (!isCurrent()) return;
|
|
213
|
+
p.setLevel(
|
|
214
|
+
Number.isFinite(level) ? Math.max(0, Math.min(1, level)) : 0
|
|
215
|
+
);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Surface per-turn latency metrics to the optional config hook.
|
|
220
|
+
if (provider.onMetrics) {
|
|
221
|
+
provider.onMetrics((metrics) => {
|
|
222
|
+
if (!isCurrent()) return;
|
|
223
|
+
s.config.voiceRecognition?.onMetrics?.(metrics);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
provider.onError((error) => {
|
|
228
|
+
if (!isCurrent()) return;
|
|
229
|
+
console.error('Voice error:', error);
|
|
230
|
+
|
|
231
|
+
// If error occurs while placeholders are pending, update assistant with error text
|
|
232
|
+
if (p.pendingAssistant) {
|
|
233
|
+
s.upsertMessage({
|
|
234
|
+
id: p.pendingAssistant,
|
|
235
|
+
role: 'assistant',
|
|
236
|
+
content: processingErrorText,
|
|
237
|
+
createdAt: new Date().toISOString(),
|
|
238
|
+
streaming: false,
|
|
239
|
+
voiceProcessing: false
|
|
240
|
+
});
|
|
241
|
+
p.setStreaming(false);
|
|
242
|
+
p.pendingUser = null;
|
|
243
|
+
p.pendingAssistant = null;
|
|
244
|
+
}
|
|
245
|
+
p.keyed?.fail(processingErrorText);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
provider.onStatusChange((status) => {
|
|
249
|
+
if (!isCurrent()) return;
|
|
250
|
+
p.setStatus(status);
|
|
251
|
+
if (status === 'listening' || status === 'idle' || status === 'disconnected') {
|
|
252
|
+
p.settlePending(status !== 'listening');
|
|
253
|
+
}
|
|
254
|
+
// Keyed turns overlap listening (full duplex), so only a call end settles them.
|
|
255
|
+
if (status === 'idle' || status === 'disconnected') {
|
|
256
|
+
p.keyed?.settle();
|
|
257
|
+
}
|
|
258
|
+
s.callbacks.onVoiceStatusChanged?.(status);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
provider.connect();
|
|
262
|
+
|
|
263
|
+
} catch (error) {
|
|
264
|
+
console.error('Failed to setup voice:', error);
|
|
265
|
+
}
|
|
266
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { AgentWidgetApproval } from "../types";
|
|
3
|
+
import { buildApprovalScript, isVoiceDecline } from "./voice-delegation";
|
|
4
|
+
|
|
5
|
+
const approval = (extra: Partial<AgentWidgetApproval>): AgentWidgetApproval => ({
|
|
6
|
+
id: "ap",
|
|
7
|
+
status: "pending",
|
|
8
|
+
agentId: "a1",
|
|
9
|
+
executionId: "e1",
|
|
10
|
+
toolName: "tool",
|
|
11
|
+
description: "",
|
|
12
|
+
...extra,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
describe("buildApprovalScript", () => {
|
|
16
|
+
it("lists every pending action, humanized, with its arguments", () => {
|
|
17
|
+
const script = buildApprovalScript([
|
|
18
|
+
approval({ toolName: "webmcp:addToCart", parameters: { sku: "AB-1", quantity: 3 } }),
|
|
19
|
+
approval({ toolName: "send-receipt", parameters: { to: { email: "n@example.com" }, copies: [] } }),
|
|
20
|
+
]);
|
|
21
|
+
expect(script).toBe(
|
|
22
|
+
"These actions need the user's approval in the chat before they happen:\n" +
|
|
23
|
+
"- add to cart with sku: AB-1; quantity: 3\n" +
|
|
24
|
+
"- send receipt with to: email n@example.com\n\n" +
|
|
25
|
+
"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.",
|
|
26
|
+
);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("stays under about 1,000 characters however much the tools carry", () => {
|
|
30
|
+
const big = { notes: "x".repeat(5_000), items: Array.from({ length: 200 }, (_, i) => ({ name: `item ${i}`, qty: i })) };
|
|
31
|
+
const script = buildApprovalScript(
|
|
32
|
+
Array.from({ length: 5 }, (_, i) =>
|
|
33
|
+
approval({ toolName: `tool_${i}`, description: "d".repeat(500), reason: "r".repeat(500), parameters: big }),
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
expect(script.length).toBeLessThanOrEqual(1_000);
|
|
37
|
+
expect(script).toContain("- tool 4");
|
|
38
|
+
expect(script.endsWith("Don't claim it's done.")).toBe(true);
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("isVoiceDecline", () => {
|
|
43
|
+
it.each([
|
|
44
|
+
"No.",
|
|
45
|
+
"no thanks",
|
|
46
|
+
"Cancel that.",
|
|
47
|
+
"cancel it",
|
|
48
|
+
"Cancel the order",
|
|
49
|
+
"cancel my pickup order",
|
|
50
|
+
"No, cancel that.",
|
|
51
|
+
"Don't place it.",
|
|
52
|
+
"don't do that",
|
|
53
|
+
"Don't place the pickup order",
|
|
54
|
+
"Never mind.",
|
|
55
|
+
"nevermind",
|
|
56
|
+
"Decline it",
|
|
57
|
+
" Nope ",
|
|
58
|
+
])("declines on %j", (text) => expect(isVoiceDecline(text)).toBe(true));
|
|
59
|
+
|
|
60
|
+
it.each([
|
|
61
|
+
"yes",
|
|
62
|
+
"go ahead",
|
|
63
|
+
"no wait, make it three",
|
|
64
|
+
"cancel the cake and add bread",
|
|
65
|
+
"don't forget the coffee",
|
|
66
|
+
"I said no sugar",
|
|
67
|
+
"can you cancel it later",
|
|
68
|
+
"not now",
|
|
69
|
+
"",
|
|
70
|
+
])("runs %j as a normal turn", (text) => expect(isVoiceDecline(text)).toBe(false));
|
|
71
|
+
});
|