steep-balloon-41261
02/17/2026, 6:18 AMtall-airline-16756
02/17/2026, 6:18 AMtall-airline-16756
02/17/2026, 6:19 AMself._verified[self._dob_key] = True) and self._check_completion() becomes true, the agent can switch flow (via _update_agent(context)), and the new flow/agent may generate a different final message.
• The LLM can rephrase, summarize, or combine tool outputs with other context before sending the final reply.
How to debug this in your code (practical steps):
1. Log each tool return and the exact time it was returned.
2. Log state changes immediately after the tool runs:
◦ self._verified and self._retry_count values.
1. Log when _update_agent(context) is called and which agent/flow becomes active.
2. Correlate logs: match tool returns to subsequent LLM/agent outputs (timestamps + conversation IDs).
3. Inspect what self._dob_verified_message and self._retry_message contain to ensure they match expectations.
What to check in your specific snippet:
• Confirm whether multiple DOB attempts occurred; a retry could produce the tool message first, then a later successful verification produces the “Thank you…” agent message.
• Verify that _update_agent(context) hands off to another agent that generates the “Thank you” text rather than the verification tool returning that text directly.
If you want, share:
• A short log excerpt showing the sequence of tool returns, state updates, and final agent messages (with timestamps), or
• The values of self._dob_verified_message, self._retry_message, and the flow that _update_agent switches to.
I can then point to the exact line where the divergence happens and suggest a minimal code/logging change to make the behavior and outputs match exactly how you expect.
Sources: Building multi-agent architectures with LiveKit agents | LiveKit | Pipeline nodes and hooks | LiveKit Documentationbetter-sandwich-65834
02/17/2026, 6:20 AMtall-airline-16756
02/17/2026, 6:21 AM_update_agent(context) or similar) and the next agent generates the "Thank you..." message.
• In short: the flow marks the verification step complete before the correctness check is used to decide whether to prompt again, so the agent continues with a completion response.
What to change so a wrong DOB triggers a retry
1. Perform validation before marking the step complete
◦ After the tool compares expected vs provided DOB, only set self._verified[self._dob_key] = True and allow completion when the values actually match.
◦ If they do not match, do NOT mark the verification step complete; instead return the retry message and keep the agent in the verification flow.
1. Ensure the agent's completion check depends on correctness, not just user input
◦ Make _check_completion() return true only when all required verification flags are true.
◦ Avoid marking the overall task complete simply because the user responded.
1. Stop the handoff to the next agent when validation fails
◦ Call _update_agent(context) (or trigger a flow handoff) only after successful validation.
◦ If you currently call _update_agent(context) based on "input received" rather than "input validated," change that condition.
1. Add explicit logging and a guard
◦ Log the tool return value, self._verified[self._dob_key], and whether _update_agent is called.
◦ Add a guard right before `_update_agent(context)`:
▪︎ If self._verified[self._dob_key] is False, skip the update and return the retry prompt.
Minimal code checklist (apply to your snippet)
• Verify the branch ordering: check validation first, then set verified flag only on match.
• Ensure self._retry_count[self._dob_key] += 1 and return self._retry_message run when there is a mismatch, and do NOT call _update_agent(context) there.
• Ensure the success branch both sets verified flag and only then calls _update_agent(context).
If you want, I can:
• Suggest a small patched version of your function showing where to gate _update_agent.
• Review a short log excerpt (tool returns, state changes, timestamps) and point to the exact step where the flow is marked complete prematurely.
Which would you prefer?
Sources: Building multi-agent architectures with LiveKit agents | LiveKit | Pipeline nodes and hooks | LiveKit Documentationbetter-sandwich-65834
02/17/2026, 6:22 AMfrom agents.utils.phone_formatter import format_phone_for_speech
from entities.customer import CustomerData, UserLanguage
from entities.session import SessionState
from livekit.agents import ChatContext, RunContext, function_tool
from prompts.provider import (
collect_info_instructions,
identity_verify_agent_instructions,
identity_verify_agent_refused_information_instructions,
identity_verify_agent_verification_failed_instructions,
)
from .application import ApplicationAgent
from .base import BaseAgent
class IdentityVerifyAgent(BaseAgent):
def __init__(
self,
customer_data: CustomerData,
company_name: str,
intro_message: str,
current_language: UserLanguage = UserLanguage.ENGLISH,
chat_ctx: ChatContext | None = None,
user_timezone: str = "UTC",
) -> None:
main_instructions = identity_verify_agent_instructions.invoke(
{
"customer_full_name": f"{customer_data.firstname} {customer_data.lastname}",
"company_name": company_name,
"intro_message": intro_message,
}
).text
collect_instructions = collect_info_instructions.invoke({}).text
instructions = f"{main_instructions}\n\n{collect_instructions}"
super().__init__(
instructions=instructions,
chat_ctx=chat_ctx,
current_language=current_language,
user_timezone=user_timezone,
)
self._dob_key = "dob"
self._ssn_key = "ssn"
self._verified: dict[str, bool] = {self._ssn_key: False, self._dob_key: False}
self._retry_count: dict[str, int] = {self._ssn_key: 0, self._dob_key: 0}
self.attempt_limit = 2
self._retry_message = (
"I'm sorry, but the information you provided does not match our records. "
"Would you like to try again? "
)
self._dob_verified_message = "Date of birth verified."
self._ssn_verified_message = "SSN verified."
async def on_enter(self) -> None:
await self.session.generate_reply()
async def _verify_dob(
self,
context: RunContext[SessionState],
month: str | None,
day: str | None,
year: str | None,
) -> str | ApplicationAgent:
"""Verify customer's date of birth."""
expected = context.userdata.customer_data
expected_year, expected_month, expected_day = expected.dob.split("-")
if expected_year == year and expected_month == month and expected_day == day:
self._verified[self._dob_key] = True
if self._check_completion():
return self._update_agent(context)
return f"{self._dob_verified_message} Current state: {self._verified}"
self._retry_count[self._dob_key] += 1
return self._retry_message
async def _verify_ssn(
self,
context: RunContext[SessionState],
ssn: str | None,
) -> str | ApplicationAgent:
"""Verify customer's SSN."""
expected = context.userdata.customer_data
if expected.ssn == ssn:
self._verified[self._ssn_key] = True
if self._check_completion():
return self._update_agent(context)
return f"{self._ssn_verified_message} Current state: {self._verified}"
self._retry_count[self._ssn_key] += 1
return self._retry_message
def _check_completion(self) -> bool:
"""Check if both DOB and SSN have been verified. Transition to next agent if so."""
return self._verified[self._dob_key] and self._verified[self._ssn_key]
def _update_agent(
self,
context: RunContext[SessionState],
) -> ApplicationAgent:
context.userdata.call_outcome.identity_verified = True
return ApplicationAgent(
customer_data=context.userdata.customer_data,
company_name=context.userdata.company_name,
application_details=context.userdata.loan_info,
chat_ctx=self.chat_ctx,
current_language=context.userdata.current_language,
user_timezone=context.userdata.customer_data.timezone,
)
def is_exceeded(self) -> bool:
"""Check if either field exceeded max retries and fail if so.
Returns:
True if verification failed, False otherwise
"""
return (
self._retry_count[self._dob_key] >= self.attempt_limit
or self._retry_count[self._ssn_key] >= self.attempt_limit
)
async def _fail(self, context: RunContext[SessionState]) -> None:
"""Fail with verification failed instructions."""
context.userdata.call_outcome.identity_verified = False
raw_phone = context.userdata.servicing_info.servicing_branch_phone_number
customer_service_number = format_phone_for_speech(
raw_phone, context.userdata.current_language.value
)
instructions = identity_verify_agent_verification_failed_instructions.invoke(
{
"customer_service_number": customer_service_number,
}
).text
await self.session.generate_reply(instructions=instructions)
await self.hangup()
@function_tool()
async def verify_dob(
self,
context: RunContext[SessionState],
month: str | None,
day: str | None,
year: str | None,
) -> str | ApplicationAgent:
"""Verify customer's date of birth.
Call this when the customer provides their date of birth.
Args:
month: Month of birth as MM or None
day: Day of birth as DD or None
year: Year of birth YYYY or None
Returns:
Status message with verification result
"""
dob_result: str | ApplicationAgent = self._dob_verified_message
if not self._verified[self._dob_key]:
dob_result = await self._verify_dob(context, month, day, year)
if self.is_exceeded():
await self._fail(context)
return dob_result
@function_tool()
async def verify_ssn(
self,
context: RunContext[SessionState],
digit1: str | None,
digit2: str | None,
digit3: str | None,
digit4: str | None,
) -> str | ApplicationAgent:
"""Verify customer's SSN last 4 digits.
Call this with the digits the customer provides.
Args:
digit1: First digit of SSN last 4 or None
digit2: Second digit of SSN last 4 or None
digit3: Third digit of SSN last 4 or None
digit4: Fourth digit of SSN last 4 or None
Returns:
Status message with verification result
"""
ssn_result: str | ApplicationAgent = self._ssn_verified_message
if not self._verified[self._ssn_key]:
ssn_value = f"{digit1}{digit2}{digit3}{digit4}"
ssn_result = await self._verify_ssn(context, ssn_value)
if self.is_exceeded():
await self._fail(context)
return ssn_result
@function_tool()
async def verify_dob_and_ssn(
self,
context: RunContext[SessionState],
month: str | None,
day: str | None,
year: str | None,
ssn_digit1: str | None,
ssn_digit2: str | None,
ssn_digit3: str | None,
ssn_digit4: str | None,
) -> str | ApplicationAgent:
"""Verify both date of birth and SSN in one call.
Call this when the customer provides both DOB and SSN information together.
Args:
month: Month of birth as MM or None
day: Day of birth as DD or None
year: Year of birth YYYY or None
ssn_digit1: First digit of SSN last 4 or None
ssn_digit2: Second digit of SSN last 4 or None
ssn_digit3: Third digit of SSN last 4 or None
ssn_digit4: Fourth digit of SSN last 4 or None
Returns:
Status message with verification results for both fields
"""
dob_result: str | ApplicationAgent = self._dob_verified_message
if not self._verified[self._dob_key]:
dob_result = await self._verify_dob(context, month, day, year)
ssn_result: str | ApplicationAgent = self._ssn_verified_message
if not self._verified[self._ssn_key]:
ssn_value = f"{ssn_digit1}{ssn_digit2}{ssn_digit3}{ssn_digit4}"
ssn_result = await self._verify_ssn(context, ssn_value)
if self.is_exceeded():
await self._fail(context)
if self._check_completion():
return self._update_agent(context)
return f"{dob_result} {ssn_result}"
@function_tool()
async def refused_information(self, context: RunContext[SessionState]) -> None:
"""Handle when customer refuses to provide verification information.
Call this when the customer explicitly refuses to provide their DOB or SSN."""
context.userdata.call_outcome.identity_verified = False
raw_phone = context.userdata.servicing_info.servicing_branch_phone_number
customer_service_number = format_phone_for_speech(
raw_phone, context.userdata.current_language.value
)
instructions = identity_verify_agent_refused_information_instructions.invoke(
{
"customer_service_number": customer_service_number,
}
).text
await self.session.generate_reply(instructions=instructions)
await self.hangup()
there is no issues with updates
it is only about why agent ingore tool output and provide wring answer?tall-airline-16756
02/17/2026, 6:23 AMawait self.session.generate_reply(instructions=self._retry_message) inside the failure branch.
◦ Return None (or a neutral marker) so the LLM has no tool message to echo and you avoid extra replies.
• This makes the retry prompt the single, authoritative user-facing message.
2) Force the LLM not to continue speaking on failure (prompt-level control)
• Update the agent instructions so the LLM must not generate any extra reply after a failed verification tool call. Example instruction: "If verification fails, only speak the provided retry message and do not generate additional confirmation or closing remarks."
• This relies on the LLM honoring instructions, so option (1) is more robust.
3) Short-circuit the flow after a failed verification
• After returning the retry, ensure the agent runtime does not proceed to the next step or call generate_reply again.
• Add an explicit guard in the caller (or after the tool return) to not produce any further reply unless verification succeeded.
Minimal code change example (Option 1 — robust)
• In _verify_dob, when mismatch:
◦ increment retry count
◦ await self.session.generate_reply(instructions=self._retry_message)
◦ return None
• Ensure callers treat None as "tool already replied; do not generate again".
Extra checks to add
• Log tool return, self._verified, and whether _update_agent was called.
• Confirm the LLM isn’t re-prompting itself from conversation instructions (remove any auto-closing phrasing in the agent instructions).
If you want, I can:
• Draft the small patch (showing exactly where to insert generate_reply and the None return).
• Or show how to change the wrapper logic so callers suppress further generation after a tool reply.
Which would you prefer?
Sources: Tool definition and use | LiveKit Documentationbetter-sandwich-65834
02/17/2026, 6:33 AMtall-airline-16756
02/17/2026, 6:33 AM