silly-teacher-46683
03/09/2026, 6:27 AMtall-airline-16756
03/09/2026, 6:27 AMtall-airline-16756
03/09/2026, 6:27 AMsilly-teacher-46683
03/09/2026, 6:27 AMtall-airline-16756
03/09/2026, 6:28 AMtransfer_sip_participant(...) is the right API to initiate a cold/SIP REFER-style transfer.
• Warm transfer idea: dialing the target first with create_sip_participant(...) is the right start for warm transfer behavior.
• Good practice: closing the LiveKit client in finally and logging progress.
Problems to fix (quick list)
• Warm transfer flow is incorrect: calling transfer_sip_participant() after creating the target participant does not implement a true warm transfer (bridging + agent exit).
• Waiting with await asyncio.sleep(5) is fragile and unreliable.
• Hardcoded config.agent.room_name may not match the current session room.
• Recreating a new LiveKitAPI client each call instead of reusing the job context API may be unnecessary and risk credential handling inconsistencies.
• transfer_to format may be wrong if you pass a raw number; SIP trunks often expect sip: or tel: URIs.
• Error handling catches exceptions but doesn’t include stack traces (use exc_info=True).
Detailed fixes and recommendations 1) Warm transfer: don’t call transfer_sip_participant() to finish a warm transfer
• What to do instead:
◦ Dial the supervisor with create_sip_participant(...).
◦ Wait for the supervisor participant to actually join the room (event-based wait), then let the caller and supervisor converse in the same room.
◦ When the handoff is complete, have the agent leave the room (which effectively completes the warm transfer).
• Why: transfer_sip_participant() is for cold transfers (SIP REFER style). Once both parties are in the same LiveKit room, no SIP transfer is required to bridge them.
2) Replace fixed sleep with event-driven wait
• Listen for participant join/connected events or await a promise/future that resolves when the supervisor’s participant identity appears in the room.
• Example approach: register a participant_connected handler and resolve an asyncio.Event/Future when the expected identity joins.
3) Use the job context for room name, participant identities, and API client
• Get room name and participant identity from get_job_context() (e.g., ctx.room.name, ctx.participant.identity) instead of config.agent.room_name.
• Reuse ctx.api if available rather than creating a new LiveKitAPI() each time. This avoids redundant initialization and keeps credential usage consistent.
4) Ensure correct transfer_to URI format
• Use a full SIP or tel URI for transfer_to, e.g. <sip:+15551234567@provider.exampl>e.com or tel:+15551234567.
• Some trunks/carriers require sip: format to preserve headers or caller ID.
5) Improve error logging
• Include tracebacks for easier debugging:
◦ logger.error("Warm transfer failed", exc_info=True)
6) Cold transfer: verify you have the correct room/participant/URI
• For cold transfers ensure participant_identity and room_name are exactly the active ones from context.
• Use `sip:`/`tel:` as above.
Concise corrected warm transfer flow (pseudo-outline)
1. ctx = get_job_context(); room_name = ctx.room.name; caller_identity = ctx.participant.identity; livekit_api = ctx.api
2. supervisor_identity = f"warm_transfer_{unique}"
3. await livekit_api.sip.create_sip_participant(CreateSIPParticipantRequest(..., participant_identity=supervisor_identity))
4. Wait for supervisor_identity to appear in room via event handler or awaited future
5. Once joined, allow conversation between caller and supervisor (agent can play intro then exit room)
6. Agent leaves room (no need to call transfer_sip_participant for the caller)
Other practical notes
• If you prefer a ready-made workflow, consider using the WarmTransferTask / prebuilt warm-transfer utilities for reliability.
• Ensure your SIP trunk supports transfers / REFER behavior if you plan to use cold transfers across trunks.
If you’d like, I can:
• Produce a small concrete code snippet showing an event-driven wait for the supervisor participant and the agent exit step, or
• Suggest exact changes to your functions to reuse get_job_context().api and replace sleep with a proper event/future.
Which would you prefer?
Sources: Pass UUI data to Genesys Cloud during SIP transfers | LiveKit | WarmTransferTask | LiveKit Documentation | Company directory phone assistant | LiveKit Documentation | Agents telephony integration | LiveKit Documentation | SIP Lifecycle Management Agent | LiveKit Documentationsilly-teacher-46683
03/09/2026, 6:30 AMtall-airline-16756
03/09/2026, 6:30 AMtel: URI by default.
2. Do you have access to get_job_context() that exposes ctx.api, ctx.room, and ctx.participant (as in your original code)? I’ll assume yes unless you tell me otherwise.
If those are fine, here’s a recommended implementation. It shows:
• cold_transfer_call: uses transfer_sip_participant with a tel: URI,
• warm_transfer_call: creates a SIP participant for the supervisor, waits for that participant to join using an event-driven wait (asyncio.Future), lets the caller and supervisor talk, and then removes the agent to complete handoff,
• reuses ctx.api and ctx.room from get_job_context(),
• improved logging and exception handling.
Code (adjust identifiers to match your codebase — this is a complete pattern to adapt):
import asyncio
import logging
from typing import Optional
logger = logging.getLogger(__name__)
# Replace these imports with the actual LiveKit API client / request types from your SDK
from livekit import api # placeholder import path
from livekit.api import CreateSIPParticipantRequest, TransferSIPParticipantRequest
# Helper to format transfer target
def format_transfer_uri(number_or_uri: str) -> str:
# If already looks like a SIP URI or tel: URI, return as-is
if number_or_uri.startswith("sip:") or number_or_uri.startswith("tel:"):
return number_or_uri
# Default to tel: for raw numbers (E.164)
return f"tel:{number_or_uri}"
class AgentService:
@staticmethod
async def transfer_call(number: str, mode: str = "cold"):
"""
Transfer call with either cold or warm transfer.
mode: 'cold' or 'warm'
number: E.164 or full URI
"""
if mode not in ("cold", "warm"):
raise ValueError("mode must be 'cold' or 'warm'")
if mode == "cold":
await AgentService.cold_transfer_call(number)
else:
await AgentService.warm_transfer_call(number)
@staticmethod
async def cold_transfer_call(number: str):
"""
Cold transfer: perform a SIP REFER-like transfer of the current participant to `number`.
"""
ctx = get_job_context()
room_name = ctx.room.name
participant_identity = ctx.participant.identity
livekit_api = ctx.api # reuse existing client from context
transfer_to = format_transfer_uri(number)
<http://logger.info|logger.info>(f"Starting cold transfer of participant={participant_identity} in room={room_name} to={transfer_to}")
try:
await livekit_api.sip.transfer_sip_participant(
TransferSIPParticipantRequest(
room_name=room_name,
participant_identity=participant_identity,
transfer_to=transfer_to,
)
)
<http://logger.info|logger.info>(f"Cold transfer initiated to {transfer_to}")
except Exception as e:
logger.error("Cold transfer failed", exc_info=True)
raise
@staticmethod
async def warm_transfer_call(number: str, wait_timeout: float = 60.0, trunk_id: Optional[str] = None):
"""
Warm transfer:
1) Dial the target (supervisor) into the same LiveKit room as a SIP participant.
2) Wait for the supervisor participant identity to appear in the room (event-driven).
3) Optionally let agent introduce, then remove the agent to complete handoff.
"""
ctx = get_job_context()
room = ctx.room
room_name = room.name
caller_identity = ctx.participant.identity
livekit_api = ctx.api
# Build a supervisor identity that we can watch for
supervisor_identity = f"warm_transfer_{int(asyncio.get_running_loop().time() * 1000)}"
transfer_target = format_transfer_uri(number)
<http://logger.info|logger.info>(f"Starting warm transfer: dialing {transfer_target} into room={room_name} as {supervisor_identity}")
# Prepare a future that'll be set when the supervisor participant joins
loop = asyncio.get_running_loop()
supervisor_joined: asyncio.Future = loop.create_future()
def on_participant_connected(ev_participant):
try:
pid = getattr(ev_participant, "identity", None)
if pid == supervisor_identity and not supervisor_joined.done():
<http://logger.info|logger.info>(f"Supervisor participant {pid} joined room {room_name}")
supervisor_joined.set_result(ev_participant)
except Exception as ex:
logger.exception("Error in on_participant_connected handler")
# Register event handler on the room for participant connected
# Adjust method name to your SDK: example uses room.on('participant_connected', handler)
room.on("participant_connected", on_participant_connected)
try:
# 1) Dial the supervisor into the room
create_req = CreateSIPParticipantRequest(
room_name=room_name,
sip_trunk_id=trunk_id,
sip_call_to=transfer_target,
participant_identity=supervisor_identity,
)
await livekit_api.sip.create_sip_participant(create_req)
<http://logger.info|logger.info>("create_sip_participant called; waiting for supervisor to join...")
# 2) Wait for the supervisor to actually appear in the room (event-driven)
try:
supervisor_participant = await asyncio.wait_for(supervisor_joined, timeout=wait_timeout)
except asyncio.TimeoutError:
logger.error("Timed out waiting for supervisor participant to join")
raise
# 3) Optional: agent can play an intro or speak to both parties
<http://logger.info|logger.info>("Supervisor joined; agent may introduce the call now.")
# Wait for an explicit signal to complete handoff, or optionally wait a short time:
# Here, we'll assume the agent signals completion by calling a function `complete_warm_transfer`.
# For demo, we'll wait a short fixed interval to simulate agent introduction.
await asyncio.sleep(2)
# 4) To complete the warm transfer: remove the agent from the room, leaving caller and supervisor together.
# Depending on your setup, you can:
# - unpublish agent tracks and call `room.local_participant.disconnect()`, or
# - issue a server-side remove participant API call.
<http://logger.info|logger.info>("Completing warm transfer by removing agent from the room (agent exits).")
# Example: server-side remove participant (if your API supports it).
# Use the correct admin API call in your SDK; placeholder below:
try:
await livekit_api.participants.remove_participant(room_name=room_name, identity=caller_identity)
<http://logger.info|logger.info>("Agent removed from room; warm transfer complete.")
except AttributeError:
# If remove_participant is not available, rely on context to disconnect agent client-side.
logger.warning("remove_participant API not available; ensure agent disconnects client-side to complete handoff.")
except Exception as e:
logger.error("Warm transfer failed", exc_info=True)
raise
finally:
# Cleanup: deregister event handler
try:
room.off("participant_connected", on_participant_connected)
except Exception:
# Some SDKs use different methods; ignore if not supported
pass
Notes and adaptations you must check
• Replace placeholder imports and request types (livekit.api, CreateSIPParticipantRequest, TransferSIPParticipantRequest) with the actual classes and client methods from the LiveKit SDK you use.
• room.on / room.off event registration API names may differ by SDK; adapt to the actual event binding API your version exposes. The key idea is to use an event-driven future instead of asyncio.sleep.
• The method I used to remove the agent at the end (livekit_api.participants.remove_participant) is a placeholder — confirm your SDK has an admin API to remove participants server-side, or have the agent client disconnect to complete the handoff.
• If you prefer the supervisor to be dialed and automatically answered before connecting to the room, check whether your trunk/provider or create_sip_participant supports wait_until_answered or similar options. If it does, set that to avoid false positives.
• Ensure your trunk and carrier support the operations you intend (REFER for cold transfer, incoming calls for create_sip_participant).
Would you like:
• A version adapted to a specific LiveKit SDK (Python package name/version) you’re using, or
• A snippet showing how the agent client can disconnect from the room to complete the warm handoff (client-side code)?
Tell me which SDK and whether you provide raw numbers or full SIP URIs, and I’ll adapt the code to be runnable in your environment.silly-teacher-46683
03/09/2026, 8:53 AM{
"level": "ERROR",
"name": "livekit.agents",
"message": "exception occurred while executing tool",
"exc_info": "Traceback (most recent call last):\n File \"C:\\Users\\Sanu Kumar\\Desktop\\AXLAI-Voicekit\\axlai-voice-agent\\.venv\\lib\\site-packages\\livekit\\agents\\voice\\generation.py\", line 591, in _traceable_fnc_tool\n val = await function_callable()\n File \"C:\\Users\\Sanu Kumar\\Desktop\\AXLAI-Voicekit\\axlai-voice-agent\\src\\services\\tool_service.py\", line 117, in _function\n result = await self._handle_transfer_call(raw_arguments)\n File \"C:\\Users\\Sanu Kumar\\Desktop\\AXLAI-Voicekit\\axlai-voice-agent\\src\\services\\tool_service.py\", line 287, in _handle_transfer_call\n return await AgentService.transfer_call(number=number, mode=mode)\n File \"C:\\Users\\Sanu Kumar\\Desktop\\AXLAI-Voicekit\\axlai-voice-agent\\src\\services\\agent_service.py\", line 87, in transfer_call\n await AgentService.cold_transfer_call(number)\n File \"C:\\Users\\Sanu Kumar\\Desktop\\AXLAI-Voicekit\\axlai-voice-agent\\src\\services\\agent_service.py\", line 100, in cold_transfer_call\n participant_identity = ctx.participant.identity\nAttributeError: 'JobContext' object has no attribute 'participant'",
"function": "transfer_call_1",
"speech_id": "speech_2a2480dd804c",
"timestamp": "2026-03-09T08:49:45.841296+00:00"
}tall-airline-16756
03/09/2026, 8:53 AMctx.participant.identity, but JobContext does not expose a participant attribute.
Why this happens
• JobContext does not include participant as a direct attribute. The JobContext gives you room, job, proc, and helper methods — but not a participant property.
• The active/calling participant(s) are available from ctx.room.remote_participants (or via ctx.wait_for_participant() / RoomIO linked participant), not ctx.participant.
How to fix it (concise, actionable)
• Replace accesses to ctx.participant.identity with one of these approaches depending on your situation:
Option A — If you want the first connected remote participant:
1. Inspect or pick the participant from ctx.room.remote_participants.
2. Use that participant’s identity.
Example:
ctx = get_job_context()
room = ctx.room
# pick the first remote participant (adjust filtering as needed)
remote_participants = list(room.remote_participants.values())
if not remote_participants:
raise RuntimeError("No remote participants in room")
caller_identity = remote_participants[0].identity
Option B — Wait for (and get) a participant when you expect one to join:
ctx = get_job_context()
# this helper may exist in your environment - otherwise implement an event waiter
participant = await ctx.wait_for_participant(timeout=15) # wait for the caller
caller_identity = participant.identity
Option C — If you already have caller identity stored when the session started:
• Store the identity in session/userdata when creating the job and read it back:
caller_identity = ctx.session.userdata.get("caller_identity")
if not caller_identity:
raise RuntimeError("caller_identity not found in session userdata")
Updated cold transfer example using Option A
async def cold_transfer_call(number: str):
ctx = get_job_context()
room_name = ctx.room.name
# Find a remote participant to transfer (adjust selection logic for your case)
remote_participants = list(ctx.room.remote_participants.values())
if not remote_participants:
logger.error("No remote participants available for transfer")
raise RuntimeError("no participant to transfer")
participant_identity = remote_participants[0].identity
transfer_to = f"tel:{number}" # or sip:... depending on your trunk
await ctx.api.sip.transfer_sip_participant(
TransferSIPParticipantRequest(
room_name=room_name,
participant_identity=participant_identity,
transfer_to=transfer_to,
)
)
Extra debugging tips
• Log current room participants before transfer:
for p in ctx.room.remote_participants.values():
<http://logger.info|logger.info>(f"participant: identity={p.identity}, kind={p.kind}")
• If multiple participant types exist (bots, supervisors, SIP participants), filter by identity pattern or participant attributes to pick the correct one.
• If you need the participant tied to voice processing (RoomIO), check for a linked participant API or use ctx.room events.
If you want, I can:
• Provide a tailored snippet that matches your original AgentService functions (cold/warm) replacing ctx.participant everywhere, or
• Show a small utility function that finds the caller participant with configurable filters (identity prefix, participant kind, or metadata).
Which would help you most now?
Sources: Agents (Node.js) | livekit/livekit_composite | DeepWiki | livekit/livekit_composite | DeepWiki | Job lifecycle | LiveKit Documentation