#5779·pipecat

Interrupting the bot right as it calls a tool can leave the tool call stuck "in progress"

Author: markbackmanCreated Sep 15, 2026Updated Sep 18, 2026

Summary

If the user interrupts while the bot is still speaking the sentence it said just before calling a tool ("Let me check the weather…"), Pipecat can lose track of the cancellation. The tool call is cancelled, but the assistant aggregator still records it as running, and it stays that way for the rest of the session.

Which tool calls are affected

Only tool calls that are cancelled when the user interrupts (cancel_on_interruption=True). That's Pipecat's default for every way of registering a tool:

  • llm.register_function(...) without cancel_on_interruption
  • @tool_options, or @tool_options(cancel_on_interruption=True)
  • @tool on an LLMWorker

Async tool calls (cancel_on_interruption=False) keep running through an interruption, so they aren't affected.

Any bot with a TTS between the LLM and the assistant aggregator can hit this, which is nearly every voice bot. It happens with HTTP-based TTS services and WebSocket-based ones, including those that pause frame processing while audio is generated.

What users see

  • The LLM is told the tool is still running. The conversation history keeps an IN_PROGRESS result for a call that was cancelled. The model may keep waiting for that result or tell the user it's still working on it.
  • has_function_calls_in_progress stays True. Any code that waits for tool calls to finish before doing something waits forever.
  • Delegated backend work never finishes. BackendLLMWorker waits for tool calls to settle before it reports a run as done, so the caller waits until its timeout.

When it happens

  1. The LLM answers with some words and a tool call, e.g. "Let me check the weather." plus get_weather.
  2. The TTS starts speaking those words, and the tool starts running.
  3. The user interrupts while the TTS is still speaking.
  4. Pipecat cancels the tool, as it should for a cancellable tool call.
  5. The aggregator ends up with the tool call recorded as running anyway.

If the user interrupts after the TTS has finished that sentence, cleanup works correctly.

Why it happens

Two messages describe the tool call's lifecycle, and they travel through the pipeline differently:

  • "The tool call is running" (FunctionCallInProgressFrame) is an ordinary frame. It waits in line behind the words the TTS is still speaking. That's intentional: it keeps the conversation history in order (the bot's words first, then the tool call). See #3023.
  • "The tool call was cancelled" (FunctionCallCancelFrame) is a system frame, so it goes straight past anything busy, including the TTS.

So when the user interrupts mid-sentence, the aggregator receives the cancellation first:

  1. The cancellation arrives for a call the aggregator has no in-progress record for yet. It ignores it.
  2. The "running" message arrives. The aggregator records the call as running and writes IN_PROGRESS into the conversation history.
  3. Nothing else ever arrives for that call, because it was already cancelled. It stays "running" for good.

A related, rarer case: a tool cancelled before its handler starts never sends a "running" message at all. The aggregator's placeholder for it is never removed, with the same result.

Reproduction

A pipeline of a tool-calling LLM, a TTS and the assistant aggregator. The LLM streams "Let me check the weather. One" and then calls a slow tool registered with the default cancel_on_interruption=True. The user interrupts 100–300 ms later, while the TTS is still producing that sentence.

When the call gets stuck, the aggregator receives: cancel → interruption → "running". Afterward has_function_calls_in_progress is True, and the conversation history holds IN_PROGRESS. Tested against main at b7e50a1b0.

Approaches that don't work

  • Clear all tool-call tracking when an interruption happens (#4303). Async tools are meant to keep running through interruptions and report their result later. Clearing the tracking means that later result is thrown away, and the history keeps telling the model the tool is still running.
  • Make "the tool call is running" a system frame too, so both messages travel the same way. That fixes the stuck call, but the tool call and its result then land in the history before the words the bot spoke first. In release evals that broke Gemini and Vertex outright (400 "Requests ending with a model turn are not supported") and made Sarvam answer with filler instead of the weather. This is the #3023 problem again.

Related

  • #3661: same symptom, closed as not planned after the reporter worked around it.
  • #4302: part 1 describes this symptom (part 2, MCP error messages, is handled in #5734).
  • #4908: a related problem with the same placeholder entries: parallel tool calls can run the LLM twice.
  • #3023: why the "running" message has to wait in line behind the bot's words.