"""Silence during a slow tool call must be broken. From a recorded session: four of nine turns went unanswered because tool-heavy turns ran for over a minute with no sound, and speaking again to check whether it was alive cancelled the turn in flight. """ import asyncio, sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from claude_llm import ClaudeCodeLLM from pipecat.frames.frames import TTSSpeakFrame class Recorder(ClaudeCodeLLM): def __init__(self, **kw): super().__init__(options=None, **kw) self.pushed = [] async def push_frame(self, frame, direction=None): self.pushed.append(frame) async def main(): # Tool runs before anything has been said: the user hears nothing otherwise. llm = Recorder() llm._said_working = False await llm._say_working(spoken=[]) said = [f.text for f in llm.pushed if isinstance(f, TTSSpeakFrame)] print(f" {'PASS' if said else 'FAIL'} speaks up when a tool runs first: {said}") # Only once per turn, however many tools run. await llm._say_working(spoken=[]) await llm._say_working(spoken=[]) said = [f.text for f in llm.pushed if isinstance(f, TTSSpeakFrame)] print(f" {'PASS' if len(said) == 1 else 'FAIL'} says it only once per turn ({len(said)})") # Already answering: adding a filler would talk over the real reply. llm2 = Recorder() llm2._said_working = False await llm2._say_working(spoken=["Running that now."]) quiet = not [f for f in llm2.pushed if isinstance(f, TTSSpeakFrame)] print(f" {'PASS' if quiet else 'FAIL'} stays quiet when it already narrated") # Opted out. llm3 = Recorder(working_phrase=None) llm3._said_working = False await llm3._say_working(spoken=[]) quiet = not [f for f in llm3.pushed if isinstance(f, TTSSpeakFrame)] print(f" {'PASS' if quiet else 'FAIL'} respects working_phrase=None") asyncio.run(main())