@@ -4432,19 +4432,51 @@ def _emit_retry_stream_event(self, *, attempt, max_attempts, delay, reason):
44324432 return
44334433 try :
44344434 from ..streaming .events import StreamEvent , StreamEventType
4435- emitter .emit (StreamEvent (
4436- type = StreamEventType .RETRY ,
4437- metadata = {
4438- "attempt" : attempt ,
4439- "max_attempts" : max_attempts ,
4440- "delay" : delay ,
4441- "reason" : reason ,
4442- },
4443- agent_id = getattr (self , 'name' , None ),
4435+ emitter .emit (self ._build_retry_stream_event (
4436+ StreamEvent , StreamEventType ,
4437+ attempt = attempt , max_attempts = max_attempts , delay = delay , reason = reason ,
44444438 ))
44454439 except Exception as _re :
44464440 logger .debug (f"Failed to emit RETRY stream event: { _re } " )
44474441
4442+ async def _aemit_retry_stream_event (self , * , attempt , max_attempts , delay , reason ):
4443+ """Async counterpart of ``_emit_retry_stream_event``.
4444+
4445+ Uses ``emit_async`` so consumers registered via the public
4446+ ``add_async_callback()`` API also receive RETRY events; ``emit`` alone
4447+ only visits synchronous callbacks. Same zero-overhead guard and
4448+ never-raise contract as the sync path.
4449+ """
4450+ emitter = getattr (self , 'stream_emitter' , None )
4451+ if emitter is None or not getattr (emitter , 'has_callbacks' , False ):
4452+ return
4453+ try :
4454+ from ..streaming .events import StreamEvent , StreamEventType
4455+ event = self ._build_retry_stream_event (
4456+ StreamEvent , StreamEventType ,
4457+ attempt = attempt , max_attempts = max_attempts , delay = delay , reason = reason ,
4458+ )
4459+ emit_async = getattr (emitter , 'emit_async' , None )
4460+ if emit_async is not None :
4461+ await emit_async (event )
4462+ else :
4463+ emitter .emit (event )
4464+ except Exception as _re :
4465+ logger .debug (f"Failed to emit RETRY stream event: { _re } " )
4466+
4467+ def _build_retry_stream_event (self , StreamEvent , StreamEventType , * , attempt , max_attempts , delay , reason ):
4468+ """Construct the RETRY ``StreamEvent`` shared by the sync/async emitters."""
4469+ return StreamEvent (
4470+ type = StreamEventType .RETRY ,
4471+ metadata = {
4472+ "attempt" : attempt ,
4473+ "max_attempts" : max_attempts ,
4474+ "delay" : delay ,
4475+ "reason" : reason ,
4476+ },
4477+ agent_id = getattr (self , 'name' , None ),
4478+ )
4479+
44484480 def _chat_completion_with_retry (self , messages , temperature = 1.0 , tools = None , stream = None , reasoning_steps = False , task_name = None , task_description = None , task_id = None , response_format = None , stream_callback = None , emit_events = True ):
44494481 """
44504482 Wrapper for _execute_unified_chat_completion that adds jittered exponential backoff retry logic.
@@ -4508,20 +4540,14 @@ def _chat_completion_with_retry(self, messages, temperature=1.0, tools=None, str
45084540
45094541 # Surface the retry as a first-class stream event so CLI/UI
45104542 # consumers can show a live "retrying in Ns" status instead of
4511- # appearing hung. Guarded by has_callbacks -> zero overhead when
4512- # nothing is listening.
4513- emitter = getattr (self , 'stream_emitter' , None )
4514- if emitter is not None and emitter .has_callbacks :
4515- from ..streaming .events import StreamEvent , StreamEventType
4516- emitter .emit (StreamEvent (
4517- type = StreamEventType .RETRY ,
4518- metadata = {
4519- "attempt" : attempt + 1 ,
4520- "max_attempts" : max_attempts ,
4521- "delay" : delay ,
4522- "reason" : str (e ),
4523- },
4524- ))
4543+ # appearing hung. Guarded so it is zero-overhead when nothing
4544+ # is listening.
4545+ self ._emit_retry_stream_event (
4546+ attempt = attempt + 1 ,
4547+ max_attempts = max_attempts ,
4548+ delay = delay ,
4549+ reason = str (e ),
4550+ )
45254551
45264552 # Log retry attempt (buffered to avoid spam during transient failures)
45274553 logger .debug (f"[{ self .name } ] Retry { attempt + 1 } /{ max_attempts } after { delay :.1f} s: { str (e )[:100 ]} " )
@@ -4604,20 +4630,15 @@ async def _achat_completion_with_retry(self, messages, temperature=1.0, tools=No
46044630 )
46054631 await self ._hook_runner .execute_async (HookEvent .ON_RETRY , retry_input )
46064632
4607- # Surface the retry as a first-class stream event (see sync path).
4608- # Guarded by has_callbacks -> zero overhead when nothing is listening.
4609- emitter = getattr (self , 'stream_emitter' , None )
4610- if emitter is not None and emitter .has_callbacks :
4611- from ..streaming .events import StreamEvent , StreamEventType
4612- await emitter .emit_async (StreamEvent (
4613- type = StreamEventType .RETRY ,
4614- metadata = {
4615- "attempt" : attempt + 1 ,
4616- "max_attempts" : max_attempts ,
4617- "delay" : delay ,
4618- "reason" : str (e ),
4619- },
4620- ))
4633+ # Surface a streaming RETRY event (see sync path for rationale).
4634+ # Use the async emitter so async-only consumers registered via
4635+ # add_async_callback() also receive the event.
4636+ await self ._aemit_retry_stream_event (
4637+ attempt = attempt + 1 ,
4638+ max_attempts = max_attempts ,
4639+ delay = delay ,
4640+ reason = str (e ),
4641+ )
46214642
46224643 # Log retry attempt
46234644 logger .debug (f"[{ self .name } ] Async retry { attempt + 1 } /{ max_attempts } after { delay :.1f} s: { str (e )[:100 ]} " )
0 commit comments