@@ -1050,7 +1050,6 @@ async def test_transcript_delta_triggers_guardrail_at_threshold(
1050
1050
await self ._wait_for_guardrail_tasks (session )
1051
1051
1052
1052
# Should have triggered guardrail and interrupted
1053
- assert session ._interrupted_by_guardrail is True
1054
1053
assert mock_model .interrupts_called == 1
1055
1054
assert len (mock_model .sent_messages ) == 1
1056
1055
assert "triggered_guardrail" in mock_model .sent_messages [0 ]
@@ -1187,14 +1186,12 @@ async def test_turn_ended_clears_guardrail_state(
1187
1186
# Wait for async guardrail tasks to complete
1188
1187
await self ._wait_for_guardrail_tasks (session )
1189
1188
1190
- assert session ._interrupted_by_guardrail is True
1191
1189
assert len (session ._item_transcripts ) == 1
1192
1190
1193
1191
# End turn
1194
1192
await session .on_event (RealtimeModelTurnEndedEvent ())
1195
1193
1196
1194
# State should be cleared
1197
- assert session ._interrupted_by_guardrail is False
1198
1195
assert len (session ._item_transcripts ) == 0
1199
1196
assert len (session ._item_guardrail_run_counts ) == 0
1200
1197
@@ -1259,7 +1256,6 @@ async def test_agent_output_guardrails_triggered(self, mock_model, triggered_gua
1259
1256
await session .on_event (transcript_event )
1260
1257
await self ._wait_for_guardrail_tasks (session )
1261
1258
1262
- assert session ._interrupted_by_guardrail is True
1263
1259
assert mock_model .interrupts_called == 1
1264
1260
assert len (mock_model .sent_messages ) == 1
1265
1261
assert "triggered_guardrail" in mock_model .sent_messages [0 ]
@@ -1272,6 +1268,63 @@ async def test_agent_output_guardrails_triggered(self, mock_model, triggered_gua
1272
1268
assert len (guardrail_events ) == 1
1273
1269
assert guardrail_events [0 ].message == "this is more than ten characters"
1274
1270
1271
+ @pytest .mark .asyncio
1272
+ async def test_concurrent_guardrail_tasks_interrupt_once_per_response (self , mock_model ):
1273
+ """Even if multiple guardrail tasks trigger concurrently for the same response_id,
1274
+ only the first should interrupt and send a message."""
1275
+ import asyncio
1276
+
1277
+ # Barrier to release both guardrail tasks at the same time
1278
+ start_event = asyncio .Event ()
1279
+
1280
+ async def async_trigger_guardrail (context , agent , output ):
1281
+ await start_event .wait ()
1282
+ return GuardrailFunctionOutput (
1283
+ output_info = {"reason" : "concurrent" }, tripwire_triggered = True
1284
+ )
1285
+
1286
+ concurrent_guardrail = OutputGuardrail (
1287
+ guardrail_function = async_trigger_guardrail , name = "concurrent_trigger"
1288
+ )
1289
+
1290
+ run_config : RealtimeRunConfig = {
1291
+ "output_guardrails" : [concurrent_guardrail ],
1292
+ "guardrails_settings" : {"debounce_text_length" : 5 },
1293
+ }
1294
+
1295
+ # Use a minimal agent (guardrails from run_config)
1296
+ agent = RealtimeAgent (name = "agent" )
1297
+ session = RealtimeSession (mock_model , agent , None , run_config = run_config )
1298
+
1299
+ # Two deltas for same item and response to enqueue two guardrail tasks
1300
+ await session .on_event (
1301
+ RealtimeModelTranscriptDeltaEvent (
1302
+ item_id = "item_1" , delta = "12345" , response_id = "resp_same"
1303
+ )
1304
+ )
1305
+ await session .on_event (
1306
+ RealtimeModelTranscriptDeltaEvent (
1307
+ item_id = "item_1" , delta = "67890" , response_id = "resp_same"
1308
+ )
1309
+ )
1310
+
1311
+ # Wait until both tasks are enqueued
1312
+ for _ in range (50 ):
1313
+ if len (session ._guardrail_tasks ) >= 2 :
1314
+ break
1315
+ await asyncio .sleep (0.01 )
1316
+
1317
+ # Release both tasks concurrently
1318
+ start_event .set ()
1319
+
1320
+ # Wait for completion
1321
+ if session ._guardrail_tasks :
1322
+ await asyncio .gather (* session ._guardrail_tasks , return_exceptions = True )
1323
+
1324
+ # Only one interrupt and one message should be sent
1325
+ assert mock_model .interrupts_called == 1
1326
+ assert len (mock_model .sent_messages ) == 1
1327
+
1275
1328
1276
1329
class TestModelSettingsIntegration :
1277
1330
"""Test suite for model settings integration in RealtimeSession."""
0 commit comments