Bug Description
When a user interrupts the bot after FunctionCallsStartedFrame is emitted but before FunctionCallInProgressFrame arrives, no FunctionCallCancelFrame is emitted by pipecat. This leaves an orphan entry in _function_calls_in_progress (with value None) that is never cleaned up.
This orphan blocks pipecat-flows transitions: _check_and_execute_transition checks has_function_calls_in_progress, which returns True because of the orphan entry, so the transition callback never fires and the flow gets permanently stuck.
Steps to Reproduce
- Set up a pipecat-flows application with an edge function that triggers a node transition
- Have the LLM call the function →
FunctionCallsStartedFrame is emitted, entry added to _function_calls_in_progress with value None
- User interrupts (speaks over the bot) before
FunctionCallInProgressFrame arrives
- No
FunctionCallCancelFrame is emitted for the orphan
- LLM retries on the next turn and calls the same function again
- The retry completes successfully (
FunctionCallsStartedFrame → FunctionCallInProgressFrame → FunctionCallResultFrame)
_check_and_execute_transition runs but has_function_calls_in_progress is True because the orphan from step 2 is still there
- The node transition never fires — the flow is stuck
Minimal Reproduction
import asyncio
from pipecat.frames.frames import (
FunctionCallFromLLM,
FunctionCallsStartedFrame,
)
from pipecat.processors.aggregators.llm_context import LLMContext
from pipecat.processors.aggregators.llm_response_universal import LLMAssistantAggregator
async def reproduce():
context = LLMContext()
aggregator = LLMAssistantAggregator(context)
# Turn 1: FunctionCallsStartedFrame fires, then user interrupts
call_1 = FunctionCallFromLLM(
context=context,
tool_call_id="call_1",
function_name="save_notes",
arguments={},
)
# Simulate: FunctionCallsStartedFrame adds orphan entry
aggregator._function_calls_in_progress[call_1.tool_call_id] = None
# Simulate: InterruptionFrame arrives — no FunctionCallCancelFrame emitted
# aggregator._function_calls_in_progress still has {"call_1": None}
# Turn 2: LLM retries with a new call ID
call_2 = FunctionCallFromLLM(
context=context,
tool_call_id="call_2",
function_name="save_notes",
arguments={},
)
aggregator._function_calls_in_progress[call_2.tool_call_id] = None
# call_2 completes normally
del aggregator._function_calls_in_progress[call_2.tool_call_id]
# BUG: call_1 orphan is still there
print(f"has_function_calls_in_progress: {aggregator.has_function_calls_in_progress}")
# Prints: True — pipecat-flows transition is blocked forever
asyncio.run(reproduce())
Expected Behavior
Either:
- pipecat should emit
FunctionCallCancelFrame for function calls that have FunctionCallsStartedFrame but no FunctionCallInProgressFrame when an interruption occurs
- Or
_handle_function_calls_started / _check_and_execute_transition should handle orphan entries (value=None) from previous turns
Actual Behavior
The orphan entry persists forever, causing has_function_calls_in_progress to return True and blocking all pipecat-flows transitions.
Impact
In production, this causes the bot to get stuck after completing a booking — the node transition to the confirmation/goodbye node never fires, so the end_call tool is never available. The bot keeps chatting with the user until the pipeline times out.
Related
Workaround
We work around this in our custom LLMAssistantAggregator subclass by clearing orphan entries (value=None) in _handle_function_calls_started before calling super():
async def _handle_function_calls_started(self, frame):
new_ids = {fc.tool_call_id for fc in frame.function_calls}
orphan_ids = [
tid for tid, val in self._function_calls_in_progress.items()
if val is None and tid not in new_ids
]
for tid in orphan_ids:
del self._function_calls_in_progress[tid]
await super()._handle_function_calls_started(frame)
Environment
- pipecat version: 0.0.107
- pipecat-flows version: 0.0.23
- LLM: OpenAI GPT
- Transport: Twilio/WebSocket
Bug Description
When a user interrupts the bot after
FunctionCallsStartedFrameis emitted but beforeFunctionCallInProgressFramearrives, noFunctionCallCancelFrameis emitted by pipecat. This leaves an orphan entry in_function_calls_in_progress(with valueNone) that is never cleaned up.This orphan blocks pipecat-flows transitions:
_check_and_execute_transitioncheckshas_function_calls_in_progress, which returnsTruebecause of the orphan entry, so the transition callback never fires and the flow gets permanently stuck.Steps to Reproduce
FunctionCallsStartedFrameis emitted, entry added to_function_calls_in_progresswith valueNoneFunctionCallInProgressFramearrivesFunctionCallCancelFrameis emitted for the orphanFunctionCallsStartedFrame→FunctionCallInProgressFrame→FunctionCallResultFrame)_check_and_execute_transitionruns buthas_function_calls_in_progressisTruebecause the orphan from step 2 is still thereMinimal Reproduction
Expected Behavior
Either:
FunctionCallCancelFramefor function calls that haveFunctionCallsStartedFramebut noFunctionCallInProgressFramewhen an interruption occurs_handle_function_calls_started/_check_and_execute_transitionshould handle orphan entries (value=None) from previous turnsActual Behavior
The orphan entry persists forever, causing
has_function_calls_in_progressto returnTrueand blocking all pipecat-flows transitions.Impact
In production, this causes the bot to get stuck after completing a booking — the node transition to the confirmation/goodbye node never fires, so the
end_calltool is never available. The bot keeps chatting with the user until the pipeline times out.Related
Workaround
We work around this in our custom
LLMAssistantAggregatorsubclass by clearing orphan entries (value=None) in_handle_function_calls_startedbefore callingsuper():Environment