"""A long tool call must not get the pipeline cancelled. Regression test: pipecat's idle timer only resets on speech frames, so a slow turn looked identical to an abandoned session and cancelled the worker and the runner mid-answer. """ import asyncio, sys from pathlib import Path sys.path.insert(0, str(Path(__file__).parent)) from pipecat.frames.frames import EndFrame, Frame, TTSSpeakFrame from pipecat.pipeline.pipeline import Pipeline from pipecat.pipeline.worker import PipelineParams, PipelineWorker from pipecat.processors.frame_processor import FrameDirection, FrameProcessor from pipecat.workers.runner import WorkerRunner class Probe(FrameProcessor): async def process_frame(self, f, d): await super().process_frame(f, d) await self.push_frame(f, d) async def run(idle_timeout_secs, label, *, expect_cancelled, quiet_for=4.0): worker = PipelineWorker( Pipeline([Probe()]), params=PipelineParams(), idle_timeout_secs=idle_timeout_secs, ) cancelled = {"yes": False} @worker.event_handler("on_idle_timeout") async def _(w): cancelled["yes"] = True runner = WorkerRunner(handle_sigint=False) await runner.add_workers(worker) task = asyncio.create_task(runner.run()) await asyncio.sleep(quiet_for) # stand in for a slow tool call alive = not task.done() await worker.queue_frames([EndFrame()]) try: await asyncio.wait_for(task, timeout=10) except Exception: pass got_cancelled = cancelled["yes"] or not alive verdict = "PASS" if got_cancelled == expect_cancelled else "FAIL" print(f" {verdict} {label}: cancelled={got_cancelled} (expected {expect_cancelled})") async def main(): # The first shows the bug is real; the second shows the fix holds. await run(2.0, "a short idle timeout still cancels", expect_cancelled=True) await run(None, "disabled: a slow turn survives", expect_cancelled=False) asyncio.run(main())