@@ -183,6 +183,13 @@ export class RealtimeSession<
183
183
#shouldIncludeAudioData: boolean ;
184
184
#interruptedByGuardrail: Record < string , boolean > = { } ;
185
185
#audioStarted = false ;
186
+ // Keeps track of the last full session config we sent (camelCase keys) so that
187
+ // subsequent updates (e.g. during agent handoffs) preserve properties that are
188
+ // not explicitly recalculated here (such as inputAudioFormat, outputAudioFormat,
189
+ // modalities, speed, toolChoice, turnDetection, etc.). Without this, updating
190
+ // the agent would drop audio format overrides (e.g. g711_ulaw) and revert to
191
+ // transport defaults causing issues for integrations like Twilio.
192
+ #lastSessionConfig: Partial < RealtimeSessionConfig > | null = null ;
186
193
187
194
constructor (
188
195
public readonly initialAgent :
@@ -313,14 +320,34 @@ export class RealtimeSession<
313
320
) ;
314
321
}
315
322
316
- return {
323
+ // Start from any previously-sent config (so we preserve values like audio formats)
324
+ // and the original options.config provided by the user. Preference order:
325
+ // 1. Last session config we sent (#lastSessionConfig)
326
+ // 2. Original options.config
327
+ // 3. Additional config passed into this invocation (explicit overrides)
328
+ // Finally we overwrite dynamic fields (instructions, voice, model, tools, tracing)
329
+ // to ensure they always reflect the current agent & runtime state.
330
+ const base : Partial < RealtimeSessionConfig > = {
331
+ ...( this . #lastSessionConfig ?? { } ) ,
332
+ ...( this . options . config ?? { } ) ,
333
+ ...( additionalConfig ?? { } ) ,
334
+ } ;
335
+
336
+ // Note: Certain fields cannot be updated after the session begins, such as voice and model
337
+ const fullConfig : Partial < RealtimeSessionConfig > = {
338
+ ...base ,
317
339
instructions,
318
340
voice : this . #currentAgent. voice ,
319
341
model : this . options . model ,
320
342
tools : this . #currentTools,
321
343
tracing : tracingConfig ,
322
- ...additionalConfig ,
323
344
} ;
345
+
346
+ // Update our cache so subsequent updates inherit the full set including any
347
+ // dynamic fields we just overwrote.
348
+ this . #lastSessionConfig = fullConfig ;
349
+
350
+ return fullConfig ;
324
351
}
325
352
326
353
async updateAgent ( newAgent : RealtimeAgent < TBaseContext > ) {
@@ -689,6 +716,9 @@ export class RealtimeSession<
689
716
url : options . url ,
690
717
initialSessionConfig : await this . #getSessionConfig( this . options . config ) ,
691
718
} ) ;
719
+ // Ensure the cached lastSessionConfig includes everything passed as the initial session config
720
+ // (the call above already set it via #getSessionConfig but in case additional overrides were
721
+ // passed directly here in the future we could merge them). For now it's a no-op.
692
722
693
723
this . #history = [ ] ;
694
724
this . emit ( 'history_updated' , this . #history) ;
0 commit comments