You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
On current main (226e8de82), the request builder appends {"role": "assistant", "content": "", "tool_calls": []} to the message array
sent to the model, and it does this in a runaway loop. When the model returns
another empty, act-less response after seeing a transcript that already ends in
its own empty turns, the builder appends one more empty assistant message and
retries the request. In captured live traffic, requests end with as many as 12
consecutive empty assistant messages, growing by exactly one per request every
couple of seconds.
For open-weight models served over an OpenAI-compatible endpoint (llama.cpp in
our case), the chat template renders each of these as a completed empty
assistant turn. A model that sees itself having said nothing 4 to 12 times in a
row degrades badly. It stops emitting tool calls and instead produces short
intention statements like "I'll read the summaries now." with no tool_call, and
the turn ends. This repeats. The visible downstream symptoms are
identical-argument tool-call retry loops (the same read_file re-issued 13
times) and guardrail halts.
v0.18.0 (662426ec3), in the same environment, never produces these messages
and never shows the failure.
One corroborating detail. With enable_thinking templates, llama.cpp actually
rejects these requests outright with HTTP 400
("Assistant response prefill is incompatible with enable_thinking."), because
it treats any trailing assistant message as prefill. We had worked around that
400 with --no-prefill-assistant, which silenced the error and, as it turns
out, converted a loud failure into silent context poisoning. The 400 first
showed up for us on 0.18.2; 0.18.0 never triggered it.
Steps to Reproduce
Steps to Reproduce
Configure a custom provider pointing at a llama.cpp llama-server
(OpenAI-compatible, stream: true, tools enabled). Model is Qwen3.6-35B-A3B
(IQ3_XXS GGUF), with chat_template_kwargs.enable_thinking: true and --no-prefill-assistant on the server. Without that flag you get the HTTP
400 above instead of the silent failure.
Run a multi-step task that uses delegate_task fan-out with async
completions. Ours was "research 3 topics with subagents in parallel, then
consolidate the summaries and write an article."
Put a transparent logging proxy between the agent and llama-server and record
each /v1/chat/completions body.
Watch the message array as the async delegation completions come back.
It shows up within about four minutes on every run. 3 of 3 runs on main
reproduced it; 0 of 4 runs on 0.18.0 did, with the same prompt, model, provider
and config. The config.yaml is byte-for-byte identical across both versions
(_config_version: 33, no migration fires). The only thing that changes is the
checked-out code.
Expected Behavior
Expected Behavior
The conversation sent to the model should never contain assistant messages with
empty content and an empty tool_calls array, and an empty model response
should not be re-appended as a context message and retried without bound.
Worth noting: the pre-API sanitizer already drops the empty tool_calls key
(#58755), but the empty assistant message itself still reaches the wire.
Actual Behavior
Actual Behavior
Captured wire traffic from one failing session, 57 requests in total. 29 of
those 57 (51%) end with two or more consecutive empty assistant messages.
Here is the terminal runaway, one request roughly every two seconds, one empty
assistant message appended each time:
seq
time
msgs
trailing empty assistant
48
09:41:07
47
3
49
09:41:11
48
4
50
09:41:12
49
5
51
09:41:14
50
6
52
09:41:16
51
7
53
09:41:17
52
8
54
09:41:19
53
9
55
09:41:21
54
10
56
09:41:23
55
11
57
09:41:24
56
12
The literal tail of request #57, with 12 of these in a row:
Behavior metric, counting assistant messages that announce an action but carry
no tool_call (main agent only, measured from the session store):
0.18.0: 2% (2 of 115 across 4 sessions)
main: 18% to 56% across 3 sessions, plus guardrail_halts and that 13x
identical-argument read_file loop on a delegation summary file.
These empty messages are never persisted to the session store (zero found in state.db for every session). They live only in the built request, which is
why the issue stays invisible unless you capture live traffic.
### Debug Report
--- hermes dump ---
version: 0.18.0 [662426ec] (2026-07-03) # config byte-identical to the main @ 226e8de82 checkout used to reproduce
os: Linux 6.17.0-40-generic x86_64
python: 3.11.15
openai_sdk: 2.24.0
profile: default
model: qwen36-35b-a3b-iq3xxs
provider: custom
terminal: local
features:
toolsets: terminal, file, web, code_execution, skills, session_search, memory, delegation, todo, clarify, browser, vision
mcp_servers: 0
memory_provider: openviking
gateway: stopped
platforms: none
skills: 215
config_overrides:
agent.tool_use_enforcement: True
compression.threshold: 0.85
display.show_reasoning: True
--- end dump ---
(IPs, paths, credentials and unrelated local tooling redacted. Log tail omitted
for size and privacy. The full sanitized report and the wire capture are
available on request; they contain private prompt data, so they are not
attached wholesale.)
Operating System
Ubuntu 24.04
Python Version
3.11.15
Hermes Version
0.18.2
Additional Logs / Traceback
### Additional Logs / Traceback
Searched open and merged issues and PRs first, per CONTRIBUTING:
- #42503 describes this same symptom from the outside ("the agent appears to
start work and then stop", post-tool progress-only turns getting treated as final). This report is the wire-level cause behind it: by the time that placeholder turn happens, the context already ends in a pile of empty assistant turns.- #63200 is phantom `content: ""` assistant messages (with tool_calls) breaking DeepSeek. Same message-hygiene family, different shape.- #11213 is the prefill/thinking 400 conflict. That error is this same artifact getting rejected loudly by llama.cpp instead of silently accepted.- Open PRs #31175, #31582 and #47280 sanitize adjacent shapes (empty content with tool_calls, partial-stream empty turns). None of them covers the accumulating `content:"" + tool_calls:[]` messages captured here.
Root Cause Analysis
Root Cause Analysis
The append site is most likely where an empty final_response gets pushed as {"role":"assistant","content":""} ahead of a follow-up request. With tool
definitions active, that serializes as content:"" , tool_calls:[]. Two things
make it a bug rather than a quirk. First, the accumulation is unbounded: 12
deep, one more per request every couple of seconds. Second, these messages
survive into requests after the #58755 normalization chokepoint, which exists
specifically to keep semantically-empty tool_call shapes off the wire. The "tool_calls": [] still riding on every captured request tells you the append
happens after that sanitizer, or on a path that skips it. That ordering is the
best lead for finding it.
What we ruled out, all verified equal between 662426ec3 and 226e8de82:
Tool schemas: same 53 tools on the wire, toolsets.py identical.
Tool-result reassembly after read_file, tested trivial, large, and with
fragmented-SSE tool-call accumulation: identical, content intact
(truncated: false).
Async delegation batch-complete message composition: identical. The good
version also processed 8 async completions in one run without trouble.
delegate_taskbackground default: False in both.
Memory-provider recall injection: disabled and verified absent on the wire,
and the failure still persisted at 56%.
Same llama-server process, build and model on every run. Only the agent code
changed, via git checkout.
One note toward the "intentional design, not a gap" rubric: a single trailing
assistant message can be a legitimate continuation or prefill mechanism on some
providers, and this report does not claim that is a bug. The bug is the
unbounded accumulation and its survival past the #58755 chokepoint.
Break the retry cycle. When the model returns an empty response and the last
context message is already an empty assistant turn, don't append another one
before re-requesting.
Treat any trailing assistant message as prefill-sensitive. The HTTP 400 with
thinking templates is the loud version of this same bug, and masking flags
like --no-prefill-assistant shouldn't be a prerequisite for open-weight
providers.
Bug Description
On current
main(226e8de82), the request builder appends{"role": "assistant", "content": "", "tool_calls": []}to the message arraysent to the model, and it does this in a runaway loop. When the model returns
another empty, act-less response after seeing a transcript that already ends in
its own empty turns, the builder appends one more empty assistant message and
retries the request. In captured live traffic, requests end with as many as 12
consecutive empty assistant messages, growing by exactly one per request every
couple of seconds.
For open-weight models served over an OpenAI-compatible endpoint (llama.cpp in
our case), the chat template renders each of these as a completed empty
assistant turn. A model that sees itself having said nothing 4 to 12 times in a
row degrades badly. It stops emitting tool calls and instead produces short
intention statements like "I'll read the summaries now." with no tool_call, and
the turn ends. This repeats. The visible downstream symptoms are
identical-argument tool-call retry loops (the same
read_filere-issued 13times) and guardrail halts.
v0.18.0(662426ec3), in the same environment, never produces these messagesand never shows the failure.
One corroborating detail. With
enable_thinkingtemplates, llama.cpp actuallyrejects these requests outright with HTTP 400
(
"Assistant response prefill is incompatible with enable_thinking."), becauseit treats any trailing assistant message as prefill. We had worked around that
400 with
--no-prefill-assistant, which silenced the error and, as it turnsout, converted a loud failure into silent context poisoning. The 400 first
showed up for us on 0.18.2; 0.18.0 never triggered it.
Steps to Reproduce
Steps to Reproduce
customprovider pointing at a llama.cppllama-server(OpenAI-compatible,
stream: true, tools enabled). Model is Qwen3.6-35B-A3B(IQ3_XXS GGUF), with
chat_template_kwargs.enable_thinking: trueand--no-prefill-assistanton the server. Without that flag you get the HTTP400 above instead of the silent failure.
delegate_taskfan-out with asynccompletions. Ours was "research 3 topics with subagents in parallel, then
consolidate the summaries and write an article."
each
/v1/chat/completionsbody.It shows up within about four minutes on every run. 3 of 3 runs on
mainreproduced it; 0 of 4 runs on 0.18.0 did, with the same prompt, model, provider
and config. The
config.yamlis byte-for-byte identical across both versions(
_config_version: 33, no migration fires). The only thing that changes is thechecked-out code.
Expected Behavior
Expected Behavior
The conversation sent to the model should never contain assistant messages with
empty content and an empty
tool_callsarray, and an empty model responseshould not be re-appended as a context message and retried without bound.
Worth noting: the pre-API sanitizer already drops the empty
tool_callskey(#58755), but the empty assistant message itself still reaches the wire.
Actual Behavior
Actual Behavior
Captured wire traffic from one failing session, 57 requests in total. 29 of
those 57 (51%) end with two or more consecutive empty assistant messages.
Here is the terminal runaway, one request roughly every two seconds, one empty
assistant message appended each time:
The literal tail of request #57, with 12 of these in a row:
Behavior metric, counting assistant messages that announce an action but carry
no tool_call (main agent only, measured from the session store):
guardrail_halts and that 13xidentical-argument
read_fileloop on a delegation summary file.These empty messages are never persisted to the session store (zero found in
state.dbfor every session). They live only in the built request, which iswhy the issue stays invisible unless you capture live traffic.
Affected Component
Agent Core (conversation loop, context compression, memory)
Messaging Platform (if gateway-related)
N/A (CLI only)
Debug Report
Operating System
Ubuntu 24.04
Python Version
3.11.15
Hermes Version
0.18.2
Additional Logs / Traceback
Root Cause Analysis
Root Cause Analysis
The append site is most likely where an empty
final_responsegets pushed as{"role":"assistant","content":""}ahead of a follow-up request. With tooldefinitions active, that serializes as
content:"" , tool_calls:[]. Two thingsmake it a bug rather than a quirk. First, the accumulation is unbounded: 12
deep, one more per request every couple of seconds. Second, these messages
survive into requests after the #58755 normalization chokepoint, which exists
specifically to keep semantically-empty tool_call shapes off the wire. The
"tool_calls": []still riding on every captured request tells you the appendhappens after that sanitizer, or on a path that skips it. That ordering is the
best lead for finding it.
What we ruled out, all verified equal between
662426ec3and226e8de82:config.yamlbyte-identical (both_config_version: 33).toolsets.pyidentical.read_file, tested trivial, large, and withfragmented-SSE tool-call accumulation: identical, content intact
(
truncated: false).version also processed 8 async completions in one run without trouble.
delegate_taskbackgrounddefault:Falsein both.and the failure still persisted at 56%.
changed, via
git checkout.One note toward the "intentional design, not a gap" rubric: a single trailing
assistant message can be a legitimate continuation or prefill mechanism on some
providers, and this report does not claim that is a bug. The bug is the
unbounded accumulation and its survival past the #58755 chokepoint.
Proposed Fix
and extend that chokepoint, or the append site itself, to drop assistant
messages that have neither content nor tool_calls.
context message is already an empty assistant turn, don't append another one
before re-requesting.
thinking templates is the loud version of this same bug, and masking flags
like
--no-prefill-assistantshouldn't be a prerequisite for open-weightproviders.
Are you willing to submit a PR for this?