@agent-native/core 0.94.1 → 0.94.3
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/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/docs/content/cloneable-saas.mdx +15 -15
- package/corpus/core/docs/content/creating-templates.mdx +1 -1
- package/corpus/core/docs/content/extensions.mdx +1 -1
- package/corpus/core/docs/content/local-file-mode.mdx +6 -6
- package/corpus/core/docs/content/locales/ar-SA/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/de-DE/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/es-ES/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/fr-FR/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/hi-IN/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/ja-JP/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/ko-KR/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/pt-BR/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-CN/voice-input.mdx +2 -2
- package/corpus/core/docs/content/locales/zh-TW/voice-input.mdx +2 -2
- package/corpus/core/docs/content/sharing.mdx +4 -4
- package/corpus/core/docs/content/template-analytics.mdx +2 -2
- package/corpus/core/docs/content/template-calendar.mdx +4 -4
- package/corpus/core/docs/content/template-clips.mdx +2 -2
- package/corpus/core/docs/content/template-content.mdx +12 -11
- package/corpus/core/docs/content/template-mail.mdx +1 -1
- package/corpus/core/docs/content/template-slides.mdx +2 -2
- package/corpus/core/docs/content/voice-input.mdx +2 -2
- package/corpus/core/docs/content/workspace.mdx +1 -1
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/artifact-response.ts +154 -1
- package/corpus/core/src/client/AssistantChat.tsx +30 -0
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +19 -1
- package/corpus/core/src/client/agent-chat.ts +6 -0
- package/corpus/core/src/client/composer/RealtimeVoiceMode.tsx +72 -2
- package/corpus/core/src/client/composer/realtime-voice-audio-level.ts +64 -0
- package/corpus/core/src/client/composer/realtime-voice-transcript.ts +134 -0
- package/corpus/core/src/client/composer/useRealtimeVoiceMode.tsx +220 -6
- package/corpus/core/src/client/use-chat-threads.ts +33 -9
- package/corpus/core/src/integrations/a2a-continuation-processor.ts +118 -31
- package/corpus/core/src/integrations/a2a-continuations-store.ts +165 -7
- package/corpus/core/src/integrations/adapters/slack.ts +77 -7
- package/corpus/core/src/integrations/types.ts +30 -0
- package/corpus/core/src/integrations/webhook-handler.ts +54 -18
- package/corpus/core/src/scripts/call-agent.ts +1 -0
- package/corpus/core/src/server/request-context.ts +2 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
- package/corpus/templates/clips/app/components/player/comments-panel.tsx +98 -1
- package/corpus/templates/clips/changelog/2026-07-10-comment-urls-open-as-clickable-links.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-10-desktop-recording-uploads-now-show-a-brief-uploaded-state-wi.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-10-desktop-recordings-now-retry-saving-their-locally-captured-t.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-10-desktop-update-prompts-are-more-compact-and-easier-to-dismiss.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-10-quiet-recordings-now-finish-saving-instead-of-getting-stuck.md +6 -0
- package/corpus/templates/clips/desktop/src/components/UpdateBanner.tsx +7 -4
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +284 -85
- package/corpus/templates/clips/desktop/src/overlays/finalizing.tsx +148 -40
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +13 -13
- package/corpus/templates/clips/desktop/src/styles.css +83 -13
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +43 -28
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +130 -65
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +10 -4
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +82 -1
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +82 -1
- package/dist/a2a/artifact-response.d.ts.map +1 -1
- package/dist/a2a/artifact-response.js +127 -1
- package/dist/a2a/artifact-response.js.map +1 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +20 -0
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +9 -0
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat.d.ts +6 -0
- package/dist/client/agent-chat.d.ts.map +1 -1
- package/dist/client/agent-chat.js.map +1 -1
- package/dist/client/composer/RealtimeVoiceMode.d.ts +3 -1
- package/dist/client/composer/RealtimeVoiceMode.d.ts.map +1 -1
- package/dist/client/composer/RealtimeVoiceMode.js +32 -3
- package/dist/client/composer/RealtimeVoiceMode.js.map +1 -1
- package/dist/client/composer/realtime-voice-audio-level.d.ts +14 -0
- package/dist/client/composer/realtime-voice-audio-level.d.ts.map +1 -0
- package/dist/client/composer/realtime-voice-audio-level.js +47 -0
- package/dist/client/composer/realtime-voice-audio-level.js.map +1 -0
- package/dist/client/composer/realtime-voice-transcript.d.ts +26 -0
- package/dist/client/composer/realtime-voice-transcript.d.ts.map +1 -0
- package/dist/client/composer/realtime-voice-transcript.js +103 -0
- package/dist/client/composer/realtime-voice-transcript.js.map +1 -0
- package/dist/client/composer/useRealtimeVoiceMode.d.ts +14 -0
- package/dist/client/composer/useRealtimeVoiceMode.d.ts.map +1 -1
- package/dist/client/composer/useRealtimeVoiceMode.js +177 -6
- package/dist/client/composer/useRealtimeVoiceMode.js.map +1 -1
- package/dist/client/use-chat-threads.d.ts.map +1 -1
- package/dist/client/use-chat-threads.js +26 -6
- package/dist/client/use-chat-threads.js.map +1 -1
- package/dist/collab/routes.d.ts +1 -1
- package/dist/integrations/a2a-continuation-processor.d.ts.map +1 -1
- package/dist/integrations/a2a-continuation-processor.js +76 -21
- package/dist/integrations/a2a-continuation-processor.js.map +1 -1
- package/dist/integrations/a2a-continuations-store.d.ts +4 -1
- package/dist/integrations/a2a-continuations-store.d.ts.map +1 -1
- package/dist/integrations/a2a-continuations-store.js +130 -6
- package/dist/integrations/a2a-continuations-store.js.map +1 -1
- package/dist/integrations/adapters/slack.d.ts.map +1 -1
- package/dist/integrations/adapters/slack.js +56 -7
- package/dist/integrations/adapters/slack.js.map +1 -1
- package/dist/integrations/types.d.ts +25 -0
- package/dist/integrations/types.d.ts.map +1 -1
- package/dist/integrations/types.js.map +1 -1
- package/dist/integrations/webhook-handler.js +54 -16
- package/dist/integrations/webhook-handler.js.map +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +1 -0
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/request-context.d.ts +2 -0
- package/dist/server/request-context.d.ts.map +1 -1
- package/dist/server/request-context.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
- package/docs/content/cloneable-saas.mdx +15 -15
- package/docs/content/creating-templates.mdx +1 -1
- package/docs/content/extensions.mdx +1 -1
- package/docs/content/local-file-mode.mdx +6 -6
- package/docs/content/locales/ar-SA/voice-input.mdx +2 -2
- package/docs/content/locales/de-DE/voice-input.mdx +2 -2
- package/docs/content/locales/es-ES/voice-input.mdx +2 -2
- package/docs/content/locales/fr-FR/voice-input.mdx +2 -2
- package/docs/content/locales/hi-IN/voice-input.mdx +2 -2
- package/docs/content/locales/ja-JP/voice-input.mdx +2 -2
- package/docs/content/locales/ko-KR/voice-input.mdx +2 -2
- package/docs/content/locales/pt-BR/voice-input.mdx +2 -2
- package/docs/content/locales/zh-CN/voice-input.mdx +2 -2
- package/docs/content/locales/zh-TW/voice-input.mdx +2 -2
- package/docs/content/sharing.mdx +4 -4
- package/docs/content/template-analytics.mdx +2 -2
- package/docs/content/template-calendar.mdx +4 -4
- package/docs/content/template-clips.mdx +2 -2
- package/docs/content/template-content.mdx +12 -11
- package/docs/content/template-mail.mdx +1 -1
- package/docs/content/template-slides.mdx +2 -2
- package/docs/content/voice-input.mdx +2 -2
- package/docs/content/workspace.mdx +1 -1
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +11 -5
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
} from "react";
|
|
11
11
|
import { createPortal } from "react-dom";
|
|
12
12
|
|
|
13
|
+
import { requestAgentChatThreadOpen } from "../agent-chat.js";
|
|
13
14
|
import {
|
|
14
15
|
SIDEBAR_STATE_CHANGE_EVENT,
|
|
15
16
|
type AgentSidebarStateChangeDetail,
|
|
@@ -18,6 +19,13 @@ import { agentNativePath } from "../api-path.js";
|
|
|
18
19
|
import { readClientAppState, setClientAppState } from "../application-state.js";
|
|
19
20
|
import { getBrowserTabId } from "../browser-tab-id.js";
|
|
20
21
|
import { useT } from "../i18n.js";
|
|
22
|
+
import {
|
|
23
|
+
createRealtimeVoiceAudioLevelStore,
|
|
24
|
+
normalizeRealtimeVoiceRms,
|
|
25
|
+
smoothRealtimeVoiceLevel,
|
|
26
|
+
type RealtimeVoiceAudioLevelStore,
|
|
27
|
+
} from "./realtime-voice-audio-level.js";
|
|
28
|
+
import { realtimeVoiceTranscriptRegistry } from "./realtime-voice-transcript.js";
|
|
21
29
|
import {
|
|
22
30
|
RealtimeVoiceModeDock,
|
|
23
31
|
type RealtimeVoiceModeCopy,
|
|
@@ -43,11 +51,54 @@ export interface RealtimeVoiceModeApi {
|
|
|
43
51
|
active: boolean;
|
|
44
52
|
errorMessage: string | null;
|
|
45
53
|
chatVisible: boolean;
|
|
54
|
+
audioLevels: RealtimeVoiceAudioLevelStore;
|
|
46
55
|
start: () => Promise<void>;
|
|
47
56
|
end: () => void;
|
|
48
57
|
toggleChat: () => void;
|
|
49
58
|
}
|
|
50
59
|
|
|
60
|
+
export interface CompletedRealtimeVoiceTranscript {
|
|
61
|
+
role: "user" | "assistant";
|
|
62
|
+
text: string;
|
|
63
|
+
providerId?: string;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Voice mode owns the chat only temporarily. Restore the captured transcript
|
|
68
|
+
* when it is still the user's active thread (or the chat has no active thread)
|
|
69
|
+
* but never pull them back after they deliberately selected another thread.
|
|
70
|
+
*/
|
|
71
|
+
export function shouldRestoreRealtimeVoiceTranscriptThread(
|
|
72
|
+
transcriptThreadId: string | undefined,
|
|
73
|
+
activeThreadId: string | undefined,
|
|
74
|
+
): transcriptThreadId is string {
|
|
75
|
+
return Boolean(
|
|
76
|
+
transcriptThreadId &&
|
|
77
|
+
(!activeThreadId || activeThreadId === transcriptThreadId),
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function extractCompletedRealtimeVoiceTranscript(
|
|
82
|
+
event: RealtimeServerEvent,
|
|
83
|
+
): CompletedRealtimeVoiceTranscript | null {
|
|
84
|
+
const userCompleted =
|
|
85
|
+
event.type === "conversation.item.input_audio_transcription.completed";
|
|
86
|
+
const assistantCompleted =
|
|
87
|
+
event.type === "response.output_audio_transcript.done";
|
|
88
|
+
if (!userCompleted && !assistantCompleted) return null;
|
|
89
|
+
const text =
|
|
90
|
+
typeof event.transcript === "string" ? event.transcript.trim() : "";
|
|
91
|
+
if (!text) return null;
|
|
92
|
+
const providerId = [event.item_id, event.response_id, event.event_id].find(
|
|
93
|
+
(value): value is string => typeof value === "string" && value.length > 0,
|
|
94
|
+
);
|
|
95
|
+
return {
|
|
96
|
+
role: userCompleted ? "user" : "assistant",
|
|
97
|
+
text,
|
|
98
|
+
...(providerId ? { providerId } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
51
102
|
export interface RealtimeVoiceModeProviderProps {
|
|
52
103
|
children: ReactNode;
|
|
53
104
|
browserTabId?: string;
|
|
@@ -232,17 +283,29 @@ function useRealtimeVoiceModeController(
|
|
|
232
283
|
const [state, setState] = useState<"idle" | RealtimeVoiceModeState>("idle");
|
|
233
284
|
const [error, setError] = useState<string | null>(null);
|
|
234
285
|
const [chatVisible, setChatVisible] = useState(false);
|
|
286
|
+
const [audioLevels] = useState(createRealtimeVoiceAudioLevelStore);
|
|
235
287
|
const stateRef = useRef(state);
|
|
236
288
|
const peerRef = useRef<RTCPeerConnection | null>(null);
|
|
237
289
|
const channelRef = useRef<RTCDataChannel | null>(null);
|
|
238
290
|
const streamRef = useRef<MediaStream | null>(null);
|
|
239
291
|
const audioRef = useRef<HTMLAudioElement | null>(null);
|
|
292
|
+
const audioContextRef = useRef<AudioContext | null>(null);
|
|
293
|
+
const inputAnalyserRef = useRef<AnalyserNode | null>(null);
|
|
294
|
+
const outputAnalyserRef = useRef<AnalyserNode | null>(null);
|
|
295
|
+
const inputMeterBufferRef = useRef<Uint8Array<ArrayBuffer> | null>(null);
|
|
296
|
+
const outputMeterBufferRef = useRef<Uint8Array<ArrayBuffer> | null>(null);
|
|
297
|
+
const inputSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
|
|
298
|
+
const outputSourceRef = useRef<MediaStreamAudioSourceNode | null>(null);
|
|
299
|
+
const meterFrameRef = useRef<number | null>(null);
|
|
300
|
+
const lastMeterSampleRef = useRef(0);
|
|
240
301
|
const abortRef = useRef<AbortController | null>(null);
|
|
241
302
|
const handledCallsRef = useRef(new Set<string>());
|
|
242
303
|
const sessionIdRef = useRef<string | undefined>(undefined);
|
|
243
304
|
const startedAtRef = useRef<string | undefined>(undefined);
|
|
244
305
|
const lastUserTextRef = useRef("");
|
|
245
306
|
const lastAssistantTextRef = useRef("");
|
|
307
|
+
const transcriptThreadIdRef = useRef<string | undefined>(undefined);
|
|
308
|
+
const transcriptSequenceRef = useRef(0);
|
|
246
309
|
|
|
247
310
|
useEffect(() => {
|
|
248
311
|
stateRef.current = state;
|
|
@@ -279,6 +342,77 @@ function useRealtimeVoiceModeController(
|
|
|
279
342
|
[syncAppState],
|
|
280
343
|
);
|
|
281
344
|
|
|
345
|
+
const startMeterLoop = useCallback(() => {
|
|
346
|
+
if (meterFrameRef.current !== null) return;
|
|
347
|
+
const sample = (timestamp: number) => {
|
|
348
|
+
meterFrameRef.current = requestAnimationFrame(sample);
|
|
349
|
+
if (timestamp - lastMeterSampleRef.current < 50) return;
|
|
350
|
+
lastMeterSampleRef.current = timestamp;
|
|
351
|
+
|
|
352
|
+
const current = audioLevels.getSnapshot();
|
|
353
|
+
let input = current.input;
|
|
354
|
+
let output = current.output;
|
|
355
|
+
const inputAnalyser = inputAnalyserRef.current;
|
|
356
|
+
const inputBuffer = inputMeterBufferRef.current;
|
|
357
|
+
if (inputAnalyser && inputBuffer) {
|
|
358
|
+
inputAnalyser.getByteTimeDomainData(inputBuffer);
|
|
359
|
+
input = smoothRealtimeVoiceLevel(
|
|
360
|
+
input,
|
|
361
|
+
normalizeRealtimeVoiceRms(inputBuffer),
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const outputAnalyser = outputAnalyserRef.current;
|
|
365
|
+
const outputBuffer = outputMeterBufferRef.current;
|
|
366
|
+
if (outputAnalyser && outputBuffer) {
|
|
367
|
+
outputAnalyser.getByteTimeDomainData(outputBuffer);
|
|
368
|
+
output = smoothRealtimeVoiceLevel(
|
|
369
|
+
output,
|
|
370
|
+
normalizeRealtimeVoiceRms(outputBuffer),
|
|
371
|
+
);
|
|
372
|
+
}
|
|
373
|
+
audioLevels.set({ input, output });
|
|
374
|
+
};
|
|
375
|
+
meterFrameRef.current = requestAnimationFrame(sample);
|
|
376
|
+
}, [audioLevels]);
|
|
377
|
+
|
|
378
|
+
const attachAudioMeter = useCallback(
|
|
379
|
+
(stream: MediaStream, channel: "input" | "output") => {
|
|
380
|
+
try {
|
|
381
|
+
const AudioCtor =
|
|
382
|
+
window.AudioContext ??
|
|
383
|
+
(
|
|
384
|
+
window as typeof window & {
|
|
385
|
+
webkitAudioContext?: typeof AudioContext;
|
|
386
|
+
}
|
|
387
|
+
).webkitAudioContext;
|
|
388
|
+
if (!AudioCtor) return;
|
|
389
|
+
const context = audioContextRef.current ?? new AudioCtor();
|
|
390
|
+
audioContextRef.current = context;
|
|
391
|
+
void context.resume().catch(() => undefined);
|
|
392
|
+
const analyser = context.createAnalyser();
|
|
393
|
+
analyser.fftSize = 512;
|
|
394
|
+
const source = context.createMediaStreamSource(stream);
|
|
395
|
+
source.connect(analyser);
|
|
396
|
+
const buffer = new Uint8Array(analyser.frequencyBinCount);
|
|
397
|
+
if (channel === "input") {
|
|
398
|
+
inputSourceRef.current?.disconnect();
|
|
399
|
+
inputSourceRef.current = source;
|
|
400
|
+
inputAnalyserRef.current = analyser;
|
|
401
|
+
inputMeterBufferRef.current = buffer;
|
|
402
|
+
} else {
|
|
403
|
+
outputSourceRef.current?.disconnect();
|
|
404
|
+
outputSourceRef.current = source;
|
|
405
|
+
outputAnalyserRef.current = analyser;
|
|
406
|
+
outputMeterBufferRef.current = buffer;
|
|
407
|
+
}
|
|
408
|
+
startMeterLoop();
|
|
409
|
+
} catch {
|
|
410
|
+
// Audio metering is visual-only; keep the realtime call healthy.
|
|
411
|
+
}
|
|
412
|
+
},
|
|
413
|
+
[startMeterLoop],
|
|
414
|
+
);
|
|
415
|
+
|
|
282
416
|
const cleanupTransport = useCallback(() => {
|
|
283
417
|
abortRef.current?.abort();
|
|
284
418
|
abortRef.current = null;
|
|
@@ -293,8 +427,25 @@ function useRealtimeVoiceModeController(
|
|
|
293
427
|
audioRef.current.srcObject = null;
|
|
294
428
|
}
|
|
295
429
|
audioRef.current = null;
|
|
430
|
+
if (meterFrameRef.current !== null) {
|
|
431
|
+
cancelAnimationFrame(meterFrameRef.current);
|
|
432
|
+
meterFrameRef.current = null;
|
|
433
|
+
}
|
|
434
|
+
inputSourceRef.current?.disconnect();
|
|
435
|
+
outputSourceRef.current?.disconnect();
|
|
436
|
+
inputSourceRef.current = null;
|
|
437
|
+
outputSourceRef.current = null;
|
|
438
|
+
inputAnalyserRef.current = null;
|
|
439
|
+
outputAnalyserRef.current = null;
|
|
440
|
+
inputMeterBufferRef.current = null;
|
|
441
|
+
outputMeterBufferRef.current = null;
|
|
442
|
+
lastMeterSampleRef.current = 0;
|
|
443
|
+
const audioContext = audioContextRef.current;
|
|
444
|
+
audioContextRef.current = null;
|
|
445
|
+
if (audioContext) void audioContext.close().catch(() => undefined);
|
|
446
|
+
audioLevels.reset();
|
|
296
447
|
handledCallsRef.current.clear();
|
|
297
|
-
}, []);
|
|
448
|
+
}, [audioLevels]);
|
|
298
449
|
|
|
299
450
|
const fail = useCallback(
|
|
300
451
|
(message: string, options?: { openKeySettings?: boolean }) => {
|
|
@@ -342,6 +493,26 @@ function useRealtimeVoiceModeController(
|
|
|
342
493
|
[browserTabId, transition],
|
|
343
494
|
);
|
|
344
495
|
|
|
496
|
+
const persistCompletedTranscript = useCallback(
|
|
497
|
+
(event: RealtimeServerEvent) => {
|
|
498
|
+
const transcript = extractCompletedRealtimeVoiceTranscript(event);
|
|
499
|
+
const threadId = transcriptThreadIdRef.current;
|
|
500
|
+
if (!transcript || !threadId) return;
|
|
501
|
+
const sessionIdentity =
|
|
502
|
+
sessionIdRef.current ?? startedAtRef.current ?? "pending";
|
|
503
|
+
const providerIdentity =
|
|
504
|
+
transcript.providerId ?? `sequence-${++transcriptSequenceRef.current}`;
|
|
505
|
+
realtimeVoiceTranscriptRegistry.publish({
|
|
506
|
+
id: `realtime-voice:${sessionIdentity}:${transcript.role}:${providerIdentity}`,
|
|
507
|
+
threadId,
|
|
508
|
+
role: transcript.role,
|
|
509
|
+
text: transcript.text,
|
|
510
|
+
createdAt: new Date().toISOString(),
|
|
511
|
+
});
|
|
512
|
+
},
|
|
513
|
+
[],
|
|
514
|
+
);
|
|
515
|
+
|
|
345
516
|
const handleServerEvent = useCallback(
|
|
346
517
|
(event: RealtimeServerEvent) => {
|
|
347
518
|
if (event.type === "session.created") {
|
|
@@ -367,14 +538,16 @@ function useRealtimeVoiceModeController(
|
|
|
367
538
|
if (typeof event.transcript === "string") {
|
|
368
539
|
lastAssistantTextRef.current = event.transcript;
|
|
369
540
|
}
|
|
541
|
+
persistCompletedTranscript(event);
|
|
370
542
|
syncAppState("speaking");
|
|
371
543
|
} else if (
|
|
372
544
|
event.type === "conversation.item.input_audio_transcription.completed"
|
|
373
545
|
) {
|
|
374
546
|
if (typeof event.transcript === "string") {
|
|
375
547
|
lastUserTextRef.current = event.transcript;
|
|
376
|
-
syncAppState("working");
|
|
377
548
|
}
|
|
549
|
+
persistCompletedTranscript(event);
|
|
550
|
+
syncAppState("working");
|
|
378
551
|
} else if (event.type === "response.done") {
|
|
379
552
|
const response = event.response;
|
|
380
553
|
const status =
|
|
@@ -410,7 +583,14 @@ function useRealtimeVoiceModeController(
|
|
|
410
583
|
transition("listening");
|
|
411
584
|
}
|
|
412
585
|
},
|
|
413
|
-
[
|
|
586
|
+
[
|
|
587
|
+
copy,
|
|
588
|
+
fail,
|
|
589
|
+
handleFunctionCall,
|
|
590
|
+
persistCompletedTranscript,
|
|
591
|
+
syncAppState,
|
|
592
|
+
transition,
|
|
593
|
+
],
|
|
414
594
|
);
|
|
415
595
|
|
|
416
596
|
const start = useCallback(async () => {
|
|
@@ -430,6 +610,9 @@ function useRealtimeVoiceModeController(
|
|
|
430
610
|
lastUserTextRef.current = "";
|
|
431
611
|
lastAssistantTextRef.current = "";
|
|
432
612
|
sessionIdRef.current = undefined;
|
|
613
|
+
transcriptThreadIdRef.current =
|
|
614
|
+
realtimeVoiceTranscriptRegistry.activeThreadId();
|
|
615
|
+
transcriptSequenceRef.current = 0;
|
|
433
616
|
transition("connecting");
|
|
434
617
|
setChatVisible(false);
|
|
435
618
|
window.dispatchEvent(new Event("agent-panel:close"));
|
|
@@ -445,6 +628,7 @@ function useRealtimeVoiceModeController(
|
|
|
445
628
|
},
|
|
446
629
|
});
|
|
447
630
|
streamRef.current = stream;
|
|
631
|
+
attachAudioMeter(stream, "input");
|
|
448
632
|
|
|
449
633
|
const peer = new RTCPeerConnection();
|
|
450
634
|
peerRef.current = peer;
|
|
@@ -455,7 +639,9 @@ function useRealtimeVoiceModeController(
|
|
|
455
639
|
audio.setAttribute("playsinline", "");
|
|
456
640
|
audioRef.current = audio;
|
|
457
641
|
peer.ontrack = (trackEvent) => {
|
|
458
|
-
|
|
642
|
+
const remoteStream = trackEvent.streams[0] ?? null;
|
|
643
|
+
audio.srcObject = remoteStream;
|
|
644
|
+
if (remoteStream) attachAudioMeter(remoteStream, "output");
|
|
459
645
|
void audio.play().catch(() => undefined);
|
|
460
646
|
};
|
|
461
647
|
|
|
@@ -501,15 +687,42 @@ function useRealtimeVoiceModeController(
|
|
|
501
687
|
const status = (startError as { status?: unknown })?.status;
|
|
502
688
|
fail(errorMessage(startError), { openKeySettings: status === 400 });
|
|
503
689
|
}
|
|
504
|
-
}, [
|
|
690
|
+
}, [
|
|
691
|
+
attachAudioMeter,
|
|
692
|
+
browserTabId,
|
|
693
|
+
copy,
|
|
694
|
+
fail,
|
|
695
|
+
handleServerEvent,
|
|
696
|
+
transition,
|
|
697
|
+
]);
|
|
505
698
|
|
|
506
699
|
const end = useCallback(() => {
|
|
507
700
|
if (stateRef.current === "idle" || stateRef.current === "ending") return;
|
|
701
|
+
const transcriptThreadId = transcriptThreadIdRef.current;
|
|
702
|
+
const activeThreadId = realtimeVoiceTranscriptRegistry.activeThreadId();
|
|
508
703
|
transition("ending");
|
|
509
704
|
cleanupTransport();
|
|
510
705
|
setError(null);
|
|
511
706
|
sessionIdRef.current = undefined;
|
|
512
707
|
startedAtRef.current = undefined;
|
|
708
|
+
transcriptThreadIdRef.current = undefined;
|
|
709
|
+
setChatVisible(true);
|
|
710
|
+
if (
|
|
711
|
+
shouldRestoreRealtimeVoiceTranscriptThread(
|
|
712
|
+
transcriptThreadId,
|
|
713
|
+
activeThreadId,
|
|
714
|
+
)
|
|
715
|
+
) {
|
|
716
|
+
requestAgentChatThreadOpen({
|
|
717
|
+
threadId: transcriptThreadId,
|
|
718
|
+
// The request is delivered asynchronously. Re-checking this at the
|
|
719
|
+
// receiver prevents a navigation that happened during that gap from
|
|
720
|
+
// being overwritten.
|
|
721
|
+
onlyIfActiveThreadId: transcriptThreadId,
|
|
722
|
+
});
|
|
723
|
+
} else {
|
|
724
|
+
window.dispatchEvent(new Event("agent-panel:open"));
|
|
725
|
+
}
|
|
513
726
|
transition("idle");
|
|
514
727
|
}, [cleanupTransport, transition]);
|
|
515
728
|
|
|
@@ -538,6 +751,7 @@ function useRealtimeVoiceModeController(
|
|
|
538
751
|
active: state !== "idle",
|
|
539
752
|
errorMessage: error,
|
|
540
753
|
chatVisible,
|
|
754
|
+
audioLevels,
|
|
541
755
|
start,
|
|
542
756
|
end,
|
|
543
757
|
toggleChat,
|
|
@@ -568,10 +782,10 @@ export function RealtimeVoiceModeProvider({
|
|
|
568
782
|
state={voice.state === "idle" ? "ending" : voice.state}
|
|
569
783
|
copy={copy}
|
|
570
784
|
chatVisible={voice.chatVisible}
|
|
785
|
+
audioLevels={voice.audioLevels}
|
|
571
786
|
onToggleChat={voice.toggleChat}
|
|
572
787
|
onEndVoiceMode={voice.end}
|
|
573
788
|
errorMessage={voice.errorMessage}
|
|
574
|
-
className="z-[270]"
|
|
575
789
|
/>,
|
|
576
790
|
document.body,
|
|
577
791
|
)
|
|
@@ -939,15 +939,39 @@ export function useChatThreads(
|
|
|
939
939
|
titleSource,
|
|
940
940
|
{ preserveUserTitle },
|
|
941
941
|
);
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
942
|
+
const payload = {
|
|
943
|
+
...threadDataPayload,
|
|
944
|
+
title,
|
|
945
|
+
scope: localScope,
|
|
946
|
+
};
|
|
947
|
+
let response = await fetch(
|
|
948
|
+
`${apiUrl}/threads/${encodeURIComponent(id)}`,
|
|
949
|
+
{
|
|
950
|
+
method: "PUT",
|
|
951
|
+
headers: { "Content-Type": "application/json" },
|
|
952
|
+
body: JSON.stringify(payload),
|
|
953
|
+
},
|
|
954
|
+
);
|
|
955
|
+
// A passive realtime-voice transcript can be the first content in a
|
|
956
|
+
// client-created thread, so no agent run has created its SQL row yet.
|
|
957
|
+
// Materialize that row idempotently and retry the same full save.
|
|
958
|
+
if (response.status === 404) {
|
|
959
|
+
const created = await fetch(`${apiUrl}/threads`, {
|
|
960
|
+
method: "POST",
|
|
961
|
+
headers: { "Content-Type": "application/json" },
|
|
962
|
+
body: JSON.stringify({ id, title, scope: localScope }),
|
|
963
|
+
});
|
|
964
|
+
if (!created.ok) return;
|
|
965
|
+
response = await fetch(
|
|
966
|
+
`${apiUrl}/threads/${encodeURIComponent(id)}`,
|
|
967
|
+
{
|
|
968
|
+
method: "PUT",
|
|
969
|
+
headers: { "Content-Type": "application/json" },
|
|
970
|
+
body: JSON.stringify(payload),
|
|
971
|
+
},
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
if (!response.ok) return;
|
|
951
975
|
emitThreadsUpdated();
|
|
952
976
|
// Update local thread list metadata. If the thread isn't in our
|
|
953
977
|
// local list yet (an optimistic-only thread that the server just
|
|
@@ -18,7 +18,11 @@ import {
|
|
|
18
18
|
type A2AContinuation,
|
|
19
19
|
} from "./a2a-continuations-store.js";
|
|
20
20
|
import { signInternalToken } from "./internal-token.js";
|
|
21
|
-
import type {
|
|
21
|
+
import type {
|
|
22
|
+
OutgoingMessage,
|
|
23
|
+
PlatformAdapter,
|
|
24
|
+
PlatformRunProgress,
|
|
25
|
+
} from "./types.js";
|
|
22
26
|
|
|
23
27
|
const PROCESSOR_PATH = `${FRAMEWORK_ROUTE_PREFIX}/integrations/process-a2a-continuation`;
|
|
24
28
|
const TERMINAL_STATES = new Set(["completed", "failed", "canceled"]);
|
|
@@ -150,6 +154,8 @@ async function processClaimedContinuation(
|
|
|
150
154
|
return;
|
|
151
155
|
}
|
|
152
156
|
|
|
157
|
+
const progress = await resumeA2AContinuationProgress(continuation, adapter);
|
|
158
|
+
|
|
153
159
|
const auth = await signContinuationToken(continuation);
|
|
154
160
|
const client = new A2AClient(continuation.agentUrl, auth.apiKey, {
|
|
155
161
|
requestTimeoutMs: POLL_REQUEST_TIMEOUT_MS,
|
|
@@ -162,6 +168,7 @@ async function processClaimedContinuation(
|
|
|
162
168
|
while (Date.now() < deadline) {
|
|
163
169
|
task = await client.getTask(continuation.a2aTaskId);
|
|
164
170
|
if (TERMINAL_STATES.has(task.status.state)) break;
|
|
171
|
+
await reportA2AContinuationProgress(continuation, progress, task);
|
|
165
172
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
166
173
|
}
|
|
167
174
|
} catch (err) {
|
|
@@ -171,6 +178,7 @@ async function processClaimedContinuation(
|
|
|
171
178
|
continuation,
|
|
172
179
|
adapter,
|
|
173
180
|
remotePollFailureReason(continuation),
|
|
181
|
+
progress,
|
|
174
182
|
);
|
|
175
183
|
return;
|
|
176
184
|
}
|
|
@@ -182,6 +190,7 @@ async function processClaimedContinuation(
|
|
|
182
190
|
continuation,
|
|
183
191
|
adapter,
|
|
184
192
|
err instanceof Error ? err.message : String(err),
|
|
193
|
+
progress,
|
|
185
194
|
);
|
|
186
195
|
return;
|
|
187
196
|
}
|
|
@@ -190,24 +199,12 @@ async function processClaimedContinuation(
|
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
if (!task || !TERMINAL_STATES.has(task.status.state)) {
|
|
193
|
-
const recoverableArtifactText = extractRecoverableArtifactText(task);
|
|
194
|
-
if (recoverableArtifactText) {
|
|
195
|
-
await deliverAndCompleteA2AContinuation(
|
|
196
|
-
continuation,
|
|
197
|
-
adapter,
|
|
198
|
-
formatContinuationArtifactText(
|
|
199
|
-
recoverableArtifactText,
|
|
200
|
-
continuation.agentUrl,
|
|
201
|
-
),
|
|
202
|
-
);
|
|
203
|
-
return;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
202
|
if (shouldStopPollingRemoteTask(continuation)) {
|
|
207
203
|
await notifyAndFailA2AContinuation(
|
|
208
204
|
continuation,
|
|
209
205
|
adapter,
|
|
210
206
|
remotePollFailureReason(continuation),
|
|
207
|
+
progress,
|
|
211
208
|
);
|
|
212
209
|
return;
|
|
213
210
|
}
|
|
@@ -219,7 +216,7 @@ async function processClaimedContinuation(
|
|
|
219
216
|
const reason =
|
|
220
217
|
extractTaskText(task) ||
|
|
221
218
|
`Remote A2A task ${continuation.a2aTaskId} ended with state ${task.status.state}`;
|
|
222
|
-
await notifyAndFailA2AContinuation(continuation, adapter, reason);
|
|
219
|
+
await notifyAndFailA2AContinuation(continuation, adapter, reason, progress);
|
|
223
220
|
return;
|
|
224
221
|
}
|
|
225
222
|
|
|
@@ -232,11 +229,64 @@ async function processClaimedContinuation(
|
|
|
232
229
|
continuation,
|
|
233
230
|
adapter,
|
|
234
231
|
`Remote A2A task ${continuation.a2aTaskId} completed without text`,
|
|
232
|
+
progress,
|
|
235
233
|
);
|
|
236
234
|
return;
|
|
237
235
|
}
|
|
238
236
|
|
|
239
|
-
await deliverAndCompleteA2AContinuation(
|
|
237
|
+
await deliverAndCompleteA2AContinuation(
|
|
238
|
+
continuation,
|
|
239
|
+
adapter,
|
|
240
|
+
text,
|
|
241
|
+
progress,
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function resumeA2AContinuationProgress(
|
|
246
|
+
continuation: A2AContinuation,
|
|
247
|
+
adapter: PlatformAdapter,
|
|
248
|
+
): Promise<PlatformRunProgress | null> {
|
|
249
|
+
if (!continuation.progressRef || !adapter.resumeRunProgress) return null;
|
|
250
|
+
try {
|
|
251
|
+
const progress = await adapter.resumeRunProgress(
|
|
252
|
+
continuation.incoming,
|
|
253
|
+
continuation.progressRef,
|
|
254
|
+
);
|
|
255
|
+
if (!progress) return null;
|
|
256
|
+
await progress.onEvent({
|
|
257
|
+
type: "agent_call_progress",
|
|
258
|
+
agent: continuation.agentName,
|
|
259
|
+
state: "working",
|
|
260
|
+
elapsedSeconds: Math.max(
|
|
261
|
+
0,
|
|
262
|
+
Math.round((Date.now() - continuation.createdAt) / 1_000),
|
|
263
|
+
),
|
|
264
|
+
detail: "Continuing in the background",
|
|
265
|
+
});
|
|
266
|
+
return progress;
|
|
267
|
+
} catch {
|
|
268
|
+
// A continuation still has a normal reply fallback. Do not log the
|
|
269
|
+
// opaque provider reference or the inbound message payload.
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function reportA2AContinuationProgress(
|
|
275
|
+
continuation: A2AContinuation,
|
|
276
|
+
progress: PlatformRunProgress | null,
|
|
277
|
+
task: Task,
|
|
278
|
+
): Promise<void> {
|
|
279
|
+
if (!progress) return;
|
|
280
|
+
await progress.onEvent({
|
|
281
|
+
type: "agent_call_progress",
|
|
282
|
+
agent: continuation.agentName,
|
|
283
|
+
state: task.status.state,
|
|
284
|
+
elapsedSeconds: Math.max(
|
|
285
|
+
0,
|
|
286
|
+
Math.round((Date.now() - continuation.createdAt) / 1_000),
|
|
287
|
+
),
|
|
288
|
+
detail: "Still working on the delegated request",
|
|
289
|
+
});
|
|
240
290
|
}
|
|
241
291
|
|
|
242
292
|
async function waitForContinuationDue(
|
|
@@ -262,6 +312,7 @@ async function notifyAndFailA2AContinuation(
|
|
|
262
312
|
continuation: A2AContinuation,
|
|
263
313
|
adapter: PlatformAdapter,
|
|
264
314
|
reason: string,
|
|
315
|
+
progress: PlatformRunProgress | null = null,
|
|
265
316
|
): Promise<void> {
|
|
266
317
|
const deliveryContinuation = await claimA2AContinuationDelivery(
|
|
267
318
|
continuation.id,
|
|
@@ -273,11 +324,14 @@ async function notifyAndFailA2AContinuation(
|
|
|
273
324
|
reason,
|
|
274
325
|
);
|
|
275
326
|
try {
|
|
327
|
+
const outgoing = adapter.formatAgentResponse(message);
|
|
276
328
|
await withTimeout(
|
|
277
|
-
|
|
278
|
-
adapter
|
|
279
|
-
deliveryContinuation
|
|
280
|
-
|
|
329
|
+
deliverA2AContinuationResponse(
|
|
330
|
+
adapter,
|
|
331
|
+
deliveryContinuation,
|
|
332
|
+
outgoing,
|
|
333
|
+
progress,
|
|
334
|
+
"error",
|
|
281
335
|
),
|
|
282
336
|
PLATFORM_SEND_TIMEOUT_MS,
|
|
283
337
|
`${deliveryContinuation.platform} failure notification timed out`,
|
|
@@ -296,6 +350,7 @@ async function deliverAndCompleteA2AContinuation(
|
|
|
296
350
|
continuation: A2AContinuation,
|
|
297
351
|
adapter: PlatformAdapter,
|
|
298
352
|
text: string,
|
|
353
|
+
progress: PlatformRunProgress | null = null,
|
|
299
354
|
): Promise<void> {
|
|
300
355
|
const deliveryContinuation = await claimA2AContinuationDelivery(
|
|
301
356
|
continuation.id,
|
|
@@ -303,11 +358,14 @@ async function deliverAndCompleteA2AContinuation(
|
|
|
303
358
|
if (!deliveryContinuation) return;
|
|
304
359
|
|
|
305
360
|
try {
|
|
361
|
+
const outgoing = adapter.formatAgentResponse(text);
|
|
306
362
|
await withTimeout(
|
|
307
|
-
|
|
308
|
-
adapter
|
|
309
|
-
deliveryContinuation
|
|
310
|
-
|
|
363
|
+
deliverA2AContinuationResponse(
|
|
364
|
+
adapter,
|
|
365
|
+
deliveryContinuation,
|
|
366
|
+
outgoing,
|
|
367
|
+
progress,
|
|
368
|
+
"done",
|
|
311
369
|
),
|
|
312
370
|
PLATFORM_SEND_TIMEOUT_MS,
|
|
313
371
|
`${deliveryContinuation.platform} response delivery timed out`,
|
|
@@ -327,6 +385,42 @@ async function deliverAndCompleteA2AContinuation(
|
|
|
327
385
|
await completeAfterSuccessfulDelivery(deliveryContinuation);
|
|
328
386
|
}
|
|
329
387
|
|
|
388
|
+
async function deliverA2AContinuationResponse(
|
|
389
|
+
adapter: PlatformAdapter,
|
|
390
|
+
continuation: A2AContinuation,
|
|
391
|
+
message: OutgoingMessage,
|
|
392
|
+
progress: PlatformRunProgress | null,
|
|
393
|
+
status: "done" | "error",
|
|
394
|
+
): Promise<void> {
|
|
395
|
+
if (progress) {
|
|
396
|
+
try {
|
|
397
|
+
await progress.onEvent({
|
|
398
|
+
type: "agent_call",
|
|
399
|
+
agent: continuation.agentName,
|
|
400
|
+
status,
|
|
401
|
+
});
|
|
402
|
+
await progress.complete(message);
|
|
403
|
+
return;
|
|
404
|
+
} catch {
|
|
405
|
+
// A resumed Slack stream can no longer be finalized (for example when
|
|
406
|
+
// chat.stopStream rejects). Preserve the final answer with the same
|
|
407
|
+
// thread reply fallback used by the initial webhook run. Also ask the
|
|
408
|
+
// adapter to terminate the native stream: otherwise Slack can keep the
|
|
409
|
+
// task card in its working state after the thread fallback succeeds.
|
|
410
|
+
try {
|
|
411
|
+
await progress.fail?.(
|
|
412
|
+
"I couldn't update the live response, but I posted the final result in this thread.",
|
|
413
|
+
);
|
|
414
|
+
} catch {
|
|
415
|
+
// The thread reply below is still the authoritative final answer.
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
await adapter.sendResponse(message, continuation.incoming, {
|
|
420
|
+
placeholderRef: continuation.placeholderRef ?? undefined,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
|
|
330
424
|
async function rescheduleAndRedispatchA2AContinuation(
|
|
331
425
|
continuationId: string,
|
|
332
426
|
): Promise<void> {
|
|
@@ -524,13 +618,6 @@ function extractTaskText(task: Task): string {
|
|
|
524
618
|
.join("\n");
|
|
525
619
|
}
|
|
526
620
|
|
|
527
|
-
function extractRecoverableArtifactText(task: Task | null): string {
|
|
528
|
-
if (!task?.status.message?.metadata?.agentNativeRecoverableArtifacts) {
|
|
529
|
-
return "";
|
|
530
|
-
}
|
|
531
|
-
return extractTaskText(task);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
621
|
function formatContinuationArtifactText(
|
|
535
622
|
text: string,
|
|
536
623
|
agentUrl: string,
|