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:
@@ -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:
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user