From 06719e48017edc6fe0d6f4d2f540aafc806e51a2 Mon Sep 17 00:00:00 2001 From: Xin Wang Date: Mon, 3 Aug 2026 16:30:18 +0800 Subject: [PATCH] fix: interrupt message speech on confirmation --- backend/services/brains/workflow_brain.py | 9 +++++++++ backend/tests/test_brains.py | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/backend/services/brains/workflow_brain.py b/backend/services/brains/workflow_brain.py index a68363e..52b7970 100644 --- a/backend/services/brains/workflow_brain.py +++ b/backend/services/brains/workflow_brain.py @@ -19,6 +19,7 @@ from pipecat.flows import ( NodeConfig, ) from pipecat.frames.frames import ( + InterruptionFrame, LLMRunFrame, LLMUpdateSettingsFrame, OutputTransportMessageUrgentFrame, @@ -1084,6 +1085,14 @@ class WorkflowBrain(BaseBrain): ), ) if result.succeeded: + if ( + completion_policy == MESSAGE_CONFIRMATION + and result.action == "confirmed" + ): + # The client-tool result closes the confirmation gate, while + # InterruptionFrame closes any speech still owned by this + # Message before the next node can enqueue new output. + await runtime.queue_frame(InterruptionFrame()) await self._emit_trace( "message_completed", nodeId=node_id, diff --git a/backend/tests/test_brains.py b/backend/tests/test_brains.py index 2687d55..f8a3943 100644 --- a/backend/tests/test_brains.py +++ b/backend/tests/test_brains.py @@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, patch from models import AssistantConfig, RuntimeTool from pipecat.flows import FlowManager from pipecat.frames.frames import ( + InterruptionFrame, LLMContextFrame, LLMFullResponseEndFrame, LLMFullResponseStartFrame, @@ -1475,6 +1476,13 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase): async def queue_frame(frame): if isinstance(frame, TTSSpeakFrame): events.append(("speech", frame.text)) + elif isinstance(frame, InterruptionFrame): + events.append("interrupted") + elif ( + isinstance(frame, OutputTransportMessageUrgentFrame) + and frame.message.get("event") == "message_completed" + ): + events.append("message_completed") class OrderedCallEnd(FakeCallEnd): def __init__(self): @@ -1536,6 +1544,7 @@ class WorkflowBrainTests(unittest.IsolatedAsyncioTestCase): self.assertEqual(result.action, "confirmed") self.assertFalse(call_end.playback_completion.done()) self.assertEqual(input_states, [False, True]) + self.assertEqual(events[-2:], ["interrupted", "message_completed"]) async def test_speech_only_message_waits_for_transport_playback(self): brain = WorkflowBrain(