diff --git a/agent/codex_responses_adapter.py b/agent/codex_responses_adapter.py index 6d41b2a9e8ba..1d14148c7aeb 100644 --- a/agent/codex_responses_adapter.py +++ b/agent/codex_responses_adapter.py @@ -1382,10 +1382,11 @@ def _normalize_codex_response( # Response contains only reasoning (encrypted thinking state and/or # human-readable summary) with no visible content or tool calls. # - # For known Codex/xAI backends, reasoning-only with status="completed" - # means "the model is still thinking and needs another turn" — treat - # it as incomplete so the Codex continuation path retries instead of - # falling into the empty-content retry loop. + # For the specially-handled backends (Codex, xAI, GitHub/Copilot), + # reasoning-only with status="completed" means "the model is still + # thinking and needs another turn" — treat it as incomplete so the + # Codex continuation path retries instead of falling into the + # empty-content retry loop. # # For all other backends (other:, etc.), trust the provider's # own response.status signal. When status == "completed" and no items @@ -1393,7 +1394,11 @@ def _normalize_codex_response( # state — forcing "incomplete" causes multi-minute stalls as the # continuation path re-issues calls (3 retries × up to 240s each). # See https://github.com/NousResearch/hermes-agent/issues/64434 - if response_status == "completed" and issuer_kind not in ("codex_backend", "xai_responses"): + if response_status == "completed" and issuer_kind not in ( + "codex_backend", + "xai_responses", + "github_responses", + ): finish_reason = "stop" else: finish_reason = "incomplete" diff --git a/tests/run_agent/test_run_agent_codex_responses.py b/tests/run_agent/test_run_agent_codex_responses.py index c0d86ddd731b..6aa381dd2321 100644 --- a/tests/run_agent/test_run_agent_codex_responses.py +++ b/tests/run_agent/test_run_agent_codex_responses.py @@ -2414,7 +2414,7 @@ def test_normalize_codex_response_reasoning_only_completed_is_stop_without_issue assert assistant_message.content == "" -def test_normalize_codex_response_reasoning_only_is_stop_for_xai_backend(monkeypatch): +def test_normalize_codex_response_reasoning_only_stays_incomplete_for_xai_backend(monkeypatch): """xAI backend also preserves incomplete for reasoning-only (same as Codex).""" agent = _build_agent(monkeypatch) from agent.codex_responses_adapter import _normalize_codex_response @@ -2427,6 +2427,25 @@ def test_normalize_codex_response_reasoning_only_is_stop_for_xai_backend(monkeyp assert assistant_message.content == "" +def test_normalize_codex_response_reasoning_only_stays_incomplete_for_github_backend(monkeypatch): + """GitHub/Copilot Responses backend preserves incomplete for reasoning-only. + + Copilot fronts the same OpenAI model family as codex_backend and exhibits + the same reasoning-only "still thinking" degeneration mode, so it must + stay on the continuation path — only unrecognized (other:*) backends + trust response.status='completed' as terminal. + """ + agent = _build_agent(monkeypatch) + from agent.codex_responses_adapter import _normalize_codex_response + response = _codex_reasoning_only_response() + assistant_message, finish_reason = _normalize_codex_response( + response, issuer_kind="github_responses" + ) + + assert finish_reason == "incomplete" + assert assistant_message.content == "" + + def test_normalize_codex_response_reasoning_with_content_is_stop(monkeypatch): """If a response has both reasoning and message content, it should still be 'stop'.""" agent = _build_agent(monkeypatch)