creamy-tiger-14805
12/26/2025, 9:55 AMtall-airline-16756
12/26/2025, 9:55 AMtall-airline-16756
12/26/2025, 9:56 AMsession.generate_reply doesn’t work when called inside a function_tool.
Short answer: tool execution pauses the agent’s normal reply flow, so calling session.generate_reply from inside a tool will conflict with the agent’s execution model and usually won’t produce a reply unless you explicitly manage session/context and control flow.
What to try next:
• Confirm you have access to the same session and RunContext inside the tool (pass context into the tool).
• Use the async call pattern and await it from the tool: await context.session.generate_reply() so it runs in the correct async flow.
• If the tool is long-running or must not block the agent, spawn a background task (outside the blocking path) that calls generate_reply after the tool finishes.
• Ensure the session is active and there are no concurrency conflicts (no other code is awaiting generation at the same time).
Could you share:
1. A minimal code snippet of the tool and how you call it (how you pass `context`/`session`), and
2. Any error messages or logs you see when you call session.generate_reply?
With that I can suggest an exact code fix.
Sources: Tool definition and use | LiveKit docs | Agent speech and audio | LiveKit docs | External data and RAG | LiveKit docscreamy-tiger-14805
12/26/2025, 9:57 AMfrom __future__ import annotations
import re
from dataclasses import dataclass
from typing import TYPE_CHECKING
from ... import llm, stt, tts, vad
from ...llm.tool_context import ToolError, function_tool
from ...types import NOT_GIVEN, NotGivenOr
from ...voice.agent import AgentTask
from ...voice.events import RunContext
from ...voice.speech_handle import SpeechHandle
if TYPE_CHECKING:
from ...voice.agent_session import TurnDetectionMode
EMAIL_REGEX = (
r"^[A-Za-z0-9][A-Za-z0-9._%+\-]*@(?:[A-Za-z0-9](?:[A-Za-z0-9\-]*[A-Za-z0-9])?\.)+[A-Za-z]{2,}$"
)
@dataclass
class GetEmailResult:
email_address: str
class GetEmailTask(AgentTask[GetEmailResult]):
def __init__(
self,
chat_ctx: NotGivenOr[llm.ChatContext] = NOT_GIVEN,
turn_detection: NotGivenOr[TurnDetectionMode | None] = NOT_GIVEN,
stt: NotGivenOr[stt.STT | None] = NOT_GIVEN,
vad: NotGivenOr[vad.VAD | None] = NOT_GIVEN,
llm: NotGivenOr[llm.LLM | llm.RealtimeModel | None] = NOT_GIVEN,
tts: NotGivenOr[tts.TTS | None] = NOT_GIVEN,
allow_interruptions: NotGivenOr[bool] = NOT_GIVEN,
) -> None:
super().__init__(
instructions=(
"You are only a single step in a broader system, responsible solely for capturing an email address.\n"
"Handle input as noisy voice transcription. Expect that users will say emails aloud with formats like:\n"
"- 'john dot doe at gmail dot com'\n"
"- 'susan underscore smith at yahoo dot co dot uk'\n"
"- 'dave dash b at protonmail dot com'\n"
"- 'jane at example' (partial—prompt for the domain)\n"
"- 'theo t h e o at livekit dot io' (name followed by spelling)\n"
"Normalize common spoken patterns silently:\n"
"- Convert words like 'dot', 'underscore', 'dash', 'plus' into symbols: `.`, `_`, `-`, `+`.\n"
"- Convert 'at' to `@`.\n"
"- Recognize patterns where users speak their name or a word, followed by spelling: e.g., 'john j o h n'.\n"
"- Filter out filler words or hesitations.\n"
"- Assume some spelling if contextually obvious (e.g. 'mike b two two' → mikeb22).\n"
"Don't mention corrections. Treat inputs as possibly imperfect but fix them silently.\n"
"Call `update_email_address` at the first opportunity whenever you form a new hypothesis about the email. "
"(before asking any questions or providing any answers.) \n"
"Don't invent new email addresses, stick strictly to what the user said. \n"
"Call `confirm_email_address` after the user confirmed the email address is correct. \n"
"If the email is unclear or invalid, or it takes too much back-and-forth, prompt for it in parts: first the part before the '@', then the domain—only if needed. \n"
"Ignore unrelated input and avoid going off-topic. Do not generate markdown, greetings, or unnecessary commentary. \n"
"Always explicitly invoke a tool when applicable. Do not simulate tool usage, no real action is taken unless the tool is explicitly called."
),
chat_ctx=chat_ctx,
turn_detection=turn_detection,
stt=stt,
vad=vad,
llm=llm,
tts=tts,
allow_interruptions=allow_interruptions,
)
self._current_email = ""
# speech_handle/turn used to update the email address.
# used to ignore the call to confirm_email_address in case the LLM is hallucinating and not asking for user confirmation
self._email_update_speech_handle: SpeechHandle | None = None
async def on_enter(self) -> None:
self.session.generate_reply(
instructions=(
"Ask the user to provide an email address. If you already have it, ask for confirmation.\n"
"Do not call `decline_email_capture`"
)
)
@function_tool
async def update_email_address(self, email: str, ctx: RunContext) -> str:
"""Update the email address provided by the user.
Args:
email: The email address provided by the user
"""
self._email_update_speech_handle = ctx.speech_handle
email = email.strip()
if not re.match(EMAIL_REGEX, email):
raise ToolError(f"Invalid email address provided: {email}")
self._current_email = email
separated_email = " ".join(email)
return (
f"The email has been updated to {email}\n"
f"Repeat the email character by character: {separated_email} if needed\n"
f"Prompt the user for confirmation, do not call `confirm_email_address` directly"
)
@function_tool
async def confirm_email_address(self, ctx: RunContext) -> None:
"""Validates/confirms the email address provided by the user."""
await ctx.wait_for_playout()
if ctx.speech_handle == self._email_update_speech_handle:
raise ToolError("error: the user must confirm the email address explicitly")
if not self._current_email.strip():
raise ToolError(
"error: no email address were provided, `update_email_address` must be called before"
)
if not self.done():
self.complete(GetEmailResult(email_address=self._current_email))
@function_tool
async def decline_email_capture(self, reason: str) -> None:
"""Handles the case when the user explicitly declines to provide an email address.
Args:
reason: A short explanation of why the user declined to provide the email address
"""
if not self.done():
self.complete(ToolError(f"couldn't get the email address: {reason}"))tall-airline-16756
12/26/2025, 9:57 AMsession.generate_reply doesn’t work when called inside a function_tool, and how to fix it in your GetEmailTask.
Why it fails
• Tool execution pauses the agent’s normal reply flow. While a function_tool is running, the agent’s main message loop is waiting for the tool to return. Calling session.generate_reply inside that paused flow conflicts with the agent execution model and typically won’t emit a reply.
• The agent won’t automatically resume or create new replies from inside a tool unless you explicitly run generate_reply on the active session/context in the correct async flow or spawn a separate task that runs after the tool returns.
What to change in your code (high level)
• Don’t call session.generate_reply directly from inside a function_tool synchronous path. Instead:
◦ If you need to trigger a response immediately after the tool finishes, return a message from the tool and let the agent handle calling generate_reply after the tool returns.
◦ If you must trigger a reply from inside the tool, call await ctx.session.generate_reply(...) using the RunContext passed into the tool and ensure you await it in the tool’s async context. That uses the correct async flow attached to the current run context.
◦ For long-running or non-blocking updates (e.g., incremental feedback while processing), spawn a background task that calls generate_reply so the tool’s primary logic can finish and return normally.
Targeted suggestions for your GetEmailTask
• update_email_address: This should not try to cause immediate agent speech via session.generate_reply. Return the update text (which you already do). Let the agent produce a reply after the tool returns so the agent’s reply flow remains consistent.
• confirm_email_address: You already await ctx.wait_for_playout() which is correct for waiting on playout. Do not call session.generate_reply here either; complete the task and let the agent respond.
• If you need the agent to speak immediately when update_email_address occurs, change the tool to:
a. Accept ctx: RunContext.
b. After updating state, call await ctx.session.generate_reply(instructions=...) (awaiting ensures correct async flow).
c. Or better: return the text and let the outer agent generate the reply — simpler and less error-prone.
Example patterns (pseudo)
• Preferred (return from tool):
◦ Tool returns a string describing update.
◦ Agent receives tool result and calls session.generate_reply(...) after tool completes.
• If calling from inside tool (use RunContext):
◦ await ctx.session.generate_reply(instructions="...") — ensure you await and that no other concurrent generate is running.
• Non-blocking feedback:
◦ Spawn background task that does await session.generate_reply(...) after the tool returns.
One short checklist to debug now
• Are you passing ctx: RunContext into the tool and using ctx.session (not self.session)?
• Are you awaiting ctx.session.generate_reply() inside the tool?
• Do logs show any concurrency errors when generate_reply is invoked?
• If you need immediate speech, can you instead return the message and let the agent call generate_reply after the tool returns?
If you want, share:
• the code where you call the tool (how RunContext / session is passed), and
• any error logs you see when you try to call generate_reply.
I’ll give a precise code patch based on that.creamy-tiger-14805
12/26/2025, 9:58 AM@function_tool
async def get_users_email(self, context: RunContext, reason: str | None = None):
"Get the user's email from the user's input."
email_result = await beta.workflows.GetEmailTask()
email_address = email_result.email_address
<http://logger.info|logger.info>(f"User's email address: {email_address}")
return f"The user's email address is {email_address}. "
give me the details of all the required changstall-airline-16756
12/26/2025, 9:58 AMget_users_email tool work correctly with GetEmailTask and with reply generation. I’ll list concrete code changes and why, plus a minimal example patch you can apply.
Summary of required changes
• Fix tool signature: put ctx: RunContext first and name it ctx (tools receive a RunContext).
• Await task creation correctly: call and await the task runner instead of directly awaiting the class.
• Use the task’s API to run it (create the task instance and call its runner, or call the workflow runner you use across your codebase).
• Don’t call self.session.generate_reply inside a tool; if you need immediate agent speech from inside the tool, call await ctx.session.generate_reply(...) and await it.
• Add error handling for ToolError and validation failures from GetEmailTask.
• Return the tool result string (or call ctx.session.generate_reply then return) — let the agent handle normal reply flow unless you intentionally call ctx.session.generate_reply.
• Ensure imports and typing match your project (RunContext, ToolError, GetEmailTask).
Concrete changes and rationale 1) Signature and param order
• Change async def get_users_email(self, context: RunContext, reason: str | None = None) to:
◦ async def get_users_email(self, ctx: RunContext, reason: str | None = None)
◦ Tool frameworks expect the RunContext as the first parameter; naming it ctx matches earlier code and examples.
2) Run the GetEmailTask correctly
• Current: email_result = await beta.workflows.GetEmailTask()
◦ Problem: you’re awaiting the class or not invoking the task runner. You must instantiate the task and run it via your agent/session API.
• Correct pattern (two common options):
◦ If your codebase provides a helper to run tasks (preferred), use it:
▪︎ email_task = beta.workflows.GetEmailTask(...)
▪︎ email_result = await self.run_task(email_task, ctx) # replace with your task-runner call
◦ Or instantiate and run via session/workflow API:
▪︎ email_task = beta.workflows.GetEmailTask(...)
▪︎ email_result = await ctx.session.run_task(email_task) # example — replace with your actual runner
3) Await properly and extract result
• After running the task, access email_result.email_address.
• Validate before using.
4) Error handling
• Wrap the run in try/except catching ToolError and other exceptions:
◦ On ToolError: return a helpful message string or re-raise as a ToolError so the agent can handle it.
◦ On unexpected exceptions: log and return a generic failure string.
5) Reply generation choices
• Preferred (simpler, safer): return a descriptive string and let the agent generate the reply after the tool returns.
◦ Example: return f"The user's email address is {email_address}."
• If you need the agent to speak immediately from inside the tool:
◦ Call await ctx.session.generate_reply(instructions="...") and await it; then return or finish.
◦ Ensure no concurrent generate is running.
6) Logging and privacy
• Keep logging minimal and avoid logging full email addresses in production. For debugging, you can log masked emails.
Minimal patched example (apply replacements where your task-runner API differs)
@function_tool
async def get_users_email(self, ctx: RunContext, reason: str | None = None):
"Get the user's email from the user's input."
try:
# instantiate the task
email_task = beta.workflows.GetEmailTask()
# run the task via your agent/session runner — replace `run_task` with your project's runner API
email_result = await self.run_task(email_task, ctx)
# OR, if your API exposes session.run_task:
# email_result = await ctx.session.run_task(email_task)
email_address = email_result.email_address
# optional: immediate speech from inside the tool (only if needed)
# await ctx.session.generate_reply(instructions=f"The user's email is {email_address}. Is this correct?")
# Prefer returning a string so the agent handles reply generation
return f"The user's email address is {email_address}."
except ToolError as te:
# propagate or return a clear message so agent can handle
return f"Could not capture email: {str(te)}"
except Exception as e:
logger.exception("Unexpected error capturing email")
return "Sorry, I couldn't capture the email due to an internal error."
Checklist to verify after changes
• RunContext is the first parameter and named ctx.
• You instantiate the task and run it with the proper runner API (don’t await the class).
• You await the task run and extract email_address.
• You handle ToolError and other exceptions.
• You either return a string (preferred) or use await ctx.session.generate_reply(...) if immediate speech is required.
• No direct calls to self.session.generate_reply inside the tool—use ctx.session if inside a tool.
If you share how tasks are normally run in your codebase (the exact function you use to run AgentTask instances — e.g., self.run_task(...) or ctx.session.run_task(...)), I’ll give the exact one-line replacement and a ready-to-drop patch.