hermes - 💡(How to fix) Fix Parallel tool calls stored as separate assistant messages break DeepSeek session replay [1 pull requests]

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…

When DeepSeek (and potentially other providers) returns parallel tool calls in a single response, Hermes Agent stores each tool call as a separate consecutive assistant message in the session file. On the next turn, when the full message history is replayed to the API, DeepSeek rejects the request with HTTP 400 because its API enforces strict alternation: every assistant message containing tool_calls must be immediately followed by tool role responses before another assistant message can appear.

Error Message

Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be
followed by tool messages responding to each 'tool_call_id'. (insufficient tool
messages following tool_calls message)", ...}}

Root Cause

The session file records parallel tool calls as interleaved assistant messages:

[
  {"role": "assistant", "tool_calls": [{"id": "call_A", "function": {"name": "session_search"}}]},
  {"role": "assistant", "tool_calls": [{"id": "call_B", "function": {"name": "search_files"}}]},
  {"role": "tool", "tool_call_id": "call_A"},
  {"role": "tool", "tool_call_id": "call_B"}
]

DeepSeek v4 API (and many other OpenAI-compatible providers) require tool responses to follow their tool_calls message immediately. The correct format should merge parallel calls into a single assistant message:

[
  {"role": "assistant", "tool_calls": [
    {"id": "call_A", "function": {"name": "session_search"}},
    {"id": "call_B", "function": {"name": "search_files"}}
  ]},
  {"role": "tool", "tool_call_id": "call_A"},
  {"role": "tool", "tool_call_id": "call_B"}
]

Fix Action

Fixed

Code Example

Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be
followed by tool messages responding to each 'tool_call_id'. (insufficient tool
messages following tool_calls message)", ...}}

---

[
  {"role": "assistant", "tool_calls": [{"id": "call_A", "function": {"name": "session_search"}}]},
  {"role": "assistant", "tool_calls": [{"id": "call_B", "function": {"name": "search_files"}}]},
  {"role": "tool", "tool_call_id": "call_A"},
  {"role": "tool", "tool_call_id": "call_B"}
]

---

[
  {"role": "assistant", "tool_calls": [
    {"id": "call_A", "function": {"name": "session_search"}},
    {"id": "call_B", "function": {"name": "search_files"}}
  ]},
  {"role": "tool", "tool_call_id": "call_A"},
  {"role": "tool", "tool_call_id": "call_B"}
]
RAW_BUFFERClick to expand / collapse

Description

When DeepSeek (and potentially other providers) returns parallel tool calls in a single response, Hermes Agent stores each tool call as a separate consecutive assistant message in the session file. On the next turn, when the full message history is replayed to the API, DeepSeek rejects the request with HTTP 400 because its API enforces strict alternation: every assistant message containing tool_calls must be immediately followed by tool role responses before another assistant message can appear.

Error

Error code: 400 - {'error': {'message': "An assistant message with 'tool_calls' must be
followed by tool messages responding to each 'tool_call_id'. (insufficient tool
messages following tool_calls message)", ...}}

Root Cause

The session file records parallel tool calls as interleaved assistant messages:

[
  {"role": "assistant", "tool_calls": [{"id": "call_A", "function": {"name": "session_search"}}]},
  {"role": "assistant", "tool_calls": [{"id": "call_B", "function": {"name": "search_files"}}]},
  {"role": "tool", "tool_call_id": "call_A"},
  {"role": "tool", "tool_call_id": "call_B"}
]

DeepSeek v4 API (and many other OpenAI-compatible providers) require tool responses to follow their tool_calls message immediately. The correct format should merge parallel calls into a single assistant message:

[
  {"role": "assistant", "tool_calls": [
    {"id": "call_A", "function": {"name": "session_search"}},
    {"id": "call_B", "function": {"name": "search_files"}}
  ]},
  {"role": "tool", "tool_call_id": "call_A"},
  {"role": "tool", "tool_call_id": "call_B"}
]

How to Reproduce

  1. Use DeepSeek v4 Pro or similar provider that returns parallel tool calls
  2. Run a conversation where the agent makes multiple parallel tool calls in one turn
  3. Send another user message
  4. The API call on step 3 fails with 400

Where the Fix Should Go

In run_agent.py, either:

Option A — Session save time: Before appending a new assistant message with tool_calls to the message list, check if the last message is also an assistant with tool_calls. If so, merge the new tool_calls into the existing message instead of appending a new one.

Option B — API send time: Before sending messages to the API, collapse consecutive assistant messages with tool_calls into a single message. This is more robust as it handles both freshly generated and legacy persisted sessions.

Environment

  • Hermes Agent version: 1.0.0 (git commit f27fcb6a8)
  • Model: deepseek-v4-pro (via api.deepseek.com/v1)
  • Platform: CLI
  • Session stored as JSON with platform: "cli"

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