@agent-native/toolkit 0.221.0-same-host-mcp-20261027-20261027081850 → 0.221.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/agent-native.eject.json +16 -1
- package/dist/app/agentkit/react/components.d.ts +5 -3
- package/dist/app/agentkit/react/components.d.ts.map +1 -1
- package/dist/app/agentkit/react/components.js +103 -42
- package/dist/app/agentkit/react/components.js.map +1 -1
- package/dist/app/agentkit/react/styles.css +93 -65
- package/dist/app/agentkit/react/voice-transcripts.d.ts +22 -1
- package/dist/app/agentkit/react/voice-transcripts.d.ts.map +1 -1
- package/dist/app/agentkit/react/voice-transcripts.js +209 -8
- package/dist/app/agentkit/react/voice-transcripts.js.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.d.ts.map +1 -1
- package/dist/app/chat/AgentKitAssistantChat.js +55 -40
- package/dist/app/chat/AgentKitAssistantChat.js.map +1 -1
- package/dist/app/chat/agentkit-chat/guided-questions.d.ts +1 -0
- package/dist/app/chat/agentkit-chat/guided-questions.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/guided-questions.js +18 -6
- package/dist/app/chat/agentkit-chat/guided-questions.js.map +1 -1
- package/dist/app/chat/agentkit-chat/index.d.ts +1 -1
- package/dist/app/chat/agentkit-chat/index.d.ts.map +1 -1
- package/dist/app/chat/agentkit-chat/index.js +1 -1
- package/dist/app/chat/agentkit-chat/index.js.map +1 -1
- package/dist/app/chat/chat/run-recovery.d.ts.map +1 -1
- package/dist/app/chat/chat/run-recovery.js +5 -1
- package/dist/app/chat/chat/run-recovery.js.map +1 -1
- package/dist/app/chat/composer/runtime-adapters.d.ts.map +1 -1
- package/dist/app/chat/composer/runtime-adapters.js +2 -0
- package/dist/app/chat/composer/runtime-adapters.js.map +1 -1
- package/dist/app/chat/reply-connection-suggestion.d.ts +29 -0
- package/dist/app/chat/reply-connection-suggestion.d.ts.map +1 -0
- package/dist/app/chat/reply-connection-suggestion.js +42 -0
- package/dist/app/chat/reply-connection-suggestion.js.map +1 -0
- package/dist/app/i18n/catalogs/ar-SA.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ar-SA.js +7 -0
- package/dist/app/i18n/catalogs/ar-SA.js.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/de-DE.js +7 -0
- package/dist/app/i18n/catalogs/de-DE.js.map +1 -1
- package/dist/app/i18n/catalogs/en-US.d.ts +7 -0
- package/dist/app/i18n/catalogs/en-US.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/en-US.js +7 -0
- package/dist/app/i18n/catalogs/en-US.js.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/es-ES.js +7 -0
- package/dist/app/i18n/catalogs/es-ES.js.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/fr-FR.js +7 -0
- package/dist/app/i18n/catalogs/fr-FR.js.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/hi-IN.js +7 -0
- package/dist/app/i18n/catalogs/hi-IN.js.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ja-JP.js +7 -0
- package/dist/app/i18n/catalogs/ja-JP.js.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/ko-KR.js +7 -0
- package/dist/app/i18n/catalogs/ko-KR.js.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/pt-BR.js +7 -0
- package/dist/app/i18n/catalogs/pt-BR.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-CN.js +7 -0
- package/dist/app/i18n/catalogs/zh-CN.js.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.d.ts.map +1 -1
- package/dist/app/i18n/catalogs/zh-TW.js +7 -0
- package/dist/app/i18n/catalogs/zh-TW.js.map +1 -1
- package/dist/app/mcp-approval/McpApprovalReview.d.ts +25 -0
- package/dist/app/mcp-approval/McpApprovalReview.d.ts.map +1 -0
- package/dist/app/mcp-approval/McpApprovalReview.js +342 -0
- package/dist/app/mcp-approval/McpApprovalReview.js.map +1 -0
- package/dist/app/onboarding/FirstRunOnboarding.d.ts.map +1 -1
- package/dist/app/onboarding/FirstRunOnboarding.js +299 -71
- package/dist/app/onboarding/FirstRunOnboarding.js.map +1 -1
- package/dist/app/onboarding/GettingStartedCard.js +1 -1
- package/dist/app/onboarding/GettingStartedCard.js.map +1 -1
- package/dist/app/onboarding/first-run-builder-credits-flag.d.ts +2 -0
- package/dist/app/onboarding/first-run-builder-credits-flag.d.ts.map +1 -0
- package/dist/app/onboarding/first-run-builder-credits-flag.js +8 -0
- package/dist/app/onboarding/first-run-builder-credits-flag.js.map +1 -0
- package/dist/app/providers/AppProviders.d.ts.map +1 -1
- package/dist/app/providers/AppProviders.js +14 -1
- package/dist/app/providers/AppProviders.js.map +1 -1
- package/dist/app/resources/McpConnectionSuggestion.d.ts +2 -4
- package/dist/app/resources/McpConnectionSuggestion.d.ts.map +1 -1
- package/dist/app/resources/McpConnectionSuggestion.js +59 -36
- package/dist/app/resources/McpConnectionSuggestion.js.map +1 -1
- package/dist/app/settings/account/PreferencesSettings.d.ts.map +1 -1
- package/dist/app/settings/account/PreferencesSettings.js +14 -1
- package/dist/app/settings/account/PreferencesSettings.js.map +1 -1
- package/dist/app/settings/account/ThemeFamilySettings.js +2 -2
- package/dist/app/settings/account/ThemeFamilySettings.js.map +1 -1
- package/dist/app/settings/account/search-entries.d.ts +1 -0
- package/dist/app/settings/account/search-entries.d.ts.map +1 -1
- package/dist/app/settings/account/search-entries.js +5 -0
- package/dist/app/settings/account/search-entries.js.map +1 -1
- package/dist/app/settings/shell/SettingsShell.d.ts.map +1 -1
- package/dist/app/settings/shell/SettingsShell.js +26 -2
- package/dist/app/settings/shell/SettingsShell.js.map +1 -1
- package/dist/app/shared/AppShellSkeleton.d.ts.map +1 -1
- package/dist/app/shared/AppShellSkeleton.js +4 -10
- package/dist/app/shared/AppShellSkeleton.js.map +1 -1
- package/dist/app/shared/EnvironmentBadge.d.ts +2 -1
- package/dist/app/shared/EnvironmentBadge.d.ts.map +1 -1
- package/dist/app/shared/EnvironmentBadge.js +2 -3
- package/dist/app/shared/EnvironmentBadge.js.map +1 -1
- package/dist/app/shared/is-builder-io-employee.d.ts +8 -0
- package/dist/app/shared/is-builder-io-employee.d.ts.map +1 -0
- package/dist/app/shared/is-builder-io-employee.js +10 -0
- package/dist/app/shared/is-builder-io-employee.js.map +1 -0
- package/dist/app/styles/agent-conversation.css +30 -17
- package/dist/app/styles/theme-families.css +22 -5
- package/dist/app/styles/tokens/agent-kit.css +3 -3
- package/dist/composer/PromptComposer.d.ts +15 -3
- package/dist/composer/PromptComposer.d.ts.map +1 -1
- package/dist/composer/PromptComposer.js +74 -16
- package/dist/composer/PromptComposer.js.map +1 -1
- package/dist/composer/RealtimeVoiceMode.d.ts +7 -0
- package/dist/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/RealtimeVoiceMode.js +9 -2
- package/dist/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/composer/TiptapComposer.d.ts +7 -0
- package/dist/composer/TiptapComposer.d.ts.map +1 -1
- package/dist/composer/TiptapComposer.js +12 -4
- package/dist/composer/TiptapComposer.js.map +1 -1
- package/dist/composer/VoiceButton.d.ts +2 -0
- package/dist/composer/VoiceButton.d.ts.map +1 -1
- package/dist/composer/VoiceButton.js +5 -4
- package/dist/composer/VoiceButton.js.map +1 -1
- package/dist/composer/index.d.ts +2 -2
- package/dist/composer/index.d.ts.map +1 -1
- package/dist/composer/index.js +1 -1
- package/dist/composer/index.js.map +1 -1
- package/dist/composer/prompt-attachments.d.ts +20 -0
- package/dist/composer/prompt-attachments.d.ts.map +1 -1
- package/dist/composer/prompt-attachments.js +188 -11
- package/dist/composer/prompt-attachments.js.map +1 -1
- package/dist/composer/realtime-voice-transcript.d.ts +27 -0
- package/dist/composer/realtime-voice-transcript.d.ts.map +1 -1
- package/dist/composer/realtime-voice-transcript.js +34 -1
- package/dist/composer/realtime-voice-transcript.js.map +1 -1
- package/dist/composer/realtime-voice-turn-metrics.d.ts +43 -0
- package/dist/composer/realtime-voice-turn-metrics.d.ts.map +1 -0
- package/dist/composer/realtime-voice-turn-metrics.js +132 -0
- package/dist/composer/realtime-voice-turn-metrics.js.map +1 -0
- package/dist/composer/runtime-adapters.d.ts +4 -1
- package/dist/composer/runtime-adapters.d.ts.map +1 -1
- package/dist/composer/runtime-adapters.js +2 -1
- package/dist/composer/runtime-adapters.js.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.d.ts +61 -9
- package/dist/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/composer/useRealtimeVoiceMode.js +536 -99
- package/dist/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/composer/voice-chat-icon.d.ts +7 -0
- package/dist/composer/voice-chat-icon.d.ts.map +1 -0
- package/dist/composer/voice-chat-icon.js +14 -0
- package/dist/composer/voice-chat-icon.js.map +1 -0
- package/dist/editor/DragHandle.d.ts +12 -1
- package/dist/editor/DragHandle.d.ts.map +1 -1
- package/dist/editor/DragHandle.js +165 -41
- package/dist/editor/DragHandle.js.map +1 -1
- package/dist/editor/RegistryBlockNode.d.ts +20 -0
- package/dist/editor/RegistryBlockNode.d.ts.map +1 -1
- package/dist/editor/RegistryBlockNode.js +58 -13
- package/dist/editor/RegistryBlockNode.js.map +1 -1
- package/dist/editor/index.d.ts +2 -2
- package/dist/editor/index.d.ts.map +1 -1
- package/dist/editor/index.js +1 -1
- package/dist/editor/index.js.map +1 -1
- package/dist/styles.css +2 -0
- package/dist/ui/dropdown-menu.d.ts.map +1 -1
- package/dist/ui/dropdown-menu.js +5 -1
- package/dist/ui/dropdown-menu.js.map +1 -1
- package/package.json +14 -4
- package/src/app/agentkit/react/components.regenerate-readiness.spec.tsx +3 -1
- package/src/app/agentkit/react/components.spec.tsx +24 -4
- package/src/app/agentkit/react/components.tsx +174 -44
- package/src/app/agentkit/react/composer-context.spec.tsx +3 -0
- package/src/app/agentkit/react/disclosures.spec.tsx +124 -0
- package/src/app/agentkit/react/lifecycle.spec.tsx +8 -2
- package/src/app/agentkit/react/message-actions.spec.tsx +161 -5
- package/src/app/agentkit/react/saved-reply-reload.spec.tsx +206 -0
- package/src/app/agentkit/react/styles.css +93 -65
- package/src/app/agentkit/react/voice-transcripts.spec.tsx +239 -4
- package/src/app/agentkit/react/voice-transcripts.ts +245 -8
- package/src/app/chat/AgentKitAssistantChat.spec.tsx +59 -11
- package/src/app/chat/AgentKitAssistantChat.tsx +74 -62
- package/src/app/chat/agentkit-chat/guided-answers-context.spec.ts +9 -0
- package/src/app/chat/agentkit-chat/guided-questions.tsx +17 -7
- package/src/app/chat/agentkit-chat/index.ts +1 -0
- package/src/app/chat/chat/run-recovery.spec.tsx +45 -0
- package/src/app/chat/chat/run-recovery.tsx +6 -1
- package/src/app/chat/composer/runtime-adapters.tsx +2 -0
- package/src/app/chat/reply-connection-suggestion.spec.ts +121 -0
- package/src/app/chat/reply-connection-suggestion.ts +76 -0
- package/src/app/i18n/catalogs/ar-SA.ts +10 -0
- package/src/app/i18n/catalogs/de-DE.ts +10 -0
- package/src/app/i18n/catalogs/en-US.ts +10 -0
- package/src/app/i18n/catalogs/es-ES.ts +10 -0
- package/src/app/i18n/catalogs/fr-FR.ts +10 -0
- package/src/app/i18n/catalogs/hi-IN.ts +10 -0
- package/src/app/i18n/catalogs/ja-JP.ts +10 -0
- package/src/app/i18n/catalogs/ko-KR.ts +10 -0
- package/src/app/i18n/catalogs/pt-BR.ts +10 -0
- package/src/app/i18n/catalogs/zh-CN.ts +10 -0
- package/src/app/i18n/catalogs/zh-TW.ts +10 -0
- package/src/app/mcp-approval/McpApprovalReview.spec.tsx +687 -0
- package/src/app/mcp-approval/McpApprovalReview.tsx +913 -0
- package/src/app/onboarding/FirstRunOnboarding.spec.tsx +736 -26
- package/src/app/onboarding/FirstRunOnboarding.tsx +473 -145
- package/src/app/onboarding/GettingStartedCard.tsx +1 -1
- package/src/app/onboarding/first-run-builder-credits-flag.spec.ts +13 -0
- package/src/app/onboarding/first-run-builder-credits-flag.ts +9 -0
- package/src/app/providers/AppProviders.spec.tsx +109 -0
- package/src/app/providers/AppProviders.tsx +53 -1
- package/src/app/resources/McpConnectionSuggestion.spec.ts +77 -12
- package/src/app/resources/McpConnectionSuggestion.tsx +69 -51
- package/src/app/settings/VoiceChatSettings.spec.tsx +6 -1
- package/src/app/settings/account/PreferencesSettings.spec.tsx +73 -0
- package/src/app/settings/account/PreferencesSettings.tsx +24 -7
- package/src/app/settings/account/ThemeFamilySettings.tsx +2 -2
- package/src/app/settings/account/search-entries.spec.ts +14 -0
- package/src/app/settings/account/search-entries.ts +9 -0
- package/src/app/settings/shell/SettingsShell.tsx +29 -2
- package/src/app/shared/AppShellSkeleton.spec.tsx +2 -2
- package/src/app/shared/AppShellSkeleton.tsx +3 -16
- package/src/app/shared/EnvironmentBadge.tsx +3 -4
- package/src/app/shared/is-builder-io-employee.ts +17 -0
- package/src/app/styles/agent-conversation.css +30 -17
- package/src/app/styles/theme-families.css +22 -5
- package/src/app/styles/theme-families.spec.ts +73 -0
- package/src/app/styles/tokens/agent-kit.css +3 -3
- package/src/composer/AgentSuggestionBar.overflow.spec.tsx +1 -1
- package/src/composer/AttachmentChip.spec.tsx +87 -0
- package/src/composer/PromptComposer.context.spec.tsx +8 -4
- package/src/composer/PromptComposer.setup-card.spec.tsx +170 -0
- package/src/composer/PromptComposer.tsx +125 -14
- package/src/composer/RealtimeVoiceMode.spec.tsx +48 -0
- package/src/composer/RealtimeVoiceMode.tsx +45 -2
- package/src/composer/TiptapComposer.tsx +25 -0
- package/src/composer/TiptapComposer.voice-toolbar.spec.tsx +4 -1
- package/src/composer/VoiceButton.tsx +8 -3
- package/src/composer/index.ts +4 -0
- package/src/composer/prompt-attachments.spec.ts +160 -8
- package/src/composer/prompt-attachments.ts +221 -12
- package/src/composer/realtime-voice-transcript.spec.ts +36 -0
- package/src/composer/realtime-voice-transcript.ts +56 -1
- package/src/composer/realtime-voice-turn-metrics.spec.ts +175 -0
- package/src/composer/realtime-voice-turn-metrics.ts +192 -0
- package/src/composer/runtime-adapters.tsx +5 -1
- package/src/composer/useRealtimeVoiceMode.session.spec.tsx +487 -2
- package/src/composer/useRealtimeVoiceMode.spec.ts +267 -65
- package/src/composer/useRealtimeVoiceMode.tsx +716 -139
- package/src/composer/voice-chat-icon.tsx +19 -0
- package/src/editor/DragHandle.spec.ts +217 -0
- package/src/editor/DragHandle.ts +214 -55
- package/src/editor/RegistryBlockNode.spec.ts +172 -2
- package/src/editor/RegistryBlockNode.tsx +91 -13
- package/src/editor/index.ts +4 -0
- package/src/styles.css +2 -0
- package/src/ui/dropdown-menu.tsx +4 -0
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { TranscriptGrouper } from "openai/helpers/live";
|
|
2
3
|
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, } from "react";
|
|
3
4
|
import { createRealtimeVoiceAudioLevelStore, normalizeRealtimeVoiceRms, smoothRealtimeVoiceLevel, } from "./realtime-voice-audio-level.js";
|
|
4
|
-
import { realtimeVoiceTranscriptRegistry } from "./realtime-voice-transcript.js";
|
|
5
|
+
import { realtimeVoiceTranscriptRegistry, } from "./realtime-voice-transcript.js";
|
|
6
|
+
import { createRealtimeVoiceTurnMetrics } from "./realtime-voice-turn-metrics.js";
|
|
5
7
|
import { useComposerRuntimeAdapters, } from "./runtime-adapters.js";
|
|
6
8
|
import { isMicrophoneAccessError, voiceDictationStartErrorMessage, } from "./useVoiceDictation.js";
|
|
7
9
|
const REALTIME_VOICE_STATE_KEY = "realtime-voice-session";
|
|
@@ -11,19 +13,23 @@ const REALTIME_VOICE_TOOL_PATH = "/_agent-native/realtime-voice/tool";
|
|
|
11
13
|
const REALTIME_VOICE_CAPABILITY_HEADER = "X-Agent-Native-Realtime-Capability";
|
|
12
14
|
const REALTIME_VOICE_PROTOCOL_HEADER = "X-Agent-Native-Realtime-Protocol";
|
|
13
15
|
const REALTIME_VOICE_MODEL_HEADER = "X-Agent-Native-Realtime-Model";
|
|
16
|
+
const REALTIME_VOICE_INTELLIGENCE_HEADER = "X-Agent-Native-Realtime-Intelligence";
|
|
17
|
+
const REALTIME_VOICE_PRIORITY_TOOLS_HEADER = "X-Agent-Native-Realtime-Priority-Tools";
|
|
14
18
|
const REALTIME_VOICE_CONNECTION_TIMEOUT_MS = 15_000;
|
|
15
19
|
const REALTIME_VOICE_MAX_TOOLS = 32;
|
|
16
20
|
const REALTIME_VOICE_MAX_TOOL_SCHEMA_BYTES = 32_000;
|
|
17
21
|
const REALTIME_VOICE_MAX_SESSION_BYTES = 64_000;
|
|
18
22
|
const REALTIME_VOICE_TOOL_UPDATE_TIMEOUT_MS = 5_000;
|
|
19
23
|
const REALTIME_VOICE_LIVE_CLOSE_DRAIN_TIMEOUT_MS = 2_000;
|
|
20
|
-
const REALTIME_VOICE_LIVE_TRANSCRIPT_GAP_MS = 750;
|
|
21
24
|
const REALTIME_VOICE_PRIORITY_TOOL_NAMES = [
|
|
22
25
|
"navigate",
|
|
23
26
|
"set-url-path",
|
|
24
27
|
"set-search-params",
|
|
25
28
|
"view-screen",
|
|
29
|
+
"chat-history",
|
|
26
30
|
"tool-search",
|
|
31
|
+
"web-search",
|
|
32
|
+
"web-request",
|
|
27
33
|
];
|
|
28
34
|
const REALTIME_VOICE_TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
29
35
|
const REALTIME_VOICE_MICROPHONE_STORAGE_KEY = "agent-native:realtime-voice-microphone";
|
|
@@ -60,6 +66,7 @@ export const DEFAULT_REALTIME_VOICE_PREFERENCES = {
|
|
|
60
66
|
language: "auto",
|
|
61
67
|
intelligence: "instant",
|
|
62
68
|
voice: "marin",
|
|
69
|
+
intelligenceChosen: false,
|
|
63
70
|
};
|
|
64
71
|
export const REALTIME_VOICE_AUDIO_CONSTRAINTS = {
|
|
65
72
|
deviceId: { ideal: "default" },
|
|
@@ -163,16 +170,31 @@ export function normalizeRealtimeVoicePreferences(value) {
|
|
|
163
170
|
const record = value && typeof value === "object" && !Array.isArray(value)
|
|
164
171
|
? value
|
|
165
172
|
: {};
|
|
173
|
+
const storedIntelligence = isOneOf(REALTIME_VOICE_INTELLIGENCE_LEVELS, record.intelligence)
|
|
174
|
+
? record.intelligence
|
|
175
|
+
: undefined;
|
|
176
|
+
// Saves from before `intelligenceChosen` stored "instant" for everyone, so
|
|
177
|
+
// only a non-default level among them shows a pick.
|
|
178
|
+
const chosenIntelligence = typeof record.intelligenceChosen === "boolean"
|
|
179
|
+
? record.intelligenceChosen
|
|
180
|
+
? storedIntelligence
|
|
181
|
+
: undefined
|
|
182
|
+
: storedIntelligence !== DEFAULT_REALTIME_VOICE_PREFERENCES.intelligence
|
|
183
|
+
? storedIntelligence
|
|
184
|
+
: undefined;
|
|
185
|
+
// A level saved as not chosen is the app default a session last reported.
|
|
186
|
+
const appDefaultIntelligence = record.intelligenceChosen === false ? storedIntelligence : undefined;
|
|
166
187
|
return {
|
|
167
188
|
language: isOneOf(REALTIME_VOICE_LANGUAGES, record.language)
|
|
168
189
|
? record.language
|
|
169
190
|
: DEFAULT_REALTIME_VOICE_PREFERENCES.language,
|
|
170
|
-
intelligence:
|
|
171
|
-
|
|
172
|
-
|
|
191
|
+
intelligence: chosenIntelligence ??
|
|
192
|
+
appDefaultIntelligence ??
|
|
193
|
+
DEFAULT_REALTIME_VOICE_PREFERENCES.intelligence,
|
|
173
194
|
voice: isOneOf(REALTIME_VOICES, record.voice)
|
|
174
195
|
? record.voice
|
|
175
196
|
: DEFAULT_REALTIME_VOICE_PREFERENCES.voice,
|
|
197
|
+
intelligenceChosen: chosenIntelligence !== undefined,
|
|
176
198
|
};
|
|
177
199
|
}
|
|
178
200
|
export function resolveRealtimeVoiceLanguage(language, browserLanguages = []) {
|
|
@@ -187,11 +209,19 @@ export function resolveRealtimeVoiceLanguage(language, browserLanguages = []) {
|
|
|
187
209
|
return "en";
|
|
188
210
|
}
|
|
189
211
|
export function realtimeVoiceReasoningEffort(intelligence, protocol) {
|
|
212
|
+
// The delegated Live model rejects "minimal", so its levels start at "low".
|
|
213
|
+
if (protocol === "live") {
|
|
214
|
+
if (intelligence === "balanced")
|
|
215
|
+
return "medium";
|
|
216
|
+
if (intelligence === "deep")
|
|
217
|
+
return "high";
|
|
218
|
+
return "low";
|
|
219
|
+
}
|
|
190
220
|
if (intelligence === "balanced")
|
|
191
221
|
return "low";
|
|
192
222
|
if (intelligence === "deep")
|
|
193
223
|
return "medium";
|
|
194
|
-
return
|
|
224
|
+
return "minimal";
|
|
195
225
|
}
|
|
196
226
|
export function createRealtimeVoicePreferenceUpdate(preferences, options = {}) {
|
|
197
227
|
if (options.protocol === "live") {
|
|
@@ -237,32 +267,131 @@ export function createRealtimeVoiceLanguageUpdate(language, browserLanguages = [
|
|
|
237
267
|
content: `Speak in ${resolvedLanguage} unless the user asks to switch languages.`,
|
|
238
268
|
};
|
|
239
269
|
}
|
|
270
|
+
function publishRealtimeVoiceWorkSegment(segment, completedAt) {
|
|
271
|
+
if (segment.calls.length === 0)
|
|
272
|
+
return;
|
|
273
|
+
realtimeVoiceTranscriptRegistry.publishWork({
|
|
274
|
+
id: segment.id,
|
|
275
|
+
threadId: segment.threadId,
|
|
276
|
+
calls: segment.calls.map((call) => ({ ...call })),
|
|
277
|
+
...(completedAt ? { completedAt } : {}),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* The reply's open work a new call joins, or new work; none without a thread.
|
|
282
|
+
* A call from a different delegation is a new request.
|
|
283
|
+
*/
|
|
284
|
+
function openRealtimeVoiceWorkSegment(ref, threadId, callId, delegationId) {
|
|
285
|
+
if (!threadId)
|
|
286
|
+
return null;
|
|
287
|
+
const current = ref.current;
|
|
288
|
+
if (current?.threadId === threadId &&
|
|
289
|
+
(!current.delegationId ||
|
|
290
|
+
!delegationId ||
|
|
291
|
+
current.delegationId === delegationId)) {
|
|
292
|
+
current.delegationId ??= delegationId;
|
|
293
|
+
return current;
|
|
294
|
+
}
|
|
295
|
+
if (current)
|
|
296
|
+
closeRealtimeVoiceWorkSegment(ref, current);
|
|
297
|
+
const segment = {
|
|
298
|
+
id: callId,
|
|
299
|
+
threadId,
|
|
300
|
+
delegationId,
|
|
301
|
+
calls: [],
|
|
302
|
+
closing: false,
|
|
303
|
+
};
|
|
304
|
+
ref.current = segment;
|
|
305
|
+
return segment;
|
|
306
|
+
}
|
|
307
|
+
/** Ends a call that waited for approval: `result` is null when it never ran. */
|
|
308
|
+
function finishPendingApproval(pending, result) {
|
|
309
|
+
const { workCall, workSegment } = pending;
|
|
310
|
+
workCall.status =
|
|
311
|
+
!result || result.status === "approval_required"
|
|
312
|
+
? "cancelled"
|
|
313
|
+
: result.status;
|
|
314
|
+
if (result)
|
|
315
|
+
workCall.output = result.output;
|
|
316
|
+
workCall.completedAt = new Date().toISOString();
|
|
317
|
+
if (!workSegment)
|
|
318
|
+
return;
|
|
319
|
+
if (workSegment.closing) {
|
|
320
|
+
closeRealtimeVoiceWorkSegment({ current: null }, workSegment);
|
|
321
|
+
}
|
|
322
|
+
else {
|
|
323
|
+
publishRealtimeVoiceWorkSegment(workSegment);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function closeRealtimeVoiceWorkSegment(ref, segment = ref.current) {
|
|
327
|
+
if (!segment)
|
|
328
|
+
return;
|
|
329
|
+
if (ref.current === segment)
|
|
330
|
+
ref.current = null;
|
|
331
|
+
segment.closing = true;
|
|
332
|
+
if (segment.calls.some((call) => call.status === "running"))
|
|
333
|
+
return;
|
|
334
|
+
const completedAt = segment.calls
|
|
335
|
+
.map((call) => call.completedAt ?? call.startedAt)
|
|
336
|
+
.sort()
|
|
337
|
+
.at(-1);
|
|
338
|
+
publishRealtimeVoiceWorkSegment(segment, completedAt);
|
|
339
|
+
}
|
|
240
340
|
/**
|
|
241
341
|
* Orders provider transcripts into conversation order. `publish` receives each
|
|
242
342
|
* final transcript; `stream`, when given, receives the growing text of a turn
|
|
243
|
-
* under the same `providerId` its final transcript will carry.
|
|
343
|
+
* under the same `providerId` its final transcript will carry. GPT-Live turns
|
|
344
|
+
* come from the SDK's transcript grouper, which splits on speaker changes and
|
|
345
|
+
* two-second silences and drops short backchannels heard over the reply.
|
|
244
346
|
*/
|
|
245
|
-
export function createRealtimeVoiceTranscriptSequencer(publish, stream
|
|
347
|
+
export function createRealtimeVoiceTranscriptSequencer(publish, stream,
|
|
348
|
+
/** Each GPT-Live turn as it grows, with its place on the session clock. */
|
|
349
|
+
onLiveTurn) {
|
|
246
350
|
const order = [];
|
|
247
351
|
const items = new Map();
|
|
248
352
|
const settledItemIds = new Set();
|
|
249
353
|
const receivedTranscriptIds = new Set();
|
|
250
|
-
const liveBuffers = new Map();
|
|
251
354
|
const streamingText = new Map();
|
|
252
355
|
const streamedText = new Map();
|
|
253
|
-
let
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
const buffer = liveBuffers.get(role);
|
|
257
|
-
if (!buffer)
|
|
258
|
-
return;
|
|
259
|
-
liveBuffers.delete(role);
|
|
260
|
-
const text = buffer.text.trim();
|
|
356
|
+
let liveGrouper;
|
|
357
|
+
const liveTranscript = (segment) => {
|
|
358
|
+
const text = segment.text.trim();
|
|
261
359
|
if (!text)
|
|
360
|
+
return undefined;
|
|
361
|
+
return stream
|
|
362
|
+
? { role: segment.speaker, text, providerId: segment.id }
|
|
363
|
+
: { role: segment.speaker, text };
|
|
364
|
+
};
|
|
365
|
+
// Listeners check that their grouper is still current, so a discarded one
|
|
366
|
+
// never publishes what it closes on the way out.
|
|
367
|
+
const openLiveGrouper = () => {
|
|
368
|
+
if (liveGrouper)
|
|
369
|
+
return liveGrouper;
|
|
370
|
+
const grouper = new TranscriptGrouper();
|
|
371
|
+
grouper.on("segment.updated", (segment) => {
|
|
372
|
+
if (grouper !== liveGrouper)
|
|
373
|
+
return;
|
|
374
|
+
onLiveTurn?.(segment);
|
|
375
|
+
const transcript = liveTranscript(segment);
|
|
376
|
+
if (stream && transcript)
|
|
377
|
+
stream(transcript);
|
|
378
|
+
});
|
|
379
|
+
grouper.on("segment.closed", ({ segment }) => {
|
|
380
|
+
const transcript = liveTranscript(segment);
|
|
381
|
+
if (grouper === liveGrouper && transcript)
|
|
382
|
+
publish(transcript);
|
|
383
|
+
});
|
|
384
|
+
liveGrouper = grouper;
|
|
385
|
+
return grouper;
|
|
386
|
+
};
|
|
387
|
+
const closeLiveGrouper = (keepText) => {
|
|
388
|
+
const grouper = liveGrouper;
|
|
389
|
+
if (!grouper)
|
|
262
390
|
return;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
391
|
+
if (!keepText)
|
|
392
|
+
liveGrouper = undefined;
|
|
393
|
+
grouper.close();
|
|
394
|
+
liveGrouper = undefined;
|
|
266
395
|
};
|
|
267
396
|
// A turn appears once every earlier turn has appeared, so a fast reply
|
|
268
397
|
// never streams in above the question it answers.
|
|
@@ -289,11 +418,6 @@ export function createRealtimeVoiceTranscriptSequencer(publish, stream) {
|
|
|
289
418
|
});
|
|
290
419
|
}
|
|
291
420
|
};
|
|
292
|
-
const flushLiveTranscripts = () => {
|
|
293
|
-
for (const buffer of [...liveBuffers.values()].sort((left, right) => left.sequence - right.sequence)) {
|
|
294
|
-
flushLiveTranscript(buffer.role);
|
|
295
|
-
}
|
|
296
|
-
};
|
|
297
421
|
const reserve = (id, role) => {
|
|
298
422
|
if (settledItemIds.has(id))
|
|
299
423
|
return;
|
|
@@ -333,35 +457,17 @@ export function createRealtimeVoiceTranscriptSequencer(publish, stream) {
|
|
|
333
457
|
handle(event) {
|
|
334
458
|
if (event.type === "session.input_transcript.delta" ||
|
|
335
459
|
event.type === "session.output_transcript.delta") {
|
|
336
|
-
const
|
|
337
|
-
? "
|
|
338
|
-
: "
|
|
339
|
-
const
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
startMs - buffer.lastEndMs > REALTIME_VOICE_LIVE_TRANSCRIPT_GAP_MS) {
|
|
346
|
-
flushLiveTranscript(role);
|
|
347
|
-
buffer = undefined;
|
|
348
|
-
}
|
|
349
|
-
if (!buffer) {
|
|
350
|
-
buffer = {
|
|
351
|
-
role,
|
|
352
|
-
text: "",
|
|
353
|
-
sequence: ++liveSequence,
|
|
354
|
-
};
|
|
355
|
-
liveBuffers.set(role, buffer);
|
|
356
|
-
}
|
|
357
|
-
if (typeof event.delta === "string")
|
|
358
|
-
buffer.text += event.delta;
|
|
359
|
-
if (endMs !== undefined)
|
|
360
|
-
buffer.lastEndMs = endMs;
|
|
361
|
-
const text = buffer.text.trim();
|
|
362
|
-
if (stream && text) {
|
|
363
|
-
stream({ role, text, providerId: liveProviderId(buffer) });
|
|
460
|
+
const type = event.type === "session.input_transcript.delta"
|
|
461
|
+
? "session.input_transcript.delta"
|
|
462
|
+
: "session.output_transcript.delta";
|
|
463
|
+
const { event_id, delta, start_ms, end_ms } = event;
|
|
464
|
+
if (typeof event_id !== "string" ||
|
|
465
|
+
typeof delta !== "string" ||
|
|
466
|
+
typeof start_ms !== "number" ||
|
|
467
|
+
typeof end_ms !== "number") {
|
|
468
|
+
throw new Error("GPT-Live sent a transcript delta without an id, text, or timing.");
|
|
364
469
|
}
|
|
470
|
+
openLiveGrouper().push({ type, event_id, delta, start_ms, end_ms });
|
|
365
471
|
return;
|
|
366
472
|
}
|
|
367
473
|
if (event.type === "conversation.item.input_audio_transcription.delta" ||
|
|
@@ -379,7 +485,7 @@ export function createRealtimeVoiceTranscriptSequencer(publish, stream) {
|
|
|
379
485
|
return;
|
|
380
486
|
}
|
|
381
487
|
if (event.type === "session.closed")
|
|
382
|
-
|
|
488
|
+
closeLiveGrouper(true);
|
|
383
489
|
if (event.type === "conversation.item.added" ||
|
|
384
490
|
event.type === "conversation.item.created") {
|
|
385
491
|
const item = event.item;
|
|
@@ -455,7 +561,7 @@ export function createRealtimeVoiceTranscriptSequencer(publish, stream) {
|
|
|
455
561
|
emitStreams();
|
|
456
562
|
},
|
|
457
563
|
flush() {
|
|
458
|
-
|
|
564
|
+
closeLiveGrouper(true);
|
|
459
565
|
if (!stream)
|
|
460
566
|
return;
|
|
461
567
|
// The session is over: a turn that streamed but never got its final
|
|
@@ -482,10 +588,11 @@ export function createRealtimeVoiceTranscriptSequencer(publish, stream) {
|
|
|
482
588
|
}
|
|
483
589
|
},
|
|
484
590
|
reset(options) {
|
|
485
|
-
|
|
486
|
-
|
|
591
|
+
// A failed response leaves what was already heard; a new session
|
|
592
|
+
// starts clean.
|
|
487
593
|
if (options?.preserveQueuedTranscripts)
|
|
488
594
|
return;
|
|
595
|
+
closeLiveGrouper(false);
|
|
489
596
|
order.length = 0;
|
|
490
597
|
items.clear();
|
|
491
598
|
settledItemIds.clear();
|
|
@@ -688,7 +795,11 @@ export async function createRealtimeVoiceSessionWithCapability(offerSdp, options
|
|
|
688
795
|
...(preferences
|
|
689
796
|
? {
|
|
690
797
|
"X-Agent-Native-Realtime-Language": resolveRealtimeVoiceLanguage(preferences.language, options.browserLanguages),
|
|
691
|
-
|
|
798
|
+
...(preferences.intelligenceChosen !== false
|
|
799
|
+
? {
|
|
800
|
+
[REALTIME_VOICE_INTELLIGENCE_HEADER]: preferences.intelligence,
|
|
801
|
+
}
|
|
802
|
+
: {}),
|
|
692
803
|
"X-Agent-Native-Realtime-Voice": preferences.voice,
|
|
693
804
|
}
|
|
694
805
|
: {}),
|
|
@@ -710,11 +821,24 @@ export async function createRealtimeVoiceSessionWithCapability(offerSdp, options
|
|
|
710
821
|
: undefined;
|
|
711
822
|
const modelHeader = response.headers.get(REALTIME_VOICE_MODEL_HEADER);
|
|
712
823
|
const model = modelHeader?.trim() || undefined;
|
|
824
|
+
const intelligenceHeader = response.headers
|
|
825
|
+
.get(REALTIME_VOICE_INTELLIGENCE_HEADER)
|
|
826
|
+
?.trim();
|
|
827
|
+
const intelligence = isOneOf(REALTIME_VOICE_INTELLIGENCE_LEVELS, intelligenceHeader)
|
|
828
|
+
? intelligenceHeader
|
|
829
|
+
: undefined;
|
|
830
|
+
const priorityToolNames = (response.headers.get(REALTIME_VOICE_PRIORITY_TOOLS_HEADER) ?? "")
|
|
831
|
+
.split(",")
|
|
832
|
+
.map((name) => name.trim())
|
|
833
|
+
.filter((name) => REALTIME_VOICE_TOOL_NAME.test(name))
|
|
834
|
+
.slice(0, REALTIME_VOICE_MAX_TOOLS);
|
|
713
835
|
return {
|
|
714
836
|
sdp,
|
|
715
837
|
...(capability ? { capability } : {}),
|
|
716
838
|
...(protocol ? { protocol } : {}),
|
|
717
839
|
...(model ? { model } : {}),
|
|
840
|
+
...(intelligence ? { intelligence } : {}),
|
|
841
|
+
...(priorityToolNames.length > 0 ? { priorityToolNames } : {}),
|
|
718
842
|
};
|
|
719
843
|
}
|
|
720
844
|
export async function createRealtimeVoiceSession(offerSdp, options = {}) {
|
|
@@ -742,6 +866,10 @@ export async function executeRealtimeVoiceTool(input) {
|
|
|
742
866
|
callId: input.callId,
|
|
743
867
|
sessionId: input.sessionId,
|
|
744
868
|
browserTabId: input.browserTabId,
|
|
869
|
+
...(input.approvalId ? { approvalId: input.approvalId } : {}),
|
|
870
|
+
...(input.declinedApprovalId
|
|
871
|
+
? { declinedApprovalId: input.declinedApprovalId }
|
|
872
|
+
: {}),
|
|
745
873
|
}),
|
|
746
874
|
signal: input.signal,
|
|
747
875
|
});
|
|
@@ -814,7 +942,11 @@ function createRealtimeVoiceToolManifestUpdate(tools, eventId, protocol = "realt
|
|
|
814
942
|
? {
|
|
815
943
|
delegation: {
|
|
816
944
|
type: "responses",
|
|
817
|
-
|
|
945
|
+
// Strict schemas make every optional parameter required.
|
|
946
|
+
responses: {
|
|
947
|
+
tools: tools.map((tool) => ({ ...tool, strict: false })),
|
|
948
|
+
tool_choice: "auto",
|
|
949
|
+
},
|
|
818
950
|
},
|
|
819
951
|
}
|
|
820
952
|
: {
|
|
@@ -824,7 +956,7 @@ function createRealtimeVoiceToolManifestUpdate(tools, eventId, protocol = "realt
|
|
|
824
956
|
},
|
|
825
957
|
};
|
|
826
958
|
}
|
|
827
|
-
export function mergeRealtimeVoiceToolManifest(currentTools, expandedTools, protocol = "realtime") {
|
|
959
|
+
export function mergeRealtimeVoiceToolManifest(currentTools, expandedTools, protocol = "realtime", appPriorityToolNames = []) {
|
|
828
960
|
const current = new Map();
|
|
829
961
|
for (const value of currentTools) {
|
|
830
962
|
const tool = normalizeRealtimeVoiceFunctionTool(value);
|
|
@@ -845,7 +977,10 @@ export function mergeRealtimeVoiceToolManifest(currentTools, expandedTools, prot
|
|
|
845
977
|
seen.add(tool.name);
|
|
846
978
|
candidates.push(tool);
|
|
847
979
|
};
|
|
848
|
-
for (const name of
|
|
980
|
+
for (const name of [
|
|
981
|
+
...REALTIME_VOICE_PRIORITY_TOOL_NAMES,
|
|
982
|
+
...appPriorityToolNames,
|
|
983
|
+
]) {
|
|
849
984
|
add(expanded.get(name) ?? current.get(name));
|
|
850
985
|
}
|
|
851
986
|
for (const tool of expanded.values())
|
|
@@ -863,6 +998,25 @@ export function mergeRealtimeVoiceToolManifest(currentTools, expandedTools, prot
|
|
|
863
998
|
}
|
|
864
999
|
return packed;
|
|
865
1000
|
}
|
|
1001
|
+
/** Tells the assistant what the user decided about a call that asked. */
|
|
1002
|
+
export function createRealtimeVoiceApprovalOutcomeEvent(toolName, result, protocol = "realtime") {
|
|
1003
|
+
const text = !result
|
|
1004
|
+
? `The user declined ${toolName}, so it did not run.`
|
|
1005
|
+
: result.status === "approval_required"
|
|
1006
|
+
? `${toolName} asked for approval again, so it has not run. The user can approve it on screen.`
|
|
1007
|
+
: `The user approved ${toolName}. ${JSON.stringify({
|
|
1008
|
+
status: result.status,
|
|
1009
|
+
output: result.output,
|
|
1010
|
+
})}`;
|
|
1011
|
+
return {
|
|
1012
|
+
type: protocol === "live" ? "response.item.create" : "conversation.item.create",
|
|
1013
|
+
item: {
|
|
1014
|
+
type: "message",
|
|
1015
|
+
role: "system",
|
|
1016
|
+
content: [{ type: "input_text", text }],
|
|
1017
|
+
},
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
866
1020
|
function createRealtimeVoiceToolResultEvent(result, protocol = "realtime") {
|
|
867
1021
|
const modelResult = {
|
|
868
1022
|
callId: result.callId,
|
|
@@ -882,6 +1036,7 @@ function createRealtimeVoiceToolResultEvent(result, protocol = "realtime") {
|
|
|
882
1036
|
export function createRealtimeVoiceToolManifestCoordinator(send, timeoutMs = REALTIME_VOICE_TOOL_UPDATE_TIMEOUT_MS) {
|
|
883
1037
|
let currentTools = [];
|
|
884
1038
|
let protocol = "realtime";
|
|
1039
|
+
let priorityToolNames = [];
|
|
885
1040
|
const queue = [];
|
|
886
1041
|
let updateSequence = 0;
|
|
887
1042
|
let active;
|
|
@@ -918,7 +1073,7 @@ export function createRealtimeVoiceToolManifestCoordinator(send, timeoutMs = REA
|
|
|
918
1073
|
drain();
|
|
919
1074
|
return;
|
|
920
1075
|
}
|
|
921
|
-
const merged = mergeRealtimeVoiceToolManifest(currentTools, expanded, protocol);
|
|
1076
|
+
const merged = mergeRealtimeVoiceToolManifest(currentTools, expanded, protocol, priorityToolNames);
|
|
922
1077
|
const mergedNames = new Set(merged.map((tool) => tool.name));
|
|
923
1078
|
const expectedNames = expanded
|
|
924
1079
|
.map((tool) => tool.name)
|
|
@@ -946,8 +1101,11 @@ export function createRealtimeVoiceToolManifestCoordinator(send, timeoutMs = REA
|
|
|
946
1101
|
setProtocol(nextProtocol) {
|
|
947
1102
|
protocol = nextProtocol;
|
|
948
1103
|
},
|
|
1104
|
+
setPriorityToolNames(names) {
|
|
1105
|
+
priorityToolNames = [...names];
|
|
1106
|
+
},
|
|
949
1107
|
setSessionTools(tools) {
|
|
950
|
-
currentTools = mergeRealtimeVoiceToolManifest([], tools, protocol);
|
|
1108
|
+
currentTools = mergeRealtimeVoiceToolManifest([], tools, protocol, priorityToolNames);
|
|
951
1109
|
if (!active)
|
|
952
1110
|
return;
|
|
953
1111
|
const names = new Set(currentTools.map((tool) => tool.name));
|
|
@@ -965,8 +1123,10 @@ export function createRealtimeVoiceToolManifestCoordinator(send, timeoutMs = REA
|
|
|
965
1123
|
clearTimeout(active.timer);
|
|
966
1124
|
active = undefined;
|
|
967
1125
|
queue.length = 0;
|
|
968
|
-
if (!options?.preserveSessionTools)
|
|
1126
|
+
if (!options?.preserveSessionTools) {
|
|
969
1127
|
currentTools = [];
|
|
1128
|
+
priorityToolNames = [];
|
|
1129
|
+
}
|
|
970
1130
|
protocol = "realtime";
|
|
971
1131
|
},
|
|
972
1132
|
getTools() {
|
|
@@ -1099,6 +1259,15 @@ export function createRealtimeVoiceModeCopy(t) {
|
|
|
1099
1259
|
voiceSettings: t("agentChat.voiceMode.voiceSettings", {
|
|
1100
1260
|
defaultValue: "Voice settings",
|
|
1101
1261
|
}),
|
|
1262
|
+
thinking: t("agentChat.status.thinking", { defaultValue: "Thinking" }),
|
|
1263
|
+
approval: {
|
|
1264
|
+
question: (toolLabel) => t("agentChat.approval.question", {
|
|
1265
|
+
tool: toolLabel,
|
|
1266
|
+
defaultValue: "Approve to run {{tool}}?",
|
|
1267
|
+
}),
|
|
1268
|
+
approve: t("agentChat.approval.approve", { defaultValue: "Approve" }),
|
|
1269
|
+
deny: t("agentChat.approval.deny", { defaultValue: "Deny" }),
|
|
1270
|
+
},
|
|
1102
1271
|
settings: {
|
|
1103
1272
|
language: t("agentChat.voiceMode.settings.language", {
|
|
1104
1273
|
defaultValue: "Language",
|
|
@@ -1289,16 +1458,43 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1289
1458
|
const lastUserTextRef = useRef("");
|
|
1290
1459
|
const lastAssistantTextRef = useRef("");
|
|
1291
1460
|
const transcriptThreadIdRef = useRef(undefined);
|
|
1461
|
+
const requestedTranscriptThreadIdRef = useRef(undefined);
|
|
1292
1462
|
const liveCloseDrainTimerRef = useRef(null);
|
|
1293
1463
|
const liveCloseDrainFinishRef = useRef(null);
|
|
1294
1464
|
const toolAbortControllersRef = useRef(new Set());
|
|
1465
|
+
const pendingApprovalsRef = useRef(new Map());
|
|
1466
|
+
const runningToolsRef = useRef(new Map());
|
|
1467
|
+
const [currentToolName, setCurrentToolName] = useState(null);
|
|
1468
|
+
const trackRunningTool = useCallback((callId, name) => {
|
|
1469
|
+
const running = runningToolsRef.current;
|
|
1470
|
+
if (name)
|
|
1471
|
+
running.set(callId, name);
|
|
1472
|
+
else
|
|
1473
|
+
running.delete(callId);
|
|
1474
|
+
setCurrentToolName([...running.values()].at(-1) ?? null);
|
|
1475
|
+
}, []);
|
|
1476
|
+
const [approvals, setApprovals] = useState([]);
|
|
1477
|
+
const syncApprovals = useCallback(() => {
|
|
1478
|
+
setApprovals([...pendingApprovalsRef.current.values()].map(({ callId, name }) => ({
|
|
1479
|
+
callId,
|
|
1480
|
+
toolName: name,
|
|
1481
|
+
})));
|
|
1482
|
+
}, []);
|
|
1295
1483
|
const transcriptSequenceRef = useRef(0);
|
|
1484
|
+
const transcriptCreatedAtRef = useRef(new Map());
|
|
1485
|
+
const workSegmentRef = useRef(null);
|
|
1486
|
+
const responseCallCountsRef = useRef(new Map());
|
|
1296
1487
|
const preferencesHydratedRef = useRef(false);
|
|
1297
1488
|
const preferencesEditedRef = useRef(false);
|
|
1298
1489
|
const preferenceWriteChainRef = useRef(Promise.resolve());
|
|
1299
1490
|
const responseCoordinator = useMemo(() => createRealtimeVoiceResponseCoordinator((event) => {
|
|
1300
1491
|
sendDataChannelEvent(channelRef.current, event);
|
|
1301
1492
|
}), []);
|
|
1493
|
+
// The metrics outlive adapter changes such as a locale switch: the
|
|
1494
|
+
// transport cleanup depends on them, and a new instance would end the call.
|
|
1495
|
+
const trackEventRef = useRef(adapters.voice.trackEvent);
|
|
1496
|
+
trackEventRef.current = adapters.voice.trackEvent;
|
|
1497
|
+
const turnMetrics = useMemo(() => createRealtimeVoiceTurnMetrics((name, properties) => trackEventRef.current(name, properties)), []);
|
|
1302
1498
|
const toolManifestCoordinator = useMemo(() => createRealtimeVoiceToolManifestCoordinator((event) => {
|
|
1303
1499
|
if (event.type === "response.create") {
|
|
1304
1500
|
responseCoordinator.request(event);
|
|
@@ -1373,12 +1569,17 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1373
1569
|
.catch(() => undefined);
|
|
1374
1570
|
}, [adapters]);
|
|
1375
1571
|
const updateLivePreferences = useCallback((next, includeVoice = false) => {
|
|
1572
|
+
turnMetrics.setContext({
|
|
1573
|
+
protocol: protocolRef.current,
|
|
1574
|
+
intelligence: next.intelligence,
|
|
1575
|
+
intelligenceSource: next.intelligenceChosen ? "user" : "default",
|
|
1576
|
+
});
|
|
1376
1577
|
sendDataChannelEvent(channelRef.current, createRealtimeVoicePreferenceUpdate(next, {
|
|
1377
1578
|
browserLanguages: typeof navigator === "undefined" ? [] : navigator.languages,
|
|
1378
1579
|
includeVoice,
|
|
1379
1580
|
protocol: protocolRef.current,
|
|
1380
1581
|
}));
|
|
1381
|
-
}, []);
|
|
1582
|
+
}, [turnMetrics]);
|
|
1382
1583
|
const setLanguage = useCallback((language) => {
|
|
1383
1584
|
const next = { ...preferencesRef.current, language };
|
|
1384
1585
|
savePreferences(next);
|
|
@@ -1388,7 +1589,11 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1388
1589
|
updateLivePreferences(next);
|
|
1389
1590
|
}, [savePreferences, updateLivePreferences]);
|
|
1390
1591
|
const setIntelligence = useCallback((intelligence) => {
|
|
1391
|
-
const next = {
|
|
1592
|
+
const next = {
|
|
1593
|
+
...preferencesRef.current,
|
|
1594
|
+
intelligence,
|
|
1595
|
+
intelligenceChosen: true,
|
|
1596
|
+
};
|
|
1392
1597
|
savePreferences(next);
|
|
1393
1598
|
updateLivePreferences(next);
|
|
1394
1599
|
}, [savePreferences, updateLivePreferences]);
|
|
@@ -1519,6 +1724,19 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1519
1724
|
]);
|
|
1520
1725
|
switchMicrophoneRef.current = setMicrophone;
|
|
1521
1726
|
const cleanupTransport = useCallback(() => {
|
|
1727
|
+
// Teardown keeps what was heard now, so no grouper timer publishes into
|
|
1728
|
+
// the chat after the session is gone.
|
|
1729
|
+
flushTranscriptsRef.current?.();
|
|
1730
|
+
turnMetrics.sessionEnded();
|
|
1731
|
+
for (const pending of pendingApprovalsRef.current.values()) {
|
|
1732
|
+
finishPendingApproval(pending, null);
|
|
1733
|
+
}
|
|
1734
|
+
pendingApprovalsRef.current.clear();
|
|
1735
|
+
syncApprovals();
|
|
1736
|
+
runningToolsRef.current.clear();
|
|
1737
|
+
setCurrentToolName(null);
|
|
1738
|
+
closeRealtimeVoiceWorkSegment(workSegmentRef);
|
|
1739
|
+
responseCallCountsRef.current.clear();
|
|
1522
1740
|
if (liveCloseDrainTimerRef.current !== null) {
|
|
1523
1741
|
window.clearTimeout(liveCloseDrainTimerRef.current);
|
|
1524
1742
|
liveCloseDrainTimerRef.current = null;
|
|
@@ -1570,7 +1788,9 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1570
1788
|
abortActiveToolCalls,
|
|
1571
1789
|
audioLevels,
|
|
1572
1790
|
responseCoordinator,
|
|
1791
|
+
syncApprovals,
|
|
1573
1792
|
toolManifestCoordinator,
|
|
1793
|
+
turnMetrics,
|
|
1574
1794
|
]);
|
|
1575
1795
|
const fail = useCallback((message, options) => {
|
|
1576
1796
|
flushTranscriptsRef.current?.();
|
|
@@ -1580,7 +1800,46 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1580
1800
|
if (options?.openKeySettings)
|
|
1581
1801
|
openOpenAiKeySettings();
|
|
1582
1802
|
}, [cleanupTransport, transition]);
|
|
1583
|
-
const
|
|
1803
|
+
const publishTranscript = useCallback((transcript, status) => {
|
|
1804
|
+
const threadId = transcriptThreadIdRef.current;
|
|
1805
|
+
if (!threadId)
|
|
1806
|
+
return;
|
|
1807
|
+
// The session id arrives after the first events; the start time keeps
|
|
1808
|
+
// every update of one turn under the same message id.
|
|
1809
|
+
const sessionIdentity = startedAtRef.current ?? "pending";
|
|
1810
|
+
const providerIdentity = transcript.providerId ?? `sequence-${++transcriptSequenceRef.current}`;
|
|
1811
|
+
const id = `realtime-voice:${sessionIdentity}:${transcript.role}:${providerIdentity}`;
|
|
1812
|
+
// Work rows sit before the first message created after their call, so a
|
|
1813
|
+
// turn keeps the time it started however late its final text arrives.
|
|
1814
|
+
let createdAt = transcriptCreatedAtRef.current.get(id);
|
|
1815
|
+
if (!createdAt) {
|
|
1816
|
+
createdAt = new Date().toISOString();
|
|
1817
|
+
transcriptCreatedAtRef.current.set(id, createdAt);
|
|
1818
|
+
}
|
|
1819
|
+
realtimeVoiceTranscriptRegistry.publish({
|
|
1820
|
+
id,
|
|
1821
|
+
threadId,
|
|
1822
|
+
role: transcript.role,
|
|
1823
|
+
text: transcript.text,
|
|
1824
|
+
createdAt,
|
|
1825
|
+
status,
|
|
1826
|
+
});
|
|
1827
|
+
}, []);
|
|
1828
|
+
const transcriptSequencer = useMemo(() => createRealtimeVoiceTranscriptSequencer((transcript) => publishTranscript(transcript, "complete"), (transcript) => publishTranscript(transcript, "streaming"), (turn) => {
|
|
1829
|
+
if (turn.speaker !== "user")
|
|
1830
|
+
return;
|
|
1831
|
+
turnMetrics.userTurnUpdated(performance.now(), turn.id, turn.endMs);
|
|
1832
|
+
}), [publishTranscript, turnMetrics]);
|
|
1833
|
+
flushTranscriptsRef.current = transcriptSequencer.flush;
|
|
1834
|
+
const greetingStarter = useMemo(() => createRealtimeVoiceGreetingStarter((event) => {
|
|
1835
|
+
if (event.type === "response.create") {
|
|
1836
|
+
responseCoordinator.request(event);
|
|
1837
|
+
}
|
|
1838
|
+
else {
|
|
1839
|
+
sendDataChannelEvent(channelRef.current, event);
|
|
1840
|
+
}
|
|
1841
|
+
}), [responseCoordinator]);
|
|
1842
|
+
const handleFunctionCall = useCallback(async (call, delegationId) => {
|
|
1584
1843
|
if (handledCallsRef.current.has(call.callId))
|
|
1585
1844
|
return;
|
|
1586
1845
|
handledCallsRef.current.add(call.callId);
|
|
@@ -1589,9 +1848,23 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1589
1848
|
const toolAbortController = new AbortController();
|
|
1590
1849
|
toolAbortControllersRef.current.add(toolAbortController);
|
|
1591
1850
|
transition("working");
|
|
1851
|
+
trackRunningTool(call.callId, call.name);
|
|
1852
|
+
const finishTurnToolCall = turnMetrics.toolCallStarted();
|
|
1853
|
+
const workCall = {
|
|
1854
|
+
id: call.callId,
|
|
1855
|
+
name: call.name,
|
|
1856
|
+
status: "running",
|
|
1857
|
+
startedAt: new Date().toISOString(),
|
|
1858
|
+
};
|
|
1859
|
+
const workSegment = openRealtimeVoiceWorkSegment(workSegmentRef, transcriptThreadIdRef.current, call.callId, delegationId);
|
|
1592
1860
|
let result;
|
|
1593
1861
|
try {
|
|
1594
1862
|
const args = parseFunctionArguments(call.argumentsText);
|
|
1863
|
+
workCall.input = args;
|
|
1864
|
+
if (workSegment) {
|
|
1865
|
+
workSegment.calls.push(workCall);
|
|
1866
|
+
publishRealtimeVoiceWorkSegment(workSegment);
|
|
1867
|
+
}
|
|
1595
1868
|
result = await executeRealtimeVoiceTool({
|
|
1596
1869
|
name: call.name,
|
|
1597
1870
|
args,
|
|
@@ -1611,42 +1884,143 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1611
1884
|
}
|
|
1612
1885
|
finally {
|
|
1613
1886
|
toolAbortControllersRef.current.delete(toolAbortController);
|
|
1887
|
+
trackRunningTool(call.callId, null);
|
|
1614
1888
|
}
|
|
1889
|
+
// Session teardown aborted the call, so it did not fail. A call that
|
|
1890
|
+
// asks for approval stays running until the user decides.
|
|
1891
|
+
const approvalId = !toolAbortController.signal.aborted &&
|
|
1892
|
+
result.status === "approval_required" &&
|
|
1893
|
+
workCall.input &&
|
|
1894
|
+
transportGeneration === transportGenerationRef.current
|
|
1895
|
+
? result.approvalId
|
|
1896
|
+
: undefined;
|
|
1897
|
+
const awaitingApproval = Boolean(approvalId);
|
|
1898
|
+
if (!awaitingApproval) {
|
|
1899
|
+
workCall.status =
|
|
1900
|
+
toolAbortController.signal.aborted ||
|
|
1901
|
+
result.status === "approval_required"
|
|
1902
|
+
? "cancelled"
|
|
1903
|
+
: result.status;
|
|
1904
|
+
workCall.output = result.output;
|
|
1905
|
+
workCall.completedAt = new Date().toISOString();
|
|
1906
|
+
}
|
|
1907
|
+
if (workSegment) {
|
|
1908
|
+
if (!workSegment.calls.includes(workCall)) {
|
|
1909
|
+
workSegment.calls.push(workCall);
|
|
1910
|
+
}
|
|
1911
|
+
if (workSegment.closing) {
|
|
1912
|
+
closeRealtimeVoiceWorkSegment(workSegmentRef, workSegment);
|
|
1913
|
+
}
|
|
1914
|
+
else {
|
|
1915
|
+
publishRealtimeVoiceWorkSegment(workSegment);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
finishTurnToolCall(performance.now(), toolAbortController.signal.aborted ? { status: "cancelled" } : result);
|
|
1615
1919
|
if (transportGeneration !== transportGenerationRef.current ||
|
|
1616
1920
|
channelRef.current !== transportChannel) {
|
|
1617
1921
|
return;
|
|
1618
1922
|
}
|
|
1619
1923
|
if (result.capability)
|
|
1620
1924
|
capabilityRef.current = result.capability;
|
|
1925
|
+
if (approvalId && workCall.input) {
|
|
1926
|
+
pendingApprovalsRef.current.set(call.callId, {
|
|
1927
|
+
callId: call.callId,
|
|
1928
|
+
name: call.name,
|
|
1929
|
+
args: workCall.input,
|
|
1930
|
+
approvalId,
|
|
1931
|
+
workCall,
|
|
1932
|
+
workSegment,
|
|
1933
|
+
});
|
|
1934
|
+
syncApprovals();
|
|
1935
|
+
}
|
|
1621
1936
|
toolManifestCoordinator.enqueue(result);
|
|
1622
|
-
}, [
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1937
|
+
}, [
|
|
1938
|
+
browserTabId,
|
|
1939
|
+
syncApprovals,
|
|
1940
|
+
toolManifestCoordinator,
|
|
1941
|
+
trackRunningTool,
|
|
1942
|
+
transition,
|
|
1943
|
+
turnMetrics,
|
|
1944
|
+
]);
|
|
1945
|
+
const resolveApproval = useCallback(async (callId, approved) => {
|
|
1946
|
+
const pending = pendingApprovalsRef.current.get(callId);
|
|
1947
|
+
if (!pending)
|
|
1626
1948
|
return;
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
const
|
|
1630
|
-
const
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1949
|
+
pendingApprovalsRef.current.delete(callId);
|
|
1950
|
+
syncApprovals();
|
|
1951
|
+
const transportGeneration = transportGenerationRef.current;
|
|
1952
|
+
const transportChannel = channelRef.current;
|
|
1953
|
+
let result = null;
|
|
1954
|
+
let aborted = false;
|
|
1955
|
+
if (approved) {
|
|
1956
|
+
transition("working");
|
|
1957
|
+
trackRunningTool(callId, pending.name);
|
|
1958
|
+
const toolAbortController = new AbortController();
|
|
1959
|
+
toolAbortControllersRef.current.add(toolAbortController);
|
|
1960
|
+
try {
|
|
1961
|
+
result = await executeRealtimeVoiceTool({
|
|
1962
|
+
name: pending.name,
|
|
1963
|
+
args: pending.args,
|
|
1964
|
+
callId: `${callId}_approved`,
|
|
1965
|
+
sessionId: sessionIdRef.current,
|
|
1966
|
+
browserTabId,
|
|
1967
|
+
capability: capabilityRef.current,
|
|
1968
|
+
signal: toolAbortController.signal,
|
|
1969
|
+
approvalId: pending.approvalId,
|
|
1970
|
+
});
|
|
1971
|
+
}
|
|
1972
|
+
catch (toolError) {
|
|
1973
|
+
result = {
|
|
1974
|
+
callId,
|
|
1975
|
+
status: "failed",
|
|
1976
|
+
output: errorMessage(toolError),
|
|
1977
|
+
};
|
|
1978
|
+
}
|
|
1979
|
+
finally {
|
|
1980
|
+
toolAbortControllersRef.current.delete(toolAbortController);
|
|
1981
|
+
trackRunningTool(callId, null);
|
|
1982
|
+
}
|
|
1983
|
+
aborted = toolAbortController.signal.aborted;
|
|
1645
1984
|
}
|
|
1646
1985
|
else {
|
|
1647
|
-
|
|
1986
|
+
// A declined record left pending would make a later identical
|
|
1987
|
+
// request's approval ambiguous on the server.
|
|
1988
|
+
await executeRealtimeVoiceTool({
|
|
1989
|
+
name: pending.name,
|
|
1990
|
+
args: pending.args,
|
|
1991
|
+
callId: `${callId}_declined`,
|
|
1992
|
+
sessionId: sessionIdRef.current,
|
|
1993
|
+
browserTabId,
|
|
1994
|
+
capability: capabilityRef.current,
|
|
1995
|
+
declinedApprovalId: pending.approvalId,
|
|
1996
|
+
}).catch((declineError) => {
|
|
1997
|
+
console.warn("Could not record the declined voice approval.", declineError);
|
|
1998
|
+
});
|
|
1648
1999
|
}
|
|
1649
|
-
|
|
2000
|
+
const transportChanged = transportGeneration !== transportGenerationRef.current ||
|
|
2001
|
+
channelRef.current !== transportChannel;
|
|
2002
|
+
// An approval that expired or no longer applies asks again; the call
|
|
2003
|
+
// stays running and the user sees the new request.
|
|
2004
|
+
const renewedApprovalId = !aborted && !transportChanged && result?.status === "approval_required"
|
|
2005
|
+
? result.approvalId
|
|
2006
|
+
: undefined;
|
|
2007
|
+
if (renewedApprovalId) {
|
|
2008
|
+
pendingApprovalsRef.current.set(callId, {
|
|
2009
|
+
...pending,
|
|
2010
|
+
approvalId: renewedApprovalId,
|
|
2011
|
+
});
|
|
2012
|
+
syncApprovals();
|
|
2013
|
+
}
|
|
2014
|
+
else {
|
|
2015
|
+
finishPendingApproval(pending, aborted || !result ? null : result);
|
|
2016
|
+
}
|
|
2017
|
+
if (aborted || transportChanged)
|
|
2018
|
+
return;
|
|
2019
|
+
if (result?.capability)
|
|
2020
|
+
capabilityRef.current = result.capability;
|
|
2021
|
+
sendDataChannelEvent(channelRef.current, createRealtimeVoiceApprovalOutcomeEvent(pending.name, result, protocolRef.current));
|
|
2022
|
+
sendDataChannelEvent(channelRef.current, { type: "response.create" });
|
|
2023
|
+
}, [browserTabId, syncApprovals, trackRunningTool, transition]);
|
|
1650
2024
|
const handleServerEvent = useCallback((event, draining = false) => {
|
|
1651
2025
|
if (draining) {
|
|
1652
2026
|
transcriptSequencer.handle(event);
|
|
@@ -1686,9 +2060,12 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1686
2060
|
transition("working");
|
|
1687
2061
|
}
|
|
1688
2062
|
else if (event.type === "input_audio_buffer.speech_started") {
|
|
2063
|
+
closeRealtimeVoiceWorkSegment(workSegmentRef);
|
|
2064
|
+
turnMetrics.userSpeechStarted(performance.now());
|
|
1689
2065
|
transition("listening");
|
|
1690
2066
|
}
|
|
1691
2067
|
else if (event.type === "input_audio_buffer.speech_stopped") {
|
|
2068
|
+
turnMetrics.userSpeechEnded(performance.now());
|
|
1692
2069
|
transition("working");
|
|
1693
2070
|
if (protocolRef.current === "realtime")
|
|
1694
2071
|
responseCoordinator.request();
|
|
@@ -1698,7 +2075,11 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1698
2075
|
lastAssistantTextRef.current = "";
|
|
1699
2076
|
transition("working");
|
|
1700
2077
|
}
|
|
2078
|
+
else if (event.type === "output_audio_buffer.started") {
|
|
2079
|
+
turnMetrics.assistantAudioStarted(performance.now());
|
|
2080
|
+
}
|
|
1701
2081
|
else if (event.type === "response.output_audio_transcript.delta") {
|
|
2082
|
+
turnMetrics.assistantResponded();
|
|
1702
2083
|
hasOutputAudioRef.current = true;
|
|
1703
2084
|
if (typeof event.delta === "string") {
|
|
1704
2085
|
lastAssistantTextRef.current += event.delta;
|
|
@@ -1724,6 +2105,7 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1724
2105
|
transition("listening");
|
|
1725
2106
|
}
|
|
1726
2107
|
else if (event.type === "session.output_transcript.delta") {
|
|
2108
|
+
turnMetrics.assistantAudioStarted(performance.now(), typeof event.start_ms === "number" ? event.start_ms : undefined);
|
|
1727
2109
|
hasOutputAudioRef.current = true;
|
|
1728
2110
|
if (typeof event.delta === "string") {
|
|
1729
2111
|
lastAssistantTextRef.current += event.delta;
|
|
@@ -1741,6 +2123,7 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1741
2123
|
? response.status
|
|
1742
2124
|
: undefined;
|
|
1743
2125
|
if (responseEvent.type === "response.failed" || status === "failed") {
|
|
2126
|
+
closeRealtimeVoiceWorkSegment(workSegmentRef);
|
|
1744
2127
|
transportGenerationRef.current += 1;
|
|
1745
2128
|
handledCallsRef.current.clear();
|
|
1746
2129
|
abortActiveToolCalls();
|
|
@@ -1783,12 +2166,30 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1783
2166
|
}));
|
|
1784
2167
|
return;
|
|
1785
2168
|
}
|
|
2169
|
+
const delegationId = typeof event.delegation_id === "string" ? event.delegation_id : "";
|
|
1786
2170
|
const calls = extractRealtimeVoiceFunctionCalls(event);
|
|
1787
|
-
for (const call of calls)
|
|
1788
|
-
void handleFunctionCall(call);
|
|
1789
|
-
|
|
2171
|
+
for (const call of calls) {
|
|
2172
|
+
void handleFunctionCall(call, delegationId || undefined);
|
|
2173
|
+
}
|
|
2174
|
+
// GPT-Live's response snapshots carry no output, so whether a response
|
|
2175
|
+
// called a tool is counted from its streamed items, per delegation.
|
|
2176
|
+
const responseCalls = responseCallCountsRef.current;
|
|
2177
|
+
if (responseEvent.type === "response.created") {
|
|
2178
|
+
responseCalls.set(delegationId, 0);
|
|
2179
|
+
}
|
|
2180
|
+
if (calls.length > 0) {
|
|
2181
|
+
responseCalls.set(delegationId, (responseCalls.get(delegationId) ?? 0) + calls.length);
|
|
2182
|
+
}
|
|
2183
|
+
if (responseBoundary && !responseCalls.get(delegationId)) {
|
|
2184
|
+
// Only the segment's own delegation ends it; an uncorrelated
|
|
2185
|
+
// boundary would split one request's work into several rows.
|
|
2186
|
+
if ((workSegmentRef.current?.delegationId ?? "") === delegationId) {
|
|
2187
|
+
closeRealtimeVoiceWorkSegment(workSegmentRef);
|
|
2188
|
+
}
|
|
1790
2189
|
transition("listening");
|
|
1791
2190
|
}
|
|
2191
|
+
if (responseBoundary)
|
|
2192
|
+
responseCalls.delete(delegationId);
|
|
1792
2193
|
if (responseBoundary) {
|
|
1793
2194
|
lastUserTextRef.current = "";
|
|
1794
2195
|
lastAssistantTextRef.current = "";
|
|
@@ -1805,6 +2206,7 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1805
2206
|
transition,
|
|
1806
2207
|
responseCoordinator,
|
|
1807
2208
|
toolManifestCoordinator,
|
|
2209
|
+
turnMetrics,
|
|
1808
2210
|
updateLivePreferences,
|
|
1809
2211
|
]);
|
|
1810
2212
|
const start = useCallback(async () => {
|
|
@@ -1829,8 +2231,10 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1829
2231
|
greetingStarter.reset();
|
|
1830
2232
|
setVoiceChangePending(false);
|
|
1831
2233
|
transcriptThreadIdRef.current =
|
|
1832
|
-
|
|
2234
|
+
requestedTranscriptThreadIdRef.current ??
|
|
2235
|
+
realtimeVoiceTranscriptRegistry.activeThreadId();
|
|
1833
2236
|
transcriptSequenceRef.current = 0;
|
|
2237
|
+
transcriptCreatedAtRef.current.clear();
|
|
1834
2238
|
transcriptSequencer.reset();
|
|
1835
2239
|
inputMutedRef.current = false;
|
|
1836
2240
|
outputMutedRef.current = false;
|
|
@@ -1904,8 +2308,10 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1904
2308
|
try {
|
|
1905
2309
|
handleServerEvent(JSON.parse(String(messageEvent.data)), stateRef.current === "ending");
|
|
1906
2310
|
}
|
|
1907
|
-
catch {
|
|
1908
|
-
//
|
|
2311
|
+
catch (eventError) {
|
|
2312
|
+
// A malformed provider event must not end a healthy call, but the
|
|
2313
|
+
// text it carried is lost, so say so.
|
|
2314
|
+
console.warn("Ignored a realtime voice event that could not be handled.", eventError);
|
|
1909
2315
|
}
|
|
1910
2316
|
};
|
|
1911
2317
|
channel.onerror = () => {
|
|
@@ -1955,7 +2361,25 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1955
2361
|
answer.model ??
|
|
1956
2362
|
(protocolRef.current === "live" ? "gpt-live-1" : "gpt-realtime-2.1");
|
|
1957
2363
|
toolManifestCoordinator.setProtocol(protocolRef.current);
|
|
2364
|
+
toolManifestCoordinator.setPriorityToolNames(answer.priorityToolNames ?? []);
|
|
1958
2365
|
greetingStarter.setProtocol(protocolRef.current);
|
|
2366
|
+
const chosen = preferencesRef.current.intelligenceChosen === true;
|
|
2367
|
+
if (!chosen &&
|
|
2368
|
+
answer.intelligence &&
|
|
2369
|
+
answer.intelligence !== preferencesRef.current.intelligence) {
|
|
2370
|
+
// Saved as not chosen, so Settings shows the app's default after a
|
|
2371
|
+
// reload and the app can still change it.
|
|
2372
|
+
savePreferences({
|
|
2373
|
+
...preferencesRef.current,
|
|
2374
|
+
intelligence: answer.intelligence,
|
|
2375
|
+
intelligenceChosen: false,
|
|
2376
|
+
});
|
|
2377
|
+
}
|
|
2378
|
+
turnMetrics.setContext({
|
|
2379
|
+
protocol: protocolRef.current,
|
|
2380
|
+
intelligence: preferencesRef.current.intelligence,
|
|
2381
|
+
intelligenceSource: chosen ? "user" : "default",
|
|
2382
|
+
});
|
|
1959
2383
|
await peer.setRemoteDescription({ type: "answer", sdp: answer.sdp });
|
|
1960
2384
|
}
|
|
1961
2385
|
catch (startError) {
|
|
@@ -1990,10 +2414,16 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
1990
2414
|
handleServerEvent,
|
|
1991
2415
|
hydratePreferences,
|
|
1992
2416
|
refreshMicrophones,
|
|
2417
|
+
savePreferences,
|
|
1993
2418
|
t,
|
|
1994
2419
|
transcriptSequencer,
|
|
1995
2420
|
transition,
|
|
2421
|
+
turnMetrics,
|
|
1996
2422
|
]);
|
|
2423
|
+
const startForThread = useCallback((options) => {
|
|
2424
|
+
requestedTranscriptThreadIdRef.current = options?.threadId;
|
|
2425
|
+
return start();
|
|
2426
|
+
}, [start]);
|
|
1997
2427
|
const end = useCallback(() => {
|
|
1998
2428
|
if (stateRef.current === "idle" || stateRef.current === "ending")
|
|
1999
2429
|
return;
|
|
@@ -2107,9 +2537,12 @@ function useRealtimeVoiceModeController(browserTabId, copy) {
|
|
|
2107
2537
|
setMicrophone,
|
|
2108
2538
|
setInputMuted,
|
|
2109
2539
|
setOutputMuted,
|
|
2110
|
-
start,
|
|
2540
|
+
start: startForThread,
|
|
2111
2541
|
end,
|
|
2112
2542
|
dismissError,
|
|
2543
|
+
currentToolName,
|
|
2544
|
+
approvals,
|
|
2545
|
+
resolveApproval,
|
|
2113
2546
|
};
|
|
2114
2547
|
}
|
|
2115
2548
|
const RealtimeVoiceModeContext = createContext(null);
|
|
@@ -2170,7 +2603,11 @@ export function buildRealtimeVoiceSettings({ copy, preferences, microphones, mic
|
|
|
2170
2603
|
})),
|
|
2171
2604
|
onValueChange: (value) => {
|
|
2172
2605
|
if (isOneOf(REALTIME_VOICE_INTELLIGENCE_LEVELS, value)) {
|
|
2173
|
-
onPreferencesChange({
|
|
2606
|
+
onPreferencesChange({
|
|
2607
|
+
...preferences,
|
|
2608
|
+
intelligence: value,
|
|
2609
|
+
intelligenceChosen: true,
|
|
2610
|
+
});
|
|
2174
2611
|
}
|
|
2175
2612
|
},
|
|
2176
2613
|
},
|