|
1 | 1 | from __future__ import annotations |
2 | 2 |
|
3 | 3 | import asyncio |
| 4 | +import contextlib |
4 | 5 | import dataclasses |
5 | 6 | import json |
6 | 7 | from typing import Any, cast |
@@ -1693,6 +1694,7 @@ def __init__(self) -> None: |
1693 | 1694 | response_id="resp_nested", |
1694 | 1695 | ) |
1695 | 1696 | ] |
| 1697 | + self.is_complete = True |
1696 | 1698 |
|
1697 | 1699 | async def stream_events(self): |
1698 | 1700 | yield stream_event |
@@ -1753,6 +1755,89 @@ async def on_stream(payload: AgentToolStreamEvent) -> None: |
1753 | 1755 | assert received_events[0]["event"] == stream_event |
1754 | 1756 |
|
1755 | 1757 |
|
| 1758 | +@pytest.mark.asyncio |
| 1759 | +async def test_agent_as_tool_streaming_reraises_caller_cancellation_with_live_run( |
| 1760 | + monkeypatch: pytest.MonkeyPatch, |
| 1761 | +) -> None: |
| 1762 | + agent = Agent(name="streamer") |
| 1763 | + stream_event = RawResponsesStreamEvent(data=cast(Any, {"type": "response_started"})) |
| 1764 | + |
| 1765 | + class DummyStreamingResult: |
| 1766 | + def __init__(self) -> None: |
| 1767 | + self.final_output = "" |
| 1768 | + self.current_agent = agent |
| 1769 | + self.new_items: list[Any] = [] |
| 1770 | + self.raw_responses = [ |
| 1771 | + ModelResponse( |
| 1772 | + output=[get_text_message("Recovered nested summary")], |
| 1773 | + usage=Usage(), |
| 1774 | + response_id="resp_nested", |
| 1775 | + ) |
| 1776 | + ] |
| 1777 | + self.is_complete = False |
| 1778 | + self.run_loop_task = asyncio.create_task(asyncio.sleep(60)) |
| 1779 | + |
| 1780 | + async def stream_events(self): |
| 1781 | + yield stream_event |
| 1782 | + raise asyncio.CancelledError("stream-cancelled") |
| 1783 | + |
| 1784 | + streaming_result = DummyStreamingResult() |
| 1785 | + |
| 1786 | + def fake_run_streamed( |
| 1787 | + cls, |
| 1788 | + starting_agent, |
| 1789 | + input, |
| 1790 | + *, |
| 1791 | + context, |
| 1792 | + max_turns, |
| 1793 | + hooks, |
| 1794 | + run_config, |
| 1795 | + previous_response_id, |
| 1796 | + auto_previous_response_id=False, |
| 1797 | + conversation_id, |
| 1798 | + session, |
| 1799 | + ): |
| 1800 | + return streaming_result |
| 1801 | + |
| 1802 | + async def unexpected_run(*args: Any, **kwargs: Any) -> None: |
| 1803 | + raise AssertionError("Runner.run should not be called when on_stream is provided.") |
| 1804 | + |
| 1805 | + monkeypatch.setattr(Runner, "run_streamed", classmethod(fake_run_streamed)) |
| 1806 | + monkeypatch.setattr(Runner, "run", classmethod(unexpected_run)) |
| 1807 | + |
| 1808 | + async def on_stream(payload: AgentToolStreamEvent) -> None: |
| 1809 | + del payload |
| 1810 | + |
| 1811 | + tool_call = ResponseFunctionToolCall( |
| 1812 | + id="call_cancelled_live", |
| 1813 | + arguments='{"input": "recover"}', |
| 1814 | + call_id="call-cancelled-live", |
| 1815 | + name="stream_tool", |
| 1816 | + type="function_call", |
| 1817 | + ) |
| 1818 | + |
| 1819 | + tool = agent.as_tool( |
| 1820 | + tool_name="stream_tool", |
| 1821 | + tool_description="Streams events", |
| 1822 | + on_stream=on_stream, |
| 1823 | + ) |
| 1824 | + |
| 1825 | + tool_context = ToolContext( |
| 1826 | + context=None, |
| 1827 | + tool_name="stream_tool", |
| 1828 | + tool_call_id=tool_call.call_id, |
| 1829 | + tool_arguments=tool_call.arguments, |
| 1830 | + tool_call=tool_call, |
| 1831 | + ) |
| 1832 | + try: |
| 1833 | + with pytest.raises(asyncio.CancelledError, match="stream-cancelled"): |
| 1834 | + await tool.on_invoke_tool(tool_context, '{"input": "recover"}') |
| 1835 | + finally: |
| 1836 | + streaming_result.run_loop_task.cancel() |
| 1837 | + with contextlib.suppress(asyncio.CancelledError): |
| 1838 | + await streaming_result.run_loop_task |
| 1839 | + |
| 1840 | + |
1756 | 1841 | @pytest.mark.asyncio |
1757 | 1842 | async def test_agent_as_tool_streaming_extractor_can_access_agent_tool_invocation( |
1758 | 1843 | monkeypatch: pytest.MonkeyPatch, |
|
0 commit comments