Skip to content

fix(tools): preserve tool flow when caller appends unrelated messages (closes #1536)#1545

Open
0xdeadd wants to merge 1 commit into
anthropics:mainfrom
0xdeadd:fix/tool-runner-append-messages
Open

fix(tools): preserve tool flow when caller appends unrelated messages (closes #1536)#1545
0xdeadd wants to merge 1 commit into
anthropics:mainfrom
0xdeadd:fix/tool-runner-append-messages

Conversation

@0xdeadd
Copy link
Copy Markdown

@0xdeadd 0xdeadd commented May 14, 2026

Closes #1536.

The bug

The reporter found that calling append_messages inside the tool_runner loop body causes an infinite tool-call loop. Their root-cause analysis is correct: any append_messages call sets _messages_modified = True, which causes __run__ to skip the auto-append of the assistant message + tool result. The next iteration sends a request with no tool result, the model re-asks for the same tool call, repeat forever.

Why the flag is wrong

_messages_modified was conflating two distinct use cases:

  1. Modifying tool results — caller appends a custom tool_result to override the SDK's auto-generated one. Auto-append should skip.
  2. Append an unrelated user message ([Bug] append_messages() inside tool runner loop causes infinite loop (advanced usage docs example) #1536) — caller appends, say, "be concise in your response" between iterations. The tool result still needs to be threaded back in, otherwise the loop breaks.

A single boolean can't distinguish these — one means "I handled the tool flow," the other means "I added context, please continue normally."

The fix

Replace the flag-based gate with a content-based check: auto-append is skipped only when the last user message already contains a tool_result block whose tool_use_id matches the one we were about to write. Unrelated messages don't match, so the tool flow stays correctly threaded.

Applied symmetrically to sync and async __run__.

Tests

  • New unit test test_tool_response_already_appended_detects_matching_tool_result covers:
  • Full tests/lib/tools/test_runners.py passes: 13 passed, 1 xfailed.
  • Existing test_custom_message_handling xfail is preserved — that pattern depends on additional re-threading logic (inserting the assistant message before the caller's tool result) and snapshot updates that are out of scope for this fix.

Repro from the issue

The reporter's repro (the "be concise" message inside the loop body) now terminates correctly: the tool result is auto-appended, the model sees its own tool answer, and produces the final text on the next iteration. Verified locally.

Closes anthropics#1536.

The tool_runner's auto-append of (assistant, tool_result) was previously
gated on a single `_messages_modified` flag that flipped to True whenever
the caller called `append_messages` inside the loop body. This conflated
two distinct use cases:

1. "Modifying tool results" — caller appends a custom tool_result to
   override the SDK's auto-generated one. Auto-append should skip.
2. "Append an unrelated user message" (anthropics#1536) — caller appends, say,
   "be concise in your response" between iterations. The tool_result
   still needs to be threaded back into history; otherwise the next
   request omits it, the model re-asks for the same tool call, and the
   loop spins forever.

This change replaces the flag-based gate with a content-based check:
auto-append is skipped only when the last user message in history
already contains a tool_result block whose tool_use_id matches the one
we were about to write. Unrelated messages don't match, so the tool
flow stays correctly threaded.

Sync and async `__run__` are updated symmetrically. The existing
`test_custom_message_handling` xfail is preserved — it depends on
additional re-threading logic and snapshot updates that are out of
scope for this fix.
@0xdeadd 0xdeadd requested a review from a team as a code owner May 14, 2026 15:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] append_messages() inside tool runner loop causes infinite loop (advanced usage docs example)

1 participant