Skip to content

fix(agent): drop tool_calls key when dedup empties the array - #80827

Closed
dahux-net wants to merge 1 commit into
NousResearch:mainfrom
dahux-net:fix/empty-tool-calls-dedup
Closed

dahux-net wants to merge 1 commit into
NousResearch:mainfrom
dahux-net:fix/empty-tool-calls-dedup

Conversation

@dahux-net

Copy link
Copy Markdown

Why

Long-lived Hermes sessions intermittently 400 on strict providers (DeepSeek):

HTTP 400: Invalid 'messages[N].tool_calls': empty array. Expected an array
with minimum length 1, but got an empty array instead.

The pre-API sanitizer (sanitize_api_messages) drops tool_calls: [] on
assistant messages (#58755), but its own tool_call_id dedup pass can
re-create it: when an assistant message's calls are ALL duplicates of ids
already seen earlier in the transcript (crash/resume glitch, retry, or a
compression window re-emitting a tool result), kept_tcs ends up empty and
the pass writes msg["tool_calls"] = [] back onto the message — after the
empty-array pass already ran.

DeepSeek rejects the empty array outright, the request fails, and because
the fallback chain's first leg resolves to the same backend, the failure
falls through to the next provider. The poisoned session 400s the same way
every subsequent turn.

Fix

When dedup empties the array, drop the tool_calls key entirely — the same
semantics the empty-array pass already applies (empty array ≡ no tool
calls):

if len(kept_tcs) != len(msg.get("tool_calls") or []):
    if kept_tcs:
        msg = {**msg, "tool_calls": kept_tcs}
    else:
        msg = {k: v for k, v in msg.items() if k != "tool_calls"}

The pre-existing empty-array case still cleans up as before; healthy
transcripts are untouched; partial dedup keeps the surviving (first)
calls.

Test

tests/run_agent/test_sanitize_dedup_empty_tool_calls.py — 3 tests:

  • all-duplicates block yields a plain assistant message, never []
    (this one fails on the old code)
  • unique-id transcript preserved
  • partial duplicates keep the first occurrence

Verified: scripts/run_tests.sh tests/run_agent/test_sanitize_dedup_empty_tool_calls.py tests/run_agent/test_message_sequence_repair.py tests/run_agent/test_tool_call_args_sanitizer.py tests/run_agent/test_thinking_only_sanitizer.py -q — all pass.

The pre-API sanitizer's tool_call_id dedup pass wrote `tool_calls: []` back
onto an assistant message whose calls were all duplicates of ids seen
earlier in the transcript (crash/resume glitch or a compression window
re-emitting a tool result). Strict OpenAI-compatible providers (DeepSeek)
reject an empty array with HTTP 400 "Invalid 'messages[N].tool_calls':
empty array" and the request falls through the fallback chain. Drop the
key entirely instead — same semantics as the empty-array pass from NousResearch#58755.
@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state duplicate This issue or pull request already exists labels Aug 7, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.

Duplicate of #64345: it implements the same all-deduped tool_calls cleanup in sanitize_api_messages, with the same strict-provider empty-array failure mechanism.

@dahux-net

Copy link
Copy Markdown
Author

Closing as redundant: upstream's general empty/invalid tool_calls sanitizer (agent/agent_runtime_helpers.py, follow-up to #58755) covers this case class — it drops the tool_calls key for any empty or non-list value, including the dedup-empties case this PR addressed. Verified present in v0.20.0. Thanks for reviewing.

@dahux-net dahux-net closed this Aug 13, 2026
@dahux-net
dahux-net deleted the fix/empty-tool-calls-dedup branch August 13, 2026 08:55
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 duplicate This issue or pull request already exists P2 Medium — degraded but workaround exists sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants