import asyncio import json import time import pytest from openai.types.responses import ResponseCompletedEvent from agents import Agent, Runner from agents.stream_events import RawResponsesStreamEvent from .fake_model import FakeModel from .test_responses import get_function_tool, get_function_tool_call, get_text_message class SlowCompleteFakeModel(FakeModel): """A FakeModel that delays before emitting the completed event in streaming.""" def __init__(self, delay_seconds: float): super().__init__() self._delay_seconds = delay_seconds async def stream_response(self, *args, **kwargs): async for ev in super().stream_response(*args, **kwargs): if isinstance(ev, ResponseCompletedEvent) and self._delay_seconds > 0: await asyncio.sleep(self._delay_seconds) yield ev @pytest.mark.asyncio async def test_simple_streaming_with_cancel(): model = FakeModel() agent = Agent(name="Joker", model=model) result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") num_events = 0 stop_after = 1 # There are two that the model gives back. async for _event in result.stream_events(): num_events += 1 if num_events == stop_after: result.cancel() assert num_events == 1, f"Expected {stop_after} visible events, but got {num_events}" @pytest.mark.asyncio async def test_multiple_events_streaming_with_cancel(): model = FakeModel() agent = Agent( name="Joker", model=model, tools=[get_function_tool("foo", "tool_result")], ) model.add_multiple_turn_outputs( [ # First turn: a message and tool call [ get_text_message("a_message"), get_function_tool_call("foo", json.dumps({"a": "b"})), ], # Second turn: text message [get_text_message("done")], ] ) result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") num_events = 0 stop_after = 2 async for _ in result.stream_events(): num_events += 1 if num_events == stop_after: result.cancel() assert num_events == stop_after, f"Expected {stop_after} visible events, but got {num_events}" @pytest.mark.asyncio async def test_cancel_prevents_further_events(): model = FakeModel() agent = Agent(name="Joker", model=model) result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") events = [] async for event in result.stream_events(): events.append(event) result.cancel() break # Cancel after first event # Try to get more events after cancel more_events = [e async for e in result.stream_events()] assert len(events) == 1 assert more_events == [], "No events should be yielded after cancel()" @pytest.mark.asyncio async def test_cancel_is_idempotent(): model = FakeModel() agent = Agent(name="Joker", model=model) result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") events = [] async for event in result.stream_events(): events.append(event) result.cancel() result.cancel() # Call cancel again break # Should not raise or misbehave assert len(events) == 1 @pytest.mark.asyncio async def test_cancel_before_streaming(): model = FakeModel() agent = Agent(name="Joker", model=model) result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") result.cancel() # Cancel before streaming events = [e async for e in result.stream_events()] assert events == [], "No events should be yielded if cancel() is called before streaming." @pytest.mark.asyncio async def test_cancel_cleans_up_resources(): model = FakeModel() agent = Agent(name="Joker", model=model) result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") # Start streaming, then cancel async for _ in result.stream_events(): result.cancel() break # After cancel, queues should be empty and is_complete True assert result.is_complete, "Result should be marked complete after cancel." assert result._event_queue.empty(), "Event queue should be empty after cancel." assert result._input_guardrail_queue.empty(), ( "Input guardrail queue should be empty after cancel." ) @pytest.mark.asyncio async def test_cancel_immediate_mode_explicit(): """Test explicit immediate mode behaves same as default.""" model = FakeModel() agent = Agent(name="Joker", model=model) result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") async for _ in result.stream_events(): result.cancel(mode="immediate") break assert result.is_complete assert result._event_queue.empty() assert result._cancel_mode == "immediate" @pytest.mark.asyncio async def test_stream_events_respects_asyncio_timeout_cancellation(): model = SlowCompleteFakeModel(delay_seconds=0.5) model.set_next_output([get_text_message("Final response")]) agent = Agent(name="TimeoutTester", model=model) result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") event_iter = result.stream_events().__aiter__() # Consume events until the output item is done so the next event is delayed. while True: event = await asyncio.wait_for(event_iter.__anext__(), timeout=1.0) if ( isinstance(event, RawResponsesStreamEvent) and event.data.type == "response.output_item.done" ): break start = time.perf_counter() with pytest.raises(asyncio.TimeoutError): await asyncio.wait_for(event_iter.__anext__(), timeout=0.1) elapsed = time.perf_counter() - start assert elapsed < 0.3, "Cancellation should propagate promptly when waiting for events." result.cancel() @pytest.mark.asyncio async def test_cancel_immediate_unblocks_waiting_stream_consumer(): block_event = asyncio.Event() class BlockingFakeModel(FakeModel): async def stream_response( self, system_instructions, input, model_settings, tools, output_schema, handoffs, tracing, *, previous_response_id=None, conversation_id=None, prompt=None, ): await block_event.wait() async for event in super().stream_response( system_instructions, input, model_settings, tools, output_schema, handoffs, tracing, previous_response_id=previous_response_id, conversation_id=conversation_id, prompt=prompt, ): yield event model = BlockingFakeModel() agent = Agent(name="Joker", model=model) result = Runner.run_streamed(agent, input="Please tell me 5 jokes.") async def consume_events(): return [event async for event in result.stream_events()] consumer_task = asyncio.create_task(consume_events()) await asyncio.sleep(0) result.cancel(mode="immediate") events = await asyncio.wait_for(consumer_task, timeout=1) assert len(events) <= 1 assert not block_event.is_set() assert result.is_complete @pytest.mark.asyncio async def test_run_loop_exception_property_is_none_on_success(): """run_loop_exception is None when the stream completes without error.""" model = FakeModel() model.set_next_output([get_text_message("hello")]) agent = Agent(name="A", model=model) result = Runner.run_streamed(agent, input="hi") async for _ in result.stream_events(): pass assert result.run_loop_exception is None @pytest.mark.asyncio async def test_run_loop_exception_surfaced_after_stream(): """run_loop_exception is set when the run loop raises before yielding events.""" class BoomModel(FakeModel): async def get_response(self, *args, **kwargs): raise RuntimeError("run loop boom") async def stream_response(self, *args, **kwargs): raise RuntimeError("run loop boom") yield # make this an async generator agent = Agent(name="A", model=BoomModel()) result = Runner.run_streamed(agent, input="hi") with pytest.raises(RuntimeError, match="run loop boom"): async for _ in result.stream_events(): pass # Property must also expose the exception for callers who want to inspect it directly. assert result.run_loop_exception is not None assert isinstance(result.run_loop_exception, RuntimeError) assert "run loop boom" in str(result.run_loop_exception)