Skip to content

Commit e73d33f

Browse files
elainegan-openaicodex
authored andcommitted
Require non-cancelled nested run for recovery
Treat streamed nested-agent recovery as valid only when the nested run loop finished without cancellation, and update the agent-tool tests to cover both the completed and cancelled run-loop cases. Co-authored-by: Codex <noreply@openai.com>
1 parent e89c8dd commit e73d33f

2 files changed

Lines changed: 86 additions & 3 deletions

File tree

src/agents/agent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ def _can_recover_cancelled_streamed_agent_tool(run_result: Any) -> bool:
153153
"""Recover text only after the nested streamed run has already reached local terminal state."""
154154
run_loop_task = getattr(run_result, "run_loop_task", None)
155155
if isinstance(run_loop_task, asyncio.Task):
156-
return run_loop_task.done()
156+
return run_loop_task.done() and not run_loop_task.cancelled()
157157

158158
return False
159159

tests/test_agent_as_tool.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,12 +1694,15 @@ def __init__(self) -> None:
16941694
response_id="resp_nested",
16951695
)
16961696
]
1697-
self.is_complete = True
1697+
self.run_loop_task = asyncio.create_task(asyncio.sleep(0))
16981698

16991699
async def stream_events(self):
17001700
yield stream_event
17011701
raise asyncio.CancelledError("stream-cancelled")
17021702

1703+
streaming_result = DummyStreamingResult()
1704+
await streaming_result.run_loop_task
1705+
17031706
def fake_run_streamed(
17041707
cls,
17051708
starting_agent,
@@ -1714,7 +1717,7 @@ def fake_run_streamed(
17141717
conversation_id,
17151718
session,
17161719
):
1717-
return DummyStreamingResult()
1720+
return streaming_result
17181721

17191722
async def unexpected_run(*args: Any, **kwargs: Any) -> None:
17201723
raise AssertionError("Runner.run should not be called when on_stream is provided.")
@@ -1838,6 +1841,86 @@ async def on_stream(payload: AgentToolStreamEvent) -> None:
18381841
await streaming_result.run_loop_task
18391842

18401843

1844+
@pytest.mark.asyncio
1845+
async def test_agent_as_tool_streaming_reraises_caller_cancellation_with_cancelled_run_loop(
1846+
monkeypatch: pytest.MonkeyPatch,
1847+
) -> None:
1848+
agent = Agent(name="streamer")
1849+
stream_event = RawResponsesStreamEvent(data=cast(Any, {"type": "response_started"}))
1850+
1851+
class DummyStreamingResult:
1852+
def __init__(self) -> None:
1853+
self.final_output = ""
1854+
self.current_agent = agent
1855+
self.new_items: list[Any] = []
1856+
self.raw_responses = [
1857+
ModelResponse(
1858+
output=[get_text_message("Recovered nested summary")],
1859+
usage=Usage(),
1860+
response_id="resp_nested",
1861+
)
1862+
]
1863+
self.run_loop_task = asyncio.create_task(asyncio.sleep(60))
1864+
1865+
async def stream_events(self):
1866+
yield stream_event
1867+
raise asyncio.CancelledError("stream-cancelled")
1868+
1869+
streaming_result = DummyStreamingResult()
1870+
streaming_result.run_loop_task.cancel()
1871+
with contextlib.suppress(asyncio.CancelledError):
1872+
await streaming_result.run_loop_task
1873+
1874+
def fake_run_streamed(
1875+
cls,
1876+
starting_agent,
1877+
input,
1878+
*,
1879+
context,
1880+
max_turns,
1881+
hooks,
1882+
run_config,
1883+
previous_response_id,
1884+
auto_previous_response_id=False,
1885+
conversation_id,
1886+
session,
1887+
):
1888+
return streaming_result
1889+
1890+
async def unexpected_run(*args: Any, **kwargs: Any) -> None:
1891+
raise AssertionError("Runner.run should not be called when on_stream is provided.")
1892+
1893+
monkeypatch.setattr(Runner, "run_streamed", classmethod(fake_run_streamed))
1894+
monkeypatch.setattr(Runner, "run", classmethod(unexpected_run))
1895+
1896+
async def on_stream(payload: AgentToolStreamEvent) -> None:
1897+
del payload
1898+
1899+
tool_call = ResponseFunctionToolCall(
1900+
id="call_cancelled_done",
1901+
arguments='{"input": "recover"}',
1902+
call_id="call-cancelled-done",
1903+
name="stream_tool",
1904+
type="function_call",
1905+
)
1906+
1907+
tool = agent.as_tool(
1908+
tool_name="stream_tool",
1909+
tool_description="Streams events",
1910+
on_stream=on_stream,
1911+
)
1912+
1913+
tool_context = ToolContext(
1914+
context=None,
1915+
tool_name="stream_tool",
1916+
tool_call_id=tool_call.call_id,
1917+
tool_arguments=tool_call.arguments,
1918+
tool_call=tool_call,
1919+
)
1920+
with pytest.raises(asyncio.CancelledError, match="stream-cancelled"):
1921+
await tool.on_invoke_tool(tool_context, '{"input": "recover"}')
1922+
1923+
18411924
@pytest.mark.asyncio
18421925
async def test_agent_as_tool_streaming_extractor_can_access_agent_tool_invocation(
18431926
monkeypatch: pytest.MonkeyPatch,

0 commit comments

Comments
 (0)