@@ -1279,6 +1279,8 @@ async def _event_stream_handler(
12791279 ctx: The run context.
12801280 events: Async iterable of streaming events (PartStartEvent, PartDeltaEvent, etc.).
12811281 """
1282+ import time as time_module
1283+
12821284 from pydantic_ai import PartDeltaEvent , PartStartEvent
12831285 from pydantic_ai .messages import TextPartDelta , ThinkingPartDelta
12841286 from rich .console import Console
@@ -1299,6 +1301,8 @@ async def _event_stream_handler(
12991301 text_buffer : dict [int , list [str ]] = {} # Buffer text for markdown
13001302 live_displays : dict [int , Live ] = {} # Live displays for streaming markdown
13011303 did_stream_anything = False # Track if we streamed any content
1304+ last_render_time : dict [int , float ] = {} # Track last render time per part
1305+ render_interval = 0.1 # Only re-render markdown every 100ms (throttle)
13021306
13031307 def _print_thinking_banner () -> None :
13041308 """Print the THINKING banner with spinner pause and line clear."""
@@ -1383,19 +1387,27 @@ def _print_response_banner() -> None:
13831387 Markdown ("" ),
13841388 console = console ,
13851389 refresh_per_second = 10 ,
1390+ vertical_overflow = "visible" , # Allow scrolling for long content
13861391 )
13871392 live .start ()
13881393 live_displays [event .index ] = live
1389- # Accumulate and update markdown
1394+ # Accumulate text and throttle markdown rendering
1395+ # (Markdown parsing is O(n), doing it on every token = O(n²) death)
13901396 text_buffer [event .index ].append (delta .content_delta )
1391- content = "" .join (text_buffer [event .index ])
1392- if event .index in live_displays :
1393- try :
1394- live_displays [event .index ].update (
1395- Markdown (content )
1396- )
1397- except Exception :
1398- pass
1397+ now = time_module .monotonic ()
1398+ last_render = last_render_time .get (event .index , 0 )
1399+
1400+ # Only re-render if enough time has passed (throttle)
1401+ if now - last_render >= render_interval :
1402+ content = "" .join (text_buffer [event .index ])
1403+ if event .index in live_displays :
1404+ try :
1405+ live_displays [event .index ].update (
1406+ Markdown (content )
1407+ )
1408+ last_render_time [event .index ] = now
1409+ except Exception :
1410+ pass
13991411 else :
14001412 # For thinking parts, stream immediately (dim)
14011413 if event .index not in banner_printed :
@@ -1407,8 +1419,18 @@ def _print_response_banner() -> None:
14071419 # PartEndEvent - finish the streaming with a newline
14081420 elif isinstance (event , PartEndEvent ):
14091421 if event .index in streaming_parts :
1410- # For text parts, stop the Live display
1422+ # For text parts, do final render then stop the Live display
14111423 if event .index in text_parts :
1424+ # Final render to ensure we show complete content
1425+ # (throttling may have skipped the last few tokens)
1426+ if event .index in live_displays and event .index in text_buffer :
1427+ try :
1428+ final_content = "" .join (text_buffer [event .index ])
1429+ live_displays [event .index ].update (
1430+ Markdown (final_content )
1431+ )
1432+ except Exception :
1433+ pass
14121434 if event .index in live_displays :
14131435 try :
14141436 live_displays [event .index ].stop ()
@@ -1417,6 +1439,8 @@ def _print_response_banner() -> None:
14171439 del live_displays [event .index ]
14181440 if event .index in text_buffer :
14191441 del text_buffer [event .index ]
1442+ # Clean up render time tracking
1443+ last_render_time .pop (event .index , None )
14201444 # For thinking parts, just print newline
14211445 elif event .index in banner_printed :
14221446 console .print () # Final newline after streaming
0 commit comments