mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-14 04:02:26 +00:00
fix(run_agent): break permanent empty-response loop from orphan tool-tail (#21385)
When empty-response terminal scaffolding fires on a tool-result turn, _drop_trailing_empty_response_scaffolding left the live history ending at a bare 'tool' message. The next user input then landed as [...tool, user], a protocol-invalid sequence that OpenRouter/Opus and other providers silently fail on (returns empty content). That retriggered the empty-retry recovery every turn, and recovery flags never hit SQLite (no column for them), so history kept looking broken on every reload. Two fixes: 1. Scaffolding strip rewinds the orphan assistant(tool_calls)+tool pair after popping sentinels. Only fires when scaffolding flags were actually present, so mid-iteration tool loops are untouched. 2. _repair_message_sequence runs right before every API call as a defensive belt: drops stray tool messages with unknown tool_call_ids, merges consecutive user messages so no user input is lost. Does NOT rewind assistant(tool_calls)+tool+user — that pattern is valid when the user redirected before the model got its continuation turn. Repro: session 20260507_044111_fa7e65. Opus-4.7/OpenRouter returned content-less response after a 42KB execute_code output, nudge+retry chain exhausted (no fallback configured), terminal sentinel appended, scaffolding stripped leaving bare tool tail, user typed 'wtf happened..' and landed as tool→user violation. Every subsequent turn collapsed in <50ms with the same 3-retry empty chain because the API request itself was malformed. Verified live via HTTP mock: pre-fix reproduced 5 api_calls/0.15s exit 'empty_response_exhausted'; post-fix 1 api_call/0.10s exit 'text_response(finish_reason=stop)'. Three-turn session flows cleanly through the scenario. Full run_agent suite: 1242 passed (0 regressions, 2 pre-existing concurrent_interrupt failures unrelated).
This commit is contained in:
parent
1d2029b2b7
commit
812ce0b987
3 changed files with 373 additions and 2 deletions
|
|
@ -21,9 +21,21 @@ def _agent_with_stubbed_persistence():
|
|||
|
||||
|
||||
def test_persist_session_strips_trailing_empty_recovery_scaffolding():
|
||||
"""After stripping scaffolding, also rewind past orphan trailing tool-result
|
||||
messages that the failed iteration left behind. Otherwise the next user
|
||||
message lands after a bare ``tool`` and produces a protocol-invalid
|
||||
sequence that most providers silently fail on, retriggering the empty-
|
||||
retry loop indefinitely.
|
||||
"""
|
||||
agent = _agent_with_stubbed_persistence()
|
||||
messages = [
|
||||
{"role": "user", "content": "run the task"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [{"id": "call_1", "type": "function",
|
||||
"function": {"name": "x", "arguments": "{}"}}],
|
||||
},
|
||||
{"role": "tool", "content": "{}", "tool_call_id": "call_1"},
|
||||
{
|
||||
"role": "assistant",
|
||||
|
|
@ -42,9 +54,11 @@ def test_persist_session_strips_trailing_empty_recovery_scaffolding():
|
|||
|
||||
AIAgent._persist_session(agent, messages, conversation_history=[])
|
||||
|
||||
# After strip + rewind, only the original user message remains. The
|
||||
# assistant(tool_calls) + tool pair is dropped because its iteration
|
||||
# never produced a real response.
|
||||
assert messages == [
|
||||
{"role": "user", "content": "run the task"},
|
||||
{"role": "tool", "content": "{}", "tool_call_id": "call_1"},
|
||||
]
|
||||
assert agent.saved_session_logs[-1] == messages
|
||||
assert all(not msg.get("_empty_recovery_synthetic") for msg in messages)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue