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.
This commit is contained in:
Brooklyn Nicholson
2026-08-06 22:02:46 -05:00
parent 0f83661808
commit 6bb630ef78
2 changed files with 66 additions and 3 deletions

View File

@@ -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:

View File

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