Skip to content

Commit aa822d3

Browse files
elainegan-openaicodex
authored andcommitted
Require successful nested run for recovery
Recover streamed nested-agent text only when the nested run loop task finished successfully, and add a regression for a run loop that completed with an exception. Co-authored-by: Codex <noreply@openai.com>
1 parent e73d33f commit aa822d3

2 files changed

Lines changed: 87 additions & 1 deletion

File tree

src/agents/agent.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ 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() and not run_loop_task.cancelled()
156+
return (
157+
run_loop_task.done()
158+
and not run_loop_task.cancelled()
159+
and run_loop_task.exception() is None
160+
)
157161

158162
return False
159163

tests/test_agent_as_tool.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1921,6 +1921,88 @@ async def on_stream(payload: AgentToolStreamEvent) -> None:
19211921
await tool.on_invoke_tool(tool_context, '{"input": "recover"}')
19221922

19231923

1924+
@pytest.mark.asyncio
1925+
async def test_agent_as_tool_streaming_reraises_caller_cancellation_with_failed_run_loop(
1926+
monkeypatch: pytest.MonkeyPatch,
1927+
) -> None:
1928+
agent = Agent(name="streamer")
1929+
stream_event = RawResponsesStreamEvent(data=cast(Any, {"type": "response_started"}))
1930+
1931+
async def fail_run_loop() -> None:
1932+
raise RuntimeError("run-loop-failed")
1933+
1934+
class DummyStreamingResult:
1935+
def __init__(self) -> None:
1936+
self.final_output = ""
1937+
self.current_agent = agent
1938+
self.new_items: list[Any] = []
1939+
self.raw_responses = [
1940+
ModelResponse(
1941+
output=[get_text_message("Recovered nested summary")],
1942+
usage=Usage(),
1943+
response_id="resp_nested",
1944+
)
1945+
]
1946+
self.run_loop_task = asyncio.create_task(fail_run_loop())
1947+
1948+
async def stream_events(self):
1949+
yield stream_event
1950+
raise asyncio.CancelledError("stream-cancelled")
1951+
1952+
streaming_result = DummyStreamingResult()
1953+
with contextlib.suppress(RuntimeError):
1954+
await streaming_result.run_loop_task
1955+
1956+
def fake_run_streamed(
1957+
cls,
1958+
starting_agent,
1959+
input,
1960+
*,
1961+
context,
1962+
max_turns,
1963+
hooks,
1964+
run_config,
1965+
previous_response_id,
1966+
auto_previous_response_id=False,
1967+
conversation_id,
1968+
session,
1969+
):
1970+
return streaming_result
1971+
1972+
async def unexpected_run(*args: Any, **kwargs: Any) -> None:
1973+
raise AssertionError("Runner.run should not be called when on_stream is provided.")
1974+
1975+
monkeypatch.setattr(Runner, "run_streamed", classmethod(fake_run_streamed))
1976+
monkeypatch.setattr(Runner, "run", classmethod(unexpected_run))
1977+
1978+
async def on_stream(payload: AgentToolStreamEvent) -> None:
1979+
del payload
1980+
1981+
tool_call = ResponseFunctionToolCall(
1982+
id="call_failed_done",
1983+
arguments='{"input": "recover"}',
1984+
call_id="call-failed-done",
1985+
name="stream_tool",
1986+
type="function_call",
1987+
)
1988+
1989+
tool = agent.as_tool(
1990+
tool_name="stream_tool",
1991+
tool_description="Streams events",
1992+
on_stream=on_stream,
1993+
)
1994+
1995+
tool_context = ToolContext(
1996+
context=None,
1997+
tool_name="stream_tool",
1998+
tool_call_id=tool_call.call_id,
1999+
tool_arguments=tool_call.arguments,
2000+
tool_call=tool_call,
2001+
)
2002+
with pytest.raises(asyncio.CancelledError, match="stream-cancelled"):
2003+
await tool.on_invoke_tool(tool_context, '{"input": "recover"}')
2004+
2005+
19242006
@pytest.mark.asyncio
19252007
async def test_agent_as_tool_streaming_extractor_can_access_agent_tool_invocation(
19262008
monkeypatch: pytest.MonkeyPatch,

0 commit comments

Comments
 (0)