fix(agent): prevent empty tool_calls arrays from reaching strict providers (#69280) - #69351
Closed
webtecnica wants to merge 2 commits into
Closed
fix(agent): prevent empty tool_calls arrays from reaching strict providers (#69280)#69351webtecnica wants to merge 2 commits into
webtecnica wants to merge 2 commits into
Conversation
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.
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
The linked #69280 discussion correctly identified this pre-API sanitizer as the safety net. The PR's additional |
This was referenced Aug 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
agent/agent_runtime_helpers.py—repair_message_sequence(root cause fix)When merging two consecutive assistant messages that both lack tool_calls, neither the
if new_calls:norelif prev_calls:branch fires, leaving a staletool_calls: []on the surviving turn. Added anelsebranch that removes the key explicitly.run_agent.py—_sanitize_tool_calls_for_strict_api(defensive fix)The guard
if not isinstance(tool_calls, list): returnpasses an empty list through, and the empty list comprehension then re-setsapi_msg["tool_calls"] = []. Addedor not tool_callsto 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
test_message_sequence_repair.pypasstest_sanitize_drops_empty_tool_calls_arraycontinues to pass