Describe the bug
When the caller hangs up during a warm-transfer consultation after the human agent has answered but before the merge, WarmTransferTask correctly interrupts the active briefing. It then calls generateReply() with callerHangupInstruction.
Because that generation still has the consultation persona, briefing context, and the agent's interrupted turn, the model can finish or repeat the briefing before complying with the hangup instruction.
In a reproduced call, the human agent heard:
- Initial briefing interrupted at:
"Carrier is unknown. Want me to"
- Hangup reply:
"Carrier is unknown. Want me to connect or repeat?\n\nThe caller has already left the line, so I am ending the call now."
The second reply offers an impossible action after the caller has already disconnected. It also makes the human agent wait through repeated context before learning why the call is ending.
This is not an interruption-mechanics issue: session.interrupt() does stop the active playout. The problem is using contextual generation for a terminal workflow notification. toolChoice: 'none' prevents tool calls, but it does not constrain the generated text to only the hangup notice.
Expected behavior
Once the caller leaves:
- Interrupt any active briefing.
- Play exactly one short, deterministic notice, for example: “The caller disconnected before I could connect you. Hanging up now.”
- Await that notice with a bounded timeout.
- Shut down the consultation session.
The notice must not repeat, complete, or summarize the briefing.
The existing silent-cancellation behavior should remain unchanged when the transfer destination has not answered yet.
Why a deterministic path is needed
Caller abandonment is a terminal state transition with one known meaning, not a conversational turn that benefits from LLM reasoning.
A deterministic notification provides:
- Exact semantics: it cannot offer to connect or repeat after the caller is gone.
- Bounded teardown latency without another LLM inference.
- A path that can avoid both LLM and TTS availability by using prerecorded audio.
- Direct regression testing: interrupt → play one notice → await playout → shutdown.
- Application-controlled wording, language, and voice.
A stronger callerHangupInstruction cannot guarantee this because the model still receives the interrupted consultation context.
Proposed API direction
It would be useful for WarmTransferTask to accept a deterministic notice with required text and optional caller-provided audio. For example:
interface CallerHangupNotice {
/** Exact text used for transcript output and TTS when no audio is supplied. */
text: string;
/** Optional prerecorded audio, created only if the caller actually hangs up. */
audio?: () => ReadableStream<AudioFrame>;
}
interface WarmTransferTaskOptions {
callerHangupNotice?: CallerHangupNotice;
}
The task would retain ownership of interruption and teardown:
const handle = session.say(notice.text, {
audio: notice.audio?.(),
allowInterruptions: false,
addToChatCtx: false,
});
await waitUntilAborted(handle.waitForPlayout(), AbortSignal.timeout(10_000));
session.shutdown();
This supports both:
- Fixed text through the configured TTS when only
text is provided.
- Prerecorded playback, while preserving exact text for transcripts, when
audio is also provided.
The API shape above is illustrative. The important capability is a non-LLM notification path. Existing callerHangupInstruction / generateReply() behavior could remain the default when no deterministic notice is configured, preserving backward compatibility.
Relevant log output
playout completed with interrupt
message: "Carrier is unknown. Want me to"
playout completed without interruption
message: "Carrier is unknown. Want me to connect or repeat?
The caller has already left the line, so I am ending the call now."
Describe your environment
@livekit/agents: 1.5.5
@livekit/rtc-node: 0.13.31
livekit-server-sdk: 2.15.3
- Node.js: 22.x
- OS: macOS
- Workflow: inbound SIP caller,
createWarmTransferTask, outbound SIP warm transfer
The current upstream main implementation still uses the same session.interrupt() → session.generateReply() path:
https://github.com/livekit/agents-js/blob/d4edb58c1f7dc1636f516ef7d379da3a473d97b4/agents/src/workflows/warm_transfer.ts
Minimal reproducible example
- Start a warm transfer with consultation instructions that produce a multi-sentence briefing.
- Have the transfer destination answer.
- Let the transfer agent begin briefing the destination.
- Disconnect the original caller mid-sentence, before the calls are merged.
- Observe that the first briefing is interrupted, but the generated caller-hangup reply may continue or repeat the interrupted briefing before announcing that the caller left.
This requires a SIP trunk and two phone legs; it is not reproducible in the text-only playground.
Additional information
This behavior was introduced as part of the answered-human notification flow in #2008. That PR initially used a fixed say() message, then changed to contextual generateReply(). The reproduced behavior demonstrates why consultation context is a liability for this terminal notification.
I can open a PR implementing the deterministic notice path once there is agreement on the preferred API shape.
Describe the bug
When the caller hangs up during a warm-transfer consultation after the human agent has answered but before the merge,
WarmTransferTaskcorrectly interrupts the active briefing. It then callsgenerateReply()withcallerHangupInstruction.Because that generation still has the consultation persona, briefing context, and the agent's interrupted turn, the model can finish or repeat the briefing before complying with the hangup instruction.
In a reproduced call, the human agent heard:
"Carrier is unknown. Want me to""Carrier is unknown. Want me to connect or repeat?\n\nThe caller has already left the line, so I am ending the call now."The second reply offers an impossible action after the caller has already disconnected. It also makes the human agent wait through repeated context before learning why the call is ending.
This is not an interruption-mechanics issue:
session.interrupt()does stop the active playout. The problem is using contextual generation for a terminal workflow notification.toolChoice: 'none'prevents tool calls, but it does not constrain the generated text to only the hangup notice.Expected behavior
Once the caller leaves:
The notice must not repeat, complete, or summarize the briefing.
The existing silent-cancellation behavior should remain unchanged when the transfer destination has not answered yet.
Why a deterministic path is needed
Caller abandonment is a terminal state transition with one known meaning, not a conversational turn that benefits from LLM reasoning.
A deterministic notification provides:
A stronger
callerHangupInstructioncannot guarantee this because the model still receives the interrupted consultation context.Proposed API direction
It would be useful for
WarmTransferTaskto accept a deterministic notice with required text and optional caller-provided audio. For example:The task would retain ownership of interruption and teardown:
This supports both:
textis provided.audiois also provided.The API shape above is illustrative. The important capability is a non-LLM notification path. Existing
callerHangupInstruction/generateReply()behavior could remain the default when no deterministic notice is configured, preserving backward compatibility.Relevant log output
Describe your environment
@livekit/agents: 1.5.5@livekit/rtc-node: 0.13.31livekit-server-sdk: 2.15.3createWarmTransferTask, outbound SIP warm transferThe current upstream
mainimplementation still uses the samesession.interrupt()→session.generateReply()path:https://github.com/livekit/agents-js/blob/d4edb58c1f7dc1636f516ef7d379da3a473d97b4/agents/src/workflows/warm_transfer.ts
Minimal reproducible example
This requires a SIP trunk and two phone legs; it is not reproducible in the text-only playground.
Additional information
This behavior was introduced as part of the answered-human notification flow in #2008. That PR initially used a fixed
say()message, then changed to contextualgenerateReply(). The reproduced behavior demonstrates why consultation context is a liability for this terminal notification.I can open a PR implementing the deterministic notice path once there is agreement on the preferred API shape.