NousResearch/hermes-agent

Codex Responses finish_reason misclassified as incomplete for reasoning-only turns

Cluster 64434. Data as of

Nothing open on GitHub. Fix merged in #64764 on 15 Jul 2026. Check it covers your case.

The short answer

0open issues
0open PRs
3items grouped
Known?
Yes. 3 related items grouped here, 1 issue already closed.
Fix event
A fix was merged to main in PR #64764. Check it covers your case. No release data is tracked.
Tracker
The maintainers' tracker marks this resolved, deciding PR #64764. That is separate from the fix event.
Still open
Nothing open on GitHub.

The members concern finish_reason being labeled "incomplete" for reasoning-only turns under api_mode=codex_responses, with one issue reporting multi-minute stalls and iteration budget exhaustion. Two items are fixes (one referencing #64434) that appear to propose trusting response.status on unrecognized Responses backends; their outcomes are not established here.

Open issues0

No open issues.

Open PRs0

No open pull requests.

Fixes on record

Merged to main. A stamp counts the issues a fix is linked to.

  • #64764 fix(codex): trust response.status for reasoning-only turns on unrecognized Responses backends

    merged to main

    files: agent/codex_responses_adapter.py, tests/agent/test_codex_responses_adapter.py, tests/run_agent/test_run_agent_codex_responses.py

    Show the diff
    • agent/codex_responses_adapter.py
      @@ -1380,12 +1380,28 @@ def _normalize_codex_response(
               finish_reason = "incomplete"
           elif (reasoning_items_raw or reasoning_parts or saw_reasoning_item) and not final_text:
               # Response contains only reasoning (encrypted thinking state and/or
      -        # human-readable summary) with no visible content or tool calls. The
      -        # model is still thinking and needs another turn to produce the actual
      -        # answer. Marking this as "stop" would send it into the empty-content
      -        # retry loop which burns retries then fails — treat it as incomplete so
      -        # the Codex continuation path handles it correctly.
      -        finish_reason = "incomplete"
      +        # human-readable summary) with no visible content or tool calls.
      +        #
      +        # 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:<base_url>, etc.), trust the provider's
      +        # own response.status signal. When status == "completed" and no items
      +        # are queued/in_progress/incomplete, reasoning alone is a valid final
      +        # 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",
      +            "github_responses",
      +        ):
      +            finish_reason = "stop"
      +        else:
      +            finish_reason = "incomplete"
           else:
               finish_reason = "stop"
           return assistant_message, finish_reason
    • tests/agent/test_codex_responses_adapter.py
      @@ -51,6 +51,12 @@ def test_normalize_codex_response_drops_transient_rs_tmp_reasoning_items():
       
       
       def test_normalize_codex_response_treats_summary_only_reasoning_as_incomplete():
      +    """Summary-only reasoning keeps the continuation path for Codex backends.
      +
      +    Since #64434, an unrecognized issuer with ``response.status="completed"``
      +    trusts the provider and returns ``stop`` — so this test pins the Codex
      +    backend explicitly, where reasoning-only still means "still thinking".
      +    """
           response = SimpleNamespace(
               status="completed",
               output=[
      @@ -63,7 +69,9 @@ def test_normalize_codex_response_treats_summary_only_reasoning_as_incomplete():
               ],
           )
       
      -    assistant_message, finish_reason = _normalize_codex_response(response)
      +    assistant_message, finish_reason = _normalize_codex_response(
      +        response, issuer_kind="codex_backend"
      +    )
       
           assert finish_reason == "incomplete"
           assert assistant_message.content == ""
    • tests/run_agent/test_run_agent_codex_responses.py
      @@ -2359,15 +2359,16 @@ def _codex_reasoning_only_response(*, encrypted_content="enc_abc123", summary_te
       
       
       def test_normalize_codex_response_marks_reasoning_only_as_incomplete(monkeypatch):
      -    """A response with only reasoning items and no content should be 'incomplete', not 'stop'.
      +    """A response with only reasoning items and no content should be 'incomplete' for Codex backends.
       
      -    Without this fix, reasoning-only responses get finish_reason='stop' which
      -    sends them into the empty-content retry loop (3 retries then failure).
      +    Codex CLI uses reasoning-only responses as a signal that the model is still
      +    thinking and needs another turn. This test verifies the Codex-specific path
      +    where issuer_kind="codex_backend" preserves the old behavior.
           """
           agent = _build_agent(monkeypatch)
           from agent.codex_responses_adapter import _normalize_codex_response
           assistant_message, finish_reason = _normalize_codex_response(
      -        _codex_reasoning_only_response()
      +        _codex_reasoning_only_response(), issuer_kind="codex_backend"
           )
       
           assert finish_reason == "incomplete"
      @@ -2377,6 +2378,74 @@ def test_normalize_codex_response_marks_reasoning_only_as_incomplete(monkeypatch
           assert assistant_message.codex_reasoning_items[0]["encrypted_content"] == "enc_abc123"
       
       
      +def test_normalize_codex_response_reasoning_only_completed_is_stop_for_other_backends(monkeypatch):
      +    """Reasoning-only with status='completed' should be 'stop' for non-Codex backends.
      +
      +    When response.status == "completed" and no items are queued/in_progress,
      +    reasoning alone is a valid final state for non-Codex backends. Forcing
      +    "incomplete" here causes multi-minute stalls (3 retries x up to 240s each).
      +    See https://github.com/NousResearch/hermes-agent/issues/64434
      +    """
      +    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="other:example-relay"
      +    )
      +
      +    assert finish_reason == "stop"
      +    assert assistant_message.content == ""
      +    assert assistant_message.codex_reasoning_items is not None
      +    assert len(assistant_message.codex_reasoning_items) == 1
      +
      +
      +def test_normalize_codex_response_reasoning_only_completed_is_stop_without_issuer(monkeypatch):
      +    """Default issuer (None) should also trust response.status='completed' for reasoning-only.
      +
      +    When no issuer_kind is provided (test or default scenario) and the provider
      +    says status='completed', reasoning-only should be treated as 'stop'.
      +    """
      +    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)
      +
      +    assert finish_reason == "stop"
      +    assert assistant_message.content == ""
      +
      +
      +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
      +    response = _codex_reasoning_only_response()
      +    assistant_message, finish_reason = _normalize_codex_response(
      +        response, issuer_kind="xai_responses"
      +    )
      +
      +    assert finish_reason == "incomplete"
      +    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)

1 PR closed without merging

From the close comments: merged via another change 1

  • #64449 fix: finish_reason misclassified as incomplete for codex_responses (#64434) · merged via another change → e14468d, #64764
Closed issues · 1
  • Closed#64434finish_reason misclassified as "incomplete" for reasoning-only completed turns on api_mode=codex_responses (causes multi-minute stalls / iteration budget exhaustion)
How this cluster was grouped
  • Link evidence (structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference.
  • Pairwise link density 0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36. 1.00 means every pair is linked.

Pick up a Lab task

Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.

Open the Lab

At a glance

Merged PRs
#64764
Closed issues
1
Attempts
1 PR closed without merging
Fix link checked
15 Jul 2026

For your agent

The same facts, typed.