fix(gateway): suppress tool-progress bubbles after interrupt (#16034)

When the LLM response carries N parallel tool calls, the agent fires
N tool.started events back-to-back before its interrupt check runs.
A user sending /stop mid-batch would see the ' Interrupting current
task' ack followed by a trail of 🔍 web_search bubbles for the remaining
events in the batch — making the interrupt feel ignored.

progress_callback and the drain loop in send_progress_messages now
check agent.is_interrupted (via agent_holder[0], the existing
cross-scope handle). Events that arrive after interrupt are dropped
at both the queueing and rendering stages. The ' Interrupting'
message is sent through a separate adapter path and is unaffected.
This commit is contained in:
Teknium
2026-04-26 05:47:37 -07:00
committed by GitHub
parent e8441c4c0f
commit 35c57cc46b
2 changed files with 247 additions and 0 deletions

View File

@@ -9370,6 +9370,22 @@ class GatewayRunner:
if event_type not in ("tool.started",):
return
# Suppress tool-progress bubbles once the user has sent `stop`.
# When the LLM response carries N parallel tool calls, the agent
# fires N "tool.started" events back-to-back before checking for
# interrupts — without this guard, a late `stop` still renders
# all N as 🔍 bubbles, making the interrupt feel ignored.
# (agent lives in run_sync's scope; agent_holder[0] is the shared
# handle across nested scopes — see line ~9607.)
try:
_agent_for_interrupt = agent_holder[0] if agent_holder else None
if _agent_for_interrupt is not None and getattr(
_agent_for_interrupt, "is_interrupted", False
):
return
except Exception:
pass
# "new" mode: only report when tool changes
if progress_mode == "new" and tool_name == last_tool[0]:
return
@@ -9476,6 +9492,22 @@ class GatewayRunner:
raw = progress_queue.get_nowait()
# Drain silently when interrupted: events queued in the
# window between tool parse and interrupt processing
# should not render as bubbles. The "⚡ Interrupting
# current task" message is sent separately and is the
# last progress-flavored bubble the user should see.
try:
_agent_for_interrupt = agent_holder[0] if agent_holder else None
if _agent_for_interrupt is not None and getattr(
_agent_for_interrupt, "is_interrupted", False
):
# Drop this event and continue draining.
await asyncio.sleep(0)
continue
except Exception:
pass
# Handle dedup messages: update last line with repeat counter
if isinstance(raw, tuple) and len(raw) == 3 and raw[0] == "__dedup__":
_, base_msg, count = raw