@@ -1758,6 +1758,189 @@ async def on_stream(payload: AgentToolStreamEvent) -> None:
17581758 assert received_events [0 ]["event" ] == stream_event
17591759
17601760
1761+ @pytest .mark .asyncio
1762+ async def test_agent_as_tool_streaming_recovers_all_text_chunks_from_raw_responses_on_cancellation (
1763+ monkeypatch : pytest .MonkeyPatch ,
1764+ ) -> None :
1765+ agent = Agent (name = "streamer" )
1766+ stream_event = RawResponsesStreamEvent (data = cast (Any , {"type" : "response_started" }))
1767+
1768+ class DummyStreamingResult :
1769+ def __init__ (self ) -> None :
1770+ self .final_output = ""
1771+ self .current_agent = agent
1772+ self .new_items : list [Any ] = []
1773+ self .raw_responses = [
1774+ ModelResponse (
1775+ output = [
1776+ ResponseOutputMessage (
1777+ id = "msg_recover" ,
1778+ role = "assistant" ,
1779+ status = "completed" ,
1780+ type = "message" ,
1781+ content = [
1782+ ResponseOutputText (
1783+ annotations = [],
1784+ text = "first " ,
1785+ type = "output_text" ,
1786+ logprobs = [],
1787+ ),
1788+ ResponseOutputText (
1789+ annotations = [],
1790+ text = "second" ,
1791+ type = "output_text" ,
1792+ logprobs = [],
1793+ ),
1794+ ],
1795+ )
1796+ ],
1797+ usage = Usage (),
1798+ response_id = "resp_nested" ,
1799+ )
1800+ ]
1801+ self .run_loop_task = asyncio .create_task (asyncio .sleep (0 ))
1802+
1803+ async def stream_events (self ):
1804+ yield stream_event
1805+ raise asyncio .CancelledError ("stream-cancelled" )
1806+
1807+ streaming_result = DummyStreamingResult ()
1808+ await streaming_result .run_loop_task
1809+
1810+ def fake_run_streamed (
1811+ cls ,
1812+ starting_agent ,
1813+ input ,
1814+ * ,
1815+ context ,
1816+ max_turns ,
1817+ hooks ,
1818+ run_config ,
1819+ previous_response_id ,
1820+ auto_previous_response_id = False ,
1821+ conversation_id ,
1822+ session ,
1823+ ):
1824+ return streaming_result
1825+
1826+ async def unexpected_run (* args : Any , ** kwargs : Any ) -> None :
1827+ raise AssertionError ("Runner.run should not be called when on_stream is provided." )
1828+
1829+ monkeypatch .setattr (Runner , "run_streamed" , classmethod (fake_run_streamed ))
1830+ monkeypatch .setattr (Runner , "run" , classmethod (unexpected_run ))
1831+
1832+ async def on_stream (payload : AgentToolStreamEvent ) -> None :
1833+ del payload
1834+
1835+ tool_call = ResponseFunctionToolCall (
1836+ id = "call_cancelled_multi_chunk" ,
1837+ arguments = '{"input": "recover"}' ,
1838+ call_id = "call-cancelled-multi-chunk" ,
1839+ name = "stream_tool" ,
1840+ type = "function_call" ,
1841+ )
1842+
1843+ tool = agent .as_tool (
1844+ tool_name = "stream_tool" ,
1845+ tool_description = "Streams events" ,
1846+ on_stream = on_stream ,
1847+ )
1848+
1849+ tool_context = ToolContext (
1850+ context = None ,
1851+ tool_name = "stream_tool" ,
1852+ tool_call_id = tool_call .call_id ,
1853+ tool_arguments = tool_call .arguments ,
1854+ tool_call = tool_call ,
1855+ )
1856+ output = await tool .on_invoke_tool (tool_context , '{"input": "recover"}' )
1857+
1858+ assert output == "first second"
1859+
1860+
1861+ @pytest .mark .asyncio
1862+ async def test_agent_as_tool_streaming_preserves_structured_output_on_cancellation (
1863+ monkeypatch : pytest .MonkeyPatch ,
1864+ ) -> None :
1865+ class StructuredOutput (BaseModel ):
1866+ answer : str
1867+
1868+ agent = Agent (name = "streamer" , output_type = StructuredOutput )
1869+ stream_event = RawResponsesStreamEvent (data = cast (Any , {"type" : "response_started" }))
1870+
1871+ class DummyStreamingResult :
1872+ def __init__ (self ) -> None :
1873+ self .final_output = StructuredOutput (answer = "structured" )
1874+ self .current_agent = agent
1875+ self .new_items : list [Any ] = []
1876+ self .raw_responses = [
1877+ ModelResponse (
1878+ output = [get_text_message ('{"answer":"text-json"}' )],
1879+ usage = Usage (),
1880+ response_id = "resp_nested" ,
1881+ )
1882+ ]
1883+ self .run_loop_task = asyncio .create_task (asyncio .sleep (0 ))
1884+
1885+ async def stream_events (self ):
1886+ yield stream_event
1887+ raise asyncio .CancelledError ("stream-cancelled" )
1888+
1889+ streaming_result = DummyStreamingResult ()
1890+ await streaming_result .run_loop_task
1891+
1892+ def fake_run_streamed (
1893+ cls ,
1894+ starting_agent ,
1895+ input ,
1896+ * ,
1897+ context ,
1898+ max_turns ,
1899+ hooks ,
1900+ run_config ,
1901+ previous_response_id ,
1902+ auto_previous_response_id = False ,
1903+ conversation_id ,
1904+ session ,
1905+ ):
1906+ return streaming_result
1907+
1908+ async def unexpected_run (* args : Any , ** kwargs : Any ) -> None :
1909+ raise AssertionError ("Runner.run should not be called when on_stream is provided." )
1910+
1911+ monkeypatch .setattr (Runner , "run_streamed" , classmethod (fake_run_streamed ))
1912+ monkeypatch .setattr (Runner , "run" , classmethod (unexpected_run ))
1913+
1914+ async def on_stream (payload : AgentToolStreamEvent ) -> None :
1915+ del payload
1916+
1917+ tool_call = ResponseFunctionToolCall (
1918+ id = "call_cancelled_structured" ,
1919+ arguments = '{"input": "recover"}' ,
1920+ call_id = "call-cancelled-structured" ,
1921+ name = "stream_tool" ,
1922+ type = "function_call" ,
1923+ )
1924+
1925+ tool = agent .as_tool (
1926+ tool_name = "stream_tool" ,
1927+ tool_description = "Streams events" ,
1928+ on_stream = on_stream ,
1929+ )
1930+
1931+ tool_context = ToolContext (
1932+ context = None ,
1933+ tool_name = "stream_tool" ,
1934+ tool_call_id = tool_call .call_id ,
1935+ tool_arguments = tool_call .arguments ,
1936+ tool_call = tool_call ,
1937+ )
1938+ output = await tool .on_invoke_tool (tool_context , '{"input": "recover"}' )
1939+
1940+ assert output == StructuredOutput (answer = "structured" )
1941+ assert not isinstance (output , str )
1942+
1943+
17611944@pytest .mark .asyncio
17621945async def test_agent_as_tool_streaming_reraises_caller_cancellation_with_live_run (
17631946 monkeypatch : pytest .MonkeyPatch ,
0 commit comments