@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
|
@@ -7,6 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
|
7
7
|
import {
|
|
8
8
|
realtimeVoiceTranscriptRegistry,
|
|
9
9
|
type RealtimeVoiceTranscriptMessage,
|
|
10
|
+
type RealtimeVoiceWorkUpdate,
|
|
10
11
|
} from "./realtime-voice-transcript.js";
|
|
11
12
|
import { ComposerRuntimeAdaptersProvider } from "./runtime-adapters.js";
|
|
12
13
|
import {
|
|
@@ -14,16 +15,20 @@ import {
|
|
|
14
15
|
RealtimeVoiceModeProvider,
|
|
15
16
|
useRealtimeVoiceMode,
|
|
16
17
|
type RealtimeVoiceModeApi,
|
|
18
|
+
type RealtimeVoiceStartOptions,
|
|
17
19
|
} from "./useRealtimeVoiceMode.js";
|
|
18
20
|
|
|
19
21
|
interface FakeChannel {
|
|
20
22
|
onmessage?: (event: { data: string }) => void;
|
|
21
23
|
close: () => void;
|
|
24
|
+
readyState?: string;
|
|
25
|
+
send?: (data: string) => void;
|
|
22
26
|
}
|
|
23
27
|
|
|
24
28
|
let channel: FakeChannel;
|
|
25
29
|
let track: { enabled: boolean; stop: () => void; onended: unknown };
|
|
26
30
|
let audio: HTMLAudioElement | undefined;
|
|
31
|
+
let setAppState: ReturnType<typeof vi.fn>;
|
|
27
32
|
|
|
28
33
|
class FakePeerConnection {
|
|
29
34
|
connectionState = "new";
|
|
@@ -46,6 +51,7 @@ beforeEach(() => {
|
|
|
46
51
|
channel = { close: vi.fn() };
|
|
47
52
|
track = { enabled: true, stop: vi.fn(), onended: null };
|
|
48
53
|
audio = undefined;
|
|
54
|
+
setAppState = vi.fn();
|
|
49
55
|
vi.stubGlobal("RTCPeerConnection", FakePeerConnection);
|
|
50
56
|
Object.defineProperty(navigator, "mediaDevices", {
|
|
51
57
|
configurable: true,
|
|
@@ -75,6 +81,11 @@ afterEach(() => {
|
|
|
75
81
|
|
|
76
82
|
async function startSession(
|
|
77
83
|
readAppState: (key: string) => unknown = () => undefined,
|
|
84
|
+
startOptions?: RealtimeVoiceStartOptions,
|
|
85
|
+
trackEvent: (
|
|
86
|
+
name: string,
|
|
87
|
+
properties: Record<string, unknown>,
|
|
88
|
+
) => void = () => undefined,
|
|
78
89
|
) {
|
|
79
90
|
let api: RealtimeVoiceModeApi | undefined;
|
|
80
91
|
function Probe() {
|
|
@@ -89,8 +100,9 @@ async function startSession(
|
|
|
89
100
|
adapters={{
|
|
90
101
|
voice: {
|
|
91
102
|
readAppState: readAppState as never,
|
|
92
|
-
setAppState:
|
|
103
|
+
setAppState: setAppState as never,
|
|
93
104
|
getBrowserTabId: () => "tab-1",
|
|
105
|
+
trackEvent,
|
|
94
106
|
},
|
|
95
107
|
}}
|
|
96
108
|
>
|
|
@@ -101,7 +113,7 @@ async function startSession(
|
|
|
101
113
|
);
|
|
102
114
|
});
|
|
103
115
|
await act(async () => {
|
|
104
|
-
void api!.start();
|
|
116
|
+
void api!.start(startOptions);
|
|
105
117
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
106
118
|
});
|
|
107
119
|
return {
|
|
@@ -116,6 +128,381 @@ async function startSession(
|
|
|
116
128
|
}
|
|
117
129
|
|
|
118
130
|
describe("realtime voice session controls", () => {
|
|
131
|
+
it("groups one reply's tool calls into one piece of work", async () => {
|
|
132
|
+
const work: RealtimeVoiceWorkUpdate[] = [];
|
|
133
|
+
const unregister = realtimeVoiceTranscriptRegistry.register({
|
|
134
|
+
threadId: "thread-work",
|
|
135
|
+
active: true,
|
|
136
|
+
append: () => true,
|
|
137
|
+
appendWork: (update) => {
|
|
138
|
+
work.push(update);
|
|
139
|
+
return true;
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
const results: Array<(value: Response) => void> = [];
|
|
143
|
+
vi.stubGlobal(
|
|
144
|
+
"fetch",
|
|
145
|
+
vi.fn(() => new Promise<Response>((resolve) => results.push(resolve))),
|
|
146
|
+
);
|
|
147
|
+
const session = await startSession();
|
|
148
|
+
const resolveCall = async (callId: string) => {
|
|
149
|
+
await act(async () => {
|
|
150
|
+
results.shift()?.(
|
|
151
|
+
Response.json({ callId, status: "completed", output: "{}" }),
|
|
152
|
+
);
|
|
153
|
+
await new Promise((done) => setTimeout(done, 0));
|
|
154
|
+
});
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
await session.send({ type: "response.created" });
|
|
158
|
+
for (const [callId, name] of [
|
|
159
|
+
["call-search", "web-search"],
|
|
160
|
+
["call-read", "web-request"],
|
|
161
|
+
] as const) {
|
|
162
|
+
await session.send({
|
|
163
|
+
type: "response.function_call_arguments.done",
|
|
164
|
+
call_id: callId,
|
|
165
|
+
name,
|
|
166
|
+
arguments: "{}",
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
// The response that asked for the tools is not the end of the reply.
|
|
170
|
+
await session.send({
|
|
171
|
+
type: "response.done",
|
|
172
|
+
response: { status: "completed", output: [] },
|
|
173
|
+
});
|
|
174
|
+
expect(session.api().currentToolName).toBe("web-request");
|
|
175
|
+
await resolveCall("call-search");
|
|
176
|
+
expect(session.api().currentToolName).toBe("web-request");
|
|
177
|
+
await resolveCall("call-read");
|
|
178
|
+
expect(session.api().currentToolName).toBeNull();
|
|
179
|
+
expect(work.every((update) => !update.completedAt)).toBe(true);
|
|
180
|
+
|
|
181
|
+
await session.send({ type: "response.created" });
|
|
182
|
+
await session.send({
|
|
183
|
+
type: "response.done",
|
|
184
|
+
response: { status: "completed", output: [] },
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const last = work.at(-1);
|
|
188
|
+
expect(new Set(work.map((update) => update.id))).toEqual(
|
|
189
|
+
new Set(["call-search"]),
|
|
190
|
+
);
|
|
191
|
+
expect(last?.calls.map((call) => [call.id, call.status])).toEqual([
|
|
192
|
+
["call-search", "completed"],
|
|
193
|
+
["call-read", "completed"],
|
|
194
|
+
]);
|
|
195
|
+
expect(last?.completedAt).toBe(last?.calls.at(-1)?.completedAt);
|
|
196
|
+
unregister();
|
|
197
|
+
session.unmount();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("groups a GPT-Live delegation's tool calls into one piece of work", async () => {
|
|
201
|
+
const work: RealtimeVoiceWorkUpdate[] = [];
|
|
202
|
+
const unregister = realtimeVoiceTranscriptRegistry.register({
|
|
203
|
+
threadId: "thread-live-work",
|
|
204
|
+
active: true,
|
|
205
|
+
append: () => true,
|
|
206
|
+
appendWork: (update) => {
|
|
207
|
+
work.push(update);
|
|
208
|
+
return true;
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
const results: Array<(value: Response) => void> = [];
|
|
212
|
+
vi.stubGlobal(
|
|
213
|
+
"fetch",
|
|
214
|
+
vi.fn(() => new Promise<Response>((resolve) => results.push(resolve))),
|
|
215
|
+
);
|
|
216
|
+
const session = await startSession();
|
|
217
|
+
const nested = (delegationId: string, event: Record<string, unknown>) =>
|
|
218
|
+
session.send({
|
|
219
|
+
type: "response.event",
|
|
220
|
+
delegation_id: delegationId,
|
|
221
|
+
event,
|
|
222
|
+
});
|
|
223
|
+
// Live response snapshots never carry their output.
|
|
224
|
+
const doneSnapshot = {
|
|
225
|
+
type: "response.done",
|
|
226
|
+
response: { status: "completed" },
|
|
227
|
+
};
|
|
228
|
+
const callResponse = async (delegationId: string, callId: string) => {
|
|
229
|
+
await nested(delegationId, { type: "response.created" });
|
|
230
|
+
await nested(delegationId, {
|
|
231
|
+
type: "response.output_item.done",
|
|
232
|
+
item: {
|
|
233
|
+
type: "function_call",
|
|
234
|
+
call_id: callId,
|
|
235
|
+
name: "web-search",
|
|
236
|
+
arguments: "{}",
|
|
237
|
+
},
|
|
238
|
+
});
|
|
239
|
+
await nested(delegationId, doneSnapshot);
|
|
240
|
+
await act(async () => {
|
|
241
|
+
results.shift()?.(
|
|
242
|
+
Response.json({ callId, status: "completed", output: "{}" }),
|
|
243
|
+
);
|
|
244
|
+
await new Promise((done) => setTimeout(done, 0));
|
|
245
|
+
});
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
await callResponse("delegation-1", "call-1");
|
|
249
|
+
await callResponse("delegation-1", "call-2");
|
|
250
|
+
await callResponse("delegation-1", "call-3");
|
|
251
|
+
expect(work.every((update) => !update.completedAt)).toBe(true);
|
|
252
|
+
await nested("delegation-1", { type: "response.created" });
|
|
253
|
+
await nested("delegation-1", doneSnapshot);
|
|
254
|
+
expect(work.at(-1)).toMatchObject({ id: "call-1" });
|
|
255
|
+
expect(work.at(-1)?.calls.map((call) => call.id)).toEqual([
|
|
256
|
+
"call-1",
|
|
257
|
+
"call-2",
|
|
258
|
+
"call-3",
|
|
259
|
+
]);
|
|
260
|
+
expect(work.at(-1)?.completedAt).toBeTruthy();
|
|
261
|
+
|
|
262
|
+
// The next request is a new delegation and new work.
|
|
263
|
+
await callResponse("delegation-2", "call-4");
|
|
264
|
+
expect(work.at(-1)).toMatchObject({ id: "call-4" });
|
|
265
|
+
unregister();
|
|
266
|
+
session.unmount();
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it.each([
|
|
270
|
+
["approved", true],
|
|
271
|
+
["declined", false],
|
|
272
|
+
])(
|
|
273
|
+
"holds a call that needs approval until the user has %s it",
|
|
274
|
+
async (decision, approved) => {
|
|
275
|
+
const callId = `call-delete-${decision}`;
|
|
276
|
+
const work: RealtimeVoiceWorkUpdate[] = [];
|
|
277
|
+
const unregister = realtimeVoiceTranscriptRegistry.register({
|
|
278
|
+
threadId: `thread-approval-${String(approved)}`,
|
|
279
|
+
active: true,
|
|
280
|
+
append: () => true,
|
|
281
|
+
appendWork: (update) => {
|
|
282
|
+
work.push(update);
|
|
283
|
+
return true;
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
const sent: Array<Record<string, unknown>> = [];
|
|
287
|
+
channel = {
|
|
288
|
+
close: vi.fn(),
|
|
289
|
+
readyState: "open",
|
|
290
|
+
send: (data: string) => sent.push(JSON.parse(data)),
|
|
291
|
+
} as FakeChannel;
|
|
292
|
+
const requests: Array<Record<string, unknown>> = [];
|
|
293
|
+
vi.stubGlobal(
|
|
294
|
+
"fetch",
|
|
295
|
+
vi.fn(async (_url: string, init?: RequestInit) => {
|
|
296
|
+
const body = JSON.parse(String(init?.body)) as Record<
|
|
297
|
+
string,
|
|
298
|
+
unknown
|
|
299
|
+
>;
|
|
300
|
+
requests.push(body);
|
|
301
|
+
return Response.json(
|
|
302
|
+
body.approvalId || body.declinedApprovalId
|
|
303
|
+
? { callId: body.callId, status: "completed", output: "deleted" }
|
|
304
|
+
: // No key: arguments too long for one still get an approval.
|
|
305
|
+
{
|
|
306
|
+
callId: body.callId,
|
|
307
|
+
status: "approval_required",
|
|
308
|
+
output: "Awaiting approval",
|
|
309
|
+
approvalId: "approval-1",
|
|
310
|
+
},
|
|
311
|
+
);
|
|
312
|
+
}),
|
|
313
|
+
);
|
|
314
|
+
const session = await startSession();
|
|
315
|
+
await session.send({ type: "response.created" });
|
|
316
|
+
await session.send({
|
|
317
|
+
type: "response.function_call_arguments.done",
|
|
318
|
+
call_id: callId,
|
|
319
|
+
name: "delete-slide",
|
|
320
|
+
arguments: JSON.stringify({ slideId: "s1" }),
|
|
321
|
+
});
|
|
322
|
+
await act(async () => {
|
|
323
|
+
await new Promise((done) => setTimeout(done, 0));
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(session.api().approvals).toEqual([
|
|
327
|
+
{ callId, toolName: "delete-slide" },
|
|
328
|
+
]);
|
|
329
|
+
expect(work.at(-1)?.calls.map((call) => call.status)).toEqual([
|
|
330
|
+
"running",
|
|
331
|
+
]);
|
|
332
|
+
sent.length = 0;
|
|
333
|
+
|
|
334
|
+
await act(async () => {
|
|
335
|
+
await session.api().resolveApproval(callId, approved);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
expect(session.api().approvals).toEqual([]);
|
|
339
|
+
expect(requests.slice(1)).toEqual([
|
|
340
|
+
approved
|
|
341
|
+
? expect.objectContaining({
|
|
342
|
+
name: "delete-slide",
|
|
343
|
+
args: { slideId: "s1" },
|
|
344
|
+
approvalId: "approval-1",
|
|
345
|
+
})
|
|
346
|
+
: expect.objectContaining({ declinedApprovalId: "approval-1" }),
|
|
347
|
+
]);
|
|
348
|
+
expect(work.at(-1)?.calls.map((call) => call.status)).toEqual([
|
|
349
|
+
approved ? "completed" : "cancelled",
|
|
350
|
+
]);
|
|
351
|
+
// A session that has not connected yet speaks GPT-Live.
|
|
352
|
+
expect(sent.map((event) => event.type)).toEqual([
|
|
353
|
+
"response.item.create",
|
|
354
|
+
"response.create",
|
|
355
|
+
]);
|
|
356
|
+
expect(JSON.stringify(sent[0])).toContain(
|
|
357
|
+
approved ? "The user approved delete-slide" : "The user declined",
|
|
358
|
+
);
|
|
359
|
+
unregister();
|
|
360
|
+
await session.unmount();
|
|
361
|
+
},
|
|
362
|
+
);
|
|
363
|
+
|
|
364
|
+
it("asks again when an approved call's approval no longer applies", async () => {
|
|
365
|
+
const work: RealtimeVoiceWorkUpdate[] = [];
|
|
366
|
+
const unregister = realtimeVoiceTranscriptRegistry.register({
|
|
367
|
+
threadId: "thread-approval-renewed",
|
|
368
|
+
active: true,
|
|
369
|
+
append: () => true,
|
|
370
|
+
appendWork: (update) => {
|
|
371
|
+
work.push(update);
|
|
372
|
+
return true;
|
|
373
|
+
},
|
|
374
|
+
});
|
|
375
|
+
const sent: Array<Record<string, unknown>> = [];
|
|
376
|
+
channel = {
|
|
377
|
+
close: vi.fn(),
|
|
378
|
+
readyState: "open",
|
|
379
|
+
send: (data: string) => sent.push(JSON.parse(data)),
|
|
380
|
+
} as FakeChannel;
|
|
381
|
+
const requests: Array<Record<string, unknown>> = [];
|
|
382
|
+
vi.stubGlobal(
|
|
383
|
+
"fetch",
|
|
384
|
+
vi.fn(async (_url: string, init?: RequestInit) => {
|
|
385
|
+
const body = JSON.parse(String(init?.body)) as Record<string, unknown>;
|
|
386
|
+
requests.push(body);
|
|
387
|
+
// The first approval expired, so the approved retry asks again.
|
|
388
|
+
return Response.json(
|
|
389
|
+
body.approvalId === "approval-2"
|
|
390
|
+
? { callId: body.callId, status: "completed", output: "deleted" }
|
|
391
|
+
: {
|
|
392
|
+
callId: body.callId,
|
|
393
|
+
status: "approval_required",
|
|
394
|
+
output: "Awaiting approval",
|
|
395
|
+
approvalKey: "delete-slide:key",
|
|
396
|
+
approvalId: body.approvalId ? "approval-2" : "approval-1",
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
}),
|
|
400
|
+
);
|
|
401
|
+
const session = await startSession();
|
|
402
|
+
await session.send({ type: "response.created" });
|
|
403
|
+
await session.send({
|
|
404
|
+
type: "response.function_call_arguments.done",
|
|
405
|
+
call_id: "call-renewed",
|
|
406
|
+
name: "delete-slide",
|
|
407
|
+
arguments: JSON.stringify({ slideId: "s1" }),
|
|
408
|
+
});
|
|
409
|
+
await act(async () => {
|
|
410
|
+
await new Promise((done) => setTimeout(done, 0));
|
|
411
|
+
});
|
|
412
|
+
sent.length = 0;
|
|
413
|
+
|
|
414
|
+
await act(async () => {
|
|
415
|
+
await session.api().resolveApproval("call-renewed", true);
|
|
416
|
+
});
|
|
417
|
+
expect(session.api().approvals).toEqual([
|
|
418
|
+
{ callId: "call-renewed", toolName: "delete-slide" },
|
|
419
|
+
]);
|
|
420
|
+
expect(work.at(-1)?.calls.map((call) => call.status)).toEqual(["running"]);
|
|
421
|
+
expect(JSON.stringify(sent[0])).toContain("asked for approval again");
|
|
422
|
+
|
|
423
|
+
await act(async () => {
|
|
424
|
+
await session.api().resolveApproval("call-renewed", true);
|
|
425
|
+
});
|
|
426
|
+
expect(requests.at(-1)).toMatchObject({ approvalId: "approval-2" });
|
|
427
|
+
expect(work.at(-1)?.calls.map((call) => call.status)).toEqual([
|
|
428
|
+
"completed",
|
|
429
|
+
]);
|
|
430
|
+
unregister();
|
|
431
|
+
await session.unmount();
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
it("records a tool call that teardown aborted as cancelled", async () => {
|
|
435
|
+
const work: RealtimeVoiceWorkUpdate[] = [];
|
|
436
|
+
const unregister = realtimeVoiceTranscriptRegistry.register({
|
|
437
|
+
threadId: "thread-teardown",
|
|
438
|
+
active: true,
|
|
439
|
+
append: () => true,
|
|
440
|
+
appendWork: (update) => {
|
|
441
|
+
work.push(update);
|
|
442
|
+
return true;
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
vi.stubGlobal(
|
|
446
|
+
"fetch",
|
|
447
|
+
vi.fn(
|
|
448
|
+
(_url: string, init?: RequestInit) =>
|
|
449
|
+
new Promise<Response>((_resolve, reject) => {
|
|
450
|
+
init?.signal?.addEventListener("abort", () =>
|
|
451
|
+
reject(new DOMException("Aborted", "AbortError")),
|
|
452
|
+
);
|
|
453
|
+
}),
|
|
454
|
+
),
|
|
455
|
+
);
|
|
456
|
+
const session = await startSession();
|
|
457
|
+
await session.send({ type: "response.created" });
|
|
458
|
+
await session.send({
|
|
459
|
+
type: "response.function_call_arguments.done",
|
|
460
|
+
call_id: "call-edit",
|
|
461
|
+
name: "web-search",
|
|
462
|
+
arguments: "{}",
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
await act(async () => {
|
|
466
|
+
session.api().end();
|
|
467
|
+
await new Promise((done) => setTimeout(done, 0));
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
expect(work.at(-1)?.calls.map((call) => [call.id, call.status])).toEqual([
|
|
471
|
+
["call-edit", "cancelled"],
|
|
472
|
+
]);
|
|
473
|
+
unregister();
|
|
474
|
+
session.unmount();
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
it("reports each turn's timing without the words spoken", async () => {
|
|
478
|
+
const trackEvent = vi.fn();
|
|
479
|
+
const session = await startSession(undefined, undefined, trackEvent);
|
|
480
|
+
|
|
481
|
+
await session.send({ type: "input_audio_buffer.speech_started" });
|
|
482
|
+
await session.send({ type: "input_audio_buffer.speech_stopped" });
|
|
483
|
+
await session.send({ type: "output_audio_buffer.started" });
|
|
484
|
+
await session.send({
|
|
485
|
+
type: "response.output_audio_transcript.delta",
|
|
486
|
+
item_id: "assistant-1",
|
|
487
|
+
delta: "Opening the secret plans",
|
|
488
|
+
});
|
|
489
|
+
expect(trackEvent).not.toHaveBeenCalled();
|
|
490
|
+
await session.send({ type: "input_audio_buffer.speech_started" });
|
|
491
|
+
|
|
492
|
+
expect(trackEvent).toHaveBeenCalledTimes(1);
|
|
493
|
+
const [name, properties] = trackEvent.mock.calls[0]!;
|
|
494
|
+
expect(name).toBe("realtime_voice_turn");
|
|
495
|
+
expect(properties).toMatchObject({
|
|
496
|
+
turn_index: 0,
|
|
497
|
+
tool_calls: 0,
|
|
498
|
+
latency_clock: "browser",
|
|
499
|
+
spoke_again_after_edit: null,
|
|
500
|
+
});
|
|
501
|
+
expect(properties.speech_to_audio_ms).toEqual(expect.any(Number));
|
|
502
|
+
expect(JSON.stringify(properties)).not.toContain("secret");
|
|
503
|
+
session.unmount();
|
|
504
|
+
});
|
|
505
|
+
|
|
119
506
|
it("mutes the microphone track and the assistant's playback", async () => {
|
|
120
507
|
const session = await startSession();
|
|
121
508
|
expect(session.api().state).toBe("connecting");
|
|
@@ -199,6 +586,103 @@ describe("realtime voice session controls", () => {
|
|
|
199
586
|
session.unmount();
|
|
200
587
|
});
|
|
201
588
|
|
|
589
|
+
it("streams into the thread it was started for until that thread is shown", async () => {
|
|
590
|
+
const onScreen: RealtimeVoiceTranscriptMessage[] = [];
|
|
591
|
+
const unregisterOnScreen = realtimeVoiceTranscriptRegistry.register({
|
|
592
|
+
threadId: "thread-on-screen",
|
|
593
|
+
active: true,
|
|
594
|
+
append: (message) => {
|
|
595
|
+
onScreen.push(message);
|
|
596
|
+
return true;
|
|
597
|
+
},
|
|
598
|
+
});
|
|
599
|
+
const session = await startSession(undefined, {
|
|
600
|
+
threadId: "thread-handoff",
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
await session.send({
|
|
604
|
+
type: "conversation.item.input_audio_transcription.completed",
|
|
605
|
+
item_id: "user-handoff",
|
|
606
|
+
transcript: "Plan my week.",
|
|
607
|
+
});
|
|
608
|
+
expect(onScreen).toEqual([]);
|
|
609
|
+
|
|
610
|
+
const handedOff: RealtimeVoiceTranscriptMessage[] = [];
|
|
611
|
+
const unregisterHandoff = realtimeVoiceTranscriptRegistry.register({
|
|
612
|
+
threadId: "thread-handoff",
|
|
613
|
+
active: true,
|
|
614
|
+
append: (message) => {
|
|
615
|
+
handedOff.push(message);
|
|
616
|
+
return true;
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
expect(
|
|
621
|
+
handedOff.map(({ threadId, role, text, status }) => ({
|
|
622
|
+
threadId,
|
|
623
|
+
role,
|
|
624
|
+
text,
|
|
625
|
+
status,
|
|
626
|
+
})),
|
|
627
|
+
).toEqual([
|
|
628
|
+
{
|
|
629
|
+
threadId: "thread-handoff",
|
|
630
|
+
role: "user",
|
|
631
|
+
text: "Plan my week.",
|
|
632
|
+
status: "complete",
|
|
633
|
+
},
|
|
634
|
+
]);
|
|
635
|
+
unregisterHandoff();
|
|
636
|
+
unregisterOnScreen();
|
|
637
|
+
session.unmount();
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it("saves the app's default level as not chosen so Settings can show it", async () => {
|
|
641
|
+
vi.stubGlobal(
|
|
642
|
+
"RTCPeerConnection",
|
|
643
|
+
class extends FakePeerConnection {
|
|
644
|
+
override createOffer() {
|
|
645
|
+
return Promise.resolve({ type: "offer", sdp: "offer-sdp" });
|
|
646
|
+
}
|
|
647
|
+
setLocalDescription() {
|
|
648
|
+
return Promise.resolve();
|
|
649
|
+
}
|
|
650
|
+
setRemoteDescription() {
|
|
651
|
+
return Promise.resolve();
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
);
|
|
655
|
+
vi.stubGlobal(
|
|
656
|
+
"fetch",
|
|
657
|
+
vi.fn(
|
|
658
|
+
async () =>
|
|
659
|
+
new Response("answer-sdp", {
|
|
660
|
+
headers: {
|
|
661
|
+
"X-Agent-Native-Realtime-Protocol": "live",
|
|
662
|
+
"X-Agent-Native-Realtime-Intelligence": "balanced",
|
|
663
|
+
},
|
|
664
|
+
}),
|
|
665
|
+
),
|
|
666
|
+
);
|
|
667
|
+
const session = await startSession();
|
|
668
|
+
await act(async () => {
|
|
669
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
expect(session.api().preferences).toMatchObject({
|
|
673
|
+
intelligence: "balanced",
|
|
674
|
+
intelligenceChosen: false,
|
|
675
|
+
});
|
|
676
|
+
expect(setAppState).toHaveBeenCalledWith(
|
|
677
|
+
REALTIME_VOICE_PREFERENCES_KEY,
|
|
678
|
+
expect.objectContaining({
|
|
679
|
+
intelligence: "balanced",
|
|
680
|
+
intelligenceChosen: false,
|
|
681
|
+
}),
|
|
682
|
+
);
|
|
683
|
+
session.unmount();
|
|
684
|
+
});
|
|
685
|
+
|
|
202
686
|
it("starts from the voice chat choices saved in Settings", async () => {
|
|
203
687
|
window.localStorage.setItem(
|
|
204
688
|
"agent-native:realtime-voice-microphone",
|
|
@@ -216,6 +700,7 @@ describe("realtime voice session controls", () => {
|
|
|
216
700
|
language: "ja",
|
|
217
701
|
intelligence: "deep",
|
|
218
702
|
voice: "cedar",
|
|
703
|
+
intelligenceChosen: true,
|
|
219
704
|
});
|
|
220
705
|
expect(navigator.mediaDevices.getUserMedia).toHaveBeenCalledWith({
|
|
221
706
|
audio: expect.objectContaining({ deviceId: { ideal: "studio" } }),
|