Skip to content

fix(agent): close orphaned assistant(tool_calls) in repair_message_sequence (#56980) - #820

Closed
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-57013
Closed

fix(agent): close orphaned assistant(tool_calls) in repair_message_sequence (#56980)#820
hashbender wants to merge 1 commit into
mainfrom
mirror/pr-57013

Conversation

@hashbender

Copy link
Copy Markdown
Owner

What does this PR do?

repair_message_sequence handles stray tool messages, consecutive-assistant merges, and consecutive-user merges, but had no pass for the inverse case: an assistant message with tool_calls whose ids are not answered by the tool messages that follow. This happens after an interruption (/stop, process kill, resume mid tool-loop) when some or all of a turn's tool calls never got a result appended.

Strict OpenAI-compatible providers (DeepSeek v4, Moonshot/Kimi) reject that message shape outright:

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)

Scope, honestly

This does not fix an actively-reproducing HTTP 400 in the main request pipeline. sanitize_api_messages (called right after repair_message_sequence on every call, in conversation_loop.py and chat_completion_helpers.py — the single path CLI, gateway, and webui all funnel through) already injects a stub result for orphaned tool_call_ids before the request reaches the provider. Verified this directly against the real pipeline for every orphan shape in NousResearch#56980; none reproduce the 400 on current main.

What this PR actually fixes: repair_message_sequence operates on the canonical, persisted messages list, while sanitize_api_messages only patches the ephemeral per-call api_messages copy. Any caller that runs repair_message_sequence without a follow-up sanitize_api_messages call — sub-agents, MoA reference-model calls, plugins, or the existing run_agent.py forwarder used only by tests — stays exposed to the exact symptom in NousResearch#56980. This closes that gap at the source instead of depending on a second, separately-invoked function to catch it downstream every single call.

Fix

Added a pass to repair_message_sequence: for each assistant(tool_calls), scan the tool messages immediately following it, and for any tool_call_id still unanswered, insert a synthetic error result:

{"role": "tool", "tool_call_id": "<id>", "name": "<tool name>", "content": "Tool execution was interrupted before a result was returned."}

Stubs the missing result instead of stripping tool_calls from the assistant message, to avoid leaving {"role": "assistant", "content": None} when a turn's calls are all orphaned, and to keep the model's stated intent visible in the transcript.

No-op when every tool_call_id already has a matching result — the existing "ongoing dialog" pattern (a complete assistant(tool_calls)+tool pair followed by a user redirect) is left untouched, as documented in the function's docstring.

Follow-up fix (NousResearch#57036): the id lookup in this pass initially read tool_calls[].id directly. Switched it to AIAgent._get_tool_call_id_static / _get_tool_call_name_static — the call_id || id extractor sanitize_api_messages and the context compressor already use — since Codex/Responses-API-style tool_calls key the id as call_id, not id. Without this, the pass silently had no effect on that shape.

Related

Fixes NousResearch#56980 (design gap this closes), fixes NousResearch#57036 (extractor follow-up).

Testing

  • tests/run_agent/test_message_sequence_repair.py: 6 new tests — fully unanswered trailing tool_calls, partially answered parallel tool_calls, orphaned tool_calls followed by an injected user/wakeup message, idempotency, call_id-keyed (Codex-shaped) orphans, and a non-regression check that a complete pair before a user redirect is left untouched.
  • Full file: python -m pytest tests/run_agent/test_message_sequence_repair.py -v — 31/31 passing.
  • Regression: tests/agent/test_context_compressor.py, tests/agent/test_replay_cleanup.py, tests/agent/test_close_interrupted_tool_sequence.py — 159/159 passing.
  • Full test_run_agent.py suite — 414/414 passing.

Type of Change

  • Bug fix / hardening (non-breaking)

Checklist


Mirror-of: NousResearch#57013
NousResearch#57013

@hashbender hashbender closed this Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant