9090_PENDING_QUESTION_NOT_ROUTED = "not_routed"
9191_PENDING_QUESTION_ANSWERED = "answered"
9292_PENDING_QUESTION_STALE_FINISHED = "stale_finished"
93+ _ACTIVE_INTERRUPT_TERMINAL_WAIT_TIMEOUT_SECONDS = 30.0
94+
95+
96+ def _new_set_asyncio_event () -> asyncio .Event :
97+ event = asyncio .Event ()
98+ event .set ()
99+ return event
93100
94101
95102class WaitingInputCancelResult (str , Enum ):
@@ -143,6 +150,7 @@ class A2APipelineRuntime:
143150 restart_after_interrupt : bool = False
144151 pause_after_interrupt : bool = False
145152 restart_requested : asyncio .Event = field (default_factory = asyncio .Event )
153+ interrupt_settled : asyncio .Event = field (default_factory = _new_set_asyncio_event )
146154
147155
148156@dataclass (frozen = True )
@@ -574,7 +582,7 @@ def fresh_pipeline_factory() -> Any:
574582 ctx .active_task_id = task .task_id
575583 elif task .active_task is owner_task :
576584 task .active_task = None
577- if ctx .active_task_id == task .task_id and getattr ( pipeline , "sidecar_status" , None ) != "running" :
585+ if ctx .active_task_id == task .task_id :
578586 ctx .active_task_id = None
579587 if replacement_owner is owner_task and hasattr (ctx .runtime , "active_owner_task" ):
580588 ctx .runtime .active_owner_task = None
@@ -725,6 +733,8 @@ async def _route_active_pipeline_interrupt(
725733 paused = False
726734 verdict : Any | None = None
727735 interrupt_received_published = False
736+ interrupt_settled = _interrupt_settled_event (runtime )
737+ interrupt_settled .clear ()
728738 try :
729739 publish_interrupt_received = getattr (publisher , "publish_interrupt_received" , None )
730740 if callable (publish_interrupt_received ):
@@ -743,9 +753,8 @@ async def _route_active_pipeline_interrupt(
743753 apply_hard_interrupt = getattr (pipeline , "apply_hard_interrupt" , None )
744754 if callable (apply_hard_interrupt ):
745755 parameters = inspect .signature (apply_hard_interrupt ).parameters
746- if pipeline_input .has_images and (
747- "source_input" in parameters
748- or any (parameter .kind == inspect .Parameter .VAR_KEYWORD for parameter in parameters .values ())
756+ if "source_input" in parameters or any (
757+ parameter .kind == inspect .Parameter .VAR_KEYWORD for parameter in parameters .values ()
749758 ):
750759 applied = apply_hard_interrupt (verdict , source_input = runner_input )
751760 else :
@@ -818,6 +827,7 @@ async def _route_active_pipeline_interrupt(
818827 await self ._complete_backup_blocked_transition (task = task , ctx = ctx )
819828 return True
820829 finally :
830+ interrupt_settled .set ()
821831 if paused and not bool (getattr (verdict , "paused" , False )):
822832 resume_agent_loops = getattr (pipeline , "resume_agent_loops" , None )
823833 if callable (resume_agent_loops ):
@@ -1148,6 +1158,22 @@ async def _consume_stream_until_restart(
11481158 finally :
11491159 next_task = None
11501160
1161+ interrupt_action = await _consume_active_interrupt_action_before_terminal (runtime , event )
1162+ if interrupt_action == "restart" :
1163+ close_stream_on_exit = True
1164+ return _StreamConsumeResult (
1165+ had_events = had_events ,
1166+ restart_requested = True ,
1167+ terminal_handoff_unavailable = terminal_handoff_unavailable ,
1168+ )
1169+ if interrupt_action == "pause" :
1170+ close_stream_on_exit = True
1171+ return _StreamConsumeResult (
1172+ had_events = had_events ,
1173+ restart_requested = False ,
1174+ terminal_handoff_unavailable = terminal_handoff_unavailable ,
1175+ )
1176+
11511177 had_events = True
11521178 await publish_mcp_warnings (
11531179 publisher .event_queue ,
@@ -3410,6 +3436,72 @@ def _restart_requested_event(runtime: Any) -> asyncio.Event:
34103436 return restart_requested
34113437
34123438
3439+ def _interrupt_settled_event (runtime : Any ) -> asyncio .Event :
3440+ interrupt_settled = getattr (runtime , "interrupt_settled" , None )
3441+ if isinstance (interrupt_settled , asyncio .Event ):
3442+ return interrupt_settled
3443+ interrupt_settled = _new_set_asyncio_event ()
3444+ runtime .interrupt_settled = interrupt_settled
3445+ return interrupt_settled
3446+
3447+
3448+ async def _consume_active_interrupt_action_before_terminal (runtime : Any , event : Any ) -> str | None :
3449+ if not _is_pipeline_terminal_stream_event (event ):
3450+ return None
3451+
3452+ action = _consume_requested_interrupt_action (runtime )
3453+ if action is not None :
3454+ return action
3455+
3456+ interrupt_settled = _interrupt_settled_event (runtime )
3457+ if interrupt_settled .is_set ():
3458+ return None
3459+
3460+ restart_event = _restart_requested_event (runtime )
3461+ settled_task = asyncio .create_task (interrupt_settled .wait ())
3462+ restart_task = asyncio .create_task (restart_event .wait ())
3463+ try :
3464+ done , _pending = await asyncio .wait (
3465+ {settled_task , restart_task },
3466+ timeout = _ACTIVE_INTERRUPT_TERMINAL_WAIT_TIMEOUT_SECONDS ,
3467+ return_when = asyncio .FIRST_COMPLETED ,
3468+ )
3469+ finally :
3470+ await _cancel_task_safely (settled_task )
3471+ await _cancel_task_safely (restart_task )
3472+
3473+ action = _consume_requested_interrupt_action (runtime )
3474+ if action is not None :
3475+ return action
3476+
3477+ if not done and not interrupt_settled .is_set ():
3478+ logger .warning (
3479+ "Timed out waiting for active A2A pipeline interrupt before publishing terminal pipeline event"
3480+ )
3481+ return None
3482+
3483+
3484+ def _consume_requested_interrupt_action (runtime : Any ) -> str | None :
3485+ restart_event = _restart_requested_event (runtime )
3486+ if bool (getattr (runtime , "restart_after_interrupt" , False )) and restart_event .is_set ():
3487+ restart_event .clear ()
3488+ runtime .restart_after_interrupt = False
3489+ return "restart"
3490+ if bool (getattr (runtime , "pause_after_interrupt" , False )) and restart_event .is_set ():
3491+ restart_event .clear ()
3492+ runtime .pause_after_interrupt = False
3493+ return "pause"
3494+ return None
3495+
3496+
3497+ def _is_pipeline_terminal_stream_event (event : Any ) -> bool :
3498+ return isinstance (event , PipelineEvent ) and event .type in {
3499+ PipelineEventType .PIPELINE_COMPLETED ,
3500+ PipelineEventType .PIPELINE_ERROR ,
3501+ PipelineEventType .BACKUP_BLOCKED ,
3502+ }
3503+
3504+
34133505def _is_active_task_record (task : Any , active_task_id : str | None ) -> bool :
34143506 return active_task_id is not None and getattr (task , "task_id" , None ) == active_task_id
34153507
0 commit comments