fix(agent): preserve final_response on failure returns - #39345
Conversation
Morad37
left a comment
There was a problem hiding this comment.
Nice one. This covers all the failure exits. The error-to-final_response mapping means callers no longer get dicts with 'error' set but 'final_response' missing. That was a subtle hole in the contract.
Review: fix(agent): preserve final_response on failure returnsVerdict: approve ✅ (no blocking issues)This PR correctly addresses the task goal — adding Findings (all non-blocking suggestions)1. AST test could benefit from a docstring (suggestion)
2. AST test dependency on source file availability (suggestion) 3. 4. Potential merge conflict signal (info) Test coverage
|
|
MeshBoard verification update on 2026-06-05 after follow-up commit 93326ec:
I attempted auto-merge before the follow-up patch and GitHub blocked it because OmarB97 does not have permission to enable auto-merge on NousResearch/hermes-agent. The current PR remains maintainer-gated rather than locally verification-gated. |
|
Superseded note: this correction was malformed by shell quoting and is replaced by the updated MeshBoard verification comment for PR head 93326ec. |
|
Superseded note: after inspecting the literal final_response None branches, I pushed follow-up commit 93326ec to strengthen the contract and the AST regression. See the updated MeshBoard verification comment for current evidence. |
Morad37
left a comment
There was a problem hiding this comment.
The follow-up patch converting final_response: None to a meaningful string in every path closes the last gap. The six-path contract is now fully consistent — all dict returns from _perform_api_call carry both error and final_response with matching values. Still good from my side.
|
Merged via #56176 (rebase-merge, your authorship preserved in git history). Your fix approach and AST regression test were reapplied to current Verified the premise still held on Thanks for the fix and the durable regression test! |
Why
A MeshBoard Hermes worker streamed real activity, then exited through
hermes -zwith:hermes -z: agent failed: 'final_response'The linked stream tap showed the upstream path before that crash: successful streaming/tool-call activity, two
ClientConnectionResetErrorstream drops, large fallback requests returning 400, small recovery/finalization requests returning 502 then 200, and finally the oneshot wrapper crashed because the agent result did not containfinal_response.Root cause:
AIAgent.run_conversation()promises a dict withfinal_response, but several terminal failure branches returned dicts with onlyerror.hermes_cli.oneshot._run_agent()indexesresult['final_response'], so those branches turn a real provider/context failure into an opaque wrapperKeyError.What changed
final_responseto the invalid-response retry-exhaustion return.final_responseto payload/context compression-exhaustion returns.erroridentical tofinal_responsefor these terminal failures so callers can both display and classify the same actionable message.run_conversation()dict return omitsfinal_response.final_response == error.Evidence
Live failing sequence from MeshBoard dispatch an automated
hermes -zwrapper run:ClientConnectionResetError: Cannot write to closing transportagent failed: 'final_response'That matches the contract hole: the real upstream/provider failure should be returned as a failed agent result, not converted into a missing-key crash.
Verification
python -m py_compile agent/conversation_loop.py hermes_cli/oneshot.pypython -m pytest tests/run_agent/test_run_agent.py::test_run_conversation_dict_returns_include_final_response tests/run_agent/test_run_agent.py::TestRetryExhaustion -q-> 4 passedpython -m pytest tests/run_agent/test_413_compression.py -q-> 20 passedpython -m pytest tests/hermes_cli/test_tui_resume_flow.py::test_oneshot_fails_closed_on_empty_final_response tests/hermes_cli/test_tui_resume_flow.py::test_oneshot_fails_closed_on_agent_exception tests/hermes_cli/test_tui_resume_flow.py::test_oneshot_prints_nonempty_final_response -q-> 3 passedgit diff --check HEAD^ HEADRisks / gaps
This does not hide the underlying provider/context failure; it preserves it in the promised
final_responsefield so wrappers, cron, gateway, and external launchers can surface the actionable error instead of crashing on a missing key. The broader stream reset/400/502 behavior can still be investigated separately, but this removes the opaquefinal_responseloss that made the worker task look like a Hermes wrapper failure.