s
Hey
t
Thanks for reaching out, Sanu. I'm an AI assistant and I'm here to help. Give me a moment while I research this topic.
Hi — thanks for reaching out! I’m here to help. What can I do for you today? If you’re not sure how to describe it, a few examples of helpful details: • Are you trying to set up a room, connect a client, or debug an error? • Any error messages, logs, room IDs, or project IDs you can share? • Which SDK or platform are you using (Web, iOS, Android, or server)? • Is this a production or a development/testing issue? Tell me what you’re seeing and what you want to achieve, and I’ll guide you through the next steps.
s
I am trying to implement transferring calls to other humans. can u tell me if any thing is wrong in my code? @staticmethod async def transfer_call(number: str, mode: str = "cold"): """ Transfer call with either cold or warm transfer. """ session_ctx = get_job_context() participant_identity = session_ctx.participant.identity if mode == "cold": await AgentService.cold_transfer_call( number, participant_identity ) elif mode == "warm": await AgentService.warm_transfer_call( number, config.telephony.outbound_trunk_id, ) else: raise ValueError("mode must be 'cold' or 'warm'") @staticmethod async def cold_transfer_call(number: str, participant_identity: str): logger.info(f"Starting cold transfer to {number}") livekit_api = api.LiveKitAPI( api_key=LIVEKIT_API_KEY, api_secret=LIVEKIT_API_SECRET, url=LIVEKIT_URL ) try: await livekit_api.sip.transfer_sip_participant( TransferSIPParticipantRequest( room_name=config.agent.room_name, participant_identity=participant_identity, transfer_to=number, ) ) logger.info(f"Cold transfer initiated to {number}") except Exception as e: logger.error(f"Cold transfer failed: {str(e)}") finally: await livekit_api.aclose() @staticmethod async def warm_transfer_call(number: str, trunk_id: str): logger.info(f"Starting warm transfer to {number}") livekit_api = api.LiveKitAPI( api_key=LIVEKIT_API_KEY, api_secret=LIVEKIT_API_SECRET, url=LIVEKIT_URL ) try: logger.info("Dialing target participant for warm transfer") await livekit_api.sip.create_sip_participant( CreateSIPParticipantRequest( room_name=config.agent.room_name, sip_trunk_id=trunk_id, sip_call_to=number, participant_identity=f"warm_transfer_{number}" ) ) logger.info("Waiting for target to join room") await asyncio.sleep(5) session_ctx = get_job_context() caller_identity = session_ctx.participant.identity logger.info("Completing warm transfer") await livekit_api.sip.transfer_sip_participant( TransferSIPParticipantRequest( room_name=config.agent.room_name, participant_identity=caller_identity, transfer_to=number, ) ) logger.info("Warm transfer completed") except Exception as e: logger.error(f"Warm transfer failed: {str(e)}") finally: await livekit_api.aclose()
t
Thanks — this is very helpful. I reviewed your code and walked through both the cold and warm transfer paths. Below I’ll: 1) summarize what’s correct, 2) call out the problems you should fix, and 3) give concise, actionable changes you can apply. Acknowledgement • I’m responding to your request to verify whether anything is wrong with your cold and warm SIP transfer implementation and to suggest fixes. What’s working • Cold transfer: using
transfer_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 Documentation
s
Can u give me a modified working fullproof code?
t
Thanks — I’ll provide a robust, self-contained example that fixes the issues we discussed: reuse job context API, use the actual room and participant identities from context, use event-driven waiting instead of sleep for warm transfers, format SIP/tel URIs, and include better error logging. Before I give the code, two quick clarifying questions so I tailor this precisely to your environment: 1. Do you want the transfer destination passed as a raw E.164 number like "+15551234567", or as a full SIP URI (e.g., "sip:+15551234567@provider.example.com")? If you pass a raw number I’ll convert it to a
tel:
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):
Copy code
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.
s
What is this issue now?
Copy code
{
  "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"
}
t
Thanks — this error message is clear and we can fix it. I’ll explain what the error means, why it’s happening, and give a small, correct code snippet you can drop into your service to resolve it. What the error means • The traceback shows: AttributeError: 'JobContext' object has no attribute 'participant'. • That means your code tried to read
ctx.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:
Copy code
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:
Copy code
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:
Copy code
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
Copy code
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:
Copy code
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