Align vocabulary around wait_for_transcript_to_end_user_turn=False

Reframe comments, docstrings, identifiers, changelog, and example
around a single explanation of the option: (1) turn strategies do not
consider user transcripts, letting the user turn end sooner, and (2)
the aggregator gathers user transcripts on its own after the turn
ends via a simple timer, then emits `on_user_turn_message_finalized`
with the new user context message.

The mechanism is generic, so internal aggregator vocabulary stays
generic ("transcript-gather", "after the user turn ends"); the
public-facing param docstring is the one place that explains the
"local turn detection drives a realtime service" use case. The stop
strategies' `wait_for_transcript` flag is pointed at as something
that's "usually flipped indirectly" by the aggregator param rather
than something to pair with it.

Renames internal state to match: `_expect_delayed_transcripts` →
`_aggregator_gathers_transcripts`, `_pending_finalization_*` →
`_transcript_gather_*`, `_finalize_delayed_user_message` →
`_finalize_user_message`, etc.
This commit is contained in:
Paul Kompfner
2026-05-18 10:18:22 -04:00
parent ee1538d18e
commit 797d09a1d5
6 changed files with 246 additions and 222 deletions

View File

@@ -71,21 +71,19 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
},
],
)
# `wait_for_transcript_to_end_user_turn=False` configures the user
# aggregator for realtime services like Gemini Live that emit user
# transcripts after the end of turn. With this flag the aggregator:
# `wait_for_transcript_to_end_user_turn=False` is the right setting
# for pipelines like this one — local turn detection driving a
# realtime service. It avoids unnecessary latency from transcript
# delay: the realtime service consumes user audio directly, so
# we don't need user transcripts in context before it can respond.
# With this option:
#
# - drops `TranscriptionUserTurnStartStrategy` from the default start
# strategies (so late-arriving realtime transcripts don't trigger
# new turns),
# - sets `wait_for_transcript=False` on the default stop strategy
# (so the turn ends without waiting for transcripts),
# - fires `on_user_turn_stopped` at the end of turn with empty
# `message.content` (no transcripts have arrived yet), and
# - defers the context flush until after a short wait that gives the
# realtime service time to emit its transcripts, then emits
# `on_user_turn_message_finalized` with the populated message so
# the user's words land in the LLM context for audit/history.
# - Turn strategies do not consider user transcripts, so the user
# turn ends sooner.
# - User transcripts are handled by the aggregator: a simple timer
# gives it time to gather them after the user turn ends, then
# the aggregator emits `on_user_turn_message_finalized` with the
# new user context message.
user_aggregator, assistant_aggregator = LLMContextAggregatorPair(
context,
user_params=LLMUserAggregatorParams(
@@ -124,17 +122,19 @@ async def run_bot(transport: BaseTransport, runner_args: RunnerArguments):
logger.info(f"Client disconnected")
await task.cancel()
# With `wait_for_transcript_to_end_user_turn=False`, `on_user_turn_stopped`
# fires at the end of turn (before any transcripts arrive), so its
# `message.content` is empty. Logged here to make the timing of the
# end-of-turn signal visible alongside the later finalization event.
# `on_user_turn_stopped` fires at the end of the user turn. With
# `wait_for_transcript_to_end_user_turn=False`, the aggregator
# hasn't gathered user transcripts yet at this point, so
# `message.content` is empty. Logged here to make the end-of-turn
# signal visible alongside the later finalization event.
@user_aggregator.event_handler("on_user_turn_stopped")
async def on_user_turn_stopped(aggregator, strategy, message: UserTurnStoppedMessage):
logger.info(f"User turn ended (strategy: {type(strategy).__name__})")
# `on_user_turn_message_finalized` fires when the user message has
# been written to context — later than `on_user_turn_stopped` in this
# mode, since transcripts arrive after the end of turn.
# been finalized into the context. Here it fires later than
# `on_user_turn_stopped`, after the aggregator has gathered the
# realtime service's user transcripts.
@user_aggregator.event_handler("on_user_turn_message_finalized")
async def on_user_turn_message_finalized(
aggregator, strategy, message: UserMessageFinalizedMessage