Skip to content

fix(agent): chat() surfaces error instead of KeyError on no-final_response result - #34770

Closed
OmarB97 wants to merge 2 commits into
NousResearch:mainfrom
OmarB97:fix/agent-chat-missing-final-response
Closed

fix(agent): chat() surfaces error instead of KeyError on no-final_response result#34770
OmarB97 wants to merge 2 commits into
NousResearch:mainfrom
OmarB97:fix/agent-chat-missing-final-response

Conversation

@OmarB97

@OmarB97 OmarB97 commented May 29, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

AIAgent.chat() raised KeyError: 'final_response' whenever run_conversation
returned via one of its error/failure paths. Those paths (API error after max
retries, billing/credits exhaustion, policy/toolguard halt, interrupts) return a
result dict shaped like {messages, completed, api_calls, error, failed} with
no final_response key, so return result["final_response"] crashed —
masking the real error behind a KeyError traceback.

This makes chat() surface the error message and degrade gracefully instead of
crashing.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • run_agent.pyAIAgent.chat() now returns
    result.get("final_response") or result.get("error") or "" instead of
    result["final_response"], so error/failure results surface their error
    string rather than raising KeyError.
  • run_agent.py — guard the CLI conversation-summary printer with
    result.get('final_response') (same omitted-key hazard).
  • tests/test_chat_final_response_resilience.py — regression tests covering the
    error path (no final_response → returns error), the happy path, and the
    empty case.

How to Test

  1. pytest tests/test_chat_final_response_resilience.py → 3 passed.
  2. Before this change, any chat() call whose underlying conversation hit an
    error path (e.g. an API error after retries) raised
    KeyError: 'final_response' at run_agent.py return result["final_response"].
    After, it returns the error string and the caller can handle/log it.

Notes

Surfaced in practice when a non-interactive hermes -z oneshot run hit a
provider error path: the launcher recorded a KeyError: 'final_response'
traceback instead of the underlying error.

…ponse result

AIAgent.chat() did `return result["final_response"]`, but several error/failure
return paths in agent.conversation_loop.run_conversation (API error after max
retries, billing/credits exhaustion, policy/toolguard halt, interrupts) return a
result dict with {messages, completed, api_calls, error, failed} and NO
'final_response' key. On those paths chat() raised `KeyError: 'final_response'`
instead of surfacing the error, so callers saw a traceback rather than the
actual failure (e.g. an API error).

Return `result.get("final_response") or result.get("error") or ""` so the error
message is surfaced and the call degrades gracefully. Also guard the CLI summary
printer with `result.get('final_response')`.

Adds tests for the error path, the happy path, and the empty case.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@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
@liuhao1024

Copy link
Copy Markdown
Contributor

Verified the fix is correct and the bug is real.

Confirmed: run_conversation() has 6+ error return paths that omit final_response (e.g., line 1471 in agent/conversation_loop.py: "Invalid API response after max retries" with "failed": True but no final_response key). The current chat() code (return result["final_response"]) raises KeyError on all of them.

The or chain result.get("final_response") or result.get("error") or "" is the right approach:

  • .get() avoids KeyError
  • Falls back to error message so the caller sees what went wrong
  • Empty string as final fallback when neither key exists

Tests cover all three branches correctly. The main() function fix (result['final_response']result.get('final_response')) is consistent — same crash path, same fix.

One minor note: the or chain means a genuinely empty-string final_response ("") would fall through to error. This is unlikely in practice (agents don't produce empty responses), but worth being aware of.

@OmarB97

OmarB97 commented Jun 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for verifying, @liuhao1024 — appreciate the independent confirmation of the error paths. The branch is conflict-free and ready for maintainer review whenever someone gets a chance.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused regression report and tests. The reported failure mode is already fixed on current main.

  • Automated hermes-sweeper review verified commit 053424c4865db0e8cc6ef9a8c2f49bf8882afd45 (fix(agent): preserve final_response on failure returns) is on current main and shipped in v2026.7.1.
  • agent/conversation_loop.py:1568-1575 now returns _final_response under both final_response and error for retry exhaustion; the same superseding fix covered the other terminal failure branches.
  • tests/run_agent/test_run_agent.py:81-94 enforces that every dictionary return from run_conversation() carries a non-None final_response.

Because AIAgent.chat()'s direct result["final_response"] now receives that guaranteed key on the reported error paths, this PR's behavior is implemented on main.

@teknium1 teknium1 closed this Jul 13, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 13, 2026
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 sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants