11import { randomUUID } from "node:crypto" ;
2- import {
2+ import type {
33 ActivityHandling ,
44 Behavior ,
55 EndSensitivity ,
6+ FunctionDeclaration ,
7+ FunctionResponse ,
68 FunctionResponseScheduling ,
9+ LiveConnectConfig ,
10+ LiveServerContent ,
11+ LiveServerMessage ,
12+ LiveServerToolCall ,
713 Modality ,
14+ RealtimeInputConfig ,
815 StartSensitivity ,
16+ ThinkingConfig ,
917 TurnCoverage ,
10- type FunctionDeclaration ,
11- type FunctionResponse ,
12- type LiveConnectConfig ,
13- type LiveServerContent ,
14- type LiveServerMessage ,
15- type LiveServerToolCall ,
16- type RealtimeInputConfig ,
17- type ThinkingConfig ,
1818} from "@google/genai" ;
1919import type { OpenClawConfig } from "openclaw/plugin-sdk/provider-onboard" ;
2020import type {
@@ -47,7 +47,7 @@ const GOOGLE_REALTIME_BROWSER_API_VERSION = "v1alpha";
4747const GOOGLE_REALTIME_BROWSER_WEBSOCKET_URL =
4848 "wss://generativelanguage.googleapis.com/ws/google.ai.generativelanguage.v1alpha.GenerativeService.BidiGenerateContentConstrained" ;
4949const MAX_PENDING_AUDIO_CHUNKS = 320 ;
50- const DEFAULT_AUDIO_STREAM_END_SILENCE_MS = 700 ;
50+ const DEFAULT_AUDIO_STREAM_END_SILENCE_MS = 500 ;
5151const GOOGLE_REALTIME_BROWSER_SESSION_TTL_MS = 30 * 60 * 1000 ;
5252const GOOGLE_REALTIME_BROWSER_NEW_SESSION_TTL_MS = 60 * 1000 ;
5353
@@ -70,6 +70,8 @@ type GoogleRealtimeVoiceProviderConfig = {
7070 turnCoverage ?: GoogleRealtimeTurnCoverage ;
7171 automaticActivityDetectionDisabled ?: boolean ;
7272 enableAffectiveDialog ?: boolean ;
73+ sessionResumption ?: boolean ;
74+ contextWindowCompression ?: boolean ;
7375 thinkingLevel ?: GoogleRealtimeThinkingLevel ;
7476 thinkingBudget ?: number ;
7577} ;
@@ -90,6 +92,8 @@ type GoogleRealtimeLiveConfig = {
9092 turnCoverage ?: GoogleRealtimeTurnCoverage ;
9193 automaticActivityDetectionDisabled ?: boolean ;
9294 enableAffectiveDialog ?: boolean ;
95+ sessionResumption ?: boolean ;
96+ contextWindowCompression ?: boolean ;
9397 thinkingLevel ?: GoogleRealtimeThinkingLevel ;
9498 thinkingBudget ?: number ;
9599} ;
@@ -209,6 +213,8 @@ function normalizeProviderConfig(
209213 turnCoverage : asTurnCoverage ( raw ?. turnCoverage ) ,
210214 automaticActivityDetectionDisabled : asBoolean ( raw ?. automaticActivityDetectionDisabled ) ,
211215 enableAffectiveDialog : asBoolean ( raw ?. enableAffectiveDialog ) ,
216+ sessionResumption : asBoolean ( raw ?. sessionResumption ) ,
217+ contextWindowCompression : asBoolean ( raw ?. contextWindowCompression ) ,
212218 thinkingLevel : asThinkingLevel ( raw ?. thinkingLevel ) ,
213219 thinkingBudget : asFiniteNumber ( raw ?. thinkingBudget ) ,
214220 } ;
@@ -223,9 +229,9 @@ function mapStartSensitivity(
223229) : StartSensitivity | undefined {
224230 switch ( value ) {
225231 case "high" :
226- return StartSensitivity . START_SENSITIVITY_HIGH ;
232+ return " START_SENSITIVITY_HIGH" as StartSensitivity ;
227233 case "low" :
228- return StartSensitivity . START_SENSITIVITY_LOW ;
234+ return " START_SENSITIVITY_LOW" as StartSensitivity ;
229235 default :
230236 return undefined ;
231237 }
@@ -236,9 +242,9 @@ function mapEndSensitivity(
236242) : EndSensitivity | undefined {
237243 switch ( value ) {
238244 case "high" :
239- return EndSensitivity . END_SENSITIVITY_HIGH ;
245+ return " END_SENSITIVITY_HIGH" as EndSensitivity ;
240246 case "low" :
241- return EndSensitivity . END_SENSITIVITY_LOW ;
247+ return " END_SENSITIVITY_LOW" as EndSensitivity ;
242248 default :
243249 return undefined ;
244250 }
@@ -249,9 +255,9 @@ function mapActivityHandling(
249255) : ActivityHandling | undefined {
250256 switch ( value ) {
251257 case "no-interruption" :
252- return ActivityHandling . NO_INTERRUPTION ;
258+ return " NO_INTERRUPTION" as ActivityHandling ;
253259 case "start-of-activity-interrupts" :
254- return ActivityHandling . START_OF_ACTIVITY_INTERRUPTS ;
260+ return " START_OF_ACTIVITY_INTERRUPTS" as ActivityHandling ;
255261 default :
256262 return undefined ;
257263 }
@@ -260,11 +266,11 @@ function mapActivityHandling(
260266function mapTurnCoverage ( value : GoogleRealtimeTurnCoverage | undefined ) : TurnCoverage | undefined {
261267 switch ( value ) {
262268 case "only-activity" :
263- return TurnCoverage . TURN_INCLUDES_ONLY_ACTIVITY ;
269+ return " TURN_INCLUDES_ONLY_ACTIVITY" as TurnCoverage ;
264270 case "all-input" :
265- return TurnCoverage . TURN_INCLUDES_ALL_INPUT ;
271+ return " TURN_INCLUDES_ALL_INPUT" as TurnCoverage ;
266272 case "audio-activity-and-all-video" :
267- return TurnCoverage . TURN_INCLUDES_AUDIO_ACTIVITY_AND_ALL_VIDEO ;
273+ return " TURN_INCLUDES_AUDIO_ACTIVITY_AND_ALL_VIDEO" as TurnCoverage ;
268274 default :
269275 return undefined ;
270276 }
@@ -316,7 +322,7 @@ function buildFunctionDeclarations(tools: RealtimeVoiceTool[] | undefined): Func
316322 parametersJsonSchema : tool . parameters ,
317323 } ;
318324 if ( tool . name === REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME ) {
319- declaration . behavior = Behavior . NON_BLOCKING ;
325+ declaration . behavior = " NON_BLOCKING" as Behavior ;
320326 }
321327 return declaration ;
322328 } ) ;
@@ -325,7 +331,7 @@ function buildFunctionDeclarations(tools: RealtimeVoiceTool[] | undefined): Func
325331function buildGoogleLiveConnectConfig ( config : GoogleRealtimeLiveConfig ) : LiveConnectConfig {
326332 const functionDeclarations = buildFunctionDeclarations ( config . tools ) ;
327333 return {
328- responseModalities : [ Modality . AUDIO ] ,
334+ responseModalities : [ " AUDIO" as Modality ] ,
329335 ...( typeof config . temperature === "number" && config . temperature > 0
330336 ? { temperature : config . temperature }
331337 : { } ) ,
@@ -359,7 +365,7 @@ function buildBrowserInitialSetup(model: string) {
359365 setup : {
360366 model : toGoogleModelResource ( model ) ,
361367 generationConfig : {
362- responseModalities : [ Modality . AUDIO ] ,
368+ responseModalities : [ " AUDIO" as Modality ] ,
363369 } ,
364370 inputAudioTranscription : { } ,
365371 outputAudioTranscription : { } ,
@@ -403,6 +409,7 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge {
403409 private audioStreamEnded = false ;
404410 private pendingFunctionNames = new Map < string , string > ( ) ;
405411 private readonly audioFormat : RealtimeVoiceAudioFormat ;
412+ private resumptionHandle : string | undefined ;
406413
407414 constructor ( private readonly config : GoogleRealtimeVoiceBridgeConfig ) {
408415 this . audioFormat = config . audioFormat ?? REALTIME_VOICE_AUDIO_FORMAT_G711_ULAW_8KHZ ;
@@ -425,7 +432,17 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge {
425432
426433 this . session = ( await ai . live . connect ( {
427434 model : this . config . model ?? GOOGLE_REALTIME_DEFAULT_MODEL ,
428- config : buildGoogleLiveConnectConfig ( this . config ) ,
435+ config : {
436+ ...buildGoogleLiveConnectConfig ( this . config ) ,
437+ ...( this . config . sessionResumption === false
438+ ? { }
439+ : {
440+ sessionResumption : this . resumptionHandle ? { handle : this . resumptionHandle } : { } ,
441+ } ) ,
442+ ...( this . config . contextWindowCompression === false
443+ ? { }
444+ : { contextWindowCompression : { slidingWindow : { } } } ) ,
445+ } ,
429446 callbacks : {
430447 onopen : ( ) => {
431448 this . connected = true ;
@@ -548,7 +565,7 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge {
548565 : { output : result } ,
549566 } ;
550567 if ( isConsultTool ) {
551- functionResponse . scheduling = FunctionResponseScheduling . WHEN_IDLE ;
568+ functionResponse . scheduling = " WHEN_IDLE" as FunctionResponseScheduling ;
552569 if ( options ?. willContinue === true ) {
553570 functionResponse . willContinue = true ;
554571 }
@@ -607,6 +624,7 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge {
607624 }
608625
609626 private handleMessage ( message : LiveServerMessage ) : void {
627+ this . captureSessionLifecycle ( message ) ;
610628 if ( message . setupComplete ) {
611629 this . handleSetupComplete ( ) ;
612630 }
@@ -618,6 +636,20 @@ class GoogleRealtimeVoiceBridge implements RealtimeVoiceBridge {
618636 }
619637 }
620638
639+ private captureSessionLifecycle ( message : LiveServerMessage ) : void {
640+ const raw = message as unknown as {
641+ goAway ?: { timeLeft ?: string } ;
642+ sessionResumptionUpdate ?: { newHandle ?: string ; resumable ?: boolean } ;
643+ } ;
644+ const update = raw . sessionResumptionUpdate ;
645+ if ( update ?. resumable && update . newHandle ) {
646+ this . resumptionHandle = update . newHandle ;
647+ }
648+ if ( raw . goAway ?. timeLeft ) {
649+ this . config . onError ?.( new Error ( `Google Live session goAway: ${ raw . goAway . timeLeft } ` ) ) ;
650+ }
651+ }
652+
621653 private handleSetupComplete ( ) : void {
622654 this . sessionConfigured = true ;
623655 for ( const chunk of this . pendingAudio . splice ( 0 ) ) {
@@ -784,6 +816,8 @@ export function buildGoogleRealtimeVoiceProvider(): RealtimeVoiceProviderPlugin
784816 turnCoverage : config . turnCoverage ,
785817 automaticActivityDetectionDisabled : config . automaticActivityDetectionDisabled ,
786818 enableAffectiveDialog : config . enableAffectiveDialog ,
819+ sessionResumption : config . sessionResumption ,
820+ contextWindowCompression : config . contextWindowCompression ,
787821 thinkingLevel : config . thinkingLevel ,
788822 thinkingBudget : config . thinkingBudget ,
789823 } ) ;
0 commit comments