refined-hydrogen-81988
12/16/2025, 7:56 PMbackground_audio = BackgroundAudioPlayer(
thinking_sound=[
AudioConfig(BuiltinAudioClip.KEYBOARD_TYPING, volume=0.8),
AudioConfig(BuiltinAudioClip.KEYBOARD_TYPING2, volume=0.7),
],
)
await background_audio.start(room=ctx.room, agent_session=session)
but instead of thinking sound I want to perform a frontend animation by calling an RPC to the frontend. What is the cleanest way to do it? event handler with user_turn_completed?tall-airline-16756
12/16/2025, 7:56 PMtall-airline-16756
12/16/2025, 7:57 PMuser_turn_completed. Use lifecycle hooks that indicate the agent is actively processing, not user_turn_completed.
Recommended approaches
• For tool calls / long-running tool work: use on_tools_call. Send an RPC to start the animation at the start of the hook and another to stop it when tools finish (use try/finally).
• For raw LLM generation: use on_llm_start to send a "start" RPC, then stop the animation when the LLM finishes (e.g., wait for LLM done or use an LLM-completion hook).
Minimal example (conceptual)
1. In the hook, find the remote participant and call:
◦ room.local_participant.perform_rpc(destination_identity=..., method="startThinkingAnimation", payload="")
1. After processing completes, call:
◦ ...perform_rpc(..., method="stopThinkingAnimation", ...)
Frontend: register startThinkingAnimation and stopThinkingAnimation RPC handlers to run your animation.
Why not user_turn_completed?
• user_turn_completed fires right after the user stops speaking (before agent processing) and is intended for preprocessing—so it triggers too early and won’t cover ongoing tool/LLM work.
If you want, I can:
• Provide a concrete code snippet for on_tools_call or on_llm_start in your language of choice.
• Show the frontend RPC registration example for LiveKit JS. Sources: Agent speech and audio | LiveKit docs | External data and RAG | LiveKit Docs | Remote method calls | LiveKit docs | Pipeline nodes and hooks | LiveKit docsrefined-hydrogen-81988
12/16/2025, 7:58 PMon_llm_start in docs, you sure? give me an exampletall-airline-16756
12/16/2025, 7:59 PMon_llm_start lifecycle hook. Use a pipeline node override (like llm_node) or on_tools_call to emit RPCs when LLM/tool work begins and ends. Below are concise examples.
1) Override llm_node (Python) — send RPC before LLM inference and stop after:
class MyAgent(Agent):
async def llm_node(self, chat_ctx, tools, model_settings):
room = self.context.room
remote = next(iter(room.remote_participants.values()), None)
if remote:
await room.local_participant.perform_rpc(
destination_identity=remote.identity,
method="startThinkingAnimation",
payload="",
)
try:
async for chunk in Agent.default.llm_node(self, chat_ctx, tools, model_settings):
yield chunk
finally:
if remote:
await room.local_participant.perform_rpc(
destination_identity=remote.identity,
method="stopThinkingAnimation",
payload="",
)
2) Use on_tools_call (for long-running tool execution):
async def on_tools_call(self, tools, ctx):
room = self.context.room
remote = next(iter(room.remote_participants.values()), None)
if remote:
await room.local_participant.perform_rpc(
destination_identity=remote.identity,
method="startThinkingAnimation",
payload="",
)
try:
return await super().on_tools_call(tools, ctx)
finally:
if remote:
await room.local_participant.perform_rpc(
destination_identity=remote.identity,
method="stopThinkingAnimation",
payload="",
)
Frontend (LiveKit JS) — register RPC handlers:
localParticipant.registerRpcMethod("startThinkingAnimation", async () => {
// start animation
return "ack";
});
localParticipant.registerRpcMethod("stopThinkingAnimation", async () => {
// stop animation
return "ack";
});
If you want, I can:
• Provide a version that targets a specific participant by identity or metadata, or
• Show how to send a data message/topic instead of RPC if that fits your frontend architecture. Which would you prefer? Sources: Pipeline nodes and hooks | LiveKit docs | Agents (Python) | livekit/livekit_composite | DeepWiki | Modifying LLM output before TTS | LiveKit docsrefined-hydrogen-81988
12/16/2025, 8:00 PMtall-airline-16756
12/16/2025, 8:01 PMon_user_turn_completed and send a "stop" RPC when the agent begins speaking. It uses perform_rpc from the room's local participant and overrides the speak_node (or the appropriate speak method) to notify the frontend right before speech output.
1) Python (agent-side)
class MyAgent(Agent):
async def on_user_turn_completed(self, ctx):
room = self.context.room
remote = next(iter(room.remote_participants.values()), None)
if remote:
await room.local_participant.perform_rpc(
destination_identity=remote.identity,
method="playThinkingAnimation",
payload="",
)
# continue normal processing
return await super().on_user_turn_completed(ctx)
async def speak_node(self, *args, **kwargs):
# called when agent is about to speak / TTS starts
room = self.context.room
remote = next(iter(room.remote_participants.values()), None)
if remote:
await room.local_participant.perform_rpc(
destination_identity=remote.identity,
method="stopThinkingAnimation",
payload="",
)
# proceed with default speak behavior (yield audio chunks or call parent)
async for chunk in Agent.default.speak_node(self, *args, **kwargs):
yield chunk
Notes:
• Use next(iter(room.remote_participants.values()), None) to pick the remote participant; adjust selection if you target specific participants.
• If your agent framework exposes a different speak hook name, use that hook (the goal is to run the "stop" RPC before TTS/audio is published).
2) Frontend (LiveKit JS) — RPC handlers
localParticipant.registerRpcMethod("playThinkingAnimation", async () => {
// start your animation / spinner
return "ack";
});
localParticipant.registerRpcMethod("stopThinkingAnimation", async () => {
// stop animation
return "ack";
});
Would you like a variant that:
• Targets a specific participant by identity/metadata, or
• Uses data messages/topics instead of RPC for broader fan-out?