refactor(voice): isolate gemini live provider core

This commit is contained in:
manjunath committed 2026-10-19 21:40:40 +05:30
commit 10916227cb
655 files changed
+45946 -26483

No files matched your search

+18 -39
View File
@@ -1,18 +1,11 @@
/**
* Protocol-independent voice lifetime and action dispatch.
* An adapter receives { emit, runAction, signal, providerDropdown } and supplies start, stop,
* An adapter receives { emit, runAction, signal } and supplies start, stop,
* sendText and sendMapEvent. It owns its microphone/connection and wire messages.
* Events: state, stop, transcript, action-call, progress, action-result,
* interruption, completion, turn-metrics. A stop event also
* covers teardown that preserves the adapter’s displayed status.
* providerDropdown() reads current trusted host capability for local refusals.
* Events: state, transcript, action-call, progress, action-result, interruption,
* completion, turn-metrics.
*/
export function createVoiceSession({
createAdapter,
runner,
signal,
providerDropdown = () => false,
}) {
export function createVoiceSession({ createAdapter, runner, signal }) {
if (typeof createAdapter !== 'function' || typeof runner !== 'function')
throw new TypeError('Voice requires an adapter factory and action runner');
const lifetime = new AbortController();
@@ -69,14 +62,6 @@ export function createVoiceSession({
!actionSignal.aborted &&
epoch === generation &&
(!options.isCurrent || options.isCurrent());
// Provider intent can revoke cooperative resources without erasing a
// result that committed before recognition. Session/turn revocation still
// owns the outer result and always suppresses stale completion.
const resourceSignal = options.providerPolicySignal
? AbortSignal.any([actionSignal, options.providerPolicySignal])
: actionSignal;
const resourceIsCurrent = () =>
isCurrent() && !options.providerPolicySignal?.aborted;
const callId = options.callId ?? null;
try {
if (!isCurrent()) throw abortError();
@@ -87,7 +72,7 @@ export function createVoiceSession({
// Step reports reach observers only while the action is current, so a
// cancelled tool cannot update the plan or start narration.
const progress = (update) => {
if (!resourceIsCurrent() || !update?.step) return;
if (!isCurrent() || !update?.step) return;
emit({
type: 'progress',
name,
@@ -99,9 +84,8 @@ export function createVoiceSession({
};
const result = await runner(name, args, {
...options,
providerDropdown: providerDropdown() === true,
signal: resourceSignal,
isCurrent: resourceIsCurrent,
signal: actionSignal,
isCurrent,
progress,
});
if (!isCurrent()) throw abortError();
@@ -134,12 +118,7 @@ export function createVoiceSession({
}
const abort = () => dispose();
try {
adapter = createAdapter({
emit,
runAction,
signal: lifetime.signal,
providerDropdown: () => !disposed && providerDropdown() === true,
});
adapter = createAdapter({ emit, runAction, signal: lifetime.signal });
for (const method of ['start', 'stop', 'sendText', 'sendMapEvent']) {
if (typeof adapter?.[method] !== 'function')
throw new TypeError('Voice adapter must implement ' + method);
@@ -168,8 +147,9 @@ export function createVoiceSession({
},
async start(options) {
if (disposed || !['idle', 'error'].includes(state)) return;
emit({ type: 'state', state: 'connecting' });
const attempt = ++startAttempt;
emit({ type: 'state', state: 'connecting' });
if (disposed || attempt !== startAttempt) return;
try {
await adapter.start(options);
} catch (error) {
@@ -185,19 +165,18 @@ export function createVoiceSession({
stop(options = {}) {
if (options.removeUi) return dispose();
if (disposed) return;
startAttempt++;
const attempt = ++startAttempt;
cancelActions();
try {
adapter.stop(options);
} finally {
if (options.preserveStatus)
emit({ type: 'stop', preserveStatus: true });
else
emit({
type: 'state',
state: 'idle',
detail: 'Voice off',
});
if (
!options.preserveStatus &&
!disposed &&
attempt === startAttempt &&
state !== 'idle'
)
emit({ type: 'state', state: 'idle', detail: 'Voice off' });
}
},
sendText: (text) => !disposed && adapter.sendText(text),