From 2021442c8a5cc982cf787e829de064c00ccc8a3d Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:58:47 -0700 Subject: [PATCH] fix: cover remaining codex empty-output gaps in fallback + normalizer (#5724) Two gaps in the codex empty-output handling: 1. _run_codex_create_stream_fallback() skipped all non-terminal events, so when the fallback path was used and the terminal response had empty output, there was no recovery. Now collects output_item.done and text deltas during the fallback stream, backfills on empty output. 2. _normalize_codex_response() hard-crashed with RuntimeError when output was empty, even when the response had output_text set. The function already had fallback logic at line 3562 to use output_text, but the guard at line 3446 killed it first. Now checks output_text before raising and synthesizes a minimal output item. --- run_agent.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/run_agent.py b/run_agent.py index f5af556bb..cc0e06bdc 100644 --- a/run_agent.py +++ b/run_agent.py @@ -3444,7 +3444,22 @@ class AIAgent: """Normalize a Responses API object to an assistant_message-like object.""" output = getattr(response, "output", None) if not isinstance(output, list) or not output: - raise RuntimeError("Responses API returned no output items") + # The Codex backend can return empty output when the answer was + # delivered entirely via stream events. Check output_text as a + # last-resort fallback before raising. + out_text = getattr(response, "output_text", None) + if isinstance(out_text, str) and out_text.strip(): + logger.debug( + "Codex response has empty output but output_text is present (%d chars); " + "synthesizing output item.", len(out_text.strip()), + ) + output = [SimpleNamespace( + type="message", role="assistant", status="completed", + content=[SimpleNamespace(type="output_text", text=out_text.strip())], + )] + response.output = output + else: + raise RuntimeError("Responses API returned no output items") response_status = getattr(response, "status", None) if isinstance(response_status, str): @@ -3997,11 +4012,28 @@ class AIAgent: return stream_or_response terminal_response = None + collected_output_items: list = [] + collected_text_deltas: list = [] try: for event in stream_or_response: event_type = getattr(event, "type", None) if not event_type and isinstance(event, dict): event_type = event.get("type") + + # Collect output items and text deltas for backfill + if event_type == "response.output_item.done": + done_item = getattr(event, "item", None) + if done_item is None and isinstance(event, dict): + done_item = event.get("item") + if done_item is not None: + collected_output_items.append(done_item) + elif event_type in ("response.output_text.delta",): + delta = getattr(event, "delta", "") + if not delta and isinstance(event, dict): + delta = event.get("delta", "") + if delta: + collected_text_deltas.append(delta) + if event_type not in {"response.completed", "response.incomplete", "response.failed"}: continue @@ -4009,6 +4041,26 @@ class AIAgent: if terminal_response is None and isinstance(event, dict): terminal_response = event.get("response") if terminal_response is not None: + # Backfill empty output from collected stream events + _out = getattr(terminal_response, "output", None) + if isinstance(_out, list) and not _out: + if collected_output_items: + terminal_response.output = list(collected_output_items) + logger.debug( + "Codex fallback stream: backfilled %d output items", + len(collected_output_items), + ) + elif collected_text_deltas: + assembled = "".join(collected_text_deltas) + terminal_response.output = [SimpleNamespace( + type="message", role="assistant", + status="completed", + content=[SimpleNamespace(type="output_text", text=assembled)], + )] + logger.debug( + "Codex fallback stream: synthesized from %d deltas (%d chars)", + len(collected_text_deltas), len(assembled), + ) return terminal_response finally: close_fn = getattr(stream_or_response, "close", None)