Skip to content

Commit b1fc173

Browse files
committed
Fix guardrails failing when interrupt_response=True
When turn_detection.interrupt_response is set to True and a guardrail triggers an interrupt, the SDK was only sending response.interrupt but not response.cancel. This caused the API to think the response was still active, leading to "Conversation already has an active response" errors on subsequent requests. Root cause: - Guardrail interrupts are SDK-side logic, but the API doesn't know about them - interrupt_response=True tells the API to auto-cancel on USER VOICE interrupts - When a guardrail triggered, the SDK would skip sending response.cancel if interrupt_response=True, assuming the API would handle it (incorrect) Solution: - Add force_cancel parameter to RealtimeModelSendInterrupt - Set force_cancel=True for guardrail interrupts - Always send response.cancel when force_cancel=True, regardless of interrupt_response setting - User voice interrupts continue to respect interrupt_response setting Changes: - src/agents/realtime/model_inputs.py: Add force_cancel parameter - src/agents/realtime/session.py: Pass force_cancel=True for guardrails - src/agents/realtime/openai_realtime.py: Check force_cancel in _send_interrupt Fixes #1907
1 parent 748ac80 commit b1fc173

3 files changed

Lines changed: 18 additions & 3 deletions

File tree

src/agents/realtime/model_inputs.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,17 @@ class RealtimeModelSendToolOutput:
9393

9494
@dataclass
9595
class RealtimeModelSendInterrupt:
96-
"""Send an interrupt to the model."""
96+
"""Send an interrupt to the model.
97+
98+
Args:
99+
force_cancel: If True, always send response.cancel regardless of
100+
interrupt_response setting. This should be True for SDK-side interrupts
101+
(e.g., guardrails) where the API doesn't know about the interrupt.
102+
For user voice interrupts, this should be False (default) to allow
103+
the API's automatic response cancellation when interrupt_response=True.
104+
"""
105+
106+
force_cancel: bool = False
97107

98108

99109
@dataclass

src/agents/realtime/openai_realtime.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -433,7 +433,10 @@ async def _send_interrupt(self, event: RealtimeModelSendInterrupt) -> None:
433433
and session.audio.input.turn_detection is not None
434434
and session.audio.input.turn_detection.interrupt_response is True,
435435
)
436-
if not automatic_response_cancellation_enabled:
436+
# Always cancel for force_cancel=True (e.g., guardrail interrupts)
437+
# For user voice interrupts (force_cancel=False), only cancel if automatic
438+
# cancellation is disabled
439+
if event.force_cancel or not automatic_response_cancellation_enabled:
437440
await self._cancel_response()
438441

439442
self._audio_state_tracker.on_interrupted()

src/agents/realtime/session.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -704,7 +704,9 @@ async def _run_output_guardrails(self, text: str, response_id: str) -> bool:
704704
)
705705

706706
# Interrupt the model
707-
await self._model.send_event(RealtimeModelSendInterrupt())
707+
# Use force_cancel=True for guardrail interrupts because they are SDK-side logic
708+
# and the API doesn't know about them (unlike user voice interrupts)
709+
await self._model.send_event(RealtimeModelSendInterrupt(force_cancel=True))
708710

709711
# Send guardrail triggered message
710712
guardrail_names = [result.guardrail.get_name() for result in triggered_results]

0 commit comments

Comments
 (0)