Skip to content

fix(agent): strip orphan tool_calls in repair_message_sequence (#56980) - #56990

Closed
liuhao1024 wants to merge 1 commit into
NousResearch:mainfrom
liuhao1024:liuhao/cron-bugfix-56980-tool-call-orphan
Closed

fix(agent): strip orphan tool_calls in repair_message_sequence (#56980)#56990
liuhao1024 wants to merge 1 commit into
NousResearch:mainfrom
liuhao1024:liuhao/cron-bugfix-56980-tool-call-orphan

Conversation

@liuhao1024

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds Pass 3 to repair_message_sequence that strips orphan tool_calls from assistant messages whose tool_call_ids have no matching tool result messages in the sequence. This prevents HTTP 400 "insufficient tool messages following tool_calls message" from strict providers (DeepSeek v4, Kimi) when context compaction, partial session resume, or retry loops leave dangling tool-call references.

Related Issue

Fixes #56980

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • agent/agent_runtime_helpers.py: Added Pass 3 in repair_message_sequence — builds a set of all tool_call_ids present in tool messages, then strips tool_calls entries from assistant messages whose ids are absent. Keeps assistant text content intact; only removes the orphan calls.
  • tests/run_agent/test_message_sequence_repair.py: Added 3 tests covering the new pass — orphan tool_calls at sequence end, parallel tool_calls with partial results missing, and no-op when all tool_calls have matching results.

How to Test

  1. Run python -m pytest tests/run_agent/test_message_sequence_repair.py -v — all 28 tests should pass, including the 3 new ones (test_repair_strips_orphan_tool_calls_at_end, test_repair_strips_orphan_parallel_tool_calls, test_repair_preserves_complete_tool_calls).
  2. The fix is verified by the new test test_repair_strips_orphan_tool_calls_at_end which simulates the exact scenario from the issue: an assistant message with tool_calls at the end of the sequence with no matching tool result.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26.4.1

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

When context compaction, partial session resume, or retry loops remove
tool results from history, assistant messages with tool_calls may end up
with no matching tool responses.  Strict providers (DeepSeek v4, Kimi)
reject such sequences with HTTP 400: "insufficient tool messages
following tool_calls message".

Add Pass 3 to repair_message_sequence: scan all tool messages for
present tool_call_ids, then strip any tool_calls whose ids are absent.
Preserves assistant text content and keeps calls that do have results.

Fixes NousResearch#56980
@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 sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state P1 High — major feature broken, no workaround labels Jul 2, 2026
@kshitijk4poor

Copy link
Copy Markdown
Collaborator

Thanks for digging into this @liuhao1024 — clean patch, tests, and a correct read of the symptom. But we're going to close this without merging, because the outgoing API path already handles this case, and doing it in repair_message_sequence would be destructive.

The orphan-tool_calls case is already fixed before every request. Right before each API call, sanitize_api_messages() runs unconditionally on both the main loop (agent/conversation_loop.py:894) and the final-summary path (agent/chat_completion_helpers.py:1536). Its "missing results" branch (missing_results = surviving_call_ids - result_call_ids) detects any assistant tool_calls whose tool_call_id has no matching tool message and injects a stub result ("[Result unavailable — see context summary above]"), producing a valid assistant(tool_calls) → tool sequence that DeepSeek v4 / Kimi accept.

I verified this end-to-end against the exact scenario from #56980 (trailing assistant(tool_calls) with the result stripped, plus the parallel-calls-partial-results variant). In both cases the sanitizer already emits a valid sequence — the last message becomes a tool result for the orphan call. That safety net (PR #1732) predates the commit in the report, so it was active when the 400 was hit.

Why Pass 3 is also the wrong shape: repair_message_sequence mutates the conversation in place and repair_message_sequence_with_cursor flushes the compacted list to the session DB. Stripping tool_calls there would permanently delete them from the persisted trajectory — the model loses the record that it ever made the call, which can make it re-issue or contradict itself. The existing sanitizer avoids this by operating on a per-call copy of the messages, leaving stored history (and prompt caching) intact.

If you can still reproduce the 400 on latest main, that would point to a real gap inside sanitize_api_messages (e.g. a call_id/id mismatch or an empty-id call), not a missing pass in repair_message_sequence. A session dump on the failing turn would let us find it. Really appreciate the thorough writeup on the issue either way.

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 P1 High — major feature broken, no workaround 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.

[Bug]: repair_message_sequence fails to prevent HTTP 400 "insufficient tool messages following tool_calls" on DeepSeek v4 after long tool-call sessions

3 participants