Why doesn't this work on_enter? self.session.say w...
# ask-ai
a
Why doesn't this work on_enter? self.session.say works fine
Copy code
handle = self.session.generate_reply(
                    instructions="say welcome Mr. blah blah blah",
                    allow_interruptions=True
                )
          await handle.wait_for_playout()
t
Thanks for reaching out, Ahron. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Thanks — I see you're asking why
self.session.generate_reply(...); await handle.wait_for_playout()
doesn't work inside
on_enter
while
self.session.say(...)
does. That comparison and the code snippet are exactly what I'm responding to. Short answer •
session.say
triggers TTS directly and is reliable for immediate playback on enter. •
generate_reply
runs the LLM pipeline (LLM generation → TTS playout) and often fails or hangs in
on_enter
because the agent's LLM/TTS pipeline or related plugins may not be fully initialized at that lifecycle moment. Why this happens (concise) • on_enter can run very early in the agent lifecycle — some resources (LLM model, plugin init, VAD, audio pipeline) may still be loading. •
generate_reply
depends on the LLM and full pipeline being ready. If those components aren't initialized, the call may silently fail, time out, or block. •
session.say
bypasses LLM generation and directly plays text, so it doesn't hit those initialization dependencies. What to try next 1. Use
session.say
for immediate greetings on enter (simple, reliable). 2. If you must use
generate_reply
, ensure the pipeline is ready first: ◦ Add a short delay in
on_enter
to let subsystems initialize: i. await asyncio.sleep(0.5) (adjust time as needed) ii. then call
generate_reply
. ◦ Or check a readiness flag from your agent/service (if available) before calling
generate_reply
. 1. Use
await
directly on
generate_reply
(instead of using
handle
then waiting) if your API supports returning a coroutine/result immediately: ◦ Example:
Copy code
await self.session.generate_reply(
       instructions="say welcome Mr. blah blah blah",
       allow_interruptions=True
     )
• If your version returns a handle, keep using the handle but ensure the pipeline is initialized first. 1. Inspect logs for errors during
on_enter
(LLM init errors, timeouts, plugin load failures). Those logs will pinpoint if generation failed vs. waited indefinitely. Questions that will help me narrow this down • Do you see any errors or timeouts in your logs when
generate_reply
is called from
on_enter
? • Are you running local LLM/TTS models or remote services for generation/playout? • Can you share whether
generate_reply
works if called a few seconds after
on_enter
(e.g., from another event or after a delay)? If you tell me the answers to those questions (especially any log lines), I can give a focused fix or a short code example that matches your environment. Sources: Interrupt User | LiveKit Documentation | Uninterruptable Agent | LiveKit Documentation
a
using user_input I do see in the logs
"input": "<|im_start|>user\nsay welcome mr blah blah blah<|im_end|
but it doesn't say it
t
Thanks — that log line is useful. You're seeing the LLM input arrive, so generation is being invoked, but no audio is played back. I'll help you narrow the cause and give actionable checks and fixes. Why this can still be silent •
generate_reply
creates a generated response that must go through the LLM → TTS → playout pipeline. Even though the LLM prompt is built (your log shows the input), the pipeline can still fail later (TTS or playout), or block if resources aren't ready during
on_enter
. •
session.say
works because it bypasses generation and goes straight to TTS/playout, so it's less sensitive to lifecycle timing. Quick checks to run now 1. Check for errors after the input log entry ◦ Look for TTS, audio playout, or pipeline initialization errors or timeouts immediately following the user input log. Those lines typically explain why nothing played. 1. Confirm the returned handle behavior ◦ Does
generate_reply
return a handle the same way in other lifecycle events? Verify whether the handle emits an error or status fields before
wait_for_playout()
returns. 1. Try a short delay in on_enter ◦ Add
await asyncio.sleep(0.5)
(start small, increase to 1–2s if needed) before calling
generate_reply
to give subsystems time to initialize. If that makes it work, it's an initialization timing issue. 1. Try
session.say
as a control test ◦ Call
await self.session.say("welcome Mr. blah blah blah")
in
on_enter
. If that plays reliably, it confirms generation/TTS/playout initialization is the problem rather than a general audio routing issue. 1. Test generate_reply after startup ◦ Trigger
generate_reply
from another event (e.g., after first user interaction or a delayed coroutine). If it works there, again points to startup timing. If you want to keep using generate_reply for a dynamic greeting • Add a readiness check or delayed retry: a. Attempt
generate_reply
. b. If it doesn't complete playout within X seconds, wait a short delay and retry once. • Or use
session.say
immediately and run
generate_reply
in the background for any follow-up conversational logic. What would help me diagnose further • Paste the log lines right after the input line (especially any TTS, playout, or error messages). • Tell me whether you use local models or remote services for LLM/TTS. • Share the SDK/library version or agent runtime version (if known). If you share those logs/version details I’ll point to the exact failure point and a minimal code patch you can drop into
on_enter
.
a
if I remove the instructions from the agent init it works fine
t
Thanks — that’s a helpful clue. You said removing the instructions from the agent’s
__init__
makes
generate_reply
work in
on_enter
. I’ll explain why that likely matters, and then ask a couple quick, specific questions so I can give a targeted fix you can drop into your code. Why having instructions in
__init__
can break
generate_reply
in
on_enter
• Instructions set at initialization are treated as system/prompts that shape the LLM’s expected behavior or conversation state. If those instructions make the agent expect a user turn or change how the LLM composes a reply, generation may not proceed to TTS/playout as you expect during
on_enter
. • Initialization-time instructions plus an immediate
generate_reply
can collide with the agent lifecycle: the LLM pipeline might be waiting for full context, plugin initialization, or a conversation turn to complete before producing audio. That would look like the LLM input being formed (your log) but no audio playout. • Removing the instructions removes that extra constraint, so the LLM produces and the pipeline playouts immediately — which matches what you observed. Practical options you can choose from • Use
session.say
in
on_enter
for a simple, reliable greeting (bypasses LLM). • Move or pass the instruction directly to the
generate_reply
call instead of setting it in
__init__
. That keeps the initial prompt local and avoids global behavior changes:
Copy code
await self.session.generate_reply(
    instructions="say welcome Mr. blah blah blah",
    allow_interruptions=True
  )
• Keep the global instructions but add a readiness delay or a check before calling
generate_reply
in `on_enter`: a. await asyncio.sleep(0.5) (adjust) b. then call
generate_reply
and wait for playout • Use
session.say
for the immediate greeting and run
generate_reply
afterward for any dynamic follow up. Two quick diagnostic questions so I can give a precise patch 1. Can you paste the exact code you use in
__init__
to set the instructions (the snippet that you removed)? 2. Are those instructions static (always the same) or do they depend on runtime values? If you share that snippet I’ll recommend the smallest change (move to per-call instructions, change wording, or add a readiness check) and provide a minimal code example you can use in
on_enter
.
a
it appears the main instructions overrides the on_enter generate_reply
t
Thanks — that makes the situation clear. You’ve identified the core cause: the agent’s main instructions (set in
__init__
) are overriding or changing the LLM behavior when you call
generate_reply
inside
on_enter
, so the generation completes (you see the LLM input) but it never produces audible playout. Why that happens (short) • Instructions set at initialization act as system-level prompts that shape the agent’s behavior and conversation flow. When those instructions conflict with an immediate
generate_reply
in
on_enter
, the LLM or pipeline can withhold or alter output (for example, waiting for a different turn or expecting a different style), so you get no audio even though the prompt is built. •
session.say
bypasses the LLM prompt pipeline and goes straight to TTS/playout, so it’s unaffected and works reliably. What you can do (practical options) • Use session.say for immediate greetings ◦ Easiest and most reliable for on-enter messages:
await self.session.say("Welcome Mr. Blah Blah Blah")
. • Pass per-call instructions to generate_reply instead of using global instructions ◦ Move the greeting or short-system prompt from
__init__
into the
generate_reply
call so the LLM uses the local instruction context for that reply only. ◦ Example:
Copy code
await self.session.generate_reply(
      instructions="You are a polite assistant. Say: 'Welcome Mr. Blah Blah Blah'",
      allow_interruptions=True
    )
• Temporarily clear or override the global instructions before calling generate_reply in on_enter ◦ If your API exposes a way to clear or replace system instructions at runtime, set a minimal or different instruction right before
generate_reply
, then restore the original after if needed. • Delay generate_reply until pipeline is ready ◦ Add a short delay (e.g.,
await asyncio.sleep(0.5)
to 1–2s) or check a readiness flag before calling
generate_reply
. If that fixes it, it’s likely an initialization/timing interaction with the main instructions. • Hybrid approach (recommended if you need both reliability and dynamic LLM output) a. Use
session.say
for the immediate greeting to ensure audio plays. b. Run
generate_reply
a moment later (background task) for any dynamic conversational follow-up constrained by the main instructions. If you want, I can give an exact minimal code patch. To pick the best patch, tell me: • The exact snippet you use in
__init__
to set the instructions (paste it). • Whether you need those instructions to remain global for all other turns, or if they can be moved to per-call context. With that I’ll provide a short code example to (a) override briefly in on_enter, (b) move instructions per-call, or (c) do the hybrid session.say + delayed generate_reply approach.
a
Can I manually trigger to execute a tool from an event listener?
t
Yes — you can manually trigger a tool from an event listener. Tools are async functions on your agent (usually decorated with
@function_tool
) and can be called programmatically from anywhere you have a reference to the agent and a suitable context. Below are practical options, a minimal example, and important notes to keep things reliable inside event handlers. Short summary of how to do it • Call the tool method directly with
await
(or schedule it with
asyncio.create_task()
from a synchronous event handler). • Pass a Run/Session context (for example
session.context
) when calling manually — the context the tool expects when invoked by the LLM must be supplied. • If the event handler runs outside the agent loop or must not block, use
asyncio.create_task()
to run the tool asynchronously. Minimal example 1. Define a tool on your agent:
Copy code
from livekit.agents import function_tool

class MyAgent(Agent):
    @function_tool()
    async def end_call(self, context) -> None:
        await context.session.generate_reply(instructions="Ending the call now. Goodbye!")
        # additional cleanup or signaling...
1. Trigger the tool from an event listener (non-blocking):
Copy code
import asyncio

# Assume `agent` and `session` are available where you register the listener
@room.on("sip_dtmf_received")
def on_dtmf(dtmf_event):
    if dtmf_event.digit == "#":
        # schedule the tool to run without blocking the event handler
        asyncio.create_task(agent.end_call(session.context))
Or, if the listener is async and you want to await:
Copy code
@room.on("some_async_event")
async def on_event(evt):
    await agent.end_call(session.context)
Key considerations and tips • Context: When the LLM normally calls a tool, it injects a RunContext. When calling manually, pass
session.context
(or construct the appropriate context) so the tool has access to
session
,
room
, and other runtime info. • Non-blocking: Many event systems expect quick handlers; use
asyncio.create_task()
so the handler returns immediately and the tool runs concurrently. • Lifecycle/readiness: If the tool triggers LLM/TTS/other subsystems, ensure those systems are initialized before calling (a short delay or readiness check can avoid silent failures). • Error handling: Wrap manual calls or tasks in try/except or add logging so you can surface tool execution errors that would otherwise be lost in background tasks. • Tool state: Tools can change room state or agent state. Consider concurrency and locking if multiple events can trigger the same tool concurrently. If you want, I can: • Show a variant that constructs a minimal RunContext (if you don’t have
session.context
available). • Provide an example that restores/overrides agent instructions before calling a tool (useful if global instructions interfere with a specific manual tool call). • Help convert one of your concrete event handlers into a safe, non-blocking caller for a specific tool — paste the handler and the tool signature and I’ll give a drop-in snippet. Sources: Tool definition and use | LiveKit Documentation