glamorous-oil-12326
12/27/2025, 1:02 AMagent_turn when interrupted during a function tool call. Even when the user waits for a while and asks if the agent is still there, the agent continues being stuck.tall-airline-16756
12/27/2025, 1:03 AMtall-airline-16756
12/27/2025, 1:03 AMagent_turn after being interrupted during a function/tool call and doesn't recover. I can help troubleshoot — a few likely causes and quick checks first, then a couple of targeted questions so I can give precise steps.
What you can try now
• Ensure the tool checks for interruptions and cancels long-running tasks. Use run_ctx.speech_handle.wait_if_not_interrupted() when awaiting async work so the tool can detect interruption and cancel (example below).
• If the tool must be atomic, call run_ctx.disallow_interruptions() at the start of the tool so it cannot be preempted during critical sections.
• Make async tasks cancellable and cancel them if run_ctx.speech_handle.interrupted is true (cancel the future and return early).
• Check whether preemptive_generation is enabled — that can change interruption behavior and lead to unexpected stuck states.
• Verify your deployment CPU performance (avoid burst-limited instances like t3/t4 that may throttle), since throttling can delay turn-detection/timeouts and leave the agent appearing stuck.
Example pattern to use in tools
wait_for_result = asyncio.ensure_future(self._a_long_running_task(query))
await run_ctx.speech_handle.wait_if_not_interrupted([wait_for_result])
if run_ctx.speech_handle.interrupted:
wait_for_result.cancel()
return None
# continue processing normally
What I need from you to dig deeper
• Agent SDK version
• Full code (or relevant excerpt) of the tool where the interruption happens
• Whether you call run_ctx.disallow_interruptions() anywhere and whether interruptions are allowed for that tool
• Whether preemptive_generation is enabled
• Deployment environment (cloud/instance type, e.g., AWS and instance family)
If you share those details (especially the tool code and SDK version), I’ll provide precise code fixes and checks to stop the agent from getting stuck.
Sources: Tool definition and use | LiveKit docs | Uninterruptable Agent | LiveKit docs | Troubleshooting Latency and Timeout Errors with Turn Detection on AWSglamorous-oil-12326
12/27/2025, 1:18 AMtall-airline-16756
12/27/2025, 1:18 AMagent_turn during call transfers or transfers-to-agent, and you have preemptive_generation enabled. I can help debug this — a few targeted checks and then the exact bits of info I need to diagnose and give a fix.
Quick checks to try now
• Temporarily disable preemptive_generation and reproduce the transfer. Preemptive generation can change interruption timing and often causes stuck transfers; disabling it isolates whether it’s the cause.
• Confirm you are not calling run_ctx.disallow_interruptions() during the transfer flow. If you are, remove it or limit its scope so transfer logic can be interrupted or detect cancellation.
• Make transfer operations cancellable even if they’re not "long-running": wrap the transfer call in a cancellable future and use run_ctx.speech_handle.wait_if_not_interrupted() so the agent can detect interruption and abort the transfer attempt.
What I need from you to continue
• The transfer tool code (both the path that transfers the call and the path that transfers to another agent) — the exact function(s) where the transfer happens.
• Whether you call run_ctx.disallow_interruptions() anywhere in those tool(s).
• Any custom interruption or timeout logic you’ve added.
• Whether the transfer uses external blocking calls (SIP/telephony API, network I/O) and whether those calls are cancellable.
• Deployment environment (cloud and instance type) and whether you’ve observed CPU throttling during reproductions.
If you prefer, paste the transfer function(s) here and I’ll point out the minimal changes (cancellable future + wait_if_not_interrupted + cancel on interrupted) tailored to your code.glamorous-oil-12326
12/27/2025, 1:19 AMtall-airline-16756
12/27/2025, 1:19 AMeager_eot when you disable preemptive_generation — they are independent settings.
What they do
• preemptive_generation controls whether the agent pre-generates output (which changes timing around interruptions). Disabling it helps isolate interruption timing issues.
• eager_eot controls end-of-turn detection behavior (whether the agent treats silence/short cues as end-of-turn earlier). It does not directly depend on preemptive_generation.
Recommended steps
1. Disable preemptive_generation first and reproduce the transfer issue to see if it resolves the stuck agent_turn.
2. If the issue persists, try toggling eager_eot to see if end-of-turn timing interacts with your transfer flow — especially if transfers rely on precise turn boundaries.
3. Meanwhile, ensure your transfer logic is cancellable:
◦ Wrap transfer calls in a cancellable future.
◦ Use run_ctx.speech_handle.wait_if_not_interrupted() and cancel the future if run_ctx.speech_handle.interrupted.
1. Report results (whether disabling preemptive_generation fixed it, and whether changing eager_eot changed behavior). If it still gets stuck, paste the transfer function code and I’ll give exact code edits.
If you want, I can show the minimal code patch to wrap a transfer call with wait_if_not_interrupted() and cancellation. Which would you prefer?glamorous-oil-12326
12/27/2025, 1:20 AMtall-airline-16756
12/27/2025, 1:21 AMimport asyncio
async def transfer_call_tool(self, run_ctx, transfer_params):
# start transfer as a cancellable future
transfer_task = asyncio.ensure_future(self._perform_transfer(transfer_params))
# wait but abort if an interruption occurs
await run_ctx.speech_handle.wait_if_not_interrupted([transfer_task])
if run_ctx.speech_handle.interrupted:
transfer_task.cancel()
return {"status": "cancelled_by_interrupt"}
# transfer completed successfully
result = await transfer_task
return {"status": "ok", "result": result}
Use the same pattern for transferring to another agent:
async def transfer_to_agent_tool(self, run_ctx, agent_id, metadata):
transfer_task = asyncio.ensure_future(self._transfer_to_agent(agent_id, metadata))
await run_ctx.speech_handle.wait_if_not_interrupted([transfer_task])
if run_ctx.speech_handle.interrupted:
transfer_task.cancel()
return {"status": "cancelled_by_interrupt"}
return {"status": "ok", "result": await transfer_task}
2) Short critical section — disallow interruptions for a minimal atomic portion (only if truly needed):
async def atomic_transfer_tool(self, run_ctx, params):
# prevent interruptions for the atomic part
run_ctx.disallow_interruptions()
try:
# perform only the minimal non-interruptible steps here
transfer_handle = await self._start_transfer_atomic(params)
finally:
run_ctx.allow_interruptions() # or exit scope to re-enable
# now wait for completion but still be interrupt-aware
transfer_task = asyncio.ensure_future(self._finish_transfer(transfer_handle))
await run_ctx.speech_handle.wait_if_not_interrupted([transfer_task])
if run_ctx.speech_handle.interrupted:
transfer_task.cancel()
return {"status": "cancelled_by_interrupt"}
return {"status": "ok", "result": await transfer_task}
Notes:
• Keep the non-interruptible section as short as possible.
• Ensure underlying transfer functions (_perform_transfer, _transfer_to_agent, _finish_transfer) support cancellation or clean up when cancelled.
• After applying this pattern, reproduce with preemptive_generation on and off to confirm behavior.
If you paste your actual transfer function, I’ll show the exact minimal edits to apply these patterns.glamorous-oil-12326
12/27/2025, 1:26 AMtall-airline-16756
12/27/2025, 1:26 AMrun_ctx.speech_handle.wait_if_not_interrupted() and run_ctx.speech_handle.interrupted).
• This change gives developers clearer control over which parts of a tool are cancelable versus which must be atomic, and avoids hidden cancellations that could cause inconsistent state during complex operations (like transfers).
Why you’re seeing the stuck behavior
• If a transfer/blocking call isn’t wrapped in a cancellable future or checked with wait_if_not_interrupted(), the agent won’t detect the interruption and will remain awaiting the blocking call — appearing stuck in agent_turn.
• preemptive_generation also affects interruption timing; with it enabled you may observe different preemption points than in 1.2, so combining preemptive generation with missing cancellation checks increases chance of stuck turns.
What to do (summary)
• Wrap transfer calls in an asyncio future and use await run_ctx.speech_handle.wait_if_not_interrupted([future]).
• If interrupted, cancel the future and return early.
• If a tiny atomic step must be non-interruptible, keep that section extremely short and use run_ctx.disallow_interruptions() only around those minimal lines.
• Test with preemptive_generation disabled first to see if behavior changes, then re-enable once transfer code is interruption-safe.
If you want, paste your transfer function and I’ll show the exact minimal edits for 1.3 to make it robust to interruptions.glamorous-oil-12326
12/27/2025, 1:28 AMtall-airline-16756
12/27/2025, 1:28 AMSpeechHandle (e.g., wait_if_not_interrupted() / speech_handle.interrupted) or use run_ctx.disallow_interruptions() for short atomic sections.
Direct references
• Docs: Tool interruption guidance and APIs
https://docs.livekit.io/agents/logic/tools/ (See the section on interruptions and wait_if_not_interrupted())
• Key code paths showing the behavior change:
◦ Agent activity interruption logic (handles speech interruption and cancellation):
https://github.com/livekit/livekit_composite/blob/5558b91a/livekit/agents/livekit-agents/livekit/agents/voice/agent_activity.py#L934-L969
• Look for speech/interrupt handling utilities in the agents repo (search speech_handle, wait_if_not_interrupted, disallow_interruptions).
• Changelog / release notes for v1.3.0 (describes the behavioral change):
https://github.com/livekit/agents/blob/main/CHANGELOG.md
How to find the exact commit diffs
1. Open the agents repo commit history and search around the v1.3.0 timeframe:
https://github.com/livekit/agents/commits/main
1. Search commit messages for keywords: interruption, interrupt, speech_handle, wait_if_not_interrupted, agent_activity.
2. Inspect diffs for files under livekit/agents/voice/ (notably agent_activity.py and any speech_handle implementation) to see the explicit API additions and removal of implicit cancellation behavior.
If you want, I can:
• Search the repo for the exact commit hashes that introduced the wait_if_not_interrupted() change and paste those commit links, or
• Inspect your transfer function and show exact minimal edits to make it interruption-safe for v1.3.10.
Which would you prefer?
Sources: Tool definition and use | LiveKit docs | examples/voice_agents | livekit/livekit_composite | DeepWiki