From 6bb630ef783e8bacdb4135fd06b6fcd2445eeb51 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 6 Aug 2026 22:02:46 -0500 Subject: [PATCH] fix(codex): split reasoning summary parts on summary_index The native Responses stream does carry summary_index, so the part boundary is structured data here rather than something to infer. Break on a change of index, and leave streams that send no index (plain reasoning_text) untouched. --- agent/codex_runtime.py | 15 ++++++ .../test_run_agent_codex_responses.py | 54 +++++++++++++++++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/agent/codex_runtime.py b/agent/codex_runtime.py index c01084c4a4..f36f1c78fe 100644 --- a/agent/codex_runtime.py +++ b/agent/codex_runtime.py @@ -1027,6 +1027,10 @@ def _consume_codex_event_stream( first_delta_fired = False active_message_phase: str | None = None commentary_text_deltas: List[str] = [] + # Last reasoning summary_index seen. The Responses stream delimits summary + # parts by this index and gives each part no separator of its own, so a + # change of index is where the blank line belongs. + active_summary_index: Any = None terminal_status: str = "completed" terminal_usage: Any = None terminal_response_id: str = None @@ -1120,6 +1124,17 @@ def _consume_codex_event_stream( if "reasoning" in event_type and "delta" in event_type: reasoning_text = _event_field(event, "delta", "") if reasoning_text and on_reasoning_delta is not None: + # Summary parts stream one after another with no separator of + # their own; summary_index is the boundary the wire gives us. + summary_index = _event_field(event, "summary_index") + if ( + summary_index is not None + and active_summary_index is not None + and summary_index != active_summary_index + ): + reasoning_text = f"\n\n{reasoning_text}" + if summary_index is not None: + active_summary_index = summary_index try: on_reasoning_delta(reasoning_text) except Exception: diff --git a/tests/run_agent/test_run_agent_codex_responses.py b/tests/run_agent/test_run_agent_codex_responses.py index 4b11b1fdb7..18dc13526b 100644 --- a/tests/run_agent/test_run_agent_codex_responses.py +++ b/tests/run_agent/test_run_agent_codex_responses.py @@ -1985,11 +1985,59 @@ def test_duplicate_detection_uses_commentary_when_hidden_reasoning_changes(monke +def test_consume_codex_stream_separates_reasoning_summary_parts(): + """summary_index is the part boundary; the wire sends no separator itself.""" + from agent.codex_runtime import _consume_codex_event_stream + + reasoning_streamed = [] + + _consume_codex_event_stream( + _FakeCreateStream([ + SimpleNamespace(type="response.created"), + SimpleNamespace( + type="response.reasoning_summary_text.delta", + summary_index=0, + delta="**Investigating culprit PRs**", + ), + SimpleNamespace( + type="response.reasoning_summary_text.delta", + summary_index=1, + delta="**Inspecting message schema**", + ), + SimpleNamespace( + type="response.reasoning_summary_text.delta", + summary_index=1, + delta=" and tool_calls content", + ), + SimpleNamespace(type="response.completed", response=SimpleNamespace(status="completed")), + ]), + model="gpt-5-codex", + on_reasoning_delta=reasoning_streamed.append, + ) + + joined = "".join(reasoning_streamed) + assert "****" not in joined + assert joined == ( + "**Investigating culprit PRs**" + "\n\n**Inspecting message schema** and tool_calls content" + ) +def test_consume_codex_stream_leaves_unindexed_reasoning_untouched(): + """Streams with no summary_index (plain reasoning_text) must not gain breaks.""" + from agent.codex_runtime import _consume_codex_event_stream + reasoning_streamed = [] + _consume_codex_event_stream( + _FakeCreateStream([ + SimpleNamespace(type="response.created"), + SimpleNamespace(type="response.reasoning_text.delta", delta="Need to "), + SimpleNamespace(type="response.reasoning_text.delta", delta="inspect files."), + SimpleNamespace(type="response.completed", response=SimpleNamespace(status="completed")), + ]), + model="gpt-5-codex", + on_reasoning_delta=reasoning_streamed.append, + ) - - - + assert "".join(reasoning_streamed) == "Need to inspect files."