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."