hermes - 💡(How to fix) Fix [Bug]: Tool Result Contamination Causes Persistent HTTP 400 Error Loop

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Error Message

Root Cause Chain: User message → LLM responds with tool_calls → run_agent._execute_tool_calls_concurrent() → _invoke_tool() throws OR returns error string → lines 7635-7640: tool_msg = {"role": "tool", "content": "Error executing tool '...': Code 400", "tool_call_id": tc.id} messages.append(tool_msg) ←污染开始 (contamination begins) → _flush_messages_to_session_db() lines 2514-2536: for msg in messages[flush_from:]: self._session_db.append_message(..., content=msg["content"], ...) ←写入DB → Session ends, next user message arrives → gateway.run.run_sync() line 3685: history = self.session_store.load_transcript(session_entry.session_id) ←重新加载 → gateway.run.run_sync() lines 8836-8838: if has_tool_calls or has_tool_call_id or is_tool_message: agent_history.append(clean_msg) ←重新放入上下文 → agent.run_conversation() → LLM API call with contaminated history → HTTP 400 again → loop repeats Key Code References: FileLine(s)Description run_agent.py 7493–7495 Tool exception caught and converted to error string run_agent.py 7635–7640 Error tool message appended to messages with no filtering run_agent.py 2514–2536 _flush_messages_to_session_db writes ALL messages including errors run_agent.py 2541–2564 _get_messages_up_to_last_assistant() — rollback helper exists but unused for tool errors gateway/session.py 1011–1057 load_transcript() returns full history including error tool results gateway/run.py 8830–8838 Tool messages passed through to agent_history without sanitization gateway/run.py 8976 run_conversation(message, conversation_history=agent_history, ...) agent/anthropic_adapter.py 1107–1142 _strip_orphaned_tool_blocks — partial fix only, does not handle invalid content in paired blocks

Root Cause

Root Cause Chain:
User message → LLM responds with tool_calls → run_agent._execute_tool_calls_concurrent()
→ _invoke_tool() throws OR returns error string → lines 7635-7640:
    tool_msg = {"role": "tool", "content": "Error executing tool '...': Code 400", "tool_call_id": tc.id}
    messages.append(tool_msg)          ←污染开始 (contamination begins)
→ _flush_messages_to_session_db() lines 2514-2536:
    for msg in messages[flush_from:]:
        self._session_db.append_message(..., content=msg["content"], ...)  ←写入DB
→ Session ends, next user message arrives
→ gateway.run.run_sync() line 3685:
    history = self.session_store.load_transcript(session_entry.session_id)  ←重新加载
→ gateway.run.run_sync() lines 8836-8838:
    if has_tool_calls or has_tool_call_id or is_tool_message:
        agent_history.append(clean_msg)  ←重新放入上下文
→ agent.run_conversation() → LLM API call with contaminated history
→ HTTP 400 again → loop repeats
Key Code References:
FileLine(s)Description
run_agent.py	7493–7495	Tool exception caught and converted to error string
run_agent.py	7635–7640	Error tool message appended to messages with no filtering
run_agent.py	2514–2536	_flush_messages_to_session_db writes ALL messages including errors
run_agent.py	2541–2564	_get_messages_up_to_last_assistant() — rollback helper exists but unused for tool errors
gateway/session.py	1011–1057	load_transcript() returns full history including error tool results
gateway/run.py	8830–8838	Tool messages passed through to agent_history without sanitization
gateway/run.py	8976	run_conversation(message, conversation_history=agent_history, ...)
agent/anthropic_adapter.py	1107–1142	_strip_orphaned_tool_blocks — partial fix only, does not handle invalid content in paired blocks

Code Example

Root Cause Chain:
User message → LLM responds with tool_calls → run_agent._execute_tool_calls_concurrent()
_invoke_tool() throws OR returns error string → lines 7635-7640:
    tool_msg = {"role": "tool", "content": "Error executing tool '...': Code 400", "tool_call_id": tc.id}
    messages.append(tool_msg)          ←污染开始 (contamination begins)
_flush_messages_to_session_db() lines 2514-2536:
    for msg in messages[flush_from:]:
        self._session_db.append_message(..., content=msg["content"], ...)  ←写入DB
Session ends, next user message arrives
→ gateway.run.run_sync() line 3685:
    history = self.session_store.load_transcript(session_entry.session_id)  ←重新加载
→ gateway.run.run_sync() lines 8836-8838:
    if has_tool_calls or has_tool_call_id or is_tool_message:
        agent_history.append(clean_msg)  ←重新放入上下文
→ agent.run_conversation()LLM API call with contaminated history
HTTP 400 again → loop repeats
Key Code References:
FileLine(s)Description
run_agent.py	74937495	Tool exception caught and converted to error string
run_agent.py	76357640	Error tool message appended to messages with no filtering
run_agent.py	25142536	_flush_messages_to_session_db writes ALL messages including errors
run_agent.py	25412564	_get_messages_up_to_last_assistant() — rollback helper exists but unused for tool errors
gateway/session.py	10111057	load_transcript() returns full history including error tool results
gateway/run.py	88308838	Tool messages passed through to agent_history without sanitization
gateway/run.py	8976	run_conversation(message, conversation_history=agent_history, ...)
agent/anthropic_adapter.py	11071142	_strip_orphaned_tool_blocks — partial fix only, does not handle invalid content in paired blocks

---
RAW_BUFFERClick to expand / collapse

Bug Description

When a tool invocation returns an error (particularly structured errors such as JSON overflow, parameter validation failures, or API-level errors that produce HTTP 400), the error result is written directly into the conversation messages list and persisted to the session database without any filtering. On every subsequent turn, this contaminated message is reloaded as part of conversation_history and re-sent to the LLM, causing a persistent loop of identical HTTP 400 errors. The system has no session isolation mechanism to break this cycle. The user experiences every subsequent message triggering the same Code 400 error regardless of the actual query content.

Steps to Reproduce

Send a message that triggers a tool call (e.g., execute_code, terminal, browser_navigate) The tool returns an error — for example, execute_code with an oversized JSON parameter triggers HTTP 400 from the LLM provider, or a tool returns a structured error containing special characters that produce malformed tool_result blocks Observe that the error is written into the conversation as a tool role message with tool_call_id Send any follow-up message (even "hello") The error recurs — the agent reports the same Code 400 error Minimal reproduction case:

Trigger a tool that returns a structured error with invalid content

e.g., execute_code with a code string exceeding the JSON parameter limit (~1500 chars)

The error "Error executing tool 'execute_code': ..." gets written to messages

On the next turn, this error appears in conversation_history

Re-sending the malformed tool_result causes HTTP 400 again

Expected Behavior

Tool error results should either: Be excluded from the messages list before persistence, or Be stored with a flag that prevents them from being re-sent to the LLM on subsequent turns, or Trigger automatic session isolation (rollback or new session branch) when a critical tool error is detected After a tool failure, subsequent user messages should succeed (possibly with a user-facing error message about the tool failure), not repeat the same error indefinitely

Actual Behavior

Tool execution throws an exception or returns an error string The error string is appended to messages as a tool role message with the original tool_call_id _flush_messages_to_session_db() writes this error message to SQLite (or JSONL) verbatim load_transcript() reloads the full history on the next turn gateway/run.py passes the contaminated agent_history to run_conversation() The LLM receives the malformed tool result again and returns HTTP 400 The error loop repeats infinitely until the user manually executes /new

Affected Component

CLI (interactive chat)

Messaging Platform (if gateway-related)

No response

Debug Report

Root Cause Chain:
User message → LLM responds with tool_calls → run_agent._execute_tool_calls_concurrent()
→ _invoke_tool() throws OR returns error string → lines 7635-7640:
    tool_msg = {"role": "tool", "content": "Error executing tool '...': Code 400", "tool_call_id": tc.id}
    messages.append(tool_msg)          ←污染开始 (contamination begins)
→ _flush_messages_to_session_db() lines 2514-2536:
    for msg in messages[flush_from:]:
        self._session_db.append_message(..., content=msg["content"], ...)  ←写入DB
→ Session ends, next user message arrives
→ gateway.run.run_sync() line 3685:
    history = self.session_store.load_transcript(session_entry.session_id)  ←重新加载
→ gateway.run.run_sync() lines 8836-8838:
    if has_tool_calls or has_tool_call_id or is_tool_message:
        agent_history.append(clean_msg)  ←重新放入上下文
→ agent.run_conversation() → LLM API call with contaminated history
→ HTTP 400 again → loop repeats
Key Code References:
FileLine(s)Description
run_agent.py	7493–7495	Tool exception caught and converted to error string
run_agent.py	7635–7640	Error tool message appended to messages with no filtering
run_agent.py	2514–2536	_flush_messages_to_session_db writes ALL messages including errors
run_agent.py	2541–2564	_get_messages_up_to_last_assistant() — rollback helper exists but unused for tool errors
gateway/session.py	1011–1057	load_transcript() returns full history including error tool results
gateway/run.py	8830–8838	Tool messages passed through to agent_history without sanitization
gateway/run.py	8976	run_conversation(message, conversation_history=agent_history, ...)
agent/anthropic_adapter.py	1107–1142	_strip_orphaned_tool_blocks — partial fix only, does not handle invalid content in paired blocks

Operating System

Linux (confirmed: UGREEN NAS container environment)

Python Version

Python: 3.13.5

Hermes Version

Hermes Agent v0.10.0 (2026.4.16)

Additional Logs / Traceback (optional)

Root Cause Analysis (optional)

No response

Proposed Fix (optional)

No response

Are you willing to submit a PR for this?

  • I'd like to fix this myself and submit a PR

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - 💡(How to fix) Fix [Bug]: Tool Result Contamination Causes Persistent HTTP 400 Error Loop