Skip to content

fix(agent): prevent empty tool_calls arrays from reaching strict providers (#69280) - #69351

Closed
webtecnica wants to merge 2 commits into
NousResearch:mainfrom
webtecnica:fix/69280-deepseek-tools
Closed

fix(agent): prevent empty tool_calls arrays from reaching strict providers (#69280)#69351
webtecnica wants to merge 2 commits into
NousResearch:mainfrom
webtecnica:fix/69280-deepseek-tools

Conversation

@webtecnica

Copy link
Copy Markdown
Contributor

Summary

Fixes #69280 — DeepSeek v4 rejects tool_calls: [] with HTTP 400 "Invalid 'messages[N].tool_calls': empty array". Two sources were causing empty arrays to reach the API.

Changes

  1. agent/agent_runtime_helpers.pyrepair_message_sequence (root cause fix)

    When merging two consecutive assistant messages that both lack tool_calls, neither the if new_calls: nor elif prev_calls: branch fires, leaving a stale tool_calls: [] on the surviving turn. Added an else branch that removes the key explicitly.

  2. run_agent.py_sanitize_tool_calls_for_strict_api (defensive fix)

    The guard if not isinstance(tool_calls, list): return passes an empty list through, and the empty list comprehension then re-sets api_msg["tool_calls"] = []. Added or not tool_calls to the early-return guard.

The per-API-call sanitizer in sanitize_api_messages (#58755) already strips empty tool_calls as a safety net, but fixing these earlier chokepoints prevents the bad state from persisting in session history.

Testing

  • All 37 existing tests in test_message_sequence_repair.py pass
  • The existing test_sanitize_drops_empty_tool_calls_array continues to pass

webtecnica and others added 2 commits July 21, 2026 01:39
Previously _count_skills() only counted SKILL.md files inside the
profile's own skills/ directory, making the WebUI profile card show
a misleading low count (e.g. 0 for 'default', 30 for 'webtecnica')
even though the profile loaded 150+ skills from global + external
dirs.

Now it scans three sources:
1. Profile-specific skills/ dir (as before)
2. Global ~/.hermes/skills/ dir (via get_default_hermes_root)
3. External dirs from skills.external_dirs config

Deduplication by skill name (from YAML frontmatter) prevents double-
counting when the same skill exists in both global and profile dirs,
matching how scan_skill_commands() loads skills at runtime.

The cache is updated to key on all scanned directories and track
their combined mtime signatures.
…iders (NousResearch#69280)

DeepSeek v4 (and other strict OpenAI-compatible providers) reject
assistant messages carrying `tool_calls: []` with HTTP 400
"Invalid 'messages[N].tool_calls': empty array". Two sources:

1. **repair_message_sequence** — consecutive-assistant merge preserves
   a pre-existing empty array on the surviving turn because neither
   the `if new_calls:` nor `elif prev_calls:` branch fires when
   both sides have empty-or-absent tool_calls, leaving the stale
   `tool_calls: []` in place. Fix: add an `else` branch that
   explicitly removes the key.

2. **_sanitize_tool_calls_for_strict_api** — the guard
   `if not isinstance(tool_calls, list): return` passes an empty
   list through, and the empty comprehension then re-sets
   `api_msg["tool_calls"] = []`. Fix: add `or not tool_calls`
   to the early-return guard.

The per-API-call sanitizer in `sanitize_api_messages` (NousResearch#58755)
already strips empty tool_calls as a safety net, but fixing these
earlier chokepoints prevents the bad state from persisting in
session history and makes the code more robust against future
refactoring of the sanitizer order.
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint comp/cli CLI entry point, hermes_cli/, setup wizard tool/skills Skills system (list, view, manage) provider/deepseek DeepSeek API sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades labels Jul 22, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for tracing the strict-provider failure path. This automated hermes-sweeper review found that the requested wire-level guarantee is already implemented on current main.

  • a7932d86c5 (fix(agent): drop empty tool_calls arrays in pre-API sanitizer) shipped in v2026.7.7.
  • agent/agent_runtime_helpers.py:29743000 removes empty or malformed tool_calls from the per-request copy before it reaches a provider; the code explicitly preserves persisted history and prompt-cache stability.
  • Both request paths invoke that final sanitizer after _sanitize_tool_calls_for_strict_api: agent/conversation_loop.py:1516, 1624 and agent/chat_completion_helpers.py:2149, 2167.
  • tests/run_agent/test_message_sequence_repair.py:715 verifies the DeepSeek empty-array case, and :747 verifies that the persisted source message is not mutated.

The linked #69280 discussion correctly identified this pre-API sanitizer as the safety net. The PR's additional hermes_cli/profiles.py skill-count change is unrelated to this already-covered provider fix.

@teknium1 teknium1 closed this Jul 30, 2026
@teknium1 teknium1 added sweeper:implemented-on-main Sweeper: behavior already present on current main sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit labels Jul 30, 2026
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 comp/cli CLI entry point, hermes_cli/, setup wizard P2 Medium — degraded but workaround exists provider/deepseek DeepSeek API sweeper:blast-broad Sweeper blast radius: broad — a core path most sessions hit sweeper:implemented-on-main Sweeper: behavior already present on current main sweeper:risk-caching Sweeper risk: may break/degrade prompt caching or cache-key stability (invariant) sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/skills Skills system (list, view, manage) type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Switching to DeepSeek fails with "messages[N].tool_calls: empty array" after context compaction

3 participants