Skip to content

fix(agent): explain abnormal turn endings instead of blank/partial reply (#34452) - #34848

Merged
teknium1 merged 3 commits into
mainfrom
hermes/hermes-2454e468
May 30, 2026
Merged

fix(agent): explain abnormal turn endings instead of blank/partial reply (#34452)#34848
teknium1 merged 3 commits into
mainfrom
hermes/hermes-2454e468

Conversation

@teknium1

@teknium1 teknium1 commented May 29, 2026

Copy link
Copy Markdown
Contributor

Summary

When a turn ends abnormally with no usable reply — empty content after retries, a partial/truncated stream, a still-pending tool result, or an iteration/budget limit — the agent now surfaces a single user-visible line explaining why the reply stopped, instead of a blank box or a bare (empty) sentinel. Normal short replies (Done.) stay completely quiet.

Closes #34452. Salvage of #34470 (@Bartok9), rebased onto current main with two follow-up fixes on top.

Changes

  • run_agent.py: _format_turn_completion_explanation() maps each turn_exit_reason to a short, actionable message; _turn_completion_explainer_enabled() is the config/env seam (mirrors _file_mutation_verifier_enabled).
  • agent/conversation_loop.py: at turn end, after the file-mutation verifier footer, replaces the (empty)/blank sentinel with the explanation, or appends it to a short truncated fragment (≤24 chars, no terminating punctuation). text_response(...) exits and long replies are never touched.
  • hermes_cli/config.py: register display.turn_completion_explainer: True in DEFAULT_CONFIG (follow-up — the original PR relied on the absent-key default; this makes it discoverable, matching file_mutation_verifier).
  • Footer prefix shortened from Turn ended without a usable reply: to No reply: (follow-up) so the 10 reason variants don't all open with the same 8-word boilerplate.

Gated by display.turn_completion_explainer (default on) with HERMES_TURN_COMPLETION_EXPLAINER override. No changes to streaming, recovery, retry/fallback machinery, or the turn_exit_reason values themselves — presentation only, so prompt caching and message-flow invariants are untouched.

Validation

Before After
Empty after retries blank box / (empty) ⚠️ No reply: the model returned empty content after retries…
Truncated stream (The) The then silence The + ⚠️ No reply: streaming stopped early…
Normal Done. Done. Done. (unchanged)
  • tests/run_agent/test_turn_completion_explainer.py — 11 passed (formatter map, env/config seam, end-to-end empty-exhausted surfaces the explanation, Done. stays quiet).
  • tests/run_agent/test_dict_tool_call_args.py — 1 passed.
  • tests/run_agent/test_run_agent.py empty/reasoning/nudge/fallback subset — 35 passed (5 assertions updated from the bare (empty) sentinel to the new explanation).
  • tests/hermes_cli/test_tools_config.py + test_tools_disable_enable.py — 100 passed under the hermetic isolation runner (config-key addition is deep-merge safe, no version bump needed).

Contributor authorship preserved: both Bartok9 commits land via rebase merge; the config/wording follow-up is a separate commit.

Infographic

turn-completion-explainer

@github-actions

github-actions Bot commented May 29, 2026

Copy link
Copy Markdown
Contributor

🔎 Lint report: hermes/hermes-2454e468 vs origin/main

ruff

Total: 0 on HEAD, 0 on base (➖ 0)

🆕 New issues: none

✅ Fixed issues: none

Unchanged: 0 pre-existing issues carried over.

ty (type checker)

Total: 9466 on HEAD, 9458 on base (🆕 +8)

🆕 New issues (7):

Rule Count
unresolved-attribute 7
First entries
tests/run_agent/test_turn_completion_explainer.py:57: [unresolved-attribute] unresolved-attribute: Unresolved attribute `save_trajectories` on type `AIAgent`
tests/run_agent/test_turn_completion_explainer.py:56: [unresolved-attribute] unresolved-attribute: Unresolved attribute `compression_enabled` on type `AIAgent`
tests/run_agent/test_turn_completion_explainer.py:53: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_cached_system_prompt` on type `AIAgent`
tests/run_agent/test_turn_completion_explainer.py:59: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_fallback_chain` on type `AIAgent`
tests/run_agent/test_turn_completion_explainer.py:54: [unresolved-attribute] unresolved-attribute: Unresolved attribute `_use_prompt_caching` on type `AIAgent`
tests/run_agent/test_turn_completion_explainer.py:168: [unresolved-attribute] unresolved-attribute: Attribute `chat` is not defined on `None` in union `None | Any`
tests/run_agent/test_turn_completion_explainer.py:55: [unresolved-attribute] unresolved-attribute: Unresolved attribute `tool_delay` on type `AIAgent`

✅ Fixed issues: none

Unchanged: 4912 pre-existing issues carried over.

Diagnostics are surfaced as warnings — this check never fails the build.

@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint labels May 29, 2026
Bartok9 and others added 3 commits May 29, 2026 19:07
When a turn ends abnormally after substantive tool calls (empty content
after retries, a partial/truncated stream, exhausted retries, or an
iteration/budget limit), the CLI/TUI response area was left blank or
showed only a fragment (e.g. "The") with no consolidated reason. The
internal turn_exit_reason values (empty_response_exhausted,
partial_stream_recovery, etc.) were never surfaced to the user.

Add a turn-completion explainer that mirrors the existing file-mutation
verifier footer: at turn end, map an abnormal turn_exit_reason to a
short, actionable message and either replace the bare "(empty)"
sentinel or append the reason after a partial fragment. Normal
text_response exits (e.g. a terse "Done.") stay quiet.

Gated by display.turn_completion_explainer (default on) with
HERMES_TURN_COMPLETION_EXPLAINER env override, matching the
file-mutation verifier seam.

Closes #34452
PR #34470 adds an explainer suffix to abnormal turn endings (e.g.
max_iterations_reached) so users see why the response is short instead
of receiving a bare/blank reply. test_tool_call_validation_accepts_dict_arguments
runs the agent at max_iterations=3 which hits the explainer path; the
existing strict-equality assertion (== "done") no longer matches once
the suffix is appended.

Switch the assertion to .startswith("done") so the test continues to
verify that the models actual text survives intact while leaving the
explainer suffix wording owned by conversation_loop (where it belongs).

Test now passes (1 passed in 0.88s).
Follow-up to the salvaged #34452 turn-completion explainer:
- Register display.turn_completion_explainer: True in DEFAULT_CONFIG so the
  setting is discoverable, matching the file_mutation_verifier precedent.
- Shorten the repeated footer prefix from 'Turn ended without a usable
  reply: ' to 'No reply: ' so the 10 reason variants don't all open with
  the same 8-word boilerplate.
- Update the 7 assertions that referenced the old prefix.
@teknium1
teknium1 force-pushed the hermes/hermes-2454e468 branch from 3394deb to c3ce7f6 Compare May 30, 2026 02:08
@teknium1
teknium1 merged commit fb0ab27 into main May 30, 2026
30 of 31 checks passed
@teknium1
teknium1 deleted the hermes/hermes-2454e468 branch May 30, 2026 02:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Agent turn ends with empty/partial reply after tools — user gets no explanation

3 participants